package render import ( "regexp" "strings" "github.com/charmbracelet/lipgloss" ) // --------------------------------------------------------------------------- // Markdown (agent_message_renderer.py) // --------------------------------------------------------------------------- var blankLineRuns = regexp.MustCompile(`\n\s*\n`) type mdHeader struct { prefix string strip int style lipgloss.Style } var mdHeaders = []mdHeader{ {"###### ", 7, Bold(Field)}, {"##### ", 6, Bold(Green)}, {"#### ", 5, Bold(Hdr16a)}, {"### ", 4, Bold(Hdr158)}, {"## ", 3, Bold(Green)}, {"# ", 2, Bold(Field)}, } // renderAssistantMarkdown ports AgentMessageRenderer.render_simple + helpers. func renderAssistantMarkdown(content string) string { if content == "" { return "" } cleaned := strings.TrimSpace(blankLineRuns.ReplaceAllString(content, "\n\n")) if cleaned == "" { return "" } return applyMarkdownStyles(cleaned) } func applyMarkdownStyles(text string) string { var out strings.Builder lines := strings.Split(text, "\n") inCode := false codeLang := "" var codeLines []string flushCode := func() { if len(codeLines) > 0 { out.WriteString(HighlightCode(strings.Join(codeLines, "\n"), codeLang)) } codeLines = nil codeLang = "" } for i := 0; i < len(lines); i++ { line := lines[i] if i > 0 && !inCode { out.WriteString("\n") } if !inCode { if rows := tableRows(lines[i:]); rows > 0 { out.WriteString(renderMarkdownTable(lines[i : i+rows])) i += rows - 1 continue } } if strings.HasPrefix(line, "```") { if !inCode { inCode = true codeLines = nil codeLang = strings.TrimSpace(strings.TrimPrefix(line, "```")) if i > 0 { out.WriteString("\n") } } else { inCode = false flushCode() } continue } if inCode { codeLines = append(codeLines, line) continue } if h := tryHeader(line); h != nil { out.WriteString(h.style.Render(line[h.strip:])) continue } switch { case strings.HasPrefix(line, "> "): out.WriteString(Col(Green).Render("┃ ") + inlineFormat(line[2:])) case strings.HasPrefix(line, "- "), strings.HasPrefix(line, "* "): out.WriteString(Col(Green).Render("• ") + inlineFormat(line[2:])) case len(line) > 2 && line[0] >= '0' && line[0] <= '9' && (line[1:3] == ". " || line[1:3] == ") "): out.WriteString(Col(Green).Render(string(line[0])+". ") + inlineFormat(line[2:])) case line == "---" || line == "***" || line == "___": out.WriteString(Col(Green).Render(strings.Repeat("─", 40))) default: out.WriteString(inlineFormat(line)) } } if inCode && len(codeLines) > 0 { flushCode() } return out.String() } func isTableRow(line string) bool { trimmed := strings.TrimSpace(line) return strings.HasPrefix(trimmed, "|") && strings.Count(trimmed, "|") >= 2 } var tableSeparatorCell = regexp.MustCompile(`^:?-+:?$`) func isTableSeparator(line string) bool { if !isTableRow(line) { return false } cells := splitTableRow(line) if len(cells) == 0 { return false } for _, cell := range cells { if !tableSeparatorCell.MatchString(strings.TrimSpace(cell)) { return false } } return true } // tableRows returns how many leading lines form a markdown table (header, // separator, then body rows), or 0 when the block is not a table. func tableRows(lines []string) int { if len(lines) < 2 || !isTableRow(lines[0]) || !isTableSeparator(lines[1]) { return 0 } rows := 2 for rows < len(lines) && isTableRow(lines[rows]) && !isTableSeparator(lines[rows]) { rows++ } return rows } func splitTableRow(line string) []string { trimmed := strings.TrimSpace(line) trimmed = strings.TrimPrefix(trimmed, "|") trimmed = strings.TrimSuffix(trimmed, "|") cells := strings.Split(trimmed, "|") for i := range cells { cells[i] = strings.TrimSpace(cells[i]) } return cells } // renderMarkdownTable draws a column-aligned table: bold header, a rule under // it, and inline-formatted body cells. func renderMarkdownTable(lines []string) string { headerStyle := func(cell string) string { return Bold(Field).Render(cell) } rows := make([][]string, 0, len(lines)-1) styleCells := func(line string, style func(string) string) []string { cells := splitTableRow(line) for i := range cells { cells[i] = style(cells[i]) } return cells } rows = append(rows, styleCells(lines[0], headerStyle)) for _, line := range lines[2:] { rows = append(rows, styleCells(line, inlineFormat)) } widths := make([]int, len(rows[0])) for _, cells := range rows { for i, cell := range cells { if i < len(widths) { widths[i] = max(widths[i], lipgloss.Width(cell)) } } } formatRow := func(cells []string) string { parts := make([]string, len(widths)) for i := range widths { cell := "" if i < len(cells) { cell = cells[i] } parts[i] = cell + strings.Repeat(" ", max(0, widths[i]-lipgloss.Width(cell))) } return strings.TrimRight(strings.Join(parts, Dim().Render(" │ ")), " ") } out := []string{formatRow(rows[0])} rule := make([]string, len(widths)) for i, width := range widths { rule[i] = strings.Repeat("─", width) } out = append(out, Dim().Render(strings.Join(rule, "─┼─"))) for _, cells := range rows[1:] { out = append(out, formatRow(cells)) } return strings.Join(out, "\n") } func tryHeader(line string) *mdHeader { for i := range mdHeaders { if strings.HasPrefix(line, mdHeaders[i].prefix) { return &mdHeaders[i] } } return nil } func isWordByte(b byte) bool { return b == '_' || b >= '0' && b <= '9' || b >= 'a' && b <= 'z' || b >= 'A' && b <= 'Z' } // canOpenEmphasis reports whether an emphasis run starting at i (with the // given marker width) follows CommonMark-style flanking rules: it must not // sit inside a word and must be followed by a non-space. func canOpenEmphasis(line string, i, width int) bool { if i > 0 && isWordByte(line[i-1]) { return false } // Underscores appear inside identifiers far more often than as emphasis, // so they only open at a word boundary. if i > 0 && line[i] == '_' && line[i-1] != ' ' && line[i-1] != '\t' { return false } after := i + width return after < len(line) && line[after] != ' ' && line[after] != '\t' } // canCloseEmphasis reports whether an emphasis run ending at end (marker // starts at end) is preceded by a non-space and not followed by a word. func canCloseEmphasis(line string, end, width int) bool { if end > 0 && (line[end-1] == ' ' || line[end-1] == '\t') { return false } after := end + width return after >= len(line) || !isWordByte(line[after]) } // findEmphasisEnd locates the closing marker for an emphasis span opened at // i, honoring the flanking rules; returns -1 when the span should be treated // as literal text. func findEmphasisEnd(line string, i int, marker string) int { from := i + len(marker) for { end := strings.Index(line[from:], marker) if end == -1 { return -1 } end += from if end == i+len(marker) { return -1 } if canCloseEmphasis(line, end, len(marker)) { return end } from = end + 1 } } // inlineFormat ports _process_inline_formatting. func inlineFormat(line string) string { var out strings.Builder i, n := 0, len(line) for i < n { if i+1 < n && (line[i:i+2] == "**" || line[i:i+2] == "__") { marker := line[i : i+2] if canOpenEmphasis(line, i, 2) { if end := findEmphasisEnd(line, i, marker); end != -1 { out.WriteString(Bold(Field).Render(line[i+2 : end])) i = end + 2 continue } } } if i+1 < n && line[i:i+2] == "~~" { if canOpenEmphasis(line, i, 2) { if end := findEmphasisEnd(line, i, "~~"); end != -1 { out.WriteString(lipgloss.NewStyle().Strikethrough(true).Foreground(Strike).Render(line[i+2 : end])) i = end + 2 continue } } } if line[i] == '`' { if end := strings.Index(line[i+1:], "`"); end != -1 { end += i + 1 out.WriteString(lipgloss.NewStyle().Bold(true).Foreground(Green).Background(CodeBg).Render(line[i+1 : end])) i = end + 1 continue } } if line[i] == '*' || line[i] == '_' { marker := string(line[i]) if i+1 < n && line[i+1] != line[i] && canOpenEmphasis(line, i, 1) { if end := findEmphasisEnd(line, i, marker); end != -1 && (end+1 >= n || line[end+1] != line[i]) { out.WriteString(lipgloss.NewStyle().Italic(true).Foreground(Mint).Render(line[i+1 : end])) i = end + 1 continue } } } out.WriteByte(line[i]) i++ } return out.String() }