Add ability to set CSS classes instead of inline styles (#103)

* Add ability to set CSS classes instead of inline styles

This allows to set a `ClassName` field in `Style` structs. Setting this
field to anything but "" will cause all other styles to be ignored. The
element will then have a `class=` tag instead with the corresponding name.

Possible reasons to use this:
* Including multiple SVGs on the same webside, using the same styles
* Desire to use strict CSP headers

* Add warning that setting `ClassName` will drop all other inline styles
This commit is contained in:
Justin Kromlinger 2018-10-12 02:21:46 +02:00 committed by Will Charczuk
parent 6735e8990a
commit f97f94425f
6 changed files with 120 additions and 7 deletions

View file

@ -0,0 +1,55 @@
package main
import (
"fmt"
"github.com/wcharczuk/go-chart"
"log"
"net/http"
)
func inlineSVGWithClasses(res http.ResponseWriter, req *http.Request) {
res.Write([]byte(
"<!DOCTYPE html><html><head>" +
"<link rel=\"stylesheet\" type=\"text/css\" href=\"/main.css\">" +
"</head>" +
"<body>"))
pie := chart.PieChart{
// Note that setting ClassName will cause all other inline styles to be dropped!
Background: chart.Style{ClassName: "background"},
Canvas: chart.Style{
ClassName: "canvas",
},
Width: 512,
Height: 512,
Values: []chart.Value{
{Value: 5, Label: "Blue", Style: chart.Style{ClassName: "blue"}},
{Value: 5, Label: "Green", Style: chart.Style{ClassName: "green"}},
{Value: 4, Label: "Gray", Style: chart.Style{ClassName: "gray"}},
},
}
err := pie.Render(chart.SVG, res)
if err != nil {
fmt.Printf("Error rendering pie chart: %v\n", err)
}
res.Write([]byte("</body>"))
}
func css(res http.ResponseWriter, req *http.Request) {
res.Header().Set("Content-Type", "text/css")
res.Write([]byte("svg .background { fill: white; }" +
"svg .canvas { fill: white; }" +
"svg path.blue { fill: blue; stroke: lightblue; }" +
"svg path.green { fill: green; stroke: lightgreen; }" +
"svg path.gray { fill: gray; stroke: lightgray; }" +
"svg text.blue { fill: white; }" +
"svg text.green { fill: white; }" +
"svg text.gray { fill: white; }"))
}
func main() {
http.HandleFunc("/", inlineSVGWithClasses)
http.HandleFunc("/main.css", css)
log.Fatal(http.ListenAndServe(":8080", nil))
}

View file

@ -49,6 +49,10 @@ func (rr *rasterRenderer) SetDPI(dpi float64) {
rr.gc.SetDPI(dpi) rr.gc.SetDPI(dpi)
} }
// SetClassName implements the interface method. However, PNGs have no classes.
func (vr *rasterRenderer) SetClassName(_ string) {
}
// SetStrokeColor implements the interface method. // SetStrokeColor implements the interface method.
func (rr *rasterRenderer) SetStrokeColor(c drawing.Color) { func (rr *rasterRenderer) SetStrokeColor(c drawing.Color) {
rr.s.StrokeColor = c rr.s.StrokeColor = c

View file

@ -18,6 +18,9 @@ type Renderer interface {
// SetDPI sets the DPI for the renderer. // SetDPI sets the DPI for the renderer.
SetDPI(dpi float64) SetDPI(dpi float64)
// SetClassName sets the current class name.
SetClassName(string)
// SetStrokeColor sets the current stroke color. // SetStrokeColor sets the current stroke color.
SetStrokeColor(drawing.Color) SetStrokeColor(drawing.Color)

View file

@ -39,6 +39,8 @@ type Style struct {
Show bool Show bool
Padding Box Padding Box
ClassName string
StrokeWidth float64 StrokeWidth float64
StrokeColor drawing.Color StrokeColor drawing.Color
StrokeDashArray []float64 StrokeDashArray []float64
@ -71,7 +73,8 @@ func (s Style) IsZero() bool {
s.FillColor.IsZero() && s.FillColor.IsZero() &&
s.FontColor.IsZero() && s.FontColor.IsZero() &&
s.FontSize == 0 && s.FontSize == 0 &&
s.Font == nil s.Font == nil &&
s.ClassName == ""
} }
// String returns a text representation of the style. // String returns a text representation of the style.
@ -87,6 +90,12 @@ func (s Style) String() string {
output = []string{"\"show\": false"} output = []string{"\"show\": false"}
} }
if s.ClassName != "" {
output = append(output, fmt.Sprintf("\"class_name\": %s", s.ClassName))
} else {
output = append(output, "\"class_name\": null")
}
if !s.Padding.IsZero() { if !s.Padding.IsZero() {
output = append(output, fmt.Sprintf("\"padding\": %s", s.Padding.String())) output = append(output, fmt.Sprintf("\"padding\": %s", s.Padding.String()))
} else { } else {
@ -155,6 +164,16 @@ func (s Style) String() string {
return "{" + strings.Join(output, ", ") + "}" return "{" + strings.Join(output, ", ") + "}"
} }
func (s Style) GetClassName(defaults ...string) string {
if s.ClassName == "" {
if len(defaults) > 0 {
return defaults[0]
}
return ""
}
return s.ClassName
}
// GetStrokeColor returns the stroke color. // GetStrokeColor returns the stroke color.
func (s Style) GetStrokeColor(defaults ...drawing.Color) drawing.Color { func (s Style) GetStrokeColor(defaults ...drawing.Color) drawing.Color {
if s.StrokeColor.IsZero() { if s.StrokeColor.IsZero() {
@ -321,6 +340,7 @@ func (s Style) GetTextRotationDegrees(defaults ...float64) float64 {
// WriteToRenderer passes the style's options to a renderer. // WriteToRenderer passes the style's options to a renderer.
func (s Style) WriteToRenderer(r Renderer) { func (s Style) WriteToRenderer(r Renderer) {
r.SetClassName(s.GetClassName())
r.SetStrokeColor(s.GetStrokeColor()) r.SetStrokeColor(s.GetStrokeColor())
r.SetStrokeWidth(s.GetStrokeWidth()) r.SetStrokeWidth(s.GetStrokeWidth())
r.SetStrokeDashArray(s.GetStrokeDashArray()) r.SetStrokeDashArray(s.GetStrokeDashArray())
@ -337,6 +357,7 @@ func (s Style) WriteToRenderer(r Renderer) {
// WriteDrawingOptionsToRenderer passes just the drawing style options to a renderer. // WriteDrawingOptionsToRenderer passes just the drawing style options to a renderer.
func (s Style) WriteDrawingOptionsToRenderer(r Renderer) { func (s Style) WriteDrawingOptionsToRenderer(r Renderer) {
r.SetClassName(s.GetClassName())
r.SetStrokeColor(s.GetStrokeColor()) r.SetStrokeColor(s.GetStrokeColor())
r.SetStrokeWidth(s.GetStrokeWidth()) r.SetStrokeWidth(s.GetStrokeWidth())
r.SetStrokeDashArray(s.GetStrokeDashArray()) r.SetStrokeDashArray(s.GetStrokeDashArray())
@ -345,6 +366,7 @@ func (s Style) WriteDrawingOptionsToRenderer(r Renderer) {
// WriteTextOptionsToRenderer passes just the text style options to a renderer. // WriteTextOptionsToRenderer passes just the text style options to a renderer.
func (s Style) WriteTextOptionsToRenderer(r Renderer) { func (s Style) WriteTextOptionsToRenderer(r Renderer) {
r.SetClassName(s.GetClassName())
r.SetFont(s.GetFont()) r.SetFont(s.GetFont())
r.SetFontColor(s.GetFontColor()) r.SetFontColor(s.GetFontColor())
r.SetFontSize(s.GetFontSize()) r.SetFontSize(s.GetFontSize())
@ -352,6 +374,8 @@ func (s Style) WriteTextOptionsToRenderer(r Renderer) {
// InheritFrom coalesces two styles into a new style. // InheritFrom coalesces two styles into a new style.
func (s Style) InheritFrom(defaults Style) (final Style) { func (s Style) InheritFrom(defaults Style) (final Style) {
final.ClassName = s.GetClassName(defaults.ClassName)
final.StrokeColor = s.GetStrokeColor(defaults.StrokeColor) final.StrokeColor = s.GetStrokeColor(defaults.StrokeColor)
final.StrokeWidth = s.GetStrokeWidth(defaults.StrokeWidth) final.StrokeWidth = s.GetStrokeWidth(defaults.StrokeWidth)
final.StrokeDashArray = s.GetStrokeDashArray(defaults.StrokeDashArray) final.StrokeDashArray = s.GetStrokeDashArray(defaults.StrokeDashArray)
@ -379,6 +403,7 @@ func (s Style) InheritFrom(defaults Style) (final Style) {
// GetStrokeOptions returns the stroke components. // GetStrokeOptions returns the stroke components.
func (s Style) GetStrokeOptions() Style { func (s Style) GetStrokeOptions() Style {
return Style{ return Style{
ClassName: s.ClassName,
StrokeDashArray: s.StrokeDashArray, StrokeDashArray: s.StrokeDashArray,
StrokeColor: s.StrokeColor, StrokeColor: s.StrokeColor,
StrokeWidth: s.StrokeWidth, StrokeWidth: s.StrokeWidth,
@ -388,6 +413,7 @@ func (s Style) GetStrokeOptions() Style {
// GetFillOptions returns the fill components. // GetFillOptions returns the fill components.
func (s Style) GetFillOptions() Style { func (s Style) GetFillOptions() Style {
return Style{ return Style{
ClassName: s.ClassName,
FillColor: s.FillColor, FillColor: s.FillColor,
} }
} }
@ -395,6 +421,7 @@ func (s Style) GetFillOptions() Style {
// GetDotOptions returns the dot components. // GetDotOptions returns the dot components.
func (s Style) GetDotOptions() Style { func (s Style) GetDotOptions() Style {
return Style{ return Style{
ClassName: s.ClassName,
StrokeDashArray: nil, StrokeDashArray: nil,
FillColor: s.DotColor, FillColor: s.DotColor,
StrokeColor: s.DotColor, StrokeColor: s.DotColor,
@ -405,6 +432,7 @@ func (s Style) GetDotOptions() Style {
// GetFillAndStrokeOptions returns the fill and stroke components. // GetFillAndStrokeOptions returns the fill and stroke components.
func (s Style) GetFillAndStrokeOptions() Style { func (s Style) GetFillAndStrokeOptions() Style {
return Style{ return Style{
ClassName: s.ClassName,
StrokeDashArray: s.StrokeDashArray, StrokeDashArray: s.StrokeDashArray,
FillColor: s.FillColor, FillColor: s.FillColor,
StrokeColor: s.StrokeColor, StrokeColor: s.StrokeColor,
@ -415,6 +443,7 @@ func (s Style) GetFillAndStrokeOptions() Style {
// GetTextOptions returns just the text components of the style. // GetTextOptions returns just the text components of the style.
func (s Style) GetTextOptions() Style { func (s Style) GetTextOptions() Style {
return Style{ return Style{
ClassName: s.ClassName,
FontColor: s.FontColor, FontColor: s.FontColor,
FontSize: s.FontSize, FontSize: s.FontSize,
Font: s.Font, Font: s.Font,

View file

@ -54,6 +54,11 @@ func (vr *vectorRenderer) SetDPI(dpi float64) {
vr.c.dpi = dpi vr.c.dpi = dpi
} }
// SetClassName implements the interface method.
func (vr *vectorRenderer) SetClassName(classname string) {
vr.s.ClassName = classname
}
// SetStrokeColor implements the interface method. // SetStrokeColor implements the interface method.
func (vr *vectorRenderer) SetStrokeColor(c drawing.Color) { func (vr *vectorRenderer) SetStrokeColor(c drawing.Color) {
vr.s.StrokeColor = c vr.s.StrokeColor = c
@ -230,20 +235,20 @@ func (c *canvas) Path(d string, style Style) {
if len(style.StrokeDashArray) > 0 { if len(style.StrokeDashArray) > 0 {
strokeDashArrayProperty = c.getStrokeDashArray(style) strokeDashArrayProperty = c.getStrokeDashArray(style)
} }
c.w.Write([]byte(fmt.Sprintf(`<path %s d="%s" style="%s"/>`, strokeDashArrayProperty, d, c.styleAsSVG(style)))) c.w.Write([]byte(fmt.Sprintf(`<path %s d="%s" %s/>`, strokeDashArrayProperty, d, c.styleAsSVG(style))))
} }
func (c *canvas) Text(x, y int, body string, style Style) { func (c *canvas) Text(x, y int, body string, style Style) {
if c.textTheta == nil { if c.textTheta == nil {
c.w.Write([]byte(fmt.Sprintf(`<text x="%d" y="%d" style="%s">%s</text>`, x, y, c.styleAsSVG(style), body))) c.w.Write([]byte(fmt.Sprintf(`<text x="%d" y="%d" %s>%s</text>`, x, y, c.styleAsSVG(style), body)))
} else { } else {
transform := fmt.Sprintf(` transform="rotate(%0.2f,%d,%d)"`, util.Math.RadiansToDegrees(*c.textTheta), x, y) transform := fmt.Sprintf(` transform="rotate(%0.2f,%d,%d)"`, util.Math.RadiansToDegrees(*c.textTheta), x, y)
c.w.Write([]byte(fmt.Sprintf(`<text x="%d" y="%d" style="%s"%s>%s</text>`, x, y, c.styleAsSVG(style), transform, body))) c.w.Write([]byte(fmt.Sprintf(`<text x="%d" y="%d" %s%s>%s</text>`, x, y, c.styleAsSVG(style), transform, body)))
} }
} }
func (c *canvas) Circle(x, y, r int, style Style) { func (c *canvas) Circle(x, y, r int, style Style) {
c.w.Write([]byte(fmt.Sprintf(`<circle cx="%d" cy="%d" r="%d" style="%s"/>`, x, y, r, c.styleAsSVG(style)))) c.w.Write([]byte(fmt.Sprintf(`<circle cx="%d" cy="%d" r="%d" %s/>`, x, y, r, c.styleAsSVG(style))))
} }
func (c *canvas) End() { func (c *canvas) End() {
@ -274,8 +279,11 @@ func (c *canvas) getFontFace(s Style) string {
return fmt.Sprintf("font-family:%s", family) return fmt.Sprintf("font-family:%s", family)
} }
// styleAsSVG returns the style as a svg style string. // styleAsSVG returns the style as a svg style or class string.
func (c *canvas) styleAsSVG(s Style) string { func (c *canvas) styleAsSVG(s Style) string {
if s.ClassName != "" {
return fmt.Sprintf("class=\"%s\"", s.ClassName)
}
sw := s.StrokeWidth sw := s.StrokeWidth
sc := s.StrokeColor sc := s.StrokeColor
fc := s.FillColor fc := s.FillColor
@ -311,5 +319,5 @@ func (c *canvas) styleAsSVG(s Style) string {
if s.Font != nil { if s.Font != nil {
pieces = append(pieces, c.getFontFace(s)) pieces = append(pieces, c.getFontFace(s))
} }
return strings.Join(pieces, ";") return fmt.Sprintf("style=\"%s\"", strings.Join(pieces, ";"))
} }

View file

@ -71,7 +71,21 @@ func TestCanvasStyleSVG(t *testing.T) {
svgString := canvas.styleAsSVG(set) svgString := canvas.styleAsSVG(set)
assert.NotEmpty(svgString) assert.NotEmpty(svgString)
assert.True(strings.HasPrefix(svgString, "style=\""))
assert.True(strings.Contains(svgString, "stroke:rgba(255,255,255,1.0)")) assert.True(strings.Contains(svgString, "stroke:rgba(255,255,255,1.0)"))
assert.True(strings.Contains(svgString, "stroke-width:5")) assert.True(strings.Contains(svgString, "stroke-width:5"))
assert.True(strings.Contains(svgString, "fill:rgba(255,255,255,1.0)")) assert.True(strings.Contains(svgString, "fill:rgba(255,255,255,1.0)"))
assert.True(strings.HasSuffix(svgString, "\""))
}
func TestCanvasClassSVG(t *testing.T) {
as := assert.New(t)
set := Style{
ClassName: "test-class",
}
canvas := &canvas{dpi: DefaultDPI}
as.Equal("class=\"test-class\"", canvas.styleAsSVG(set))
} }