package converter
import (
"strings"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
// === Markdown to Confluence XML tests ===
func TestMarkdownToConfluence_Heading(t *testing.T) {
result, err := MarkdownToConfluence([]byte("# Hello World"))
require.NoError(t, err)
assert.Contains(t, result, "<h1>Hello World</h1>")
}
func TestMarkdownToConfluence_AllHeadingLevels(t *testing.T) {
for i, tag := range []string{"h1", "h2", "h3", "h4", "h5", "h6"} {
prefix := strings.Repeat("#", i+1)
result, err := MarkdownToConfluence([]byte(prefix + " Heading"))
require.NoError(t, err)
assert.Contains(t, result, "<"+tag+">Heading</"+tag+">", "level %d", i+1)
}
}
func TestMarkdownToConfluence_Bold(t *testing.T) {
result, err := MarkdownToConfluence([]byte("**bold text**"))
require.NoError(t, err)
assert.Contains(t, result, "<strong>bold text</strong>")
}
func TestMarkdownToConfluence_Italic(t *testing.T) {
result, err := MarkdownToConfluence([]byte("*italic text*"))
require.NoError(t, err)
assert.Contains(t, result, "<em>italic text</em>")
}
func TestMarkdownToConfluence_Strikethrough(t *testing.T) {
result, err := MarkdownToConfluence([]byte("~~deleted~~"))
require.NoError(t, err)
assert.Contains(t, result, "<del>deleted</del>")
}
func TestMarkdownToConfluence_InlineCode(t *testing.T) {
result, err := MarkdownToConfluence([]byte("`code`"))
require.NoError(t, err)
assert.Contains(t, result, "<code>code</code>")
}
func TestMarkdownToConfluence_CodeBlock(t *testing.T) {
input := "```go\nfmt.Println(\"hello\")\n```"
result, err := MarkdownToConfluence([]byte(input))
require.NoError(t, err)
assert.Contains(t, result, `ac:name="code"`)
assert.Contains(t, result, `ac:name="language">go`)
assert.Contains(t, result, `<![CDATA[fmt.Println("hello")]]>`)
}
func TestMarkdownToConfluence_CodeBlockNoLanguage(t *testing.T) {
input := "```\nsome code\n```"
result, err := MarkdownToConfluence([]byte(input))
require.NoError(t, err)
assert.Contains(t, result, `ac:name="code"`)
assert.NotContains(t, result, `ac:name="language"`)
}
func TestMarkdownToConfluence_Table(t *testing.T) {
input := "| A | B |\n|---|---|\n| 1 | 2 |"
result, err := MarkdownToConfluence([]byte(input))
require.NoError(t, err)
assert.Contains(t, result, "<table>")
assert.Contains(t, result, "<th><p>A</p></th>")
assert.Contains(t, result, "<td><p>1</p></td>")
}
func TestMarkdownToConfluence_Blockquote(t *testing.T) {
result, err := MarkdownToConfluence([]byte("> Important note"))
require.NoError(t, err)
assert.Contains(t, result, `ac:name="info"`)
assert.Contains(t, result, "Important note")
}
func TestMarkdownToConfluence_Image(t *testing.T) {
result, err := MarkdownToConfluence([]byte(""))
require.NoError(t, err)
assert.Contains(t, result, "<ac:image")
assert.Contains(t, result, `ri:value="https://example.com/img.png"`)
assert.Contains(t, result, `ac:alt="alt text"`)
}
func TestMarkdownToConfluence_Link(t *testing.T) {
result, err := MarkdownToConfluence([]byte("[Click here](https://example.com)"))
require.NoError(t, err)
assert.Contains(t, result, `<a href="https://example.com">Click here</a>`)
}
func TestMarkdownToConfluence_UnorderedList(t *testing.T) {
input := "- One\n- Two\n- Three"
result, err := MarkdownToConfluence([]byte(input))
require.NoError(t, err)
assert.Contains(t, result, "<ul>")
assert.Contains(t, result, "<li>")
}
func TestMarkdownToConfluence_OrderedList(t *testing.T) {
input := "1. First\n2. Second\n3. Third"
result, err := MarkdownToConfluence([]byte(input))
require.NoError(t, err)
assert.Contains(t, result, "<ol>")
}
func TestMarkdownToConfluence_NestedList(t *testing.T) {
input := "- Parent\n - Child"
result, err := MarkdownToConfluence([]byte(input))
require.NoError(t, err)
assert.Equal(t, 2, strings.Count(result, "<ul>"), "expected 2 ul tags for nested list")
}
func TestMarkdownToConfluence_HorizontalRule(t *testing.T) {
result, err := MarkdownToConfluence([]byte("---"))
require.NoError(t, err)
assert.Contains(t, result, "<hr/>")
}
func TestMarkdownToConfluence_TaskList(t *testing.T) {
input := "- [x] Done\n- [ ] Todo"
result, err := MarkdownToConfluence([]byte(input))
require.NoError(t, err)
assert.Contains(t, result, "<ac:task-list>")
assert.Contains(t, result, "<ac:task>")
assert.Contains(t, result, "<ac:task-id>")
assert.Contains(t, result, "<ac:task-status>complete</ac:task-status>")
assert.Contains(t, result, "<ac:task-status>incomplete</ac:task-status>")
assert.Contains(t, result, "<ac:task-body>")
assert.Contains(t, result, "</ac:task-body>")
}
func TestMarkdownToConfluence_Paragraph(t *testing.T) {
result, err := MarkdownToConfluence([]byte("Hello world"))
require.NoError(t, err)
assert.Contains(t, result, "<p>Hello world</p>")
}
func TestMarkdownToConfluence_HardLineBreak(t *testing.T) {
result, err := MarkdownToConfluence([]byte("Line one \nLine two"))
require.NoError(t, err)
assert.Contains(t, result, "<br/>")
}
func TestMarkdownToConfluence_InlineCommentMarker(t *testing.T) {
input := `Before <span data-inline-comment="abc-123">commented text</span> after`
result, err := MarkdownToConfluence([]byte(input))
require.NoError(t, err)
assert.Contains(t, result, `<ac:inline-comment-marker ac:ref="abc-123">`)
assert.Contains(t, result, "</ac:inline-comment-marker>")
assert.Contains(t, result, "commented text")
}
// === Confluence XML to Markdown tests ===
func TestConfluenceToMarkdown_Heading(t *testing.T) {
result, err := ConfluenceToMarkdown("<h1>Hello World</h1>")
require.NoError(t, err)
assert.Contains(t, result, "# Hello World")
}
func TestConfluenceToMarkdown_Bold(t *testing.T) {
result, err := ConfluenceToMarkdown("<p><strong>bold</strong></p>")
require.NoError(t, err)
assert.Contains(t, result, "**bold**")
}
func TestConfluenceToMarkdown_Italic(t *testing.T) {
result, err := ConfluenceToMarkdown("<p><em>italic</em></p>")
require.NoError(t, err)
assert.Contains(t, result, "*italic*")
}
func TestConfluenceToMarkdown_Strikethrough(t *testing.T) {
result, err := ConfluenceToMarkdown("<p><del>deleted</del></p>")
require.NoError(t, err)
assert.Contains(t, result, "~~deleted~~")
}
func TestConfluenceToMarkdown_InlineCode(t *testing.T) {
result, err := ConfluenceToMarkdown("<p><code>code</code></p>")
require.NoError(t, err)
assert.Contains(t, result, "`code`")
}
func TestConfluenceToMarkdown_CodeBlock(t *testing.T) {
input := `<ac:structured-macro ac:name="code" ac:schema-version="1"><ac:parameter ac:name="language">python</ac:parameter><ac:plain-text-body><![CDATA[print("hello")]]></ac:plain-text-body></ac:structured-macro>`
result, err := ConfluenceToMarkdown(input)
require.NoError(t, err)
assert.Contains(t, result, "```python")
assert.Contains(t, result, `print("hello")`)
}
func TestConfluenceToMarkdown_Link(t *testing.T) {
result, err := ConfluenceToMarkdown(`<p><a href="https://example.com">Click</a></p>`)
require.NoError(t, err)
assert.Contains(t, result, "[Click](https://example.com)")
}
func TestConfluenceToMarkdown_UnorderedList(t *testing.T) {
result, err := ConfluenceToMarkdown("<ul>\n<li>One</li>\n<li>Two</li>\n</ul>")
require.NoError(t, err)
assert.Contains(t, result, "- One")
assert.Contains(t, result, "- Two")
}
func TestConfluenceToMarkdown_OrderedList(t *testing.T) {
result, err := ConfluenceToMarkdown("<ol>\n<li>First</li>\n<li>Second</li>\n</ol>")
require.NoError(t, err)
assert.Contains(t, result, "1. First")
assert.Contains(t, result, "2. Second")
}
func TestConfluenceToMarkdown_Table(t *testing.T) {
input := `<table><tbody><tr><th><p>Name</p></th><th><p>Age</p></th></tr><tr><td><p>Alice</p></td><td><p>30</p></td></tr></tbody></table>`
result, err := ConfluenceToMarkdown(input)
require.NoError(t, err)
assert.Contains(t, result, "| Name |")
assert.Contains(t, result, "| Alice |")
assert.Contains(t, result, "|---|---|")
}
func TestConfluenceToMarkdown_InfoPanel(t *testing.T) {
input := `<ac:structured-macro ac:name="info" ac:schema-version="1"><ac:rich-text-body><p>Important note</p></ac:rich-text-body></ac:structured-macro>`
result, err := ConfluenceToMarkdown(input)
require.NoError(t, err)
assert.Contains(t, result, "> Important note")
}
func TestConfluenceToMarkdown_HorizontalRule(t *testing.T) {
result, err := ConfluenceToMarkdown("<hr/>")
require.NoError(t, err)
assert.Contains(t, result, "---")
}
func TestConfluenceToMarkdown_Emoticons(t *testing.T) {
tests := []struct {
name string
input string
expected string
}{
{"plus", `<ac:emoticon ac:name="plus"/>`, "(+)"},
{"minus", `<ac:emoticon ac:name="minus"/>`, "(-)"},
{"question", `<ac:emoticon ac:name="question"/>`, "(?)"},
{"tick", `<ac:emoticon ac:name="tick"/>`, "(v)"},
{"cross", `<ac:emoticon ac:name="cross"/>`, "(x)"},
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
result, err := ConfluenceToMarkdown(tc.input)
require.NoError(t, err)
assert.Contains(t, result, tc.expected)
})
}
}
func TestConfluenceToMarkdown_InlineCommentMarker(t *testing.T) {
input := `<p>Before <ac:inline-comment-marker ac:ref="abc-123">commented text</ac:inline-comment-marker> after</p>`
result, err := ConfluenceToMarkdown(input)
require.NoError(t, err)
assert.Contains(t, result, `<span data-inline-comment="abc-123">`)
assert.Contains(t, result, "commented text")
assert.Contains(t, result, "</span>")
}
// === User reference tests ===
func TestConfluenceToMarkdown_UserReference(t *testing.T) {
input := `<p>Author: <ac:link><ri:user ri:userkey="3cddbcec40cb91700140cb9345ed0b5c"/></ac:link></p>`
result, err := ConfluenceToMarkdown(input)
require.NoError(t, err)
assert.Contains(t, result, `<span data-user-key="3cddbcec40cb91700140cb9345ed0b5c"/>`)
}
func TestMarkdownToConfluence_UserReference(t *testing.T) {
input := `Author: <span data-user-key="3cddbcec40cb91700140cb9345ed0b5c"/>`
result, err := MarkdownToConfluence([]byte(input))
require.NoError(t, err)
assert.Contains(t, result, `<ac:link><ri:user ri:userkey="3cddbcec40cb91700140cb9345ed0b5c"/></ac:link>`)
}
func TestRoundTrip_UserReference(t *testing.T) {
xmlInput := `<p>Author: <ac:link><ri:user ri:userkey="abc123"/></ac:link> wrote this</p>`
md, err := ConfluenceToMarkdown(xmlInput)
require.NoError(t, err)
require.Contains(t, md, `data-user-key="abc123"`)
xmlOutput, err := MarkdownToConfluence([]byte(md))
require.NoError(t, err)
assert.Contains(t, xmlOutput, `ri:userkey="abc123"`)
}
// === Attachment image tests ===
func TestConfluenceToMarkdown_AttachmentImage(t *testing.T) {
input := `<p><ac:image><ri:attachment ri:filename="screenshot.png"/></ac:image></p>`
result, err := ConfluenceToMarkdown(input)
require.NoError(t, err)
assert.Contains(t, result, `<span data-attachment="screenshot.png"/>`)
}
func TestConfluenceToMarkdown_AttachmentImageWithAlt(t *testing.T) {
input := `<p><ac:image ac:alt="My Screenshot"><ri:attachment ri:filename="screenshot.png"/></ac:image></p>`
result, err := ConfluenceToMarkdown(input)
require.NoError(t, err)
assert.Contains(t, result, `data-attachment="screenshot.png"`)
assert.Contains(t, result, `data-alt="My Screenshot"`)
}
func TestMarkdownToConfluence_AttachmentImage(t *testing.T) {
input := `Image: <span data-attachment="screenshot.png"/>`
result, err := MarkdownToConfluence([]byte(input))
require.NoError(t, err)
assert.Contains(t, result, `<ac:image><ri:attachment ri:filename="screenshot.png"/></ac:image>`)
}
func TestMarkdownToConfluence_AttachmentImageWithAlt(t *testing.T) {
input := `<span data-attachment="screenshot.png" data-alt="My Shot"/>`
result, err := MarkdownToConfluence([]byte(input))
require.NoError(t, err)
assert.Contains(t, result, `ac:alt="My Shot"`)
assert.Contains(t, result, `ri:filename="screenshot.png"`)
}
func TestRoundTrip_AttachmentImage(t *testing.T) {
xmlInput := `<p><ac:image><ri:attachment ri:filename="Pasted image 20260325004147.png"/></ac:image></p>`
md, err := ConfluenceToMarkdown(xmlInput)
require.NoError(t, err)
require.Contains(t, md, `data-attachment="Pasted image 20260325004147.png"`)
xmlOutput, err := MarkdownToConfluence([]byte(md))
require.NoError(t, err)
assert.Contains(t, xmlOutput, `ri:filename="Pasted image 20260325004147.png"`)
}
// === Inline spacing tests ===
func TestConfluenceToMarkdown_SpaceAfterBold(t *testing.T) {
input := `<ul><li><strong>ATE:</strong> Запуск плейбуков</li></ul>`
result, err := ConfluenceToMarkdown(input)
require.NoError(t, err)
assert.Contains(t, result, "**ATE:** Запуск плейбуков")
}
func TestConfluenceToMarkdown_SpaceBetweenInlineElements(t *testing.T) {
input := `<ul><li><strong>Bold</strong> then <em>italic</em> text</li></ul>`
result, err := ConfluenceToMarkdown(input)
require.NoError(t, err)
assert.Contains(t, result, "**Bold** then *italic* text")
}
// === Layout tests ===
func TestConfluenceToMarkdown_Layout(t *testing.T) {
input := `<ac:layout><ac:layout-section ac:type="single"><ac:layout-cell><p>Content</p></ac:layout-cell></ac:layout-section></ac:layout>`
result, err := ConfluenceToMarkdown(input)
require.NoError(t, err)
assert.Contains(t, result, "<!-- ac:layout -->")
assert.Contains(t, result, `<!-- ac:layout-section type="single" -->`)
assert.Contains(t, result, "<!-- ac:layout-cell -->")
assert.Contains(t, result, "Content")
assert.Contains(t, result, "<!-- /ac:layout-cell -->")
assert.Contains(t, result, "<!-- /ac:layout-section -->")
assert.Contains(t, result, "<!-- /ac:layout -->")
}
func TestConfluenceToMarkdown_LayoutTwoColumn(t *testing.T) {
input := `<ac:layout><ac:layout-section ac:type="two_equal"><ac:layout-cell><p>Left</p></ac:layout-cell><ac:layout-cell><p>Right</p></ac:layout-cell></ac:layout-section></ac:layout>`
result, err := ConfluenceToMarkdown(input)
require.NoError(t, err)
assert.Contains(t, result, `type="two_equal"`)
assert.Contains(t, result, "Left")
assert.Contains(t, result, "Right")
assert.Equal(t, 2, strings.Count(result, "<!-- ac:layout-cell -->"))
assert.Equal(t, 2, strings.Count(result, "<!-- /ac:layout-cell -->"))
}
func TestMarkdownToConfluence_Layout(t *testing.T) {
input := `<!-- ac:layout -->
<!-- ac:layout-section type="single" -->
<!-- ac:layout-cell -->
Hello world
<!-- /ac:layout-cell -->
<!-- /ac:layout-section -->
<!-- /ac:layout -->
`
result, err := MarkdownToConfluence([]byte(input))
require.NoError(t, err)
assert.Contains(t, result, "<ac:layout>")
assert.Contains(t, result, `<ac:layout-section ac:type="single">`)
assert.Contains(t, result, "<ac:layout-cell>")
assert.Contains(t, result, "Hello world")
assert.Contains(t, result, "</ac:layout-cell>")
assert.Contains(t, result, "</ac:layout-section>")
assert.Contains(t, result, "</ac:layout>")
}
func TestRoundTrip_Layout(t *testing.T) {
xmlInput := `<ac:layout><ac:layout-section ac:type="two_equal"><ac:layout-cell><p>Left column</p></ac:layout-cell><ac:layout-cell><p>Right column</p></ac:layout-cell></ac:layout-section></ac:layout>`
md, err := ConfluenceToMarkdown(xmlInput)
require.NoError(t, err)
xmlOutput, err := MarkdownToConfluence([]byte(md))
require.NoError(t, err)
assert.Contains(t, xmlOutput, "<ac:layout>")
assert.Contains(t, xmlOutput, `ac:type="two_equal"`)
assert.Contains(t, xmlOutput, "<ac:layout-cell>")
assert.Contains(t, xmlOutput, "Left column")
assert.Contains(t, xmlOutput, "Right column")
assert.Contains(t, xmlOutput, "</ac:layout-cell>")
assert.Contains(t, xmlOutput, "</ac:layout-section>")
assert.Contains(t, xmlOutput, "</ac:layout>")
}
// === Round-trip tests ===
func TestRoundTrip_Basic(t *testing.T) {
input := `# Test
Some **bold** and *italic* text.
## Code
` + "```go\nfunc main() {}\n```" + `
## List
- One
- Two
- Three
| H1 | H2 |
|----|-----|
| A | B |
`
xml, err := MarkdownToConfluence([]byte(input))
require.NoError(t, err)
md, err := ConfluenceToMarkdown(xml)
require.NoError(t, err)
for _, check := range []string{"# Test", "**bold**", "*italic*", "```go", "func main() {}", "- One", "| H1", "| A"} {
assert.Contains(t, md, check, "round-trip should preserve %q", check)
}
}
func TestRoundTrip_Blockquote(t *testing.T) {
input := "> This is a blockquote\n"
xml, err := MarkdownToConfluence([]byte(input))
require.NoError(t, err)
md, err := ConfluenceToMarkdown(xml)
require.NoError(t, err)
assert.Contains(t, md, "> This is a blockquote")
}
func TestRoundTrip_Link(t *testing.T) {
input := "[Example](https://example.com)\n"
xml, err := MarkdownToConfluence([]byte(input))
require.NoError(t, err)
md, err := ConfluenceToMarkdown(xml)
require.NoError(t, err)
assert.Contains(t, md, "[Example](https://example.com)")
}
func TestRoundTrip_InlineCommentMarker(t *testing.T) {
xmlInput := `<p>Hello <ac:inline-comment-marker ac:ref="b2f6ce98-4dc9-45e0-a9b6-b4a5109657ca">important text</ac:inline-comment-marker> world</p>`
md, err := ConfluenceToMarkdown(xmlInput)
require.NoError(t, err)
require.Contains(t, md, `data-inline-comment="b2f6ce98-4dc9-45e0-a9b6-b4a5109657ca"`)
xmlOutput, err := MarkdownToConfluence([]byte(md))
require.NoError(t, err)
assert.Contains(t, xmlOutput, `<ac:inline-comment-marker ac:ref="b2f6ce98-4dc9-45e0-a9b6-b4a5109657ca">`)
assert.Contains(t, xmlOutput, "</ac:inline-comment-marker>")
assert.Contains(t, xmlOutput, "important text")
}
func TestRoundTrip_InlineCommentFromRealXML(t *testing.T) {
xmlInput := `<p>Товарищ! <ac:inline-comment-marker ac:ref="b2f6ce98-4dc9-45e0-a9b6-b4a5109657ca">Не майся дурью, копируй этот шаблон и редактируй его!</ac:inline-comment-marker> Ускоришь написание RFC!</p>`
md, err := ConfluenceToMarkdown(xmlInput)
require.NoError(t, err)
xmlOutput, err := MarkdownToConfluence([]byte(md))
require.NoError(t, err)
assert.Contains(t, xmlOutput, `ac:ref="b2f6ce98-4dc9-45e0-a9b6-b4a5109657ca"`)
assert.Contains(t, xmlOutput, "Не майся дурью")
}