From f97f94425fa445314c69c47c647e885a050ef62b Mon Sep 17 00:00:00 2001 From: Justin Kromlinger Date: Fri, 12 Oct 2018 02:21:46 +0200 Subject: [PATCH] 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 --- _examples/css_classes/main.go | 55 +++++++++++++++++++++++++++++++++++ raster_renderer.go | 4 +++ renderer.go | 3 ++ style.go | 31 +++++++++++++++++++- vector_renderer.go | 20 +++++++++---- vector_renderer_test.go | 14 +++++++++ 6 files changed, 120 insertions(+), 7 deletions(-) create mode 100644 _examples/css_classes/main.go diff --git a/_examples/css_classes/main.go b/_examples/css_classes/main.go new file mode 100644 index 0000000..d650e96 --- /dev/null +++ b/_examples/css_classes/main.go @@ -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( + "" + + "" + + "" + + "")) + + 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("")) +} + +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)) +} diff --git a/raster_renderer.go b/raster_renderer.go index 1f18309..18b4fef 100644 --- a/raster_renderer.go +++ b/raster_renderer.go @@ -49,6 +49,10 @@ func (rr *rasterRenderer) SetDPI(dpi float64) { rr.gc.SetDPI(dpi) } +// SetClassName implements the interface method. However, PNGs have no classes. +func (vr *rasterRenderer) SetClassName(_ string) { +} + // SetStrokeColor implements the interface method. func (rr *rasterRenderer) SetStrokeColor(c drawing.Color) { rr.s.StrokeColor = c diff --git a/renderer.go b/renderer.go index 7eb06bb..68a668b 100644 --- a/renderer.go +++ b/renderer.go @@ -18,6 +18,9 @@ type Renderer interface { // SetDPI sets the DPI for the renderer. SetDPI(dpi float64) + // SetClassName sets the current class name. + SetClassName(string) + // SetStrokeColor sets the current stroke color. SetStrokeColor(drawing.Color) diff --git a/style.go b/style.go index 9d1b268..eafc552 100644 --- a/style.go +++ b/style.go @@ -39,6 +39,8 @@ type Style struct { Show bool Padding Box + ClassName string + StrokeWidth float64 StrokeColor drawing.Color StrokeDashArray []float64 @@ -71,7 +73,8 @@ func (s Style) IsZero() bool { s.FillColor.IsZero() && s.FontColor.IsZero() && s.FontSize == 0 && - s.Font == nil + s.Font == nil && + s.ClassName == "" } // String returns a text representation of the style. @@ -87,6 +90,12 @@ func (s Style) String() string { 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() { output = append(output, fmt.Sprintf("\"padding\": %s", s.Padding.String())) } else { @@ -155,6 +164,16 @@ func (s Style) String() string { 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. func (s Style) GetStrokeColor(defaults ...drawing.Color) drawing.Color { if s.StrokeColor.IsZero() { @@ -321,6 +340,7 @@ func (s Style) GetTextRotationDegrees(defaults ...float64) float64 { // WriteToRenderer passes the style's options to a renderer. func (s Style) WriteToRenderer(r Renderer) { + r.SetClassName(s.GetClassName()) r.SetStrokeColor(s.GetStrokeColor()) r.SetStrokeWidth(s.GetStrokeWidth()) r.SetStrokeDashArray(s.GetStrokeDashArray()) @@ -337,6 +357,7 @@ func (s Style) WriteToRenderer(r Renderer) { // WriteDrawingOptionsToRenderer passes just the drawing style options to a renderer. func (s Style) WriteDrawingOptionsToRenderer(r Renderer) { + r.SetClassName(s.GetClassName()) r.SetStrokeColor(s.GetStrokeColor()) r.SetStrokeWidth(s.GetStrokeWidth()) r.SetStrokeDashArray(s.GetStrokeDashArray()) @@ -345,6 +366,7 @@ func (s Style) WriteDrawingOptionsToRenderer(r Renderer) { // WriteTextOptionsToRenderer passes just the text style options to a renderer. func (s Style) WriteTextOptionsToRenderer(r Renderer) { + r.SetClassName(s.GetClassName()) r.SetFont(s.GetFont()) r.SetFontColor(s.GetFontColor()) r.SetFontSize(s.GetFontSize()) @@ -352,6 +374,8 @@ func (s Style) WriteTextOptionsToRenderer(r Renderer) { // InheritFrom coalesces two styles into a new style. func (s Style) InheritFrom(defaults Style) (final Style) { + final.ClassName = s.GetClassName(defaults.ClassName) + final.StrokeColor = s.GetStrokeColor(defaults.StrokeColor) final.StrokeWidth = s.GetStrokeWidth(defaults.StrokeWidth) final.StrokeDashArray = s.GetStrokeDashArray(defaults.StrokeDashArray) @@ -379,6 +403,7 @@ func (s Style) InheritFrom(defaults Style) (final Style) { // GetStrokeOptions returns the stroke components. func (s Style) GetStrokeOptions() Style { return Style{ + ClassName: s.ClassName, StrokeDashArray: s.StrokeDashArray, StrokeColor: s.StrokeColor, StrokeWidth: s.StrokeWidth, @@ -388,6 +413,7 @@ func (s Style) GetStrokeOptions() Style { // GetFillOptions returns the fill components. func (s Style) GetFillOptions() Style { return Style{ + ClassName: s.ClassName, FillColor: s.FillColor, } } @@ -395,6 +421,7 @@ func (s Style) GetFillOptions() Style { // GetDotOptions returns the dot components. func (s Style) GetDotOptions() Style { return Style{ + ClassName: s.ClassName, StrokeDashArray: nil, FillColor: s.DotColor, StrokeColor: s.DotColor, @@ -405,6 +432,7 @@ func (s Style) GetDotOptions() Style { // GetFillAndStrokeOptions returns the fill and stroke components. func (s Style) GetFillAndStrokeOptions() Style { return Style{ + ClassName: s.ClassName, StrokeDashArray: s.StrokeDashArray, FillColor: s.FillColor, StrokeColor: s.StrokeColor, @@ -415,6 +443,7 @@ func (s Style) GetFillAndStrokeOptions() Style { // GetTextOptions returns just the text components of the style. func (s Style) GetTextOptions() Style { return Style{ + ClassName: s.ClassName, FontColor: s.FontColor, FontSize: s.FontSize, Font: s.Font, diff --git a/vector_renderer.go b/vector_renderer.go index 6f9b6f4..c154424 100644 --- a/vector_renderer.go +++ b/vector_renderer.go @@ -54,6 +54,11 @@ func (vr *vectorRenderer) SetDPI(dpi float64) { vr.c.dpi = dpi } +// SetClassName implements the interface method. +func (vr *vectorRenderer) SetClassName(classname string) { + vr.s.ClassName = classname +} + // SetStrokeColor implements the interface method. func (vr *vectorRenderer) SetStrokeColor(c drawing.Color) { vr.s.StrokeColor = c @@ -230,20 +235,20 @@ func (c *canvas) Path(d string, style Style) { if len(style.StrokeDashArray) > 0 { strokeDashArrayProperty = c.getStrokeDashArray(style) } - c.w.Write([]byte(fmt.Sprintf(``, strokeDashArrayProperty, d, c.styleAsSVG(style)))) + c.w.Write([]byte(fmt.Sprintf(``, strokeDashArrayProperty, d, c.styleAsSVG(style)))) } func (c *canvas) Text(x, y int, body string, style Style) { if c.textTheta == nil { - c.w.Write([]byte(fmt.Sprintf(`%s`, x, y, c.styleAsSVG(style), body))) + c.w.Write([]byte(fmt.Sprintf(`%s`, x, y, c.styleAsSVG(style), body))) } else { transform := fmt.Sprintf(` transform="rotate(%0.2f,%d,%d)"`, util.Math.RadiansToDegrees(*c.textTheta), x, y) - c.w.Write([]byte(fmt.Sprintf(`%s`, x, y, c.styleAsSVG(style), transform, body))) + c.w.Write([]byte(fmt.Sprintf(`%s`, x, y, c.styleAsSVG(style), transform, body))) } } func (c *canvas) Circle(x, y, r int, style Style) { - c.w.Write([]byte(fmt.Sprintf(``, x, y, r, c.styleAsSVG(style)))) + c.w.Write([]byte(fmt.Sprintf(``, x, y, r, c.styleAsSVG(style)))) } func (c *canvas) End() { @@ -274,8 +279,11 @@ func (c *canvas) getFontFace(s Style) string { 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 { + if s.ClassName != "" { + return fmt.Sprintf("class=\"%s\"", s.ClassName) + } sw := s.StrokeWidth sc := s.StrokeColor fc := s.FillColor @@ -311,5 +319,5 @@ func (c *canvas) styleAsSVG(s Style) string { if s.Font != nil { pieces = append(pieces, c.getFontFace(s)) } - return strings.Join(pieces, ";") + return fmt.Sprintf("style=\"%s\"", strings.Join(pieces, ";")) } diff --git a/vector_renderer_test.go b/vector_renderer_test.go index ccd7044..19c38b6 100644 --- a/vector_renderer_test.go +++ b/vector_renderer_test.go @@ -71,7 +71,21 @@ func TestCanvasStyleSVG(t *testing.T) { svgString := canvas.styleAsSVG(set) 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-width:5")) 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)) }