diff --git a/d2compiler/compile.go b/d2compiler/compile.go index 97f55f4a1..41b260f11 100644 --- a/d2compiler/compile.go +++ b/d2compiler/compile.go @@ -383,7 +383,9 @@ func (c *compiler) compileField(obj *d2graph.Object, f *d2ir.Field) { } else if isReserved { c.compileReserved(&obj.Attributes, f) - return + if keyword != "icon" { + return + } } else if f.Name.ScalarString() == "style" && f.Name.IsUnquoted() { if f.Map() == nil || len(f.Map().Fields) == 0 { c.errorf(f.LastRef().AST(), `"style" expected to be set to a map of key-values, or contain an additional keyword like "style.opacity: 0.4"`) @@ -473,7 +475,10 @@ func (c *compiler) compileLabel(attrs *d2graph.Attributes, f d2ir.Node) { } attrs.Label.Value = scalar.ScalarString() default: - attrs.Label.Value = scalar.ScalarString() + name := f.LastPrimaryKey().Key.Path[0].UnquotedString.Value[0].String + if *name != "icon" { + attrs.Label.Value = scalar.ScalarString() + } } attrs.Label.MapKey = f.LastPrimaryKey() } @@ -757,7 +762,15 @@ func (c *compiler) compileStyleField(attrs *d2graph.Attributes, f *d2ir.Field) { } compileStyleFieldInit(attrs, f) scalar := f.Primary().Value - err := attrs.Style.Apply(f.Name.ScalarString(), scalar.ScalarString()) + + parentKeyword := f.LastPrimaryKey().Key.Path[0].ScalarString() + + var err error + if parentKeyword == "icon" { + err = attrs.IconStyle.Apply(f.Name.ScalarString(), scalar.ScalarString()) + } else { + err = attrs.Style.Apply(f.Name.ScalarString(), scalar.ScalarString()) + } if err != nil { c.errorf(scalar, err.Error()) return @@ -779,7 +792,12 @@ func compileStyleFieldInit(attrs *d2graph.Attributes, f *d2ir.Field) { case "stroke-dash": attrs.Style.StrokeDash = &d2graph.Scalar{MapKey: f.LastPrimaryKey()} case "border-radius": - attrs.Style.BorderRadius = &d2graph.Scalar{MapKey: f.LastPrimaryKey()} + if attrs.Style.BorderRadius == nil { + attrs.Style.BorderRadius = &d2graph.Scalar{MapKey: f.LastPrimaryKey()} + } + if attrs.IconStyle.BorderRadius == nil { + attrs.IconStyle.BorderRadius = &d2graph.Scalar{MapKey: f.LastPrimaryKey()} + } case "shadow": attrs.Style.Shadow = &d2graph.Scalar{MapKey: f.LastPrimaryKey()} case "3d": diff --git a/d2exporter/export.go b/d2exporter/export.go index dc98b51e3..f52134da1 100644 --- a/d2exporter/export.go +++ b/d2exporter/export.go @@ -194,6 +194,9 @@ func applyStyles(shape *d2target.Shape, obj *d2graph.Object) { if obj.Style.DoubleBorder != nil { shape.DoubleBorder, _ = strconv.ParseBool(obj.Style.DoubleBorder.Value) } + if obj.IconStyle.BorderRadius != nil { + shape.IconBorderRadius, _ = strconv.Atoi(obj.IconStyle.BorderRadius.Value) + } } func toShape(obj *d2graph.Object, g *d2graph.Graph) d2target.Shape { diff --git a/d2graph/d2graph.go b/d2graph/d2graph.go index ed387918a..ba9a02657 100644 --- a/d2graph/d2graph.go +++ b/d2graph/d2graph.go @@ -196,10 +196,11 @@ type Attributes struct { Label Scalar `json:"label"` LabelDimensions d2target.TextDimensions `json:"labelDimensions"` - Style Style `json:"style"` - Icon *url.URL `json:"icon,omitempty"` - Tooltip *Scalar `json:"tooltip,omitempty"` - Link *Scalar `json:"link,omitempty"` + Style Style `json:"style"` + Icon *url.URL `json:"icon,omitempty"` + IconStyle Style `json:"iconStyle"` + Tooltip *Scalar `json:"tooltip,omitempty"` + Link *Scalar `json:"link,omitempty"` WidthAttr *Scalar `json:"width,omitempty"` HeightAttr *Scalar `json:"height,omitempty"` diff --git a/d2renderers/d2svg/d2svg.go b/d2renderers/d2svg/d2svg.go index 8c28b9e5e..bfea683de 100644 --- a/d2renderers/d2svg/d2svg.go +++ b/d2renderers/d2svg/d2svg.go @@ -1163,6 +1163,7 @@ func drawShape(writer, appendixWriter io.Writer, diagramHash string, targetShape } case d2target.ShapeImage: + fmt.Fprint(writer, clipPathForIconBorderRadius(diagramHash, targetShape)) el := d2themes.NewThemableElement("image", inlineTheme) el.X = float64(targetShape.Pos.X) el.Y = float64(targetShape.Pos.Y) @@ -1172,6 +1173,7 @@ func drawShape(writer, appendixWriter io.Writer, diagramHash string, targetShape el.Fill = fill el.Stroke = stroke el.Style = style + el.ClipPath = fmt.Sprintf("%v-%v-icon", diagramHash, targetShape.ID) fmt.Fprint(writer, el.Render()) // TODO should standardize "" to rectangle @@ -1364,12 +1366,13 @@ func drawShape(writer, appendixWriter io.Writer, diagramHash string, targetShape tl := iconPosition.GetPointOnBox(box, label.PADDING, float64(iconSize), float64(iconSize)) - fmt.Fprintf(writer, ``, + fmt.Fprintf(writer, ``, html.EscapeString(targetShape.Icon.String()), tl.X, tl.Y, iconSize, iconSize, + targetShape.IconBorderRadius, ) } diff --git a/d2renderers/d2svg/table.go b/d2renderers/d2svg/table.go index f7b2782db..1fb2f15b4 100644 --- a/d2renderers/d2svg/table.go +++ b/d2renderers/d2svg/table.go @@ -41,6 +41,33 @@ func clipPathForBorderRadius(diagramHash string, shape d2target.Shape) string { return out + `fill="none" /> ` } +func clipPathForIconBorderRadius(diagramHash string, shape d2target.Shape) string { + box := geo.NewBox( + geo.NewPoint(float64(shape.Pos.X), float64(shape.Pos.Y)), + float64(shape.Width), + float64(shape.Height), + ) + topX, topY := box.TopLeft.X+box.Width, box.TopLeft.Y + + out := fmt.Sprintf(``, diagramHash, shape.ID) + out += fmt.Sprintf(` ` +} + func tableHeader(diagramHash string, shape d2target.Shape, box *geo.Box, text string, textWidth, textHeight, fontSize float64, inlineTheme *d2themes.Theme) string { rectEl := d2themes.NewThemableElement("rect", inlineTheme) rectEl.X, rectEl.Y = box.TopLeft.X, box.TopLeft.Y diff --git a/d2target/d2target.go b/d2target/d2target.go index 3da7cec08..c50f16db7 100644 --- a/d2target/d2target.go +++ b/d2target/d2target.go @@ -493,11 +493,12 @@ type Shape struct { Multiple bool `json:"multiple"` DoubleBorder bool `json:"double-border"` - Tooltip string `json:"tooltip"` - Link string `json:"link"` - PrettyLink string `json:"prettyLink,omitempty"` - Icon *url.URL `json:"icon"` - IconPosition string `json:"iconPosition"` + Tooltip string `json:"tooltip"` + Link string `json:"link"` + PrettyLink string `json:"prettyLink,omitempty"` + Icon *url.URL `json:"icon"` + IconBorderRadius int `json:"iconBorderRadius"` + IconPosition string `json:"iconPosition"` // Whether the shape should allow shapes behind it to bleed through // Currently just used for sequence diagram groups