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:
parent
6735e8990a
commit
f97f94425f
6 changed files with 120 additions and 7 deletions
55
_examples/css_classes/main.go
Normal file
55
_examples/css_classes/main.go
Normal 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))
|
||||||
|
}
|
|
@ -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
|
||||||
|
|
|
@ -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)
|
||||||
|
|
||||||
|
|
31
style.go
31
style.go
|
@ -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,
|
||||||
|
|
|
@ -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, ";"))
|
||||||
}
|
}
|
||||||
|
|
|
@ -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))
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue