Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 16 additions & 0 deletions internal/markdown/codeblock.go
Original file line number Diff line number Diff line change
Expand Up @@ -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<div class=\"svg-container\">\n%s</div>\n", code))
case "mermaid":
return []byte(fmt.Sprintf("\n<pre class=\"mermaid\">\n%s</pre>\n", code))
Expand All @@ -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 {
Expand Down
42 changes: 42 additions & 0 deletions internal/markdown/codeblock_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,25 @@ func TestPreprocessCodeBlocks_SVG(t *testing.T) {
}
}

func TestPreprocessCodeBlocks_SVGWithBlankLines(t *testing.T) {
input := []byte("```svg\n<svg xmlns=\"http://www.w3.org/2000/svg\">\n\n <!-- comment -->\n <rect width=\"100\" height=\"100\"/>\n\n <circle cx=\"50\" cy=\"50\" r=\"40\"/>\n\n</svg>\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, "<rect") {
t.Error("should preserve SVG content")
}
if !strings.Contains(s, "<circle") {
t.Error("should preserve SVG content")
}
}

func TestPreprocessCodeBlocks_Mermaid(t *testing.T) {
input := []byte("```mermaid\ngraph TD\n A-->B\n```\n")
result := PreprocessCodeBlocks(input, "")
Expand All @@ -63,6 +82,29 @@ func TestPreprocessCodeBlocks_Mermaid(t *testing.T) {
}
}

func TestConvert_SVGWithBlankLines(t *testing.T) {
input := []byte("# Title\n\n```svg\n<svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 100 100\">\n\n <rect width=\"100\" height=\"100\" fill=\"#fff\"/>\n\n <text x=\"50\" y=\"50\">Hello</text>\n\n</svg>\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, "</svg>") {
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, "&lt;rect") || strings.Contains(s, "&lt;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, "")
Expand Down
Loading