, etc. - skip tag but process children
p = after;
continue;
}
@@ -542,7 +698,8 @@ HTMLConvertResult html_to_elements(const char* html, size_t len) {
// Handle entities
if (*p == '&') {
Buffer* target;
- if (state.in_a) target = &state.a_text;
+ if (state.in_td) target = &state.cell_text;
+ else if (state.in_a) target = &state.a_text;
else if (state.in_h1 || state.in_h2) target = &state.h_text;
else if (state.bq_depth > 0) target = &state.bq_text;
else target = &text_buf;
@@ -552,16 +709,18 @@ HTMLConvertResult html_to_elements(const char* html, size_t len) {
continue;
}
- // Regular character
+ // Regular character — collapse whitespace like HTML (unless in )
char c = *p++;
- if (state.in_a) {
- buf_append_char(&state.a_text, c);
+ if (state.in_td) {
+ buf_append_html_char(&state.cell_text, c, state.in_pre);
+ } else if (state.in_a) {
+ buf_append_html_char(&state.a_text, c, state.in_pre);
} else if (state.in_h1 || state.in_h2) {
- buf_append_char(&state.h_text, c);
+ buf_append_html_char(&state.h_text, c, state.in_pre);
} else if (state.bq_depth > 0) {
- buf_append_char(&state.bq_text, c);
+ buf_append_html_char(&state.bq_text, c, state.in_pre);
} else {
- buf_append_char(&text_buf, c);
+ buf_append_html_char(&text_buf, c, state.in_pre);
}
}
@@ -598,6 +757,19 @@ HTMLConvertResult html_to_elements(const char* html, size_t len) {
buf_free(&state.bq_prev);
}
+ // Flush unclosed table
+ if (state.capture_depth > 0 && state.table_data.len > 0) {
+ HTMLElement* e = result_add(&result);
+ e->type = HELEM_TABLE;
+ e->text = buf_finish(&state.table_data);
+ char hdr_buf[16];
+ snprintf(hdr_buf, sizeof(hdr_buf), "%d", state.header_rows);
+ e->attr1 = strdup(hdr_buf);
+ } else {
+ buf_free(&state.table_data);
+ }
+ buf_free(&state.cell_text);
+
result.ok = 1;
return result;
}
diff --git a/clib/htmlconv.h b/clib/htmlconv.h
index 7d9c1fd8510d42ed756030ad47f4cd3c09fbeada..46ed840e11281145b29bc19dae275d1dcf6b195e 100644
--- a/clib/htmlconv.h
+++ b/clib/htmlconv.h
@@ -10,6 +10,7 @@ enum {
HELEM_LINK = 3,
HELEM_IMAGE = 4,
HELEM_BLOCKQUOTE = 5,
+ HELEM_TABLE = 6,
};
typedef struct {
diff --git a/clib/types.go b/clib/types.go
index 3aefa83af827a8eebf76a6cb558e086abeb15446..64611e55c47fdf898ed375211e881085cabf1409 100644
--- a/clib/types.go
+++ b/clib/types.go
@@ -15,6 +15,7 @@ const (
HElemLink = 3
HElemImage = 4
HElemBlockquote = 5
+ HElemTable = 6
)
// HTMLElement represents a parsed element from an HTML document.
diff --git a/view/html.go b/view/html.go
index a0ceb9524bfa1d0c4bb93e72909cd2120d8aec75..d72279b5744e9ce0fb643481e0942bed3ff0a130 100644
--- a/view/html.go
+++ b/view/html.go
@@ -663,6 +663,15 @@ func processBody(rawBody string, inline map[string]string, h1Style, h2Style, bod
text.WriteString(fmt.Sprintf("\n %s \n", linkStyle().Render(fmt.Sprintf("[Image: %s, %s]", alt, src))))
}
+ case clib.HElemTable:
+ headerRows := 0
+ if elem.Attr1 != "" {
+ fmt.Sscanf(elem.Attr1, "%d", &headerRows)
+ }
+ text.WriteString("\n")
+ text.WriteString(renderTable(elem.Text, headerRows))
+ text.WriteString("\n")
+
case clib.HElemBlockquote:
var from, date string
prevText := elem.Attr2
@@ -721,6 +730,110 @@ func processBody(rawBody string, inline map[string]string, h1Style, h2Style, bod
return bodyStyle.Render(result), placements, nil
}
+func tableHeaderStyle() lipgloss.Style {
+ return lipgloss.NewStyle().Bold(true).Foreground(theme.ActiveTheme.Accent)
+}
+
+func tableBorderStyle() lipgloss.Style {
+ return lipgloss.NewStyle().Foreground(theme.ActiveTheme.Secondary)
+}
+
+// renderTable renders table data as a Unicode box-drawing table.
+// data is tab-separated cells, newline-separated rows.
+// headerRows is the number of header rows.
+func renderTable(data string, headerRows int) string {
+ rows := strings.Split(data, "\n")
+ if len(rows) == 0 {
+ return ""
+ }
+
+ // Parse into 2D grid and trim cell whitespace
+ var grid [][]string
+ maxCols := 0
+ for _, row := range rows {
+ cells := strings.Split(row, "\t")
+ trimmed := make([]string, len(cells))
+ for i, c := range cells {
+ trimmed[i] = strings.TrimSpace(c)
+ }
+ grid = append(grid, trimmed)
+ if len(trimmed) > maxCols {
+ maxCols = len(trimmed)
+ }
+ }
+
+ // Normalize: ensure all rows have the same number of columns
+ for i := range grid {
+ for len(grid[i]) < maxCols {
+ grid[i] = append(grid[i], "")
+ }
+ }
+
+ // Calculate column widths
+ colWidths := make([]int, maxCols)
+ for _, row := range grid {
+ for j, cell := range row {
+ if len(cell) > colWidths[j] {
+ colWidths[j] = len(cell)
+ }
+ }
+ }
+
+ // Minimum width per column
+ for i := range colWidths {
+ if colWidths[i] < 3 {
+ colWidths[i] = 3
+ }
+ }
+
+ bs := tableBorderStyle()
+ hs := tableHeaderStyle()
+
+ // Build horizontal borders
+ buildBorder := func(left, mid, right, fill string) string {
+ var b strings.Builder
+ b.WriteString(bs.Render(left))
+ for j, w := range colWidths {
+ b.WriteString(bs.Render(strings.Repeat(fill, w+2)))
+ if j < len(colWidths)-1 {
+ b.WriteString(bs.Render(mid))
+ }
+ }
+ b.WriteString(bs.Render(right))
+ return b.String()
+ }
+
+ topBorder := buildBorder("┌", "┬", "┐", "─")
+ midBorder := buildBorder("├", "┼", "┤", "─")
+ botBorder := buildBorder("└", "┴", "┘", "─")
+
+ var out strings.Builder
+ out.WriteString(topBorder)
+ out.WriteString("\n")
+
+ for i, row := range grid {
+ out.WriteString(bs.Render("│"))
+ for j, cell := range row {
+ padded := cell + strings.Repeat(" ", colWidths[j]-len(cell))
+ if i < headerRows {
+ out.WriteString(" " + hs.Render(padded) + " ")
+ } else {
+ out.WriteString(" " + padded + " ")
+ }
+ out.WriteString(bs.Render("│"))
+ }
+ out.WriteString("\n")
+
+ if i < headerRows && (i+1 == headerRows || i+1 == len(grid)) {
+ out.WriteString(midBorder)
+ out.WriteString("\n")
+ }
+ }
+
+ out.WriteString(botBorder)
+ return out.String()
+}
+
func quoteBoxStyle() lipgloss.Style {
return lipgloss.NewStyle().
Border(lipgloss.RoundedBorder()).