diff --git a/main.go b/main.go
index 39a87f7..c2fca13 100644
--- a/main.go
+++ b/main.go
@@ -282,6 +282,117 @@ func isTableLine(line string) bool {
return strings.HasPrefix(trimmed, "|") || strings.HasSuffix(trimmed, "|")
}
+func wrapCell(s string, width int) []string {
+ if width <= 0 { return []string{""} }
+ s = strings.ReplaceAll(s, "
", "\n")
+ s = strings.ReplaceAll(s, "
", "\n")
+ s = strings.ReplaceAll(s, "
", "\n")
+ var lines []string
+ for _, p := range strings.Split(s, "\n") {
+ p = strings.TrimSpace(p)
+ if p == "" {
+ lines = append(lines, "")
+ continue
+ }
+ if visibleLen(p) <= width {
+ lines = append(lines, p)
+ continue
+ }
+ words := strings.Fields(p)
+ if len(words) == 0 {
+ lines = append(lines, "")
+ continue
+ }
+ var curLine string
+ var activeEsc string
+ updateActiveEsc := func(token string) {
+ for i := 0; i < len(token); {
+ if token[i] == 0x1b {
+ j := i + 1
+ if j < len(token) && token[j] == '[' {
+ j++
+ for j < len(token) && !(token[j] >= 0x40 && token[j] <= 0x7e) { j++ }
+ if j < len(token) { j++ }
+ }
+ esc := token[i:j]
+ if esc == "\033[0m" || esc == "\033[m" {
+ activeEsc = ""
+ } else {
+ activeEsc = esc
+ }
+ i = j
+ } else {
+ i++
+ }
+ }
+ }
+ flushLine := func() {
+ if curLine != "" {
+ out := curLine
+ if COL && activeEsc != "" && !strings.HasSuffix(out, "\033[0m") {
+ out += "\033[0m"
+ }
+ lines = append(lines, out)
+ curLine = ""
+ }
+ }
+ for _, word := range words {
+ wLen := visibleLen(word)
+ if wLen > width {
+ flushLine()
+ var chunk strings.Builder
+ cLen := 0
+ for i := 0; i < len(word); {
+ if word[i] == 0x1b {
+ j := i + 1
+ if j < len(word) && word[j] == '[' {
+ j++
+ for j < len(word) && !(word[j] >= 0x40 && word[j] <= 0x7e) { j++ }
+ if j < len(word) { j++ }
+ }
+ chunk.WriteString(word[i:j])
+ updateActiveEsc(word[i:j])
+ i = j
+ continue
+ }
+ r, size := utf8.DecodeRuneInString(word[i:])
+ if cLen >= width {
+ if COL && activeEsc != "" { chunk.WriteString("\033[0m") }
+ lines = append(lines, chunk.String())
+ chunk.Reset()
+ if COL && activeEsc != "" { chunk.WriteString(activeEsc) }
+ cLen = 0
+ }
+ chunk.WriteRune(r)
+ cLen++
+ i += size
+ }
+ if chunk.Len() > 0 { curLine = chunk.String() }
+ continue
+ }
+ cLen := visibleLen(curLine)
+ if curLine == "" {
+ curLine = word
+ updateActiveEsc(word)
+ } else if cLen+1+wLen <= width {
+ curLine += " " + word
+ updateActiveEsc(word)
+ } else {
+ flushLine()
+ if COL && activeEsc != "" {
+ curLine = activeEsc + word
+ } else {
+ curLine = word
+ }
+ updateActiveEsc(word)
+ }
+ }
+ flushLine()
+ }
+ if len(lines) == 0 { lines = []string{""} }
+ return lines
+}
+
func renderTable(lines []string) []string {
if len(lines) == 0 { return nil }
var rows [][]string
@@ -334,6 +445,29 @@ func renderTable(lines []string) []string {
if colWidths[i] < 3 { colWidths[i] = 3 }
}
+ maxTableWidth := termWidth()
+ if maxTableWidth < 20 { maxTableWidth = 80 }
+ overhead := 3*numCols + 1
+ availContent := maxTableWidth - overhead
+ if availContent < numCols*3 { availContent = numCols * 3 }
+
+ tot := 0
+ for _, w := range colWidths { tot += w }
+
+ for tot > availContent {
+ maxIdx := 0
+ maxVal := colWidths[0]
+ for i := 1; i < numCols; i++ {
+ if colWidths[i] > maxVal {
+ maxVal = colWidths[i]
+ maxIdx = i
+ }
+ }
+ if maxVal <= 3 { break }
+ colWidths[maxIdx]--
+ tot--
+ }
+
var res []string
var topParts []string
@@ -341,13 +475,25 @@ func renderTable(lines []string) []string {
res = append(res, c("┌"+strings.Join(topParts, "┬")+"┐", 2))
if hasHeader {
- var hCells []string
+ headerCols := make([][]string, numCols)
+ maxHeaderLines := 1
for i, h := range headerRow {
- rh := c(h, 1, 36)
- pad := strings.Repeat(" ", colWidths[i]-visibleLen(h))
- hCells = append(hCells, " "+rh+pad+" ")
+ wrapped := wrapCell(h, colWidths[i])
+ if len(wrapped) > maxHeaderLines { maxHeaderLines = len(wrapped) }
+ headerCols[i] = wrapped
+ }
+
+ for lineIdx := 0; lineIdx < maxHeaderLines; lineIdx++ {
+ var hCells []string
+ for i := 0; i < numCols; i++ {
+ txt := ""
+ if lineIdx < len(headerCols[i]) { txt = headerCols[i][lineIdx] }
+ rh := c(txt, 1, 36)
+ pad := strings.Repeat(" ", colWidths[i]-visibleLen(txt))
+ hCells = append(hCells, " "+rh+pad+" ")
+ }
+ res = append(res, c("│", 2)+strings.Join(hCells, c("│", 2))+c("│", 2))
}
- res = append(res, c("│", 2)+strings.Join(hCells, c("│", 2))+c("│", 2))
var midParts []string
for _, w := range colWidths { midParts = append(midParts, strings.Repeat("─", w+2)) }
@@ -355,13 +501,25 @@ func renderTable(lines []string) []string {
}
for _, r := range rows {
- var rCells []string
+ rowCols := make([][]string, numCols)
+ maxRowLines := 1
for i, cell := range r {
rc := renderInline(cell)
- pad := strings.Repeat(" ", colWidths[i]-visibleLen(rc))
- rCells = append(rCells, " "+rc+pad+" ")
+ wrapped := wrapCell(rc, colWidths[i])
+ if len(wrapped) > maxRowLines { maxRowLines = len(wrapped) }
+ rowCols[i] = wrapped
+ }
+
+ for lineIdx := 0; lineIdx < maxRowLines; lineIdx++ {
+ var rCells []string
+ for i := 0; i < numCols; i++ {
+ txt := ""
+ if lineIdx < len(rowCols[i]) { txt = rowCols[i][lineIdx] }
+ pad := strings.Repeat(" ", colWidths[i]-visibleLen(txt))
+ rCells = append(rCells, " "+txt+pad+" ")
+ }
+ res = append(res, c("│", 2)+strings.Join(rCells, c("│", 2))+c("│", 2))
}
- res = append(res, c("│", 2)+strings.Join(rCells, c("│", 2))+c("│", 2))
}
var botParts []string
diff --git a/main_test.go b/main_test.go
index 572da42..aa54813 100644
--- a/main_test.go
+++ b/main_test.go
@@ -1519,6 +1519,67 @@ func TestRenderTableFormatting(t *testing.T) {
}
}
+func TestWrapCell(t *testing.T) {
+ COL = true
+ defer func() { COL = false }()
+
+ lines := wrapCell("Short text", 20)
+ if len(lines) != 1 || lines[0] != "Short text" {
+ t.Errorf("wrapCell short = %v", lines)
+ }
+
+ lines = wrapCell("The quick brown fox jumps over the lazy dog", 15)
+ if len(lines) < 3 {
+ t.Errorf("wrapCell long = %v", lines)
+ }
+ for _, l := range lines {
+ if visibleLen(l) > 15 {
+ t.Errorf("line %q visible length = %d > 15", l, visibleLen(l))
+ }
+ }
+
+ // With ANSI escape codes
+ styled := "\033[1;36mAlice In Wonderland\033[0m"
+ lines = wrapCell(styled, 10)
+ if len(lines) != 2 {
+ t.Errorf("wrapCell styled count = %d, lines = %v", len(lines), lines)
+ }
+ for _, l := range lines {
+ if visibleLen(l) > 10 {
+ t.Errorf("styled line %q length = %d > 10", l, visibleLen(l))
+ }
+ }
+}
+
+func TestRenderTableWidthConstraint(t *testing.T) {
+ COL = true
+ defer func() { COL = false }()
+
+ // Create an extra-wide table with long text
+ raw := strings.Join([]string{
+ "| Long Column Header One | Extremely Long Column Header Two That Would Definitely Overflow | Another Very Wide Column Header Three |",
+ "|---|---|---|",
+ "| Some detailed explanation that is very long and has lots of words in it | Another paragraph of text that continues on and on without stopping | Final column with even more long descriptions |",
+ }, "\n")
+
+ rendered := renderMD(raw)
+ lines := strings.Split(rendered, "\n")
+ maxW := termWidth()
+ if maxW < 20 { maxW = 80 }
+
+ for i, ln := range lines {
+ vl := visibleLen(ln)
+ if vl > maxW {
+ t.Errorf("table line %d visible length = %d, exceeds max width %d:\n%s", i, vl, maxW, ln)
+ }
+ }
+
+ // Ensure content was wrapped rather than dropped
+ if !strings.Contains(rendered, "detailed") || !strings.Contains(rendered, "explanation") || !strings.Contains(rendered, "paragraph") {
+ t.Errorf("rendered table should contain all words wrapped across lines:\n%s", rendered)
+ }
+}
+
func TestRenderMDNoColorFallback(t *testing.T) {
COL = false
raw := "# Heading\n**bold** and `code`\n- list item"