// 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(` %s `, e.getCommonDefs()) } // createSVGFooter creates SVG footer func (e *SVGExporter) createSVGFooter() string { return "" } // 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, "%s`, e.width/2, e.height/2, message) svg += e.createSVGFooter() return e.wrapWithViewBox(svg, e.width, e.height) } // createPlaceholderSVG creates a placeholder SVG func (e *SVGExporter) createPlaceholderSVG(title, message string) string { svg := e.createSVGHeader() svg += fmt.Sprintf(``) svg += fmt.Sprintf(`%s`, e.width/2, e.height/2-20, title) svg += fmt.Sprintf(`%s`, e.width/2, e.height/2+20, message) svg += e.createSVGFooter() return e.wrapWithViewBox(svg, e.width, e.height) } // getCommonDefs returns common SVG definitions func (e *SVGExporter) getCommonDefs() string { return ` ` } // Style methods based on mermaid.js themes func (e *SVGExporter) getPieChartStyles() string { return `` } func (e *SVGExporter) getOrganizationStyles() string { return `` } func (e *SVGExporter) getFlowchartStyles() string { return `` } func (e *SVGExporter) getSequenceStyles() string { return `` } func (e *SVGExporter) getGanttStyles() string { return `` } // Layout and rendering helpers type Position struct { X, Y int } func (e *SVGExporter) calculateFlowchartLayout(diagram *ast.Flowchart) map[string]Position { positions := make(map[string]Position) x, y := 100, 100 col := 0 maxCols := 3 for id := range diagram.Vertices { positions[id] = Position{X: x, Y: y} col++ if col >= maxCols { col = 0 x = 100 y += 120 } else { x += 200 } } return positions } func (e *SVGExporter) renderFlowchartNodeSVG(vertex *ast.FlowVertex, pos Position) string { text := vertex.ID if vertex.Text != nil { text = *vertex.Text } return fmt.Sprintf(` %s `, pos.X, pos.Y, text) } func (e *SVGExporter) renderFlowchartEdgeSVG(edge *ast.FlowEdge, from, to Position) string { return fmt.Sprintf(``, from.X, from.Y, to.X, to.Y) } func (e *SVGExporter) renderOrgNodeSVG(node *ast.OrganizationNode, x, y, level int) string { svg := fmt.Sprintf(` %s `, x, y, node.Name) // Render children if len(node.Children) > 0 { childY := y + 100 totalWidth := len(node.Children) * 200 startX := x - totalWidth/2 + 100 for i, child := range node.Children { childX := startX + i*200 // Connection line svg += fmt.Sprintf(``, x, y+25, childX, childY-25) // Recursively render child svg += e.renderOrgNodeSVG(child, childX, childY, level+1) } } return svg } // createArcPath creates SVG arc path for pie slices func (e *SVGExporter) createArcPath(centerX, centerY, radius, startAngle, endAngle float64) string { x1 := centerX + radius*math.Cos(startAngle) y1 := centerY + radius*math.Sin(startAngle) x2 := centerX + radius*math.Cos(endAngle) y2 := centerY + radius*math.Sin(endAngle) largeArc := 0 if endAngle-startAngle > math.Pi { largeArc = 1 } return fmt.Sprintf("M %g %g L %g %g A %g %g 0 %d 1 %g %g Z", centerX, centerY, x1, y1, radius, radius, largeArc, x2, y2) } // min returns the minimum of two integers func min(a, b int) int { if a < b { return a } return b } // New diagram type SVG export methods // exportMindmapToSVG exports mindmap to SVG func (e *SVGExporter) exportMindmapToSVG(diagram *ast.MindmapDiagram) (string, error) { svg := fmt.Sprintf(` Mindmap Diagram %d nodes `, e.width, e.height, e.width/2, e.width/2, len(diagram.Nodes)) return svg, nil } // exportKanbanToSVG exports kanban to SVG func (e *SVGExporter) exportKanbanToSVG(diagram *ast.KanbanDiagram) (string, error) { svg := fmt.Sprintf(` Kanban Board %d columns `, e.width, e.height, e.width/2, e.width/2, len(diagram.Columns)) return svg, nil } // exportGitToSVG exports git diagram to SVG func (e *SVGExporter) exportGitToSVG(diagram *ast.GitDiagram) (string, error) { svg := fmt.Sprintf(` Git Graph %d commits `, e.width, e.height, e.width/2, e.width/2, len(diagram.Commits)) return svg, nil } // exportSankeyToSVG exports sankey diagram to SVG func (e *SVGExporter) exportSankeyToSVG(diagram *ast.SankeyDiagram) (string, error) { svg := fmt.Sprintf(` Sankey Diagram %d links `, e.width, e.height, e.width/2, e.width/2, len(diagram.Links)) return svg, nil } // exportXYChartToSVG exports XY chart to SVG func (e *SVGExporter) exportXYChartToSVG(diagram *ast.XYChartDiagram) (string, error) { svg := fmt.Sprintf(` XY Chart %d series `, e.width, e.height, e.width/2, e.width/2, len(diagram.Series)) return svg, nil } // exportRadarToSVG exports radar chart to SVG func (e *SVGExporter) exportRadarToSVG(diagram *ast.RadarDiagram) (string, error) { svg := fmt.Sprintf(` Radar Chart %d series `, e.width, e.height, e.width/2, e.width/2, len(diagram.Series)) return svg, nil } // exportTreemapToSVG exports treemap to SVG func (e *SVGExporter) exportTreemapToSVG(diagram *ast.TreemapDiagram) (string, error) { svg := fmt.Sprintf(` Treemap %d nodes `, e.width, e.height, e.width/2, e.width/2, len(diagram.Nodes)) return svg, nil } // exportPacketToSVG exports packet diagram to SVG func (e *SVGExporter) exportPacketToSVG(diagram *ast.PacketDiagram) (string, error) { svg := fmt.Sprintf(` Packet Diagram %d flows `, e.width, e.height, e.width/2, e.width/2, len(diagram.Flows)) return svg, nil } // exportInfoToSVG exports info diagram to SVG func (e *SVGExporter) exportInfoToSVG(diagram *ast.InfoDiagram) (string, error) { svg := fmt.Sprintf(` Info Diagram %d items `, e.width, e.height, e.width/2, e.width/2, len(diagram.Items)) return svg, nil } // exportC4ToSVG exports C4 diagram to SVG func (e *SVGExporter) exportC4ToSVG(diagram ast.Diagram) (string, error) { // Get element count based on diagram type elementCount := 0 switch d := diagram.(type) { case *ast.C4ContextDiagram: elementCount = len(d.Elements) case *ast.C4ContainerDiagram: elementCount = len(d.Elements) case *ast.C4ComponentDiagram: elementCount = len(d.Elements) case *ast.C4DynamicDiagram: elementCount = len(d.Elements) case *ast.C4DeploymentDiagram: elementCount = len(d.Elements) } svg := fmt.Sprintf(` C4 Diagram %d elements `, e.width, e.height, e.width/2, e.width/2, elementCount) return svg, nil } // exportQuadrantToSVG exports quadrant chart to SVG func (e *SVGExporter) exportQuadrantToSVG(diagram *ast.QuadrantChart) (string, error) { svg := fmt.Sprintf(` Quadrant Chart %d points `, e.width, e.height, e.width/2, e.width/2, len(diagram.Points)) return svg, nil }