package svg
import (
"fmt"
"sort"
"strconv"
"strings"
)
// Emit renders the document as an SVG file.
func Emit(d *Doc) string {
var b strings.Builder
fmt.Fprintf(&b, `\n")
return b.String()
}
func emitElem(b *strings.Builder, el *Elem, depth int) {
ind := strings.Repeat(" ", depth)
attrs := attrString(el.Attrs)
switch el.Kind {
case "rect":
fmt.Fprintf(b, `%s`+"\n",
ind, num(el.Nums[0]), num(el.Nums[1]), num(el.Nums[2]), num(el.Nums[3]), attrs)
case "circle":
fmt.Fprintf(b, `%s`+"\n",
ind, num(el.Nums[0]), num(el.Nums[1]), num(el.Nums[2]), attrs)
case "ellipse":
fmt.Fprintf(b, `%s`+"\n",
ind, num(el.Nums[0]), num(el.Nums[1]), num(el.Nums[2]), num(el.Nums[3]), attrs)
case "line":
fmt.Fprintf(b, `%s`+"\n",
ind, num(el.Nums[0]), num(el.Nums[1]), num(el.Nums[2]), num(el.Nums[3]), attrs)
case "polyline", "polygon":
pts := make([]string, len(el.Points))
for i, p := range el.Points {
pts[i] = num(p[0]) + "," + num(p[1])
}
fmt.Fprintf(b, `%s<%s points="%s"%s/>`+"\n", ind, el.Kind, strings.Join(pts, " "), attrs)
case "path":
fmt.Fprintf(b, `%s`+"\n", ind, escape(el.D), attrs)
case "text":
fmt.Fprintf(b, `%s%s`+"\n",
ind, num(el.Nums[0]), num(el.Nums[1]), attrs, escape(el.Text))
case "group":
fmt.Fprintf(b, "%s\n", ind, attrs)
for _, kid := range el.Kids {
emitElem(b, kid, depth+1)
}
fmt.Fprintf(b, "%s\n", ind)
}
}
// defaults SVG would otherwise pick that surprise agents: lines and
// paths get a visible stroke if none set; polyline defaults fill=none
// so it doesn't render as a filled blob.
func effectiveAttrs(el *Elem) map[string]string {
out := map[string]string{}
for k, v := range el.Attrs {
out[k] = v
}
switch el.Kind {
case "line", "polyline":
if out["stroke"] == "" {
out["stroke"] = "black"
}
if el.Kind == "polyline" && out["fill"] == "" {
out["fill"] = "none"
}
case "path":
if out["stroke"] == "" && out["fill"] == "" {
out["stroke"] = "black"
out["fill"] = "none"
}
}
return out
}
func attrString(attrs map[string]string) string {
if len(attrs) == 0 {
return ""
}
keys := make([]string, 0, len(attrs))
for k := range attrs {
keys = append(keys, k)
}
sort.Strings(keys)
var b strings.Builder
for _, k := range keys {
fmt.Fprintf(&b, ` %s="%s"`, k, escape(attrs[k]))
}
return b.String()
}
func num(f float64) string {
return strconv.FormatFloat(f, 'f', -1, 64)
}
func escape(s string) string {
r := strings.NewReplacer("&", "&", "<", "<", ">", ">", `"`, """)
return r.Replace(s)
}
// normalize applies the effective attrs (visibility defaults) onto the
// tree before emit/raster so both outputs agree.
func (d *Doc) normalize() {
var walk func(els []*Elem)
walk = func(els []*Elem) {
for _, el := range els {
el.Attrs = effectiveAttrs(el)
if el.Kind == "group" {
walk(el.Kids)
}
}
}
walk(d.Elems)
}
// Build parses src and emits SVG in one step.
func Build(src string) (string, *Doc, error) {
doc, err := Parse(src)
if err != nil {
return "", nil, err
}
doc.normalize()
return Emit(doc), doc, nil
}