- 模块名重命名 yunshu -> hub.gaomia.site/titor/YunShu - Go 版本升级 1.21 -> 1.25 - src/ 目录删除,所有文件移至根目录 - 新增 pkg/mdprint/: Markdown AST 解析+ANSI 渲染 - 新增 pkg/style/: 终端颜色样式(8色 ANSI + 24位真彩色) - 新增 pkg/termui/: 终端输入组件(交互式输入/密码/确认) - 更新文档:AGENTS.md、architecture.md、changelog.md、taolun.md - gitignore 通配符修复 yunshu.exe -> yunshu.exe*
205 lines
4.2 KiB
Go
205 lines
4.2 KiB
Go
package mdprint
|
|
|
|
import (
|
|
"fmt"
|
|
"strings"
|
|
|
|
"hub.gaomia.site/titor/YunShu/pkg/style"
|
|
)
|
|
|
|
func renderNode(node Node) string {
|
|
switch n := node.(type) {
|
|
case Heading:
|
|
return renderHeading(n)
|
|
case Paragraph:
|
|
return renderParagraph(n)
|
|
case CodeBlock:
|
|
return renderCodeBlock(n)
|
|
case Blockquote:
|
|
return renderBlockquote(n)
|
|
case List:
|
|
return renderList(n)
|
|
case Table:
|
|
return renderTable(n)
|
|
case ThematicBreak:
|
|
return "\n" + style.Dim.Render("────────────────────────────") + "\n"
|
|
case Text:
|
|
return style.New().Render(n.Text)
|
|
case Bold:
|
|
return renderBold(n)
|
|
case Italic:
|
|
return renderItalic(n)
|
|
case Code:
|
|
return renderInlineCode(n)
|
|
case Link:
|
|
return renderLink(n)
|
|
default:
|
|
return fmt.Sprintf("%v", n)
|
|
}
|
|
}
|
|
|
|
func renderInline(nodes []Node) string {
|
|
var b strings.Builder
|
|
for _, child := range nodes {
|
|
b.WriteString(renderNode(child))
|
|
}
|
|
return b.String()
|
|
}
|
|
|
|
var headingConfig = []struct {
|
|
symbol string
|
|
style *style.Style
|
|
}{
|
|
{"▪ ", style.New().Bold().FgHex("#6B8E9B")},
|
|
{"▪ ", style.New().Bold().FgHex("#89A894")},
|
|
{"▪ ", style.New().Bold().FgHex("#A6C0B5")},
|
|
{"▫ ", style.New().Bold().FgHex("#C3B1BD")},
|
|
{"▫ ", style.New().Bold().FgHex("#7B8E8A")},
|
|
{"", style.New().Bold().Dim()},
|
|
}
|
|
|
|
func renderHeading(h Heading) string {
|
|
cfg := headingConfig[0]
|
|
if h.Level-1 < len(headingConfig) {
|
|
cfg = headingConfig[h.Level-1]
|
|
}
|
|
suffix := ""
|
|
if h.Level == 1 {
|
|
suffix = "\n"
|
|
}
|
|
return "\n" + cfg.style.Render(cfg.symbol+renderInline(h.Content)) + suffix
|
|
}
|
|
|
|
func renderParagraph(p Paragraph) string {
|
|
return renderInline(p.Content)
|
|
}
|
|
|
|
func renderCodeBlock(c CodeBlock) string {
|
|
var b strings.Builder
|
|
if c.Lang != "" {
|
|
b.WriteString(style.Dim.Render(c.Lang + " ") + "\n")
|
|
}
|
|
for _, line := range strings.Split(c.Body, "\n") {
|
|
b.WriteString(" " + style.New().Fg(style.ColorYellow).Render(line) + "\n")
|
|
}
|
|
return strings.TrimRight(b.String(), "\n")
|
|
}
|
|
|
|
func renderBlockquote(q Blockquote) string {
|
|
var b strings.Builder
|
|
for i, child := range q.Children {
|
|
if i > 0 {
|
|
b.WriteString("\n")
|
|
}
|
|
rendered := renderNode(child)
|
|
for _, line := range strings.Split(rendered, "\n") {
|
|
b.WriteString(style.Dim.Render("│ ") + line + "\n")
|
|
}
|
|
}
|
|
return strings.TrimRight(b.String(), "\n")
|
|
}
|
|
|
|
func renderList(l List) string {
|
|
var b strings.Builder
|
|
for i, item := range l.Items {
|
|
if i > 0 {
|
|
b.WriteString("\n")
|
|
}
|
|
prefix := "- "
|
|
if l.Ordered {
|
|
prefix = fmt.Sprintf("%d. ", i+1)
|
|
}
|
|
if item.Checked != nil {
|
|
check := " "
|
|
if *item.Checked {
|
|
check = "x"
|
|
}
|
|
prefix = fmt.Sprintf("- [%s] ", check)
|
|
}
|
|
b.WriteString(style.Dim.Render(prefix) + renderInline(item.Content))
|
|
}
|
|
return b.String()
|
|
}
|
|
|
|
func renderTable(t Table) string {
|
|
if len(t.Headers) == 0 {
|
|
return ""
|
|
}
|
|
var b strings.Builder
|
|
|
|
colWidths := make([]int, len(t.Headers))
|
|
for i, h := range t.Headers {
|
|
colWidths[i] = len(h)
|
|
}
|
|
for _, row := range t.Rows {
|
|
for i, cell := range row {
|
|
if i < len(colWidths) && len(cell) > colWidths[i] {
|
|
colWidths[i] = len(cell)
|
|
}
|
|
}
|
|
}
|
|
|
|
renderRow := func(cells []string, isHeader bool) {
|
|
b.WriteString("| ")
|
|
for i, cell := range cells {
|
|
if i > 0 {
|
|
b.WriteString(" | ")
|
|
}
|
|
if i < len(colWidths) {
|
|
cell = fmt.Sprintf("%-*s", colWidths[i], cell)
|
|
}
|
|
if isHeader {
|
|
b.WriteString(style.Bold.Render(cell))
|
|
} else {
|
|
b.WriteString(cell)
|
|
}
|
|
}
|
|
b.WriteString(" |")
|
|
}
|
|
|
|
renderRow(t.Headers, true)
|
|
|
|
sep := "| "
|
|
for i, w := range colWidths {
|
|
if i > 0 {
|
|
sep += " | "
|
|
}
|
|
sep += strings.Repeat("-", w)
|
|
}
|
|
sep += " |"
|
|
b.WriteString("\n" + style.Dim.Render(sep))
|
|
|
|
for _, row := range t.Rows {
|
|
b.WriteString("\n")
|
|
renderRow(row, false)
|
|
}
|
|
|
|
return b.String()
|
|
}
|
|
|
|
func renderBold(b Bold) string {
|
|
return style.New().Bold().Render(renderInline(b.Content))
|
|
}
|
|
|
|
func renderItalic(i Italic) string {
|
|
return style.New().Italic().Render(renderInline(i.Content))
|
|
}
|
|
|
|
func renderInlineCode(c Code) string {
|
|
return style.New().Fg(style.ColorYellow).Render("`" + c.Text + "`")
|
|
}
|
|
|
|
func renderLink(l Link) string {
|
|
text := renderInline(l.Content)
|
|
url := l.URL
|
|
if text == "" {
|
|
text = url
|
|
url = ""
|
|
}
|
|
styled := style.New().Underline().Fg(style.ColorBlue).Render(text)
|
|
if url != "" {
|
|
styled += style.Dim.Render(" (" + url + ")")
|
|
}
|
|
return styled
|
|
}
|