svg-maker: svgc - .svgd text format -> SVG for agent-helm sharing
Line-based DSL (shapes, text, groups, color vars) with strict validation and line-numbered errors, SVG emitter with visibility defaults, truecolor terminal preview (same half-block technique as spritec), info with bbox + outside-canvas warnings. Verified end-to-end: svgc build -> helmd share -> visible through the hub. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011KikHkfCiC3yELbsMN8fT9
This commit is contained in:
135
svg-maker/svg/emit.go
Normal file
135
svg-maker/svg/emit.go
Normal file
@@ -0,0 +1,135 @@
|
||||
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, `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 %s %s" width="%s" height="%s">`,
|
||||
num(d.W), num(d.H), num(d.W), num(d.H))
|
||||
b.WriteString("\n")
|
||||
if d.Bg != "" {
|
||||
fmt.Fprintf(&b, ` <rect width="100%%" height="100%%" fill="%s"/>`+"\n", d.Bg)
|
||||
}
|
||||
for _, el := range d.Elems {
|
||||
emitElem(&b, el, 1)
|
||||
}
|
||||
b.WriteString("</svg>\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<rect x="%s" y="%s" width="%s" height="%s"%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<circle cx="%s" cy="%s" r="%s"%s/>`+"\n",
|
||||
ind, num(el.Nums[0]), num(el.Nums[1]), num(el.Nums[2]), attrs)
|
||||
case "ellipse":
|
||||
fmt.Fprintf(b, `%s<ellipse cx="%s" cy="%s" rx="%s" ry="%s"%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<line x1="%s" y1="%s" x2="%s" y2="%s"%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<path d="%s"%s/>`+"\n", ind, escape(el.D), attrs)
|
||||
case "text":
|
||||
fmt.Fprintf(b, `%s<text x="%s" y="%s"%s>%s</text>`+"\n",
|
||||
ind, num(el.Nums[0]), num(el.Nums[1]), attrs, escape(el.Text))
|
||||
case "group":
|
||||
fmt.Fprintf(b, "%s<g%s>\n", ind, attrs)
|
||||
for _, kid := range el.Kids {
|
||||
emitElem(b, kid, depth+1)
|
||||
}
|
||||
fmt.Fprintf(b, "%s</g>\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
|
||||
}
|
||||
Reference in New Issue
Block a user