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

309 lines
7.3 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package mdprint
import (
"strings"
"testing"
)
func stripANSI(s string) string {
var b strings.Builder
i := 0
for i < len(s) {
if s[i] == '\033' && i+1 < len(s) && s[i+1] == '[' {
j := i + 2
for j < len(s) && s[j] != 'm' {
j++
}
if j < len(s) {
i = j + 1
continue
}
}
b.WriteByte(s[i])
i++
}
return b.String()
}
func TestParseInline_Text(t *testing.T) {
nodes := parseInline("hello world")
if len(nodes) != 1 {
t.Fatalf("expected 1 node, got %d", len(nodes))
}
_, ok := nodes[0].(Text)
if !ok {
t.Fatalf("expected Text, got %T", nodes[0])
}
}
func TestParseInline_Bold(t *testing.T) {
nodes := parseInline("**bold**")
if len(nodes) != 1 {
t.Fatalf("expected 1 node, got %d", len(nodes))
}
b, ok := nodes[0].(Bold)
if !ok {
t.Fatalf("expected Bold, got %T", nodes[0])
}
if len(b.Content) != 1 {
t.Fatalf("expected 1 child, got %d", len(b.Content))
}
txt, ok := b.Content[0].(Text)
if !ok {
t.Fatalf("expected Text in Bold, got %T", b.Content[0])
}
if txt.Text != "bold" {
t.Fatalf("expected 'bold', got '%s'", txt.Text)
}
}
func TestParseInline_Italic(t *testing.T) {
nodes := parseInline("*italic*")
if len(nodes) != 1 {
t.Fatalf("expected 1 node, got %d", len(nodes))
}
_, ok := nodes[0].(Italic)
if !ok {
t.Fatalf("expected Italic, got %T", nodes[0])
}
}
func TestParseInline_Code(t *testing.T) {
nodes := parseInline("`code`")
if len(nodes) != 1 {
t.Fatalf("expected 1 node, got %d", len(nodes))
}
c, ok := nodes[0].(Code)
if !ok {
t.Fatalf("expected Code, got %T", nodes[0])
}
if c.Text != "code" {
t.Fatalf("expected 'code', got '%s'", c.Text)
}
}
func TestParseInline_Link(t *testing.T) {
nodes := parseInline("[text](url)")
if len(nodes) != 1 {
t.Fatalf("expected 1 node, got %d", len(nodes))
}
link, ok := nodes[0].(Link)
if !ok {
t.Fatalf("expected Link, got %T", nodes[0])
}
if link.URL != "url" {
t.Fatalf("expected URL 'url', got '%s'", link.URL)
}
if len(link.Content) != 1 {
t.Fatalf("expected 1 child in link, got %d", len(link.Content))
}
txt, ok := link.Content[0].(Text)
if !ok {
t.Fatalf("expected Text in Link, got %T", link.Content[0])
}
if txt.Text != "text" {
t.Fatalf("expected 'text', got '%s'", txt.Text)
}
}
func TestParseInline_Mixed(t *testing.T) {
nodes := parseInline("a **b** c")
if len(nodes) != 3 {
t.Fatalf("expected 3 nodes, got %d: %#v", len(nodes), nodes)
}
t1, ok := nodes[0].(Text)
if !ok || t1.Text != "a " {
t.Fatalf("expected 'a ' Text, got %#v", nodes[0])
}
_, ok = nodes[1].(Bold)
if !ok {
t.Fatalf("expected Bold at index 1, got %T", nodes[1])
}
t2, ok := nodes[2].(Text)
if !ok || t2.Text != " c" {
t.Fatalf("expected ' c' Text, got %#v", nodes[2])
}
}
func TestParseBlocks_Heading(t *testing.T) {
blocks := parseBlocks("# Title")
if len(blocks) != 1 {
t.Fatalf("expected 1 block, got %d", len(blocks))
}
h, ok := blocks[0].(Heading)
if !ok {
t.Fatalf("expected Heading, got %T", blocks[0])
}
if h.Level != 1 {
t.Fatalf("expected level 1, got %d", h.Level)
}
if stripANSI(renderNode(h)) != "\n▪ Title\n" {
t.Fatalf("got '%s'", stripANSI(renderNode(h)))
}
}
func TestParseBlocks_CodeFence(t *testing.T) {
blocks := parseBlocks("```go\npackage main\n```")
if len(blocks) != 1 {
t.Fatalf("expected 1 block, got %d", len(blocks))
}
cb, ok := blocks[0].(CodeBlock)
if !ok {
t.Fatalf("expected CodeBlock, got %T", blocks[0])
}
if cb.Lang != "go" {
t.Fatalf("expected lang 'go', got '%s'", cb.Lang)
}
body := stripANSI(renderNode(cb))
if !strings.Contains(body, "package main") {
t.Fatalf("expected 'package main' in body")
}
}
func TestParseBlocks_Blockquote(t *testing.T) {
blocks := parseBlocks("> hello\n> world")
if len(blocks) != 1 {
t.Fatalf("expected 1 block, got %d", len(blocks))
}
_, ok := blocks[0].(Blockquote)
if !ok {
t.Fatalf("expected Blockquote, got %T", blocks[0])
}
}
func TestParseBlocks_UnorderedList(t *testing.T) {
blocks := parseBlocks("- a\n- b\n- c")
if len(blocks) != 1 {
t.Fatalf("expected 1 block, got %d", len(blocks))
}
l, ok := blocks[0].(List)
if !ok {
t.Fatalf("expected List, got %T", blocks[0])
}
if len(l.Items) != 3 {
t.Fatalf("expected 3 items, got %d", len(l.Items))
}
}
func TestParseBlocks_OrderedList(t *testing.T) {
blocks := parseBlocks("1. a\n2. b")
if len(blocks) != 1 {
t.Fatalf("expected 1 block, got %d", len(blocks))
}
l, ok := blocks[0].(List)
if !ok {
t.Fatalf("expected List, got %T", blocks[0])
}
if !l.Ordered {
t.Fatalf("expected ordered list")
}
}
func TestParseBlocks_Table(t *testing.T) {
blocks := parseBlocks("| H1 | H2 |\n|---|----|\n| A | B |")
if len(blocks) != 1 {
t.Fatalf("expected 1 block, got %d", len(blocks))
}
tbl, ok := blocks[0].(Table)
if !ok {
t.Fatalf("expected Table, got %T", blocks[0])
}
if len(tbl.Headers) != 2 {
t.Fatalf("expected 2 headers, got %d", len(tbl.Headers))
}
if len(tbl.Rows) != 1 {
t.Fatalf("expected 1 row, got %d", len(tbl.Rows))
}
}
func TestParseBlocks_ThematicBreak(t *testing.T) {
blocks := parseBlocks("---")
if len(blocks) != 1 {
t.Fatalf("expected 1 block, got %d", len(blocks))
}
_, ok := blocks[0].(ThematicBreak)
if !ok {
t.Fatalf("expected ThematicBreak, got %T", blocks[0])
}
}
func TestParseBlocks_Paragraph(t *testing.T) {
blocks := parseBlocks("hello\nworld")
if len(blocks) != 1 {
t.Fatalf("expected 1 block, got %d", len(blocks))
}
_, ok := blocks[0].(Paragraph)
if !ok {
t.Fatalf("expected Paragraph, got %T", blocks[0])
}
}
func TestParseBlocks_MultiBlock(t *testing.T) {
content := "# Title\n\nhello **world**\n\n> quote"
blocks := parseBlocks(content)
if len(blocks) != 3 {
t.Fatalf("expected 3 blocks, got %d", len(blocks))
}
}
func TestRender_HeadingLevels(t *testing.T) {
for level := 1; level <= 6; level++ {
prefix := strings.Repeat("#", level)
blocks := parseBlocks(prefix + " test")
if len(blocks) != 1 {
t.Fatalf("heading level %d: expected 1 block", level)
}
h, ok := blocks[0].(Heading)
if !ok {
t.Fatalf("heading level %d: expected Heading", level)
}
if h.Level != level {
t.Fatalf("heading level %d: expected level=%d", level, level)
}
result := stripANSI(renderNode(h))
var expected string
switch level {
case 1:
expected = "\n▪ test\n"
case 2, 3:
expected = "\n▪ test"
case 4, 5:
expected = "\n▫ test"
case 6:
expected = "\ntest"
}
if result != expected {
t.Fatalf("heading level %d: got '%q', expected '%q'", level, result, expected)
}
}
}
func TestRender_BoldItalic(t *testing.T) {
nodes := parseInline("**bold** and *italic*")
r := stripANSI(renderInline(nodes))
if r != "bold and italic" {
t.Fatalf("got '%s'", r)
}
}
func TestRender_Link(t *testing.T) {
nodes := parseInline("[click](https://example.com)")
r := stripANSI(renderInline(nodes))
if !strings.Contains(r, "click") || !strings.Contains(r, "https://example.com") {
t.Fatalf("got '%s'", r)
}
}
func TestRender_FullDocument(t *testing.T) {
content := "# 天气报告\n\n今天**晴**,温度 *适中*。\n\n> 温馨提示:出门带伞\n\n- 温度25°C\n- 湿度40%\n\n代码`fmt.Println(\"hello\")`\n\n| 项目 | 值 |\n|------|----|\n| AQI | 42 |\n"
blocks := parseBlocks(content)
if len(blocks) == 0 {
t.Fatal("expected blocks")
}
_, ok := blocks[0].(Heading)
if !ok {
t.Fatal("first block should be Heading")
}
}