// Copyright (c) 2026 Petr BalvĂ­n (https://petrbalvin.org) // SPDX-License-Identifier: MIT package diagram import ( "fmt" "strings" ) // point is a coordinate in the layout. Everything is an integer, so the // same layout always writes the same bytes. type point struct { x, y int } // svgBuilder writes SVG elements in the order they are given. type svgBuilder struct { b strings.Builder width int height int } func newSVGBuilder(width, height int) *svgBuilder { s := &svgBuilder{width: width, height: height} fmt.Fprintf(&s.b, ``, width, height) return s } // rect draws a rectangle, rounded when rx is positive. func (s *svgBuilder) rect(x, y, w, h, rx int, attrs string) { if w < 0 || h < 0 { return } if rx > 0 { fmt.Fprintf(&s.b, ``, x, y, w, h, rx, attrs) } else { fmt.Fprintf(&s.b, ``, x, y, w, h, attrs) } } func (s *svgBuilder) line(x1, y1, x2, y2 int, attrs string) { fmt.Fprintf(&s.b, ``, x1, y1, x2, y2, attrs) } func (s *svgBuilder) path(d, attrs string) { s.b.WriteString(``) } // text writes one line of text. The anchor is middle, start or end. func (s *svgBuilder) text(p point, str, anchor, attrs string) { s.b.WriteString(``) s.b.WriteString(escapeXML(str)) s.b.WriteString(``) } func (s *svgBuilder) polygon(pts []point, attrs string) { if len(pts) < 3 { return } s.b.WriteString(``) } func (s *svgBuilder) finish() []byte { s.b.WriteString("") return []byte(s.b.String()) } var xmlEscaper = strings.NewReplacer("&", "&", "<", "<", ">", ">", `"`, """) func escapeXML(s string) string { return xmlEscaper.Replace(s) } // textWidth estimates the width a string needs at the given font size. // Characters outside the basic plane are wide: the estimate stays on the // generous side so labels keep their padding. func textWidth(s string, size int) int { w := 0 for _, r := range s { if r > 0x2E7F { w += size } else { w += (size*6 + 4) / 10 } } return w } // labelLines splits a label into its lines. func labelLines(s string) []string { if s == "" { return []string{""} } normalised := strings.ReplaceAll(s, "
", "
") normalised = strings.ReplaceAll(normalised, "
", "
") lines := strings.Split(normalised, "
") if len(lines) == 0 { return []string{""} } return lines } // styleString joins style pairs into a style attribute, declaration order // preserved. func styleString(pairs []stylePair) string { if len(pairs) == 0 { return "" } var b strings.Builder b.WriteString(` style="`) for i, p := range pairs { if i > 0 { b.WriteString(";") } b.WriteString(p.key) b.WriteString(":") b.WriteString(p.value) } b.WriteString(`"`) return b.String() }