package format
import (
"strings"
"testing"
"unicode/utf8"
"github.com/stretchr/testify/assert"
)
func TestPrettyXML_Paragraph(t *testing.T) {
input := `<p>Hello <strong>world</strong></p>`
result := PrettyXML(input, " ")
assert.Equal(t, "<p>\n Hello <strong>world</strong>\n</p>\n", result)
}
func TestPrettyXML_NestedBlocks(t *testing.T) {
input := `<ul><li>One</li><li>Two</li></ul>`
result := PrettyXML(input, " ")
// li should be inlined since content is short
assert.Contains(t, result, " <li>One</li>\n")
assert.Contains(t, result, " <li>Two</li>\n")
}
func TestPrettyXML_InlineStaysInline(t *testing.T) {
input := `<p>Text with <strong>bold</strong> and <em>italic</em></p>`
result := PrettyXML(input, " ")
assert.Contains(t, result, "Text with <strong>bold</strong> and <em>italic</em>")
}
func TestPrettyXML_CodeBlockCDATAPreserved(t *testing.T) {
input := `<ac:structured-macro ac:name="code"><ac:parameter ac:name="language">go</ac:parameter><ac:plain-text-body><![CDATA[func main() {
fmt.Println("hello")
}]]></ac:plain-text-body></ac:structured-macro>`
result := PrettyXML(input, " ")
assert.Contains(t, result, `<![CDATA[func main() {
fmt.Println("hello")
}]]>`)
}
func TestPrettyXML_HeadingsInlined(t *testing.T) {
input := `<h1>Title</h1><h2>Subtitle</h2>`
result := PrettyXML(input, " ")
assert.Contains(t, result, "<h1>Title</h1>\n")
assert.Contains(t, result, "<h2>Subtitle</h2>\n")
}
func TestPrettyXML_HeadingsWithInlineMarkup(t *testing.T) {
input := `<h2>Section <strong>Important</strong></h2>`
result := PrettyXML(input, " ")
assert.Contains(t, result, "<h2>Section <strong>Important</strong></h2>\n")
}
func TestPrettyXML_SelfClosingBlock(t *testing.T) {
input := `<p>Before</p><hr/><p>After</p>`
result := PrettyXML(input, " ")
// Formatter normalizes self-closing tags to "<tag />" form.
assert.Contains(t, result, "<hr />\n")
}
func TestPrettyXML_Table(t *testing.T) {
input := `<table><tbody><tr><th><p>Name</p></th><td><p>Value</p></td></tr></tbody></table>`
result := PrettyXML(input, " ")
assert.Contains(t, result, "<table>\n")
assert.Contains(t, result, " <tbody>\n")
assert.Contains(t, result, " <tr>\n")
}
func TestPrettyXML_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 := PrettyXML(input, " ")
assert.Contains(t, result, "<ac:layout>\n")
assert.Contains(t, result, " <ac:layout-section ac:type=\"single\">\n")
assert.Contains(t, result, " <ac:layout-cell>\n")
}
func TestPrettyXML_TaskList(t *testing.T) {
input := `<ac:task-list><ac:task><ac:task-id>1</ac:task-id><ac:task-status>complete</ac:task-status><ac:task-body>Done</ac:task-body></ac:task></ac:task-list>`
result := PrettyXML(input, " ")
assert.Contains(t, result, "<ac:task-list>\n")
assert.Contains(t, result, " <ac:task>\n")
// task-id and task-status should be inlined
assert.Contains(t, result, "<ac:task-id>1</ac:task-id>")
assert.Contains(t, result, "<ac:task-status>complete</ac:task-status>")
}
func TestPrettyXML_CommentsPreserved(t *testing.T) {
input := `<!-- MD_CONTENT_START --><p>Content</p><!-- MD_CONTENT_END -->`
result := PrettyXML(input, " ")
assert.Contains(t, result, "<!-- MD_CONTENT_START -->")
assert.Contains(t, result, "<!-- MD_CONTENT_END -->")
}
func TestPrettyXML_InlineElements(t *testing.T) {
input := `<p>Link: <a href="https://example.com">click</a> and <code>code</code></p>`
result := PrettyXML(input, " ")
assert.Contains(t, result, `Link: <a href="https://example.com">click</a> and <code>code</code>`)
}
func TestPrettyXML_EmptyInput(t *testing.T) {
result := PrettyXML("", " ")
assert.Equal(t, "\n", result)
}
func TestPrettyXML_UserAndAttachmentInline(t *testing.T) {
input := `<p>By <ac:link><ri:user ri:userkey="abc123"/></ac:link> see <ac:image><ri:attachment ri:filename="img.png"/></ac:image></p>`
result := PrettyXML(input, " ")
// Formatter normalizes self-closing tags to "<tag />" form.
assert.Contains(t, result, `<ac:link><ri:user ri:userkey="abc123" /></ac:link>`)
}
func TestPrettyXML_CustomIndent(t *testing.T) {
input := `<ul><li>Item</li></ul>`
result := PrettyXML(input, "\t")
assert.Contains(t, result, "\t<li>Item</li>")
}
func TestPrettyXML_LiInlinedShort(t *testing.T) {
input := `<ul><li>Short item</li></ul>`
result := PrettyXML(input, " ")
assert.Contains(t, result, " <li>Short item</li>\n")
}
func TestPrettyXML_LiExpandedLong(t *testing.T) {
longText := strings.Repeat("слово ", 30) // ~180 chars in Cyrillic
input := `<ul><li>` + longText + `</li></ul>`
result := PrettyXML(input, " ")
// Should NOT be inlined — too long
assert.Contains(t, result, " <li>\n")
}
func TestPrettyXML_LongLineWrapped(t *testing.T) {
longText := strings.Repeat("word ", 30) // 150 chars
input := `<p>` + longText + `</p>`
result := PrettyXML(input, " ")
for _, line := range strings.Split(result, "\n") {
w := utf8.RuneCountInString(line)
if w > 125 { // allow small overshoot for tags
t.Errorf("line too long (%d runes): %s", w, line)
}
}
}
func TestPrettyXML_LongLineUTF8(t *testing.T) {
// Russian text: each Cyrillic char is 1 rune but 2 bytes
longText := strings.Repeat("Привет мир ", 15) // ~165 runes
input := `<p>` + longText + `</p>`
result := PrettyXML(input, " ")
for _, line := range strings.Split(result, "\n") {
w := utf8.RuneCountInString(line)
if w > 125 {
t.Errorf("line too long (%d runes): %s", w, line[:80])
}
}
}
func TestPrettyXML_LongLinePreservesTagIntegrity(t *testing.T) {
input := `<p>Text <strong>bold text here</strong> more text and <a href="https://example.com/very/long/path">some link</a> even more text to make line long enough to wrap around the boundary limit</p>`
result := PrettyXML(input, " ")
// Tags should not be split across lines
assert.NotContains(t, result, "<strong\n")
assert.NotContains(t, result, "</strong\n")
}
// A single space between two adjacent inline elements is significant and must
// survive formatting. It used to be dropped as a presumed indentation artifact,
// gluing the elements together (e.g. "</strong><code>"). Regression for the
// "missing space before inline code" report.
func TestPrettyXML_SpaceBetweenInlineElements(t *testing.T) {
cases := []struct{ input, want string }{
{`<p>Bold <strong>word</strong> <code>code</code> here.</p>`, "</strong> <code>"},
{`<p>Link <a href="http://x">t</a> <code>code</code> here.</p>`, "</a> <code>"},
{`<p>Word <em>em</em> <code>code</code>.</p>`, "</em> <code>"},
}
for _, tc := range cases {
result := PrettyXML(tc.input, " ")
assert.Contains(t, result, tc.want, tc.input)
}
}
// The same significant space must also survive when the line is long enough to
// be wrapped: wrapLine used to drop whitespace that sat between a word/tag and
// an adjacent tag, because strings.Fields discarded trailing space and tags
// ignored any pending space.
func TestPrettyXML_WrapPreservesSpaceBeforeTag(t *testing.T) {
long := strings.Repeat("слово ", 25) // push past the 120-rune wrap width
input := `<p>` + long + `<strong>жирный</strong> <code>код</code> хвост предложения.</p>`
result := PrettyXML(input, " ")
assert.NotContains(t, result, "</strong><code>")
assert.Contains(t, result, "</strong> <code>")
}
// The literal reported pattern: a plain word followed by inline code on a line
// long enough to wrap. wrapLine's strings.Fields dropped the word's trailing
// space, gluing it to <code> ("манифеста<code>" instead of "манифеста <code>").
func TestPrettyXML_WrapPreservesSpaceBeforeCodeAfterWord(t *testing.T) {
long := strings.Repeat("текст ", 25) // push past the 120-rune wrap width
input := `<p>` + long + `манифеста <code>app.manifest.toml</code> и далее.</p>`
result := PrettyXML(input, " ")
assert.NotContains(t, result, "манифеста<code>")
assert.Contains(t, result, "манифеста <code>")
}
// A wrap must never fall inside <code>...</code>: doing so injects the
// continuation indent into the code text, which round-trips as a spurious space
// inside the span. The whole span is kept on one line regardless of where the
// boundary lands. Probes several lengths so the wrap point sweeps across the
// code span's open tag.
func TestPrettyXML_WrapKeepsCodeSpanWhole(t *testing.T) {
for n := 18; n <= 24; n++ {
input := `<p>` + strings.Repeat("word ", n) + `tail <code>app.manifest.lock</code> rest of it.</p>`
result := PrettyXML(input, " ")
for _, line := range strings.Split(result, "\n") {
trimmed := strings.TrimSpace(line)
// No line may start or end mid-span (open without close, or vice versa).
assert.False(t, strings.HasPrefix(trimmed, "app.manifest.lock"),
"code content split onto its own line at n=%d: %q", n, line)
if strings.Contains(line, "<code>") {
assert.Contains(t, line, "</code>",
"<code> opened but not closed on same line at n=%d: %q", n, line)
}
}
}
}