package telegram
import (
"strings"
"testing"
)
// --- markdownToTelegramHTML: core formatting ---
func TestMarkdownToTelegramHTML_Empty(t *testing.T) {
got := markdownToTelegramHTML("")
if got != "" {
t.Errorf("markdownToTelegramHTML(\"\") = %q, want empty", got)
}
}
func TestMarkdownToTelegramHTML_Bold(t *testing.T) {
tests := []struct {
name string
input string
want string // substring that must appear
}{
{"double-star bold", "**bold text**", "bold text"},
{"double-underscore bold", "__bold text__", "bold text"},
{"inline bold", "normal **bold** normal", "bold"},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got := markdownToTelegramHTML(tt.input)
if !strings.Contains(got, tt.want) {
t.Errorf("markdownToTelegramHTML(%q) = %q, want substring %q", tt.input, got, tt.want)
}
})
}
}
func TestMarkdownToTelegramHTML_Italic(t *testing.T) {
got := markdownToTelegramHTML("_italic_")
if !strings.Contains(got, "italic") {
t.Errorf("markdownToTelegramHTML(_italic_) = %q, want italic", got)
}
}
func TestMarkdownToTelegramHTML_Strikethrough(t *testing.T) {
got := markdownToTelegramHTML("~~strike~~")
if !strings.Contains(got, "strike") {
t.Errorf("markdownToTelegramHTML(~~strike~~) = %q, want strike", got)
}
}
func TestMarkdownToTelegramHTML_InlineCode(t *testing.T) {
got := markdownToTelegramHTML("`myvar`")
if !strings.Contains(got, "myvar") {
t.Errorf("markdownToTelegramHTML(`myvar`) = %q, want myvar", got)
}
}
func TestMarkdownToTelegramHTML_CodeBlock(t *testing.T) {
got := markdownToTelegramHTML("```\ncode here\n```")
if !strings.Contains(got, "
") {
t.Errorf("code block should produce , got: %q", got)
}
if !strings.Contains(got, "code here") {
t.Errorf("code block content missing in: %q", got)
}
}
func TestMarkdownToTelegramHTML_CodeBlockWithLanguage(t *testing.T) {
got := markdownToTelegramHTML("```python\nprint('hi')\n```")
if !strings.Contains(got, "") {
t.Errorf("code block with language should produce , got: %q", got)
}
if !strings.Contains(got, "print") {
t.Errorf("code content missing, got: %q", got)
}
}
func TestMarkdownToTelegramHTML_Headers(t *testing.T) {
// Headers should be stripped (no native header in Telegram HTML).
tests := []struct {
input string
deny string // must NOT contain raw markdown header
want string // should contain the text
}{
{"# Header One", "#", "Header One"},
{"## Sub Header", "##", "Sub Header"},
{"### H3", "###", "H3"},
}
for _, tt := range tests {
t.Run(tt.input, func(t *testing.T) {
got := markdownToTelegramHTML(tt.input)
if strings.Contains(got, tt.deny) {
t.Errorf("header marker %q should be stripped, got: %q", tt.deny, got)
}
if !strings.Contains(got, tt.want) {
t.Errorf("header text %q missing in: %q", tt.want, got)
}
})
}
}
func TestMarkdownToTelegramHTML_Blockquote(t *testing.T) {
// Blockquotes should be stripped.
got := markdownToTelegramHTML("> quoted text")
if strings.Contains(got, ">") {
t.Errorf("blockquote > should be stripped, got: %q", got)
}
if !strings.Contains(got, "quoted text") {
t.Errorf("blockquote text should remain, got: %q", got)
}
}
func TestMarkdownToTelegramHTML_ListItems(t *testing.T) {
tests := []struct {
input string
want string
}{
{"- item one", "• item one"},
{"* item two", "• item two"},
}
for _, tt := range tests {
t.Run(tt.input, func(t *testing.T) {
got := markdownToTelegramHTML(tt.input)
if !strings.Contains(got, tt.want) {
t.Errorf("markdownToTelegramHTML(%q) = %q, want substring %q", tt.input, got, tt.want)
}
})
}
}
func TestMarkdownToTelegramHTML_Link(t *testing.T) {
got := markdownToTelegramHTML("[click here](https://example.com)")
if !strings.Contains(got, `href="https://example.com"`) {
t.Errorf("link should produce href, got: %q", got)
}
if !strings.Contains(got, "click here") {
t.Errorf("link text missing in: %q", got)
}
}
func TestMarkdownToTelegramHTML_HTMLEscaping(t *testing.T) {
// Plain text with special chars should be escaped.
got := markdownToTelegramHTML("a & b < c > d")
if !strings.Contains(got, "&") {
t.Errorf("& should be escaped, got: %q", got)
}
if !strings.Contains(got, "<") {
t.Errorf("< should be escaped, got: %q", got)
}
if !strings.Contains(got, ">") {
t.Errorf("> should be escaped, got: %q", got)
}
}
func TestMarkdownToTelegramHTML_HTMLTagsConvertedFirst(t *testing.T) {
// LLM-emitted HTML tags should be converted to markdown first, then re-rendered as Telegram HTML.
// → **bold** → bold; → _italic_ → italic, etc.
t.Run("html bold tag round-trips", func(t *testing.T) {
got := markdownToTelegramHTML("bold")
if !strings.Contains(got, "bold") {
t.Errorf("bold should remain bold in output, got: %q", got)
}
})
t.Run("html italic tag round-trips", func(t *testing.T) {
// italic → _italic_ → italic through the full pipeline.
got := markdownToTelegramHTML("italic")
if !strings.Contains(got, "italic") {
t.Errorf("italic should remain italic in output, got: %q", got)
}
})
t.Run("html em tag round-trips", func(t *testing.T) {
got := markdownToTelegramHTML("emphasis")
if !strings.Contains(got, "emphasis") {
t.Errorf("emphasis should become emphasis, got: %q", got)
}
})
t.Run("html strike tag round-trips", func(t *testing.T) {
got := markdownToTelegramHTML("struck")
if !strings.Contains(got, "struck") {
t.Errorf("struck should remain as strike in output, got: %q", got)
}
})
t.Run("html code tag round-trips", func(t *testing.T) {
got := markdownToTelegramHTML("var")
if !strings.Contains(got, "var") {
t.Errorf("var should produce code element, got: %q", got)
}
})
t.Run("html br produces newline", func(t *testing.T) {
got := markdownToTelegramHTML("line1
line2")
if !strings.Contains(got, "line1") || !strings.Contains(got, "line2") {
t.Errorf("both lines should appear after
conversion, got: %q", got)
}
})
}
// --- extractMarkdownTables ---
func TestExtractMarkdownTables_Simple(t *testing.T) {
input := "| Col1 | Col2 |\n|------|------|\n| A | B |"
tm := extractMarkdownTables(input)
if len(tm.rendered) != 1 {
t.Fatalf("expected 1 table, got %d", len(tm.rendered))
}
if !strings.Contains(tm.text, "\x00TB0\x00") {
t.Errorf("expected TB0 placeholder in text, got: %q", tm.text)
}
}
func TestExtractMarkdownTables_NoTable(t *testing.T) {
input := "just plain text\nno table here"
tm := extractMarkdownTables(input)
if len(tm.rendered) != 0 {
t.Errorf("expected 0 tables, got %d", len(tm.rendered))
}
if tm.text != input {
t.Errorf("text without table should be unchanged, got: %q", tm.text)
}
}
func TestExtractMarkdownTables_MultipleTables(t *testing.T) {
input := "| A | B |\n|---|---|\n| 1 | 2 |\n\nText\n\n| C | D |\n|---|---|\n| 3 | 4 |"
tm := extractMarkdownTables(input)
if len(tm.rendered) != 2 {
t.Errorf("expected 2 tables, got %d", len(tm.rendered))
}
}
func TestExtractMarkdownTables_TableInTelegramHTML(t *testing.T) {
// End-to-end: table in final HTML should be inside not .
input := "| Name | Value |\n|------|-------|\n| foo | bar |"
got := markdownToTelegramHTML(input)
if !strings.Contains(got, "") {
t.Errorf("table should be wrapped in , got: %q", got)
}
if strings.Contains(got, "") {
t.Errorf("table should NOT use , got: %q", got)
}
}
// --- renderTableAsCode ---
func TestRenderTableAsCode_Basic(t *testing.T) {
lines := []string{
"| Name | Score |",
"|------|-------|",
"| Alice | 100 |",
"| Bob | 85 |",
}
result := renderTableAsCode(lines)
resultLines := strings.Split(result, "\n")
if len(resultLines) < 4 { // header + sep + 2 data rows
t.Fatalf("expected at least 4 output lines, got %d: %q", len(resultLines), result)
}
// All rows should start and end with |.
for i, line := range resultLines {
if !strings.HasPrefix(line, "|") || !strings.HasSuffix(line, "|") {
t.Errorf("line %d should start and end with |, got: %q", i, line)
}
}
// All rows should have equal display width.
headerWidth := displayWidth(resultLines[0])
for i := 1; i < len(resultLines); i++ {
if displayWidth(resultLines[i]) != headerWidth {
t.Errorf("row %d width %d != header width %d\nrow: %q", i, displayWidth(resultLines[i]), headerWidth, resultLines[i])
}
}
}
func TestRenderTableAsCode_TooFewLines(t *testing.T) {
// Less than 2 lines → return as-is.
lines := []string{"| Only header |"}
result := renderTableAsCode(lines)
if result != "| Only header |" {
t.Errorf("single-line table should return as-is, got: %q", result)
}
}
// --- parseTableRow ---
func TestParseTableRow(t *testing.T) {
tests := []struct {
input string
want []string
}{
{"| A | B | C |", []string{"A", "B", "C"}},
{"| **Bold** | _italic_ |", []string{"Bold", "italic"}},
{"| `code` | normal |", []string{"code", "normal"}},
{"| ~~strike~~ | text |", []string{"strike", "text"}},
}
for _, tt := range tests {
t.Run(tt.input, func(t *testing.T) {
got := parseTableRow(tt.input)
if len(got) != len(tt.want) {
t.Fatalf("parseTableRow(%q) = %v, want %v", tt.input, got, tt.want)
}
for i := range got {
if got[i] != tt.want[i] {
t.Errorf("cell[%d] = %q, want %q", i, got[i], tt.want[i])
}
}
})
}
}
// --- stripInlineMarkdown ---
func TestStripInlineMarkdown(t *testing.T) {
tests := []struct {
input string
want string
}{
{"**bold**", "bold"},
{"__bold__", "bold"},
{"_italic_", "italic"},
{"*italic*", "italic"},
{"~~strike~~", "strike"},
{"`code`", "code"},
{"plain text", "plain text"},
{"**bold** and _italic_", "bold and italic"},
}
for _, tt := range tests {
t.Run(tt.input, func(t *testing.T) {
got := stripInlineMarkdown(tt.input)
if got != tt.want {
t.Errorf("stripInlineMarkdown(%q) = %q, want %q", tt.input, got, tt.want)
}
})
}
}
// --- escapeHTML ---
func TestEscapeHTML(t *testing.T) {
tests := []struct {
input string
want string
}{
{"a & b", "a & b"},
{"a < b", "a < b"},
{"a > b", "a > b"},
{"a & b < c > d", "a & b < c > d"},
{"no special chars", "no special chars"},
{"", ""},
}
for _, tt := range tests {
t.Run(tt.input, func(t *testing.T) {
got := escapeHTML(tt.input)
if got != tt.want {
t.Errorf("escapeHTML(%q) = %q, want %q", tt.input, got, tt.want)
}
})
}
}
// --- chunkHTML: additional cases ---
func TestChunkHTML_ExactFit(t *testing.T) {
text := "hello"
got := chunkHTML(text, 5)
if len(got) != 1 || got[0] != "hello" {
t.Errorf("chunkHTML exact fit = %v, want [hello]", got)
}
}
func TestChunkHTML_EmptyString(t *testing.T) {
got := chunkHTML("", 100)
if len(got) != 1 || got[0] != "" {
t.Errorf("chunkHTML(\"\") = %v, want [\"\"]", got)
}
}
func TestChunkHTML_PrefersParagraphBreak(t *testing.T) {
// "para1\n\npara2" — should split at \n\n if maxLen allows.
input := "para1\n\npara2\n\npara3"
got := chunkHTML(input, 10)
// Each chunk should not exceed 10 chars.
for _, chunk := range got {
if len(chunk) > 10 {
t.Errorf("chunk %q exceeds maxLen 10", chunk)
}
}
}
func TestChunkHTML_PreservesAllContent(t *testing.T) {
input := "The quick brown fox jumps over the lazy dog"
got := chunkHTML(input, 10)
// Rejoin and verify content (allowing boundary trimming of spaces).
joined := strings.Join(got, " ")
for _, word := range []string{"quick", "brown", "fox", "jumps", "lazy", "dog"} {
if !strings.Contains(joined, word) {
t.Errorf("word %q lost in chunking: %v", word, got)
}
}
}
// --- chunkPlainText: delegates to chunkHTML ---
func TestChunkPlainText_Delegates(t *testing.T) {
// chunkPlainText is an alias for chunkHTML.
input := "line one\nline two\nline three"
got := chunkPlainText(input, 15)
want := chunkHTML(input, 15)
if len(got) != len(want) {
t.Errorf("chunkPlainText != chunkHTML result")
}
}
// --- htmlTagToMarkdown ---
func TestHTMLTagToMarkdown(t *testing.T) {
tests := []struct {
input string
wantSubs []string // substrings that must appear in output
}{
{"
", []string{"\n"}},
{"
", []string{"\n"}},
{"bold", []string{"**bold**"}},
{"bold", []string{"**bold**"}},
{"italic", []string{"_italic_"}},
{"italic", []string{"_italic_"}},
{"struck", []string{"~~struck~~"}},
{"deleted", []string{"~~deleted~~"}},
{"var", []string{"`var`"}},
{`link`, []string{"[link](https://example.com)"}},
}
for _, tt := range tests {
t.Run(tt.input, func(t *testing.T) {
got := htmlTagToMarkdown(tt.input)
for _, sub := range tt.wantSubs {
if !strings.Contains(got, sub) {
t.Errorf("htmlTagToMarkdown(%q) = %q, missing %q", tt.input, got, sub)
}
}
})
}
}