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, ``, num(d.W), num(d.H), num(d.W), num(d.H)) b.WriteString("\n") if d.Bg != "" { fmt.Fprintf(&b, ` `+"\n", d.Bg) } for _, el := range d.Elems { emitElem(&b, el, 1) } b.WriteString("\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 }