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*
This commit is contained in:
287
pkg/mdprint/parse.go
Normal file
287
pkg/mdprint/parse.go
Normal file
@@ -0,0 +1,287 @@
|
||||
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
|
||||
}
|
||||
Reference in New Issue
Block a user