Files
YunShu/pkg/mdprint/parse.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

288 lines
5.6 KiB
Go

package mdprint
import (
"strings"
)
func parseBlocks(content string) []Node {
lines := strings.Split(content, "\n")
var blocks []Node
i := 0
for i < len(lines) {
line := lines[i]
trim := strings.TrimSpace(line)
if trim == "" {
i++
continue
}
if level := headingLevel(trim); level > 0 {
rest := strings.TrimSpace(trim[level:])
blocks = append(blocks, Heading{Level: level, Content: parseInline(rest)})
i++
continue
}
if trim == "---" || trim == "***" || trim == "___" {
blocks = append(blocks, ThematicBreak{})
i++
continue
}
if isFence(trim) {
b, lang, consumed := parseFence(lines, i)
blocks = append(blocks, CodeBlock{Lang: lang, Body: b})
i += consumed
continue
}
if strings.HasPrefix(trim, ">") {
q, consumed := parseQuote(lines, i)
blocks = append(blocks, q)
i += consumed
continue
}
if mark, ordered, ok := listMarker(trim); ok {
items, consumed := parseListItems(lines, i, mark)
blocks = append(blocks, List{Ordered: ordered, Items: items})
i += consumed
continue
}
if strings.HasPrefix(trim, "|") && i+1 < len(lines) && isTableSep(strings.TrimSpace(lines[i+1])) {
t, consumed := parseTable(lines, i)
blocks = append(blocks, t)
i += consumed
continue
}
p, consumed := parseParagraph(lines, i)
blocks = append(blocks, p)
i += consumed
}
return blocks
}
func headingLevel(s string) int {
n := 0
for _, c := range s {
if c == '#' {
n++
} else {
break
}
}
if n > 0 && n <= 6 && len(s) > n && s[n] == ' ' {
return n
}
return 0
}
func isFence(s string) bool {
count := 0
for _, c := range s {
if c == '`' || c == '~' {
count++
} else {
break
}
}
return count >= 3
}
func fenceChar(s string) rune {
for _, c := range s {
return c
}
return '`'
}
func parseFence(lines []string, start int) (body, lang string, consumed int) {
first := strings.TrimSpace(lines[start])
ch := fenceChar(first)
lang = strings.TrimSpace(first[3:])
var buf strings.Builder
consumed = 1
for i := start + 1; i < len(lines); i++ {
consumed++
if strings.TrimSpace(lines[i]) == strings.Repeat(string(ch), 3) {
break
}
if buf.Len() > 0 {
buf.WriteString("\n")
}
buf.WriteString(lines[i])
}
body = buf.String()
return
}
func parseQuote(lines []string, start int) (Blockquote, int) {
var contents []Node
var buf []string
flush := func() {
if len(buf) > 0 {
text := strings.Join(buf, "\n")
contents = append(contents, Paragraph{Content: parseInline(text)})
buf = nil
}
}
i := start
for i < len(lines) {
trim := strings.TrimSpace(lines[i])
if !strings.HasPrefix(trim, ">") {
break
}
rest := trim[1:]
if strings.HasPrefix(rest, " ") {
rest = rest[1:]
}
if rest == "" {
flush()
} else {
buf = append(buf, rest)
}
i++
}
flush()
return Blockquote{Children: contents}, i - start
}
func listMarker(s string) (mark string, ordered bool, ok bool) {
trim := strings.TrimSpace(s)
if len(trim) >= 2 && (trim[0] == '-' || trim[0] == '*') && trim[1] == ' ' {
return string(trim[0]), false, true
}
if len(trim) >= 3 && trim[0] >= '1' && trim[0] <= '9' && trim[1] == '.' && trim[2] == ' ' {
return ".", true, true
}
return "", false, false
}
func parseListItems(lines []string, start int, mark string) ([]ListItem, int) {
var items []ListItem
consumed := 0
for i := start; i < len(lines); i++ {
trim := strings.TrimSpace(lines[i])
m, _, ok := listMarker(trim)
if !ok || m != mark {
break
}
consumed++
rest := strings.TrimSpace(trim[len(mark)+1:])
var checked *bool
if strings.HasPrefix(rest, "[ ] ") {
f := false
checked = &f
rest = rest[4:]
} else if strings.HasPrefix(rest, "[x] ") {
t := true
checked = &t
rest = rest[4:]
} else if strings.HasPrefix(rest, "[X] ") {
t := true
checked = &t
rest = rest[4:]
}
items = append(items, ListItem{
Checked: checked,
Content: parseInline(rest),
})
}
return items, consumed
}
func isTableSep(s string) bool {
if !strings.HasPrefix(s, "|") || !strings.HasSuffix(s, "|") {
return false
}
s = s[1 : len(s)-1]
cells := strings.Split(s, "|")
if len(cells) == 0 {
return false
}
for _, c := range cells {
c = strings.TrimSpace(c)
if c == "" {
return false
}
for _, ch := range c {
if ch != '-' && ch != ':' {
return false
}
}
}
return true
}
func parseTable(lines []string, start int) (Table, int) {
headerLine := strings.TrimSpace(lines[start])
headers := splitTableRow(headerLine)
consumed := 2
var rows [][]string
for i := start + 2; i < len(lines); i++ {
trim := strings.TrimSpace(lines[i])
if !strings.HasPrefix(trim, "|") {
break
}
rows = append(rows, splitTableRow(trim))
consumed++
}
return Table{Headers: headers, Rows: rows}, consumed
}
func splitTableRow(s string) []string {
s = strings.TrimSpace(s)
if strings.HasPrefix(s, "|") {
s = s[1:]
}
if strings.HasSuffix(s, "|") {
s = s[:len(s)-1]
}
cells := strings.Split(s, "|")
result := make([]string, len(cells))
for i, c := range cells {
result[i] = strings.TrimSpace(c)
}
return result
}
func parseParagraph(lines []string, start int) (Paragraph, int) {
var contentLines []string
consumed := 0
for i := start; i < len(lines); i++ {
trim := strings.TrimSpace(lines[i])
if trim == "" {
break
}
if headingLevel(trim) > 0 {
break
}
if trim == "---" || trim == "***" || trim == "___" {
break
}
if isFence(trim) {
break
}
if strings.HasPrefix(trim, ">") {
break
}
if _, _, ok := listMarker(trim); ok {
break
}
contentLines = append(contentLines, lines[i])
consumed++
}
text := strings.Join(contentLines, "\n")
return Paragraph{Content: parseInline(text)}, consumed
}