svg.go 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543
  1. // Package exporter provides high-quality SVG export functionality based on mermaid.js rendering logic
  2. package exporter
  3. import (
  4. "fmt"
  5. "math"
  6. "strings"
  7. "mermaid-go/pkg/ast"
  8. )
  9. // SVGExporter exports diagrams to high-quality SVG format
  10. type SVGExporter struct {
  11. width int
  12. height int
  13. theme string
  14. }
  15. // NewSVGExporter creates a new SVG exporter
  16. func NewSVGExporter() *SVGExporter {
  17. return &SVGExporter{
  18. width: 800,
  19. height: 600,
  20. theme: "default",
  21. }
  22. }
  23. // SetSize sets the SVG canvas size
  24. func (e *SVGExporter) SetSize(width, height int) *SVGExporter {
  25. e.width = width
  26. e.height = height
  27. return e
  28. }
  29. // SetTheme sets the SVG theme
  30. func (e *SVGExporter) SetTheme(theme string) *SVGExporter {
  31. e.theme = theme
  32. return e
  33. }
  34. // ExportToSVG exports a diagram to SVG format
  35. func (e *SVGExporter) ExportToSVG(diagram ast.Diagram) (string, error) {
  36. switch d := diagram.(type) {
  37. case *ast.PieChart:
  38. return e.exportPieChartToSVG(d)
  39. case *ast.OrganizationDiagram:
  40. return e.exportOrganizationToSVG(d)
  41. case *ast.Flowchart:
  42. return e.exportFlowchartToSVG(d)
  43. case *ast.SequenceDiagram:
  44. return e.exportSequenceToSVG(d)
  45. case *ast.GanttDiagram:
  46. return e.exportGanttToSVG(d)
  47. case *ast.TimelineDiagram:
  48. return e.exportTimelineToSVG(d)
  49. case *ast.UserJourneyDiagram:
  50. return e.exportJourneyToSVG(d)
  51. case *ast.ArchitectureDiagram:
  52. return e.exportArchitectureToSVG(d)
  53. case *ast.BPMNDiagram:
  54. return e.exportBPMNToSVG(d)
  55. case *ast.ClassDiagram:
  56. return e.exportClassToSVG(d)
  57. case *ast.StateDiagram:
  58. return e.exportStateToSVG(d)
  59. case *ast.ERDiagram:
  60. return e.exportERToSVG(d)
  61. default:
  62. return "", fmt.Errorf("unsupported diagram type for SVG export: %T", diagram)
  63. }
  64. }
  65. // exportPieChartToSVG exports pie chart to SVG (based on mermaid.js pieRenderer.ts)
  66. func (e *SVGExporter) exportPieChartToSVG(diagram *ast.PieChart) (string, error) {
  67. // Calculate total value
  68. total := 0.0
  69. for _, slice := range diagram.Data {
  70. total += slice.Value
  71. }
  72. if total == 0 {
  73. return e.createEmptySVG("Empty Pie Chart"), nil
  74. }
  75. // Filter out slices < 1%
  76. var validSlices []*ast.PieSlice
  77. for _, slice := range diagram.Data {
  78. if (slice.Value/total)*100 >= 1 {
  79. validSlices = append(validSlices, slice)
  80. }
  81. }
  82. // Mermaid.js pie chart dimensions
  83. margin := 40
  84. legendRectSize := 18
  85. legendSpacing := 4
  86. height := 450
  87. pieWidth := height
  88. radius := float64(min(pieWidth, height)/2 - margin)
  89. centerX, centerY := float64(pieWidth/2), float64(height/2)
  90. // Colors from mermaid.js theme
  91. colors := []string{
  92. "#ff6b6b", "#4ecdc4", "#45b7d1", "#96ceb4", "#feca57", "#ff9ff3",
  93. "#54a0ff", "#5f27cd", "#00d2d3", "#ff9ff3", "#54a0ff", "#5f27cd",
  94. }
  95. svg := e.createSVGHeader()
  96. svg += e.getPieChartStyles()
  97. // Main group with transform
  98. svg += fmt.Sprintf(`<g transform="translate(%g,%g)">`, centerX, centerY)
  99. // Outer circle
  100. svg += fmt.Sprintf(`<circle cx="0" cy="0" r="%g" class="pieOuterCircle"/>`, radius+1)
  101. // Generate pie slices
  102. startAngle := 0.0
  103. for i, slice := range validSlices {
  104. angle := (slice.Value / total) * 2 * math.Pi
  105. color := colors[i%len(colors)]
  106. // Create arc path
  107. arcPath := e.createArcPath(0, 0, radius, startAngle, startAngle+angle)
  108. svg += fmt.Sprintf(`<path d="%s" fill="%s" class="pieCircle"/>`, arcPath, color)
  109. // Add percentage text
  110. midAngle := startAngle + angle/2
  111. textRadius := radius * 0.7 // textPosition from mermaid.js
  112. textX := textRadius * math.Cos(midAngle)
  113. textY := textRadius * math.Sin(midAngle)
  114. percentage := fmt.Sprintf("%.0f%%", (slice.Value/total)*100)
  115. svg += fmt.Sprintf(`<text x="%g" y="%g" text-anchor="middle" class="slice">%s</text>`,
  116. textX, textY, percentage)
  117. startAngle += angle
  118. }
  119. svg += "</g>" // Close main group
  120. // Add title
  121. if diagram.Title != nil {
  122. svg += fmt.Sprintf(`<text x="%d" y="25" text-anchor="middle" class="pieTitleText">%s</text>`,
  123. e.width/2, *diagram.Title)
  124. }
  125. // Add legend
  126. legendX := float64(pieWidth + margin)
  127. legendY := centerY - float64(len(validSlices)*22)/2
  128. for i, slice := range validSlices {
  129. color := colors[i%len(colors)]
  130. y := legendY + float64(i*22)
  131. // Legend rectangle
  132. svg += fmt.Sprintf(`<rect x="%g" y="%g" width="%d" height="%d" fill="%s" stroke="%s"/>`,
  133. legendX, y-9, legendRectSize, legendRectSize, color, color)
  134. // Legend text
  135. labelText := slice.Label
  136. if diagram.Config != nil {
  137. if showData, ok := diagram.Config["showData"].(bool); ok && showData {
  138. labelText = fmt.Sprintf("%s [%.0f]", slice.Label, slice.Value)
  139. }
  140. }
  141. svg += fmt.Sprintf(`<text x="%g" y="%g" class="legend">%s</text>`,
  142. legendX+float64(legendRectSize+legendSpacing), y+5, labelText)
  143. }
  144. // Calculate total width for viewBox
  145. totalWidth := pieWidth + margin + legendRectSize + legendSpacing + 200 // Approximate legend width
  146. svg += e.createSVGFooter()
  147. // Set proper viewBox
  148. return e.wrapWithViewBox(svg, totalWidth, height), nil
  149. }
  150. // exportOrganizationToSVG exports organization chart to SVG
  151. func (e *SVGExporter) exportOrganizationToSVG(diagram *ast.OrganizationDiagram) (string, error) {
  152. svg := e.createSVGHeader()
  153. svg += e.getOrganizationStyles()
  154. if diagram.Root != nil {
  155. svg += e.renderOrgNodeSVG(diagram.Root, e.width/2, 80, 0)
  156. }
  157. // Add title
  158. if diagram.Title != nil {
  159. svg += fmt.Sprintf(`<text x="%d" y="30" text-anchor="middle" class="title">%s</text>`,
  160. e.width/2, *diagram.Title)
  161. }
  162. svg += e.createSVGFooter()
  163. return e.wrapWithViewBox(svg, e.width, e.height), nil
  164. }
  165. // exportFlowchartToSVG exports flowchart to SVG
  166. func (e *SVGExporter) exportFlowchartToSVG(diagram *ast.Flowchart) (string, error) {
  167. svg := e.createSVGHeader()
  168. svg += e.getFlowchartStyles()
  169. // Simple grid layout
  170. nodePositions := e.calculateFlowchartLayout(diagram)
  171. // Render edges first (so they appear behind nodes)
  172. for _, edge := range diagram.Edges {
  173. fromPos, fromExists := nodePositions[edge.Start]
  174. toPos, toExists := nodePositions[edge.End]
  175. if fromExists && toExists {
  176. svg += e.renderFlowchartEdgeSVG(edge, fromPos, toPos)
  177. }
  178. }
  179. // Render nodes
  180. for id, vertex := range diagram.Vertices {
  181. if pos, exists := nodePositions[id]; exists {
  182. svg += e.renderFlowchartNodeSVG(vertex, pos)
  183. }
  184. }
  185. svg += e.createSVGFooter()
  186. return e.wrapWithViewBox(svg, e.width, e.height), nil
  187. }
  188. // exportSequenceToSVG exports sequence diagram to SVG
  189. func (e *SVGExporter) exportSequenceToSVG(diagram *ast.SequenceDiagram) (string, error) {
  190. svg := e.createSVGHeader()
  191. svg += e.getSequenceStyles()
  192. participantWidth := e.width / (len(diagram.Participants) + 1)
  193. participantY := 60
  194. // Draw participants
  195. for i, participant := range diagram.Participants {
  196. x := participantWidth * (i + 1)
  197. svg += fmt.Sprintf(`<rect x="%d" y="%d" width="120" height="40" class="participant"/>`,
  198. x-60, participantY-20)
  199. svg += fmt.Sprintf(`<text x="%d" y="%d" text-anchor="middle" class="participantText">%s</text>`,
  200. x, participantY+5, participant.Name)
  201. // Lifeline
  202. svg += fmt.Sprintf(`<line x1="%d" y1="%d" x2="%d" y2="%d" class="lifeline"/>`,
  203. x, participantY+20, x, e.height-50)
  204. }
  205. // Draw messages
  206. messageY := participantY + 60
  207. for _, message := range diagram.Messages {
  208. fromX, toX := 0, 0
  209. for i, p := range diagram.Participants {
  210. x := participantWidth * (i + 1)
  211. if p.ID == message.From {
  212. fromX = x
  213. }
  214. if p.ID == message.To {
  215. toX = x
  216. }
  217. }
  218. // Message arrow
  219. svg += fmt.Sprintf(`<line x1="%d" y1="%d" x2="%d" y2="%d" class="messageArrow" marker-end="url(#arrowhead)"/>`,
  220. fromX, messageY, toX, messageY)
  221. // Message text
  222. svg += fmt.Sprintf(`<text x="%d" y="%d" text-anchor="middle" class="messageText">%s</text>`,
  223. (fromX+toX)/2, messageY-5, message.Message)
  224. messageY += 50
  225. }
  226. svg += e.createSVGFooter()
  227. return e.wrapWithViewBox(svg, e.width, e.height), nil
  228. }
  229. // exportGanttToSVG exports Gantt chart to SVG
  230. func (e *SVGExporter) exportGanttToSVG(diagram *ast.GanttDiagram) (string, error) {
  231. svg := e.createSVGHeader()
  232. svg += e.getGanttStyles()
  233. y := 80
  234. if diagram.Title != nil {
  235. svg += fmt.Sprintf(`<text x="%d" y="30" text-anchor="middle" class="title">%s</text>`,
  236. e.width/2, *diagram.Title)
  237. }
  238. // Draw sections and tasks
  239. for _, section := range diagram.Sections {
  240. // Section header
  241. svg += fmt.Sprintf(`<text x="20" y="%d" class="sectionText">%s</text>`, y, section.Name)
  242. y += 30
  243. // Tasks
  244. for _, task := range section.Tasks {
  245. // Task bar
  246. barWidth := 200
  247. svg += fmt.Sprintf(`<rect x="50" y="%d" width="%d" height="20" class="taskBar"/>`,
  248. y-10, barWidth)
  249. // Task name
  250. svg += fmt.Sprintf(`<text x="%d" y="%d" class="taskText">%s</text>`,
  251. 60+barWidth, y+5, task.Name)
  252. y += 35
  253. }
  254. y += 15
  255. }
  256. svg += e.createSVGFooter()
  257. return e.wrapWithViewBox(svg, e.width, e.height), nil
  258. }
  259. // Placeholder implementations for other diagram types
  260. func (e *SVGExporter) exportTimelineToSVG(diagram *ast.TimelineDiagram) (string, error) {
  261. return e.createPlaceholderSVG("Timeline", "Timeline SVG export coming soon"), nil
  262. }
  263. func (e *SVGExporter) exportJourneyToSVG(diagram *ast.UserJourneyDiagram) (string, error) {
  264. return e.createPlaceholderSVG("User Journey", "User Journey SVG export coming soon"), nil
  265. }
  266. func (e *SVGExporter) exportArchitectureToSVG(diagram *ast.ArchitectureDiagram) (string, error) {
  267. return e.createPlaceholderSVG("Architecture", "Architecture SVG export coming soon"), nil
  268. }
  269. func (e *SVGExporter) exportBPMNToSVG(diagram *ast.BPMNDiagram) (string, error) {
  270. return e.createPlaceholderSVG("BPMN", "BPMN SVG export coming soon"), nil
  271. }
  272. func (e *SVGExporter) exportClassToSVG(diagram *ast.ClassDiagram) (string, error) {
  273. return e.createPlaceholderSVG("Class Diagram", "Class Diagram SVG export coming soon"), nil
  274. }
  275. func (e *SVGExporter) exportStateToSVG(diagram *ast.StateDiagram) (string, error) {
  276. return e.createPlaceholderSVG("State Diagram", "State Diagram SVG export coming soon"), nil
  277. }
  278. func (e *SVGExporter) exportERToSVG(diagram *ast.ERDiagram) (string, error) {
  279. return e.createPlaceholderSVG("ER Diagram", "ER Diagram SVG export coming soon"), nil
  280. }
  281. // Helper methods
  282. // createSVGHeader creates SVG header with proper namespace and definitions
  283. func (e *SVGExporter) createSVGHeader() string {
  284. return fmt.Sprintf(`<?xml version="1.0" encoding="UTF-8"?>
  285. <svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
  286. <defs>
  287. %s
  288. </defs>
  289. `, e.getCommonDefs())
  290. }
  291. // createSVGFooter creates SVG footer
  292. func (e *SVGExporter) createSVGFooter() string {
  293. return "</svg>"
  294. }
  295. // wrapWithViewBox wraps SVG content with proper viewBox
  296. func (e *SVGExporter) wrapWithViewBox(content string, width, height int) string {
  297. // Insert viewBox after the opening svg tag
  298. viewBox := fmt.Sprintf(` viewBox="0 0 %d %d" width="%d" height="%d"`, width, height, width, height)
  299. return strings.Replace(content, "<svg xmlns=", "<svg"+viewBox+" xmlns=", 1)
  300. }
  301. // createEmptySVG creates an empty SVG with message
  302. func (e *SVGExporter) createEmptySVG(message string) string {
  303. svg := e.createSVGHeader()
  304. svg += fmt.Sprintf(`<text x="%d" y="%d" text-anchor="middle" class="emptyMessage">%s</text>`,
  305. e.width/2, e.height/2, message)
  306. svg += e.createSVGFooter()
  307. return e.wrapWithViewBox(svg, e.width, e.height)
  308. }
  309. // createPlaceholderSVG creates a placeholder SVG
  310. func (e *SVGExporter) createPlaceholderSVG(title, message string) string {
  311. svg := e.createSVGHeader()
  312. svg += fmt.Sprintf(`<rect width="100%%" height="100%%" fill="#f8f9fa"/>`)
  313. svg += fmt.Sprintf(`<text x="%d" y="%d" text-anchor="middle" class="title">%s</text>`,
  314. e.width/2, e.height/2-20, title)
  315. svg += fmt.Sprintf(`<text x="%d" y="%d" text-anchor="middle" class="message">%s</text>`,
  316. e.width/2, e.height/2+20, message)
  317. svg += e.createSVGFooter()
  318. return e.wrapWithViewBox(svg, e.width, e.height)
  319. }
  320. // getCommonDefs returns common SVG definitions
  321. func (e *SVGExporter) getCommonDefs() string {
  322. return `
  323. <marker id="arrowhead" markerWidth="10" markerHeight="7" refX="9" refY="3.5" orient="auto">
  324. <polygon points="0 0, 10 3.5, 0 7" fill="#333"/>
  325. </marker>
  326. <marker id="arrowheadWhite" markerWidth="10" markerHeight="7" refX="9" refY="3.5" orient="auto">
  327. <polygon points="0 0, 10 3.5, 0 7" fill="#fff"/>
  328. </marker>`
  329. }
  330. // Style methods based on mermaid.js themes
  331. func (e *SVGExporter) getPieChartStyles() string {
  332. return `<style>
  333. .pieOuterCircle { fill: none; stroke: #333; stroke-width: 2; }
  334. .pieCircle { stroke: #fff; stroke-width: 2; }
  335. .slice { font-family: Arial, sans-serif; font-size: 14px; fill: #fff; font-weight: bold; }
  336. .pieTitleText { font-family: Arial, sans-serif; font-size: 20px; font-weight: bold; fill: #333; }
  337. .legend { font-family: Arial, sans-serif; font-size: 14px; fill: #333; }
  338. </style>`
  339. }
  340. func (e *SVGExporter) getOrganizationStyles() string {
  341. return `<style>
  342. .orgNode { fill: #e3f2fd; stroke: #1976d2; stroke-width: 2; }
  343. .orgText { font-family: Arial, sans-serif; font-size: 12px; fill: #333; text-anchor: middle; }
  344. .orgEdge { stroke: #1976d2; stroke-width: 2; }
  345. .title { font-family: Arial, sans-serif; font-size: 18px; font-weight: bold; fill: #333; }
  346. </style>`
  347. }
  348. func (e *SVGExporter) getFlowchartStyles() string {
  349. return `<style>
  350. .flowNode { fill: #fff; stroke: #333; stroke-width: 2; }
  351. .flowText { font-family: Arial, sans-serif; font-size: 12px; fill: #333; text-anchor: middle; }
  352. .flowEdge { stroke: #333; stroke-width: 2; fill: none; }
  353. </style>`
  354. }
  355. func (e *SVGExporter) getSequenceStyles() string {
  356. return `<style>
  357. .participant { fill: #e3f2fd; stroke: #1976d2; stroke-width: 2; }
  358. .participantText { font-family: Arial, sans-serif; font-size: 12px; fill: #333; }
  359. .lifeline { stroke: #ccc; stroke-width: 1; stroke-dasharray: 5,5; }
  360. .messageArrow { stroke: #333; stroke-width: 2; }
  361. .messageText { font-family: Arial, sans-serif; font-size: 11px; fill: #333; }
  362. </style>`
  363. }
  364. func (e *SVGExporter) getGanttStyles() string {
  365. return `<style>
  366. .taskBar { fill: #4ecdc4; stroke: #26a69a; stroke-width: 1; }
  367. .taskText { font-family: Arial, sans-serif; font-size: 12px; fill: #333; }
  368. .sectionText { font-family: Arial, sans-serif; font-size: 14px; font-weight: bold; fill: #333; }
  369. .title { font-family: Arial, sans-serif; font-size: 18px; font-weight: bold; fill: #333; }
  370. </style>`
  371. }
  372. // Layout and rendering helpers
  373. type Position struct {
  374. X, Y int
  375. }
  376. func (e *SVGExporter) calculateFlowchartLayout(diagram *ast.Flowchart) map[string]Position {
  377. positions := make(map[string]Position)
  378. x, y := 100, 100
  379. col := 0
  380. maxCols := 3
  381. for id := range diagram.Vertices {
  382. positions[id] = Position{X: x, Y: y}
  383. col++
  384. if col >= maxCols {
  385. col = 0
  386. x = 100
  387. y += 120
  388. } else {
  389. x += 200
  390. }
  391. }
  392. return positions
  393. }
  394. func (e *SVGExporter) renderFlowchartNodeSVG(vertex *ast.FlowVertex, pos Position) string {
  395. text := vertex.ID
  396. if vertex.Text != nil {
  397. text = *vertex.Text
  398. }
  399. return fmt.Sprintf(`
  400. <g transform="translate(%d,%d)">
  401. <rect x="-50" y="-20" width="100" height="40" class="flowNode"/>
  402. <text x="0" y="5" class="flowText">%s</text>
  403. </g>`, pos.X, pos.Y, text)
  404. }
  405. func (e *SVGExporter) renderFlowchartEdgeSVG(edge *ast.FlowEdge, from, to Position) string {
  406. return fmt.Sprintf(`<line x1="%d" y1="%d" x2="%d" y2="%d" class="flowEdge" marker-end="url(#arrowhead)"/>`,
  407. from.X, from.Y, to.X, to.Y)
  408. }
  409. func (e *SVGExporter) renderOrgNodeSVG(node *ast.OrganizationNode, x, y, level int) string {
  410. svg := fmt.Sprintf(`
  411. <g transform="translate(%d,%d)">
  412. <rect x="-80" y="-25" width="160" height="50" class="orgNode"/>
  413. <text x="0" y="5" class="orgText">%s</text>
  414. </g>`, x, y, node.Name)
  415. // Render children
  416. if len(node.Children) > 0 {
  417. childY := y + 100
  418. totalWidth := len(node.Children) * 200
  419. startX := x - totalWidth/2 + 100
  420. for i, child := range node.Children {
  421. childX := startX + i*200
  422. // Connection line
  423. svg += fmt.Sprintf(`<line x1="%d" y1="%d" x2="%d" y2="%d" class="orgEdge"/>`,
  424. x, y+25, childX, childY-25)
  425. // Recursively render child
  426. svg += e.renderOrgNodeSVG(child, childX, childY, level+1)
  427. }
  428. }
  429. return svg
  430. }
  431. // createArcPath creates SVG arc path for pie slices
  432. func (e *SVGExporter) createArcPath(centerX, centerY, radius, startAngle, endAngle float64) string {
  433. x1 := centerX + radius*math.Cos(startAngle)
  434. y1 := centerY + radius*math.Sin(startAngle)
  435. x2 := centerX + radius*math.Cos(endAngle)
  436. y2 := centerY + radius*math.Sin(endAngle)
  437. largeArc := 0
  438. if endAngle-startAngle > math.Pi {
  439. largeArc = 1
  440. }
  441. return fmt.Sprintf("M %g %g L %g %g A %g %g 0 %d 1 %g %g Z",
  442. centerX, centerY, x1, y1, radius, radius, largeArc, x2, y2)
  443. }
  444. // min returns the minimum of two integers
  445. func min(a, b int) int {
  446. if a < b {
  447. return a
  448. }
  449. return b
  450. }