diff --git a/internal/markdown/codeblock.go b/internal/markdown/codeblock.go index efa7399..76ac922 100644 --- a/internal/markdown/codeblock.go +++ b/internal/markdown/codeblock.go @@ -24,6 +24,10 @@ func PreprocessCodeBlocks(source []byte, plantumlServer string) []byte { switch lang { case "svg": + // Remove blank lines from SVG content to prevent goldmark from + // splitting the HTML block (CommonMark HTML block type 6 ends at + // a blank line). + code = removeBlankLines(code) return []byte(fmt.Sprintf("\n
\n%s
\n", code)) case "mermaid": return []byte(fmt.Sprintf("\n
\n%s
\n", code)) @@ -43,6 +47,18 @@ func PreprocessCodeBlocks(source []byte, plantumlServer string) []byte { }) } +// removeBlankLines removes blank lines (empty or whitespace-only) from the text. +func removeBlankLines(s string) string { + var b strings.Builder + for _, line := range strings.Split(s, "\n") { + if strings.TrimSpace(line) != "" { + b.WriteString(line) + b.WriteByte('\n') + } + } + return b.String() +} + // encodePlantUML encodes PlantUML text for the PlantUML server URL. // Uses the ~h (hex encoding) format: each byte is converted to its 2-digit hex representation. func encodePlantUML(text string) string { diff --git a/internal/markdown/codeblock_test.go b/internal/markdown/codeblock_test.go index 5b2ddac..8a0ca6f 100644 --- a/internal/markdown/codeblock_test.go +++ b/internal/markdown/codeblock_test.go @@ -53,6 +53,25 @@ func TestPreprocessCodeBlocks_SVG(t *testing.T) { } } +func TestPreprocessCodeBlocks_SVGWithBlankLines(t *testing.T) { + input := []byte("```svg\n\n\n \n \n\n \n\n\n```\n") + result := PreprocessCodeBlocks(input, "") + s := string(result) + + if !strings.Contains(s, "svg-container") { + t.Error("should render SVG container") + } + if strings.Contains(s, "\n\n") { + t.Error("should not contain blank lines in SVG output (would break goldmark HTML block parsing)") + } + if !strings.Contains(s, "B\n```\n") result := PreprocessCodeBlocks(input, "") @@ -63,6 +82,29 @@ func TestPreprocessCodeBlocks_Mermaid(t *testing.T) { } } +func TestConvert_SVGWithBlankLines(t *testing.T) { + input := []byte("# Title\n\n```svg\n\n\n \n\n Hello\n\n\n```\n\nParagraph after SVG.\n") + result, err := Convert(input, "") + if err != nil { + t.Fatal(err) + } + s := string(result) + + if !strings.Contains(s, "svg-container") { + t.Error("should contain svg-container div") + } + if !strings.Contains(s, "") { + t.Error("should contain closing svg tag") + } + if !strings.Contains(s, "Paragraph after SVG.") { + t.Error("should contain paragraph after SVG") + } + // The SVG should not be broken apart by goldmark + if strings.Contains(s, "<rect") || strings.Contains(s, "<text") { + t.Error("SVG elements should not be HTML-escaped (goldmark should treat as HTML block)") + } +} + func TestPreprocessCodeBlocks_NoSpecialBlocks(t *testing.T) { input := []byte("```go\nfmt.Println(\"hello\")\n```\n") result := PreprocessCodeBlocks(input, "")