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") } }