improved markdown table support for go port
This commit is contained in:
@@ -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, "<br>", "\n")
|
||||
s = strings.ReplaceAll(s, "<br/>", "\n")
|
||||
s = strings.ReplaceAll(s, "<br />", "\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))
|
||||
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))
|
||||
}
|
||||
|
||||
var midParts []string
|
||||
for _, w := range colWidths { midParts = append(midParts, strings.Repeat("─", w+2)) }
|
||||
@@ -355,14 +501,26 @@ 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))
|
||||
}
|
||||
}
|
||||
|
||||
var botParts []string
|
||||
for _, w := range colWidths { botParts = append(botParts, strings.Repeat("─", w+2)) }
|
||||
|
||||
@@ -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"
|
||||
|
||||
Reference in New Issue
Block a user