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, "

Hello World

") } 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", "level %d", i+1) } } func TestMarkdownToConfluence_Bold(t *testing.T) { result, err := MarkdownToConfluence([]byte("**bold text**")) require.NoError(t, err) assert.Contains(t, result, "bold text") } func TestMarkdownToConfluence_Italic(t *testing.T) { result, err := MarkdownToConfluence([]byte("*italic text*")) require.NoError(t, err) assert.Contains(t, result, "italic text") } func TestMarkdownToConfluence_Strikethrough(t *testing.T) { result, err := MarkdownToConfluence([]byte("~~deleted~~")) require.NoError(t, err) assert.Contains(t, result, "deleted") } func TestMarkdownToConfluence_InlineCode(t *testing.T) { result, err := MarkdownToConfluence([]byte("`code`")) require.NoError(t, err) assert.Contains(t, result, "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, ``) } 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, "") assert.Contains(t, result, "") assert.Contains(t, result, "") } 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("![alt text](https://example.com/img.png)")) require.NoError(t, err) assert.Contains(t, result, "Click here`) } 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, "

A

1

Name

Age

Alice

30

` 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 := `

Important note

` result, err := ConfluenceToMarkdown(input) require.NoError(t, err) assert.Contains(t, result, "> Important note") } func TestConfluenceToMarkdown_HorizontalRule(t *testing.T) { result, err := ConfluenceToMarkdown("
") require.NoError(t, err) assert.Contains(t, result, "---") } func TestConfluenceToMarkdown_Emoticons(t *testing.T) { tests := []struct { name string input string expected string }{ {"plus", ``, "(+)"}, {"minus", ``, "(-)"}, {"question", ``, "(?)"}, {"tick", ``, "(v)"}, {"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 := `

Before commented text after

` result, err := ConfluenceToMarkdown(input) require.NoError(t, err) assert.Contains(t, result, ``) assert.Contains(t, result, "commented text") assert.Contains(t, result, "") } // === User reference tests === func TestConfluenceToMarkdown_UserReference(t *testing.T) { input := `

Author:

` result, err := ConfluenceToMarkdown(input) require.NoError(t, err) assert.Contains(t, result, ``) } func TestMarkdownToConfluence_UserReference(t *testing.T) { input := `Author: ` result, err := MarkdownToConfluence([]byte(input)) require.NoError(t, err) assert.Contains(t, result, ``) } func TestRoundTrip_UserReference(t *testing.T) { xmlInput := `

Author: wrote this

` 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 := `

` result, err := ConfluenceToMarkdown(input) require.NoError(t, err) assert.Contains(t, result, ``) } func TestConfluenceToMarkdown_AttachmentImageWithAlt(t *testing.T) { input := `

` 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: ` result, err := MarkdownToConfluence([]byte(input)) require.NoError(t, err) assert.Contains(t, result, ``) } func TestMarkdownToConfluence_AttachmentImageWithAlt(t *testing.T) { input := `` 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 := `

` 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 := `
  • ATE: Запуск плейбуков
` result, err := ConfluenceToMarkdown(input) require.NoError(t, err) assert.Contains(t, result, "**ATE:** Запуск плейбуков") } func TestConfluenceToMarkdown_SpaceBetweenInlineElements(t *testing.T) { input := `
  • Bold then italic text
` 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 := `

Content

` result, err := ConfluenceToMarkdown(input) require.NoError(t, err) assert.Contains(t, result, "") assert.Contains(t, result, ``) assert.Contains(t, result, "") assert.Contains(t, result, "Content") assert.Contains(t, result, "") assert.Contains(t, result, "") assert.Contains(t, result, "") } func TestConfluenceToMarkdown_LayoutTwoColumn(t *testing.T) { input := `

Left

Right

` 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, "")) assert.Equal(t, 2, strings.Count(result, "")) } func TestMarkdownToConfluence_Layout(t *testing.T) { input := ` Hello world ` result, err := MarkdownToConfluence([]byte(input)) require.NoError(t, err) assert.Contains(t, result, "") assert.Contains(t, result, ``) assert.Contains(t, result, "") assert.Contains(t, result, "Hello world") assert.Contains(t, result, "") assert.Contains(t, result, "") assert.Contains(t, result, "") } func TestRoundTrip_Layout(t *testing.T) { xmlInput := `

Left column

Right column

` md, err := ConfluenceToMarkdown(xmlInput) require.NoError(t, err) xmlOutput, err := MarkdownToConfluence([]byte(md)) require.NoError(t, err) assert.Contains(t, xmlOutput, "") assert.Contains(t, xmlOutput, `ac:type="two_equal"`) assert.Contains(t, xmlOutput, "") assert.Contains(t, xmlOutput, "Left column") assert.Contains(t, xmlOutput, "Right column") assert.Contains(t, xmlOutput, "") assert.Contains(t, xmlOutput, "") assert.Contains(t, xmlOutput, "") } // === 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 := `

Hello important text world

` 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, ``) assert.Contains(t, xmlOutput, "") assert.Contains(t, xmlOutput, "important text") } func TestConfluenceToMarkdown_AdjacentCodeMerge(t *testing.T) { tests := []struct { name string input string expected string }{ { name: "directly adjacent code elements merged", input: `

helloworld

`, expected: "`helloworld`", }, { name: "adjacent code with span inside", input: `

plan : vclock

`, expected: "`plan : vclock`", }, { name: "single code element unchanged", input: `

single

`, expected: "`single`", }, { name: "non-adjacent code elements stay separate", input: `

a text b

`, expected: "`a` text `b`", }, { // Regression: whitespace-only text between two elements // previously caused the merge logic to swallow the gap and // concatenate the spans. name: "whitespace-separated code elements stay separate", input: `

Благодаря previous_backup_id tt restore plan видит разрыв.

`, expected: "`previous_backup_id` `tt restore plan`", }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { result, err := ConfluenceToMarkdown(tt.input) require.NoError(t, err) assert.Contains(t, result, tt.expected) }) } } func TestRoundTrip_ListNoTrailingWhitespace(t *testing.T) { xmlInput := `
  • item 1
  • item 2
  • item 3
` md, err := ConfluenceToMarkdown(xmlInput) require.NoError(t, err) xmlOutput, err := MarkdownToConfluence([]byte(md)) require.NoError(t, err) assert.Contains(t, xmlOutput, "
  • item 1
  • ") assert.Contains(t, xmlOutput, "
  • item 2
  • ") assert.Contains(t, xmlOutput, "
  • item 3
  • ") assert.NotContains(t, xmlOutput, "item 1\n") assert.NotContains(t, xmlOutput, "item 1 ") } func TestRoundTrip_InlineCommentFromRealXML(t *testing.T) { xmlInput := `

    Товарищ! Не майся дурью, копируй этот шаблон и редактируй его! Ускоришь написание RFC!

    ` 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, "Не майся дурью") } // TestRoundTrip_TablePipeInCode locks in the fix for table cells whose // contents include a literal `|`. Without escaping, the pipe was treated as a // column separator at re-parse time, splitting a single cell across columns // and dropping content. Cells must escape `|` as `\|` per the GFM spec. func TestRoundTrip_TablePipeInCode(t *testing.T) { xmlInput := `` + `` + `` + `` + `` + `

    Команда

    Где

    Действие

    mode --target=<incremental|full> --storage=<config>

    менеджер

    Возвращает mode + from_vclock.

    ` md, err := ConfluenceToMarkdown(xmlInput) require.NoError(t, err) // The pipe inside the code span must be escaped in the markdown so the // table parser does not treat it as a column boundary. assert.Contains(t, md, `\|`) xmlOutput, err := MarkdownToConfluence([]byte(md)) require.NoError(t, err) // All three columns must survive the round-trip. assert.Contains(t, xmlOutput, `mode --target=<incremental|full> --storage=<config>`) assert.Contains(t, xmlOutput, "менеджер") assert.Contains(t, xmlOutput, "from_vclock") } // TestEscapeTablePipes_NoDoubleEscape guards the helper against doubling up // already-escaped pipes (e.g. when a cell's rendered markdown was produced by // an upstream that already escaped some pipes itself). func TestEscapeTablePipes_NoDoubleEscape(t *testing.T) { cases := map[string]string{ "": "", "plain": "plain", "a|b": `a\|b`, `already\|done`: `already\|done`, "`a|b`": "`a\\|b`", } for in, want := range cases { got := escapeTablePipes(in) assert.Equal(t, want, got, "input=%q", in) } } // TestRoundTrip_PageLink locks in the fix for . // These page references have no plain markdown equivalent and previously got // dropped on the way to MD, leaving an empty paragraph after a round-trip. func TestRoundTrip_PageLink(t *testing.T) { xmlInput := `

    Основной документ:

    ` md, err := ConfluenceToMarkdown(xmlInput) require.NoError(t, err) require.Contains(t, md, `data-page-link`) require.Contains(t, md, `data-space-key="TSC"`) require.Contains(t, md, `data-content-title="Бекапы и Point-in-time recovery в кластере Tarantool"`) xmlOutput, err := MarkdownToConfluence([]byte(md)) require.NoError(t, err) assert.Contains(t, xmlOutput, ``) assert.Contains(t, xmlOutput, "Основной документ:") } // TestRoundTrip_NestedOrderedList guards against the regression where a // nested
      inside an outer
      1. was flattened into a single flat list // because the markdown was emitted with a 2-space indent (insufficient for // ordered lists, which need ≥3) and a blank line that turned the list loose. func TestRoundTrip_NestedOrderedList(t *testing.T) { xmlInput := `
          ` + `
        1. Outer one.
            ` + `
          1. Inner one.
          2. ` + `
          3. Inner two.
          4. ` + `
        2. ` + `
        3. Outer two.
        4. ` + `
        5. Outer three.
        6. ` + `
        ` md, err := ConfluenceToMarkdown(xmlInput) require.NoError(t, err) xmlOutput, err := MarkdownToConfluence([]byte(md)) require.NoError(t, err) // Nested
          must survive — a flattened version would have 5 sibling
        1. s. // Whitespace between closing tags is normalized away in this assertion. noWS := strings.Join(strings.Fields(xmlOutput), "") assert.Contains(t, noWS, "
        2. Outerone.
            ") assert.Contains(t, xmlOutput, "
          1. Inner one.
          2. ") assert.Contains(t, xmlOutput, "
          3. Inner two.
          4. ") assert.Contains(t, noWS, "
        3. ") assert.Contains(t, xmlOutput, "
        4. Outer two.
        5. ") // List must remain tight — no

          wrappers around items. assert.NotContains(t, xmlOutput, "

        6. Outer one.") assert.NotContains(t, xmlOutput, "

        7. Inner one.") } // TestRoundTrip_NestedMixedList guards the same fix for an OL containing a // nested UL, which is what the tt.xml mismatch exercised. func TestRoundTrip_NestedMixedList(t *testing.T) { xmlInput := `

            ` + `
          1. Outer one.
              ` + `
            • Bullet one.
            • ` + `
            • Bullet two.
            • ` + `
          2. ` + `
          3. Outer two.
          4. ` + `
          ` md, err := ConfluenceToMarkdown(xmlInput) require.NoError(t, err) xmlOutput, err := MarkdownToConfluence([]byte(md)) require.NoError(t, err) noWS := strings.Join(strings.Fields(xmlOutput), "") assert.Contains(t, noWS, "
        8. Outerone.
            ") assert.Contains(t, xmlOutput, "
          • Bullet one.
          • ") assert.Contains(t, xmlOutput, "
          • Bullet two.
          • ") assert.Contains(t, noWS, "
        9. ") assert.Contains(t, xmlOutput, "
        10. Outer two.
        11. ") assert.NotContains(t, xmlOutput, "
        12. Outer one.") } // TestRoundTrip_DoubleNestedUnorderedList covers a UL nested inside another // UL — pre-fix the cumulative indent computation didn't account for the // parent list, so deeply nested items collapsed. func TestRoundTrip_DoubleNestedUnorderedList(t *testing.T) { xmlInput := `

            ` + `
          • Top one.
              ` + `
            • Mid one.
                ` + `
              • Deep one.
              • ` + `
              • Deep two.
              • ` + `
            • ` + `
            • Mid two.
            • ` + `
          • ` + `
          ` md, err := ConfluenceToMarkdown(xmlInput) require.NoError(t, err) xmlOutput, err := MarkdownToConfluence([]byte(md)) require.NoError(t, err) noWS := strings.Join(strings.Fields(xmlOutput), "") assert.Contains(t, noWS, "
        13. Topone.
            ") assert.Contains(t, noWS, "
          • Midone.
              ") assert.Contains(t, xmlOutput, "
            • Deep one.
            • ") assert.Contains(t, xmlOutput, "
            • Deep two.
            • ") assert.Contains(t, xmlOutput, "
            • Mid two.
            • ") // Three nesting levels collapse to three closing
            s. assert.Equal(t, 3, strings.Count(xmlOutput, "
          ")) }