Files
YunShu/pkg/mdprint/render.go
titor d2b9b2c4bb refactor: 项目结构重组,src/ 扁平化为根目录,提取 pkg/ 子包
- 模块名重命名 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*
2026-05-09 03:55:56 +08:00

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
}