added markdown support for go port
This commit is contained in:
@@ -16,6 +16,7 @@ import (
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
@@ -154,6 +155,126 @@ func col(cfg Cfg) bool {
|
||||
return isTerminal(int(os.Stdout.Fd()))
|
||||
}
|
||||
|
||||
type mdState struct {
|
||||
inCode bool
|
||||
lang string
|
||||
}
|
||||
|
||||
var (
|
||||
reCode = regexp.MustCompile("`([^`]+)`")
|
||||
reLink = regexp.MustCompile(`\[([^\]]+)\]\(([^)]+)\)`)
|
||||
reBI1 = regexp.MustCompile(`\*\*\*(.*?)\*\*\*`)
|
||||
reBI2 = regexp.MustCompile(`___(.*?)___`)
|
||||
reB1 = regexp.MustCompile(`\*\*(.*?)\*\*`)
|
||||
reB2 = regexp.MustCompile(`__(.*?)__`)
|
||||
reI1 = regexp.MustCompile(`\*(.*?)\*`)
|
||||
reI2 = regexp.MustCompile(`_(.*?)_`)
|
||||
reS = regexp.MustCompile(`~~(.*?)~~`)
|
||||
reOrd = regexp.MustCompile(`^(\d+\.)\s+(.*)`)
|
||||
)
|
||||
|
||||
func renderInline(s string) string {
|
||||
if !COL { return s }
|
||||
var codes []string
|
||||
s = reCode.ReplaceAllStringFunc(s, func(m string) string {
|
||||
codes = append(codes, c(m[1:len(m)-1], 33))
|
||||
return fmt.Sprintf("\x00CD%d\x00", len(codes)-1)
|
||||
})
|
||||
s = reLink.ReplaceAllStringFunc(s, func(m string) string {
|
||||
sm := reLink.FindStringSubmatch(m)
|
||||
if len(sm) == 3 { return c(sm[1], 4, 36) + " " + c("("+sm[2]+")", 2) }
|
||||
return m
|
||||
})
|
||||
s = reBI1.ReplaceAllStringFunc(s, func(m string) string { return c(m[3:len(m)-3], 1, 3) })
|
||||
s = reBI2.ReplaceAllStringFunc(s, func(m string) string { return c(m[3:len(m)-3], 1, 3) })
|
||||
s = reB1.ReplaceAllStringFunc(s, func(m string) string { return c(m[2:len(m)-2], 1) })
|
||||
s = reB2.ReplaceAllStringFunc(s, func(m string) string { return c(m[2:len(m)-2], 1) })
|
||||
s = reI1.ReplaceAllStringFunc(s, func(m string) string { return c(m[1:len(m)-1], 3) })
|
||||
s = reI2.ReplaceAllStringFunc(s, func(m string) string { return c(m[1:len(m)-1], 3) })
|
||||
s = reS.ReplaceAllStringFunc(s, func(m string) string { return c(m[2:len(m)-2], 9) })
|
||||
for i, code := range codes {
|
||||
s = strings.ReplaceAll(s, fmt.Sprintf("\x00CD%d\x00", i), code)
|
||||
}
|
||||
return s
|
||||
}
|
||||
|
||||
func renderMDLine(line string, st *mdState) string {
|
||||
if !COL { return line }
|
||||
trimmed := strings.TrimSpace(line)
|
||||
if strings.HasPrefix(trimmed, "```") {
|
||||
if !st.inCode {
|
||||
st.inCode = true
|
||||
st.lang = strings.TrimSpace(strings.TrimPrefix(trimmed, "```"))
|
||||
title := ""
|
||||
if st.lang != "" { title = " [ " + st.lang + " ]" }
|
||||
return c("───"+title+"──────────────────────────────────────────", 2)
|
||||
}
|
||||
st.inCode = false
|
||||
st.lang = ""
|
||||
return c("───────────────────────────────────────────────────", 2)
|
||||
}
|
||||
if st.inCode {
|
||||
return c(" ", 2) + c(line, 32)
|
||||
}
|
||||
if trimmed == "---" || trimmed == "***" || trimmed == "___" || trimmed == "----" || trimmed == "------" {
|
||||
return c("───────────────────────────────────────────────────", 2)
|
||||
}
|
||||
if strings.HasPrefix(trimmed, "#") {
|
||||
lvl := 0
|
||||
for lvl < len(trimmed) && trimmed[lvl] == '#' { lvl++ }
|
||||
if lvl < len(trimmed) && trimmed[lvl] == ' ' {
|
||||
htext := strings.TrimSpace(trimmed[lvl:])
|
||||
switch lvl {
|
||||
case 1: return c("■ ", 35) + c(htext, 1, 37)
|
||||
case 2: return c("▲ ", 34) + c(htext, 1, 36)
|
||||
case 3: return c("● ", 32) + c(htext, 1, 32)
|
||||
case 4: return c("◆ ", 33) + c(htext, 1, 33)
|
||||
default: return c(htext, 1)
|
||||
}
|
||||
}
|
||||
}
|
||||
if strings.HasPrefix(trimmed, ">") {
|
||||
qtext := strings.TrimSpace(strings.TrimPrefix(trimmed, ">"))
|
||||
return c("▎ ", 34) + c(renderInline(qtext), 3)
|
||||
}
|
||||
if strings.HasPrefix(trimmed, "- [ ] ") || strings.HasPrefix(trimmed, "* [ ] ") {
|
||||
return strings.Repeat(" ", len(line)-len(strings.TrimLeft(line, " "))) + c("☐ ", 33) + renderInline(trimmed[6:])
|
||||
}
|
||||
if strings.HasPrefix(trimmed, "- [x] ") || strings.HasPrefix(trimmed, "* [x] ") || strings.HasPrefix(trimmed, "- [X] ") || strings.HasPrefix(trimmed, "* [X] ") {
|
||||
return strings.Repeat(" ", len(line)-len(strings.TrimLeft(line, " "))) + c("☑ ", 32) + renderInline(trimmed[6:])
|
||||
}
|
||||
if strings.HasPrefix(trimmed, "- ") || strings.HasPrefix(trimmed, "* ") || strings.HasPrefix(trimmed, "+ ") {
|
||||
indent := strings.Repeat(" ", len(line)-len(strings.TrimLeft(line, " ")))
|
||||
return indent + c("• ", 36) + renderInline(trimmed[2:])
|
||||
}
|
||||
if m := reOrd.FindStringSubmatch(trimmed); len(m) == 3 {
|
||||
indent := strings.Repeat(" ", len(line)-len(strings.TrimLeft(line, " ")))
|
||||
return indent + c(m[1]+" ", 33) + renderInline(m[2])
|
||||
}
|
||||
if strings.HasPrefix(trimmed, "|") && strings.HasSuffix(trimmed, "|") {
|
||||
isSep := true
|
||||
for _, ch := range strings.ReplaceAll(strings.ReplaceAll(trimmed, "|", ""), ":", "") {
|
||||
if ch != '-' && ch != ' ' { isSep = false; break }
|
||||
}
|
||||
if isSep { return c(trimmed, 2) }
|
||||
cells := strings.Split(trimmed[1:len(trimmed)-1], "|")
|
||||
var rcells []string
|
||||
for _, cell := range cells { rcells = append(rcells, " "+renderInline(strings.TrimSpace(cell))+" ") }
|
||||
return c("│", 2) + strings.Join(rcells, c("│", 2)) + c("│", 2)
|
||||
}
|
||||
return renderInline(line)
|
||||
}
|
||||
|
||||
func renderMD(text string) string {
|
||||
lines := strings.Split(text, "\n")
|
||||
var out []string
|
||||
var st mdState
|
||||
for _, ln := range lines {
|
||||
out = append(out, renderMDLine(ln, &st))
|
||||
}
|
||||
return strings.Join(out, "\n")
|
||||
}
|
||||
|
||||
type Message struct {
|
||||
Role string `json:"role"`
|
||||
Content *string `json:"content"`
|
||||
@@ -297,6 +418,8 @@ func llm(cfg *Cfg, msgs []Message, tools []map[string]any) (Message, error) {
|
||||
func parseStream(r io.Reader) (Message, error) {
|
||||
var content, reas string
|
||||
var rh, ch bool
|
||||
var lineBuf string
|
||||
var mdSt mdState
|
||||
tcs := map[int]*ToolCall{}
|
||||
var order []int
|
||||
sc := bufio.NewScanner(r)
|
||||
@@ -319,8 +442,15 @@ func parseStream(r io.Reader) (Message, error) {
|
||||
if dl.Content != "" {
|
||||
if rh && !ch { fmt.Print("\n" + c("--- reasoning end ---", 36) + "\n\n") }
|
||||
ch = true
|
||||
fmt.Print(dl.Content)
|
||||
content += dl.Content
|
||||
lineBuf += dl.Content
|
||||
for {
|
||||
idx := strings.IndexByte(lineBuf, '\n')
|
||||
if idx == -1 { break }
|
||||
curLine := lineBuf[:idx]
|
||||
lineBuf = lineBuf[idx+1:]
|
||||
fmt.Println(renderMDLine(curLine, &mdSt))
|
||||
}
|
||||
}
|
||||
for _, tc := range dl.ToolCalls {
|
||||
t, ok := tcs[tc.Index]
|
||||
@@ -334,10 +464,11 @@ func parseStream(r io.Reader) (Message, error) {
|
||||
if tc.Function.Arguments != "" { t.Function.Arguments += tc.Function.Arguments }
|
||||
}
|
||||
}
|
||||
if lineBuf != "" {
|
||||
fmt.Println(renderMDLine(lineBuf, &mdSt))
|
||||
}
|
||||
if rh && !ch {
|
||||
fmt.Print("\n" + c("--- reasoning end ---", 36) + "\n")
|
||||
} else if ch {
|
||||
fmt.Print("\n")
|
||||
}
|
||||
m := Message{Role: "assistant"}
|
||||
if content != "" { m.Content = strp(content) }
|
||||
@@ -410,7 +541,7 @@ func AL(cfg *Cfg, msgs []Message, sp string, depth int) ([]Message, error) {
|
||||
if m.ReasoningContent != "" {
|
||||
fmt.Println(c("--- reasoning start ---", 36) + "\n" + c(m.ReasoningContent, 2) + "\n" + c("--- reasoning end ---", 36))
|
||||
}
|
||||
if m.Content != nil { fmt.Println(*m.Content) }
|
||||
if m.Content != nil { fmt.Println(renderMD(*m.Content)) }
|
||||
}
|
||||
if len(m.ToolCalls) == 0 { done = true; break }
|
||||
for _, tc := range m.ToolCalls {
|
||||
|
||||
+119
@@ -1373,3 +1373,122 @@ func TestLLMForwardsRelevantParameters(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
// ---------- Markdown rendering tests ----------
|
||||
|
||||
func TestRenderInline(t *testing.T) {
|
||||
COL = true
|
||||
defer func() { COL = false }()
|
||||
|
||||
// Code
|
||||
out := renderInline("Use `go test -v` command")
|
||||
if !strings.Contains(out, "\033[33mgo test -v\033[0m") {
|
||||
t.Errorf("renderInline code = %q", out)
|
||||
}
|
||||
|
||||
// Bold
|
||||
out = renderInline("This is **bold** text")
|
||||
if !strings.Contains(out, "\033[1mbold\033[0m") {
|
||||
t.Errorf("renderInline bold = %q", out)
|
||||
}
|
||||
|
||||
// Italic
|
||||
out = renderInline("This is *italic* text")
|
||||
if !strings.Contains(out, "\033[3mitalic\033[0m") {
|
||||
t.Errorf("renderInline italic = %q", out)
|
||||
}
|
||||
|
||||
// Bold + Italic
|
||||
out = renderInline("This is ***important*** text")
|
||||
if !strings.Contains(out, "\033[1;3mimportant\033[0m") {
|
||||
t.Errorf("renderInline bold+italic = %q", out)
|
||||
}
|
||||
|
||||
// Strikethrough
|
||||
out = renderInline("This is ~~deleted~~ text")
|
||||
if !strings.Contains(out, "\033[9mdeleted\033[0m") {
|
||||
t.Errorf("renderInline strikethrough = %q", out)
|
||||
}
|
||||
|
||||
// Link
|
||||
out = renderInline("Visit [Go](https://go.dev) site")
|
||||
if !strings.Contains(out, "\033[4;36mGo\033[0m") || !strings.Contains(out, "https://go.dev") {
|
||||
t.Errorf("renderInline link = %q", out)
|
||||
}
|
||||
|
||||
// Code shielding (asterisks inside code should not become italic)
|
||||
out = renderInline("Run `foo * bar` now")
|
||||
if !strings.Contains(out, "\033[33mfoo * bar\033[0m") {
|
||||
t.Errorf("renderInline code shield = %q", out)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRenderMDBlocks(t *testing.T) {
|
||||
COL = true
|
||||
defer func() { COL = false }()
|
||||
|
||||
// Headings
|
||||
h1 := renderMD("# Title One")
|
||||
if !strings.Contains(h1, "Title One") || !strings.Contains(h1, "\033[35m■ \033[0m") {
|
||||
t.Errorf("renderMD H1 = %q", h1)
|
||||
}
|
||||
h2 := renderMD("## Subtitle")
|
||||
if !strings.Contains(h2, "Subtitle") || !strings.Contains(h2, "\033[34m▲ \033[0m") {
|
||||
t.Errorf("renderMD H2 = %q", h2)
|
||||
}
|
||||
h3 := renderMD("### Section")
|
||||
if !strings.Contains(h3, "Section") || !strings.Contains(h3, "\033[32m● \033[0m") {
|
||||
t.Errorf("renderMD H3 = %q", h3)
|
||||
}
|
||||
|
||||
// Code block
|
||||
codeMD := "```go\nfunc main() {\n println(1)\n}\n```"
|
||||
renderedCode := renderMD(codeMD)
|
||||
if !strings.Contains(renderedCode, "[ go ]") || !strings.Contains(renderedCode, "println(1)") {
|
||||
t.Errorf("renderMD code block = %q", renderedCode)
|
||||
}
|
||||
|
||||
// Lists
|
||||
ul := renderMD("- Item A\n- Item B")
|
||||
if !strings.Contains(ul, "• ") || !strings.Contains(ul, "Item A") {
|
||||
t.Errorf("renderMD unordered list = %q", ul)
|
||||
}
|
||||
|
||||
ol := renderMD("1. Step 1\n2. Step 2")
|
||||
if !strings.Contains(ol, "1. ") || !strings.Contains(ol, "Step 1") {
|
||||
t.Errorf("renderMD ordered list = %q", ol)
|
||||
}
|
||||
|
||||
tasks := renderMD("- [ ] Pending\n- [x] Finished")
|
||||
if !strings.Contains(tasks, "☐ ") || !strings.Contains(tasks, "☑ ") {
|
||||
t.Errorf("renderMD task list = %q", tasks)
|
||||
}
|
||||
|
||||
// Blockquote
|
||||
bq := renderMD("> Important quote")
|
||||
if !strings.Contains(bq, "▎ ") || !strings.Contains(bq, "Important quote") {
|
||||
t.Errorf("renderMD blockquote = %q", bq)
|
||||
}
|
||||
|
||||
// Horizontal rule
|
||||
hr := renderMD("---")
|
||||
if !strings.Contains(hr, "────") {
|
||||
t.Errorf("renderMD hr = %q", hr)
|
||||
}
|
||||
|
||||
// Table
|
||||
tbl := renderMD("| Col A | Col B |\n|---|---|\n| Val 1 | Val 2 |")
|
||||
if !strings.Contains(tbl, "Col A") || !strings.Contains(tbl, "Val 1") {
|
||||
t.Errorf("renderMD table = %q", tbl)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRenderMDNoColorFallback(t *testing.T) {
|
||||
COL = false
|
||||
raw := "# Heading\n**bold** and `code`\n- list item"
|
||||
out := renderMD(raw)
|
||||
if out != raw {
|
||||
t.Errorf("renderMD with COL=false should return raw text, got %q", out)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user