// Package exporter provides high-quality SVG export functionality based on mermaid.js rendering logic
package exporter
import (
"fmt"
"math"
"strings"
"mermaid-go/pkg/ast"
)
// SVGExporter exports diagrams to high-quality SVG format
type SVGExporter struct {
width int
height int
theme string
}
// NewSVGExporter creates a new SVG exporter
func NewSVGExporter() *SVGExporter {
return &SVGExporter{
width: 800,
height: 600,
theme: "default",
}
}
// SetSize sets the SVG canvas size
func (e *SVGExporter) SetSize(width, height int) *SVGExporter {
e.width = width
e.height = height
return e
}
// SetTheme sets the SVG theme
func (e *SVGExporter) SetTheme(theme string) *SVGExporter {
e.theme = theme
return e
}
// ExportToSVG exports a diagram to SVG format
func (e *SVGExporter) ExportToSVG(diagram ast.Diagram) (string, error) {
switch d := diagram.(type) {
case *ast.PieChart:
return e.exportPieChartToSVG(d)
case *ast.OrganizationDiagram:
return e.exportOrganizationToSVG(d)
case *ast.Flowchart:
return e.exportFlowchartToSVG(d)
case *ast.SequenceDiagram:
return e.exportSequenceToSVG(d)
case *ast.GanttDiagram:
return e.exportGanttToSVG(d)
case *ast.TimelineDiagram:
return e.exportTimelineToSVG(d)
case *ast.UserJourneyDiagram:
return e.exportJourneyToSVG(d)
case *ast.ArchitectureDiagram:
return e.exportArchitectureToSVG(d)
case *ast.BPMNDiagram:
return e.exportBPMNToSVG(d)
case *ast.ClassDiagram:
return e.exportClassToSVG(d)
case *ast.StateDiagram:
return e.exportStateToSVG(d)
case *ast.ERDiagram:
return e.exportERToSVG(d)
case *ast.MindmapDiagram:
return e.exportMindmapToSVG(d)
case *ast.KanbanDiagram:
return e.exportKanbanToSVG(d)
case *ast.GitDiagram:
return e.exportGitToSVG(d)
case *ast.SankeyDiagram:
return e.exportSankeyToSVG(d)
case *ast.XYChartDiagram:
return e.exportXYChartToSVG(d)
case *ast.RadarDiagram:
return e.exportRadarToSVG(d)
case *ast.TreemapDiagram:
return e.exportTreemapToSVG(d)
case *ast.PacketDiagram:
return e.exportPacketToSVG(d)
case *ast.InfoDiagram:
return e.exportInfoToSVG(d)
case *ast.C4ContextDiagram:
return e.exportC4ToSVG(d)
case *ast.C4ContainerDiagram:
return e.exportC4ToSVG(d)
case *ast.C4ComponentDiagram:
return e.exportC4ToSVG(d)
case *ast.C4DynamicDiagram:
return e.exportC4ToSVG(d)
case *ast.C4DeploymentDiagram:
return e.exportC4ToSVG(d)
case *ast.QuadrantChart:
return e.exportQuadrantToSVG(d)
default:
return "", fmt.Errorf("unsupported diagram type for SVG export: %T", diagram)
}
}
// exportPieChartToSVG exports pie chart to SVG (based on mermaid.js pieRenderer.ts)
func (e *SVGExporter) exportPieChartToSVG(diagram *ast.PieChart) (string, error) {
// Calculate total value
total := 0.0
for _, slice := range diagram.Data {
total += slice.Value
}
if total == 0 {
return e.createEmptySVG("Empty Pie Chart"), nil
}
// Filter out slices < 1%
var validSlices []*ast.PieSlice
for _, slice := range diagram.Data {
if (slice.Value/total)*100 >= 1 {
validSlices = append(validSlices, slice)
}
}
// Mermaid.js pie chart dimensions
margin := 40
legendRectSize := 18
legendSpacing := 4
height := 450
pieWidth := height
radius := float64(min(pieWidth, height)/2 - margin)
centerX, centerY := float64(pieWidth/2), float64(height/2)
// Colors from mermaid.js theme
colors := []string{
"#ff6b6b", "#4ecdc4", "#45b7d1", "#96ceb4", "#feca57", "#ff9ff3",
"#54a0ff", "#5f27cd", "#00d2d3", "#ff9ff3", "#54a0ff", "#5f27cd",
}
svg := e.createSVGHeader()
svg += e.getPieChartStyles()
// Main group with transform
svg += fmt.Sprintf(``, centerX, centerY)
// Outer circle
svg += fmt.Sprintf(``, radius+1)
// Generate pie slices
startAngle := 0.0
for i, slice := range validSlices {
angle := (slice.Value / total) * 2 * math.Pi
color := colors[i%len(colors)]
// Create arc path
arcPath := e.createArcPath(0, 0, radius, startAngle, startAngle+angle)
svg += fmt.Sprintf(``, arcPath, color)
// Add percentage text
midAngle := startAngle + angle/2
textRadius := radius * 0.7 // textPosition from mermaid.js
textX := textRadius * math.Cos(midAngle)
textY := textRadius * math.Sin(midAngle)
percentage := fmt.Sprintf("%.0f%%", (slice.Value/total)*100)
svg += fmt.Sprintf(`%s`,
textX, textY, percentage)
startAngle += angle
}
svg += "" // Close main group
// Add title
if diagram.Title != nil {
svg += fmt.Sprintf(`%s`,
e.width/2, *diagram.Title)
}
// Add legend
legendX := float64(pieWidth + margin)
legendY := centerY - float64(len(validSlices)*22)/2
for i, slice := range validSlices {
color := colors[i%len(colors)]
y := legendY + float64(i*22)
// Legend rectangle
svg += fmt.Sprintf(``,
legendX, y-9, legendRectSize, legendRectSize, color, color)
// Legend text
labelText := slice.Label
if diagram.Config != nil {
if showData, ok := diagram.Config["showData"].(bool); ok && showData {
labelText = fmt.Sprintf("%s [%.0f]", slice.Label, slice.Value)
}
}
svg += fmt.Sprintf(`%s`,
legendX+float64(legendRectSize+legendSpacing), y+5, labelText)
}
// Calculate total width for viewBox
totalWidth := pieWidth + margin + legendRectSize + legendSpacing + 200 // Approximate legend width
svg += e.createSVGFooter()
// Set proper viewBox
return e.wrapWithViewBox(svg, totalWidth, height), nil
}
// exportOrganizationToSVG exports organization chart to SVG
func (e *SVGExporter) exportOrganizationToSVG(diagram *ast.OrganizationDiagram) (string, error) {
svg := e.createSVGHeader()
svg += e.getOrganizationStyles()
if diagram.Root != nil {
svg += e.renderOrgNodeSVG(diagram.Root, e.width/2, 80, 0)
}
// Add title
if diagram.Title != nil {
svg += fmt.Sprintf(`%s`,
e.width/2, *diagram.Title)
}
svg += e.createSVGFooter()
return e.wrapWithViewBox(svg, e.width, e.height), nil
}
// exportFlowchartToSVG exports flowchart to SVG
func (e *SVGExporter) exportFlowchartToSVG(diagram *ast.Flowchart) (string, error) {
svg := e.createSVGHeader()
svg += e.getFlowchartStyles()
// Simple grid layout
nodePositions := e.calculateFlowchartLayout(diagram)
// Render edges first (so they appear behind nodes)
for _, edge := range diagram.Edges {
fromPos, fromExists := nodePositions[edge.Start]
toPos, toExists := nodePositions[edge.End]
if fromExists && toExists {
svg += e.renderFlowchartEdgeSVG(edge, fromPos, toPos)
}
}
// Render nodes
for id, vertex := range diagram.Vertices {
if pos, exists := nodePositions[id]; exists {
svg += e.renderFlowchartNodeSVG(vertex, pos)
}
}
svg += e.createSVGFooter()
return e.wrapWithViewBox(svg, e.width, e.height), nil
}
// exportSequenceToSVG exports sequence diagram to SVG
func (e *SVGExporter) exportSequenceToSVG(diagram *ast.SequenceDiagram) (string, error) {
svg := e.createSVGHeader()
svg += e.getSequenceStyles()
participantWidth := e.width / (len(diagram.Participants) + 1)
participantY := 60
// Draw participants
for i, participant := range diagram.Participants {
x := participantWidth * (i + 1)
svg += fmt.Sprintf(``,
x-60, participantY-20)
svg += fmt.Sprintf(`%s`,
x, participantY+5, participant.Name)
// Lifeline
svg += fmt.Sprintf(``,
x, participantY+20, x, e.height-50)
}
// Draw messages
messageY := participantY + 60
for _, message := range diagram.Messages {
fromX, toX := 0, 0
for i, p := range diagram.Participants {
x := participantWidth * (i + 1)
if p.ID == message.From {
fromX = x
}
if p.ID == message.To {
toX = x
}
}
// Message arrow
svg += fmt.Sprintf(``,
fromX, messageY, toX, messageY)
// Message text
svg += fmt.Sprintf(`%s`,
(fromX+toX)/2, messageY-5, message.Message)
messageY += 50
}
svg += e.createSVGFooter()
return e.wrapWithViewBox(svg, e.width, e.height), nil
}
// exportGanttToSVG exports Gantt chart to SVG
func (e *SVGExporter) exportGanttToSVG(diagram *ast.GanttDiagram) (string, error) {
svg := e.createSVGHeader()
svg += e.getGanttStyles()
y := 80
if diagram.Title != nil {
svg += fmt.Sprintf(`%s`,
e.width/2, *diagram.Title)
}
// Draw sections and tasks
for _, section := range diagram.Sections {
// Section header
svg += fmt.Sprintf(`%s`, y, section.Name)
y += 30
// Tasks
for _, task := range section.Tasks {
// Task bar
barWidth := 200
svg += fmt.Sprintf(``,
y-10, barWidth)
// Task name
svg += fmt.Sprintf(`%s`,
60+barWidth, y+5, task.Name)
y += 35
}
y += 15
}
svg += e.createSVGFooter()
return e.wrapWithViewBox(svg, e.width, e.height), nil
}
// Placeholder implementations for other diagram types
func (e *SVGExporter) exportTimelineToSVG(diagram *ast.TimelineDiagram) (string, error) {
return e.createPlaceholderSVG("Timeline", "Timeline SVG export coming soon"), nil
}
func (e *SVGExporter) exportJourneyToSVG(diagram *ast.UserJourneyDiagram) (string, error) {
return e.createPlaceholderSVG("User Journey", "User Journey SVG export coming soon"), nil
}
func (e *SVGExporter) exportArchitectureToSVG(diagram *ast.ArchitectureDiagram) (string, error) {
return e.createPlaceholderSVG("Architecture", "Architecture SVG export coming soon"), nil
}
func (e *SVGExporter) exportBPMNToSVG(diagram *ast.BPMNDiagram) (string, error) {
return e.createPlaceholderSVG("BPMN", "BPMN SVG export coming soon"), nil
}
func (e *SVGExporter) exportClassToSVG(diagram *ast.ClassDiagram) (string, error) {
return e.createPlaceholderSVG("Class Diagram", "Class Diagram SVG export coming soon"), nil
}
func (e *SVGExporter) exportStateToSVG(diagram *ast.StateDiagram) (string, error) {
return e.createPlaceholderSVG("State Diagram", "State Diagram SVG export coming soon"), nil
}
func (e *SVGExporter) exportERToSVG(diagram *ast.ERDiagram) (string, error) {
return e.createPlaceholderSVG("ER Diagram", "ER Diagram SVG export coming soon"), nil
}
// Helper methods
// createSVGHeader creates SVG header with proper namespace and definitions
func (e *SVGExporter) createSVGHeader() string {
return fmt.Sprintf(`
"
}
// wrapWithViewBox wraps SVG content with proper viewBox
func (e *SVGExporter) wrapWithViewBox(content string, width, height int) string {
// Insert viewBox after the opening svg tag
viewBox := fmt.Sprintf(` viewBox="0 0 %d %d" width="%d" height="%d"`, width, height, width, height)
return strings.Replace(content, "