added markdown support for go port

This commit is contained in:
Luxferre
2026-08-15 08:50:39 +03:00
parent c8d1836614
commit 99aa96ce0e
2 changed files with 254 additions and 4 deletions
+135 -4
View File
@@ -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 {