package d2svg
import (
"bytes"
_ "embed"
"encoding/base64"
"errors"
"fmt"
"hash/fnv"
"html"
"io"
"sort"
"strings"
"math"
"github.com/alecthomas/chroma/v2"
"github.com/alecthomas/chroma/v2/formatters"
"github.com/alecthomas/chroma/v2/lexers"
"github.com/alecthomas/chroma/v2/styles"
"oss.terrastruct.com/d2/d2ast"
"oss.terrastruct.com/d2/d2graph"
"oss.terrastruct.com/d2/d2renderers/d2fonts"
"oss.terrastruct.com/d2/d2renderers/d2latex"
"oss.terrastruct.com/d2/d2renderers/d2sketch"
"oss.terrastruct.com/d2/d2target"
"oss.terrastruct.com/d2/d2themes"
"oss.terrastruct.com/d2/d2themes/d2themescatalog"
"oss.terrastruct.com/d2/lib/color"
"oss.terrastruct.com/d2/lib/geo"
"oss.terrastruct.com/d2/lib/jsrunner"
"oss.terrastruct.com/d2/lib/label"
"oss.terrastruct.com/d2/lib/shape"
"oss.terrastruct.com/d2/lib/svg"
"oss.terrastruct.com/d2/lib/textmeasure"
"oss.terrastruct.com/d2/lib/version"
"oss.terrastruct.com/util-go/go2"
)
const (
DEFAULT_PADDING = 100
appendixIconRadius = 16
)
var multipleOffset = geo .NewVector (d2target .MULTIPLE_OFFSET , -d2target .MULTIPLE_OFFSET )
var TooltipIcon string
var LinkIcon string
var BaseStylesheet string
var MarkdownCSS string
var dots string
var lines string
var grain string
var paper string
type RenderOpts struct {
Pad *int64
Sketch *bool
Center *bool
ThemeID *int64
DarkThemeID *int64
ThemeOverrides *d2target .ThemeOverrides
DarkThemeOverrides *d2target .ThemeOverrides
Font string
Scale *float64
MasterID string
NoXMLTag *bool
Salt *string
}
func dimensions(diagram *d2target .Diagram , pad int ) (left , top , width , height int ) {
tl , br := diagram .BoundingBox ()
left = tl .X - pad
top = tl .Y - pad
width = br .X - tl .X + pad *2
height = br .Y - tl .Y + pad *2
return left , top , width , height
}
func arrowheadMarkerID(diagramHash string , isTarget bool , connection d2target .Connection ) string {
var arrowhead d2target .Arrowhead
if isTarget {
arrowhead = connection .DstArrow
} else {
arrowhead = connection .SrcArrow
}
return fmt .Sprintf ("mk-%s-%s" , diagramHash , hash (fmt .Sprintf ("%s,%t,%d,%s" ,
arrowhead , isTarget , connection .StrokeWidth , connection .Stroke ,
)))
}
func arrowheadMarker(isTarget bool , id string , connection d2target .Connection , inlineTheme *d2themes .Theme ) string {
arrowhead := connection .DstArrow
if !isTarget {
arrowhead = connection .SrcArrow
}
strokeWidth := float64 (connection .StrokeWidth )
width , height := arrowhead .Dimensions (strokeWidth )
var path string
switch arrowhead {
case d2target .ArrowArrowhead :
polygonEl := d2themes .NewThemableElement ("polygon" , inlineTheme )
polygonEl .Fill = connection .Stroke
polygonEl .ClassName = "connection"
polygonEl .Attributes = fmt .Sprintf (`stroke-width="%d"` , connection .StrokeWidth )
if isTarget {
polygonEl .Points = fmt .Sprintf ("%f,%f %f,%f %f,%f %f,%f" ,
0. , 0. ,
width , height /2 ,
0. , height ,
width /4 , height /2 ,
)
} else {
polygonEl .Points = fmt .Sprintf ("%f,%f %f,%f %f,%f %f,%f" ,
0. , height /2 ,
width , 0. ,
width *3 /4 , height /2 ,
width , height ,
)
}
path = polygonEl .Render ()
case d2target .UnfilledTriangleArrowhead :
polygonEl := d2themes .NewThemableElement ("polygon" , inlineTheme )
polygonEl .Fill = d2target .BG_COLOR
polygonEl .Stroke = connection .Stroke
polygonEl .ClassName = "connection"
polygonEl .Attributes = fmt .Sprintf (`stroke-width="%d"` , connection .StrokeWidth )
inset := strokeWidth / 2
if isTarget {
polygonEl .Points = fmt .Sprintf ("%f,%f %f,%f %f,%f" ,
inset , inset ,
width -inset , height /2.0 ,
inset , height -inset ,
)
} else {
polygonEl .Points = fmt .Sprintf ("%f,%f %f,%f %f,%f" ,
width -inset , inset ,
inset , height /2.0 ,
width -inset , height -inset ,
)
}
path = polygonEl .Render ()
case d2target .TriangleArrowhead :
polygonEl := d2themes .NewThemableElement ("polygon" , inlineTheme )
polygonEl .Fill = connection .Stroke
polygonEl .ClassName = "connection"
polygonEl .Attributes = fmt .Sprintf (`stroke-width="%d"` , connection .StrokeWidth )
if isTarget {
polygonEl .Points = fmt .Sprintf ("%f,%f %f,%f %f,%f" ,
0. , 0. ,
width , height /2.0 ,
0. , height ,
)
} else {
polygonEl .Points = fmt .Sprintf ("%f,%f %f,%f %f,%f" ,
width , 0. ,
0. , height /2.0 ,
width , height ,
)
}
path = polygonEl .Render ()
case d2target .LineArrowhead :
polylineEl := d2themes .NewThemableElement ("polyline" , inlineTheme )
polylineEl .Fill = color .None
polylineEl .ClassName = "connection"
polylineEl .Stroke = connection .Stroke
polylineEl .Attributes = fmt .Sprintf (`stroke-width="%d"` , connection .StrokeWidth )
if isTarget {
polylineEl .Points = fmt .Sprintf ("%f,%f %f,%f %f,%f" ,
strokeWidth /2 , strokeWidth /2 ,
width -strokeWidth /2 , height /2 ,
strokeWidth /2 , height -strokeWidth /2 ,
)
} else {
polylineEl .Points = fmt .Sprintf ("%f,%f %f,%f %f,%f" ,
width -strokeWidth /2 , strokeWidth /2 ,
strokeWidth /2 , height /2 ,
width -strokeWidth /2 , height -strokeWidth /2 ,
)
}
path = polylineEl .Render ()
case d2target .FilledDiamondArrowhead :
polygonEl := d2themes .NewThemableElement ("polygon" , inlineTheme )
polygonEl .ClassName = "connection"
polygonEl .Fill = connection .Stroke
polygonEl .Attributes = fmt .Sprintf (`stroke-width="%d"` , connection .StrokeWidth )
if isTarget {
polygonEl .Points = fmt .Sprintf ("%f,%f %f,%f %f,%f %f,%f" ,
0. , height /2.0 ,
width /2.0 , 0. ,
width , height /2.0 ,
width /2.0 , height ,
)
} else {
polygonEl .Points = fmt .Sprintf ("%f,%f %f,%f %f,%f %f,%f" ,
0. , height /2.0 ,
width /2.0 , 0. ,
width , height /2.0 ,
width /2.0 , height ,
)
}
path = polygonEl .Render ()
case d2target .DiamondArrowhead :
polygonEl := d2themes .NewThemableElement ("polygon" , inlineTheme )
polygonEl .ClassName = "connection"
polygonEl .Fill = d2target .BG_COLOR
polygonEl .Stroke = connection .Stroke
polygonEl .Attributes = fmt .Sprintf (`stroke-width="%d"` , connection .StrokeWidth )
if isTarget {
polygonEl .Points = fmt .Sprintf ("%f,%f %f,%f %f,%f %f,%f" ,
0. , height /2.0 ,
width /2 , height /8 ,
width , height /2.0 ,
width /2.0 , height *0.9 ,
)
} else {
polygonEl .Points = fmt .Sprintf ("%f,%f %f,%f %f,%f %f,%f" ,
width /8 , height /2.0 ,
width *0.6 , height /8 ,
width *1.1 , height /2.0 ,
width *0.6 , height *7 /8 ,
)
}
path = polygonEl .Render ()
case d2target .FilledCircleArrowhead :
radius := width / 2
circleEl := d2themes .NewThemableElement ("circle" , inlineTheme )
circleEl .Cy = radius
circleEl .R = radius - strokeWidth /2
circleEl .Fill = connection .Stroke
circleEl .ClassName = "connection"
circleEl .Attributes = fmt .Sprintf (`stroke-width="%d"` , connection .StrokeWidth )
if isTarget {
circleEl .Cx = radius + strokeWidth /2
} else {
circleEl .Cx = radius - strokeWidth /2
}
path = circleEl .Render ()
case d2target .CircleArrowhead :
radius := width / 2
circleEl := d2themes .NewThemableElement ("circle" , inlineTheme )
circleEl .Cy = radius
circleEl .R = radius - strokeWidth
circleEl .Fill = d2target .BG_COLOR
circleEl .Stroke = connection .Stroke
circleEl .Attributes = fmt .Sprintf (`stroke-width="%d"` , connection .StrokeWidth )
if isTarget {
circleEl .Cx = radius + strokeWidth /2
} else {
circleEl .Cx = radius - strokeWidth /2
}
path = circleEl .Render ()
case d2target .FilledBoxArrowhead :
polygonEl := d2themes .NewThemableElement ("polygon" , inlineTheme )
polygonEl .ClassName = "connection"
polygonEl .Fill = connection .Stroke
polygonEl .Attributes = fmt .Sprintf (`stroke-width="%d"` , connection .StrokeWidth )
if isTarget {
polygonEl .Points = fmt .Sprintf ("%f,%f %f,%f %f,%f %f,%f" ,
0. , 0. ,
0. , height ,
width , height ,
width , 0. ,
)
} else {
polygonEl .Points = fmt .Sprintf ("%f,%f %f,%f %f,%f %f,%f" ,
0. , 0. ,
0. , height ,
width , height ,
width , 0. ,
)
}
path = polygonEl .Render ()
case d2target .BoxArrowhead :
polygonEl := d2themes .NewThemableElement ("polygon" , inlineTheme )
polygonEl .ClassName = "connection"
polygonEl .Fill = d2target .BG_COLOR
polygonEl .Stroke = connection .Stroke
polygonEl .Attributes = fmt .Sprintf (`stroke-width="%d"` , connection .StrokeWidth )
polygonEl .Style = fmt .Sprintf ("%sstroke-linejoin:miter;" , polygonEl .Style )
inset := strokeWidth / 2
if isTarget {
polygonEl .Points = fmt .Sprintf ("%f,%f %f,%f %f,%f %f,%f" ,
inset , inset ,
inset , height -inset ,
width -inset , height -inset ,
width -inset , inset ,
)
} else {
polygonEl .Points = fmt .Sprintf ("%f,%f %f,%f %f,%f %f,%f" ,
inset , inset ,
inset , height -inset ,
width -inset , height -inset ,
width -inset , inset ,
)
}
path = polygonEl .Render ()
case d2target .CfOne , d2target .CfMany , d2target .CfOneRequired , d2target .CfManyRequired :
offset := 3.0 + float64 (connection .StrokeWidth )*1.8
var modifierEl *d2themes .ThemableElement
if arrowhead == d2target .CfOneRequired || arrowhead == d2target .CfManyRequired {
modifierEl = d2themes .NewThemableElement ("path" , inlineTheme )
modifierEl .D = fmt .Sprintf ("M%f,%f %f,%f" ,
offset , 0. ,
offset , height ,
)
modifierEl .Fill = d2target .BG_COLOR
modifierEl .Stroke = connection .Stroke
modifierEl .ClassName = "connection"
modifierEl .Attributes = fmt .Sprintf (`stroke-width="%d"` , connection .StrokeWidth )
} else {
modifierEl = d2themes .NewThemableElement ("circle" , inlineTheme )
modifierEl .Cx = offset /2.0 + 2.0
modifierEl .Cy = height / 2.0
modifierEl .R = offset / 2.0
modifierEl .Fill = d2target .BG_COLOR
modifierEl .Stroke = connection .Stroke
modifierEl .ClassName = "connection"
modifierEl .Attributes = fmt .Sprintf (`stroke-width="%d"` , connection .StrokeWidth )
}
childPathEl := d2themes .NewThemableElement ("path" , inlineTheme )
if arrowhead == d2target .CfMany || arrowhead == d2target .CfManyRequired {
childPathEl .D = fmt .Sprintf ("M%f,%f %f,%f M%f,%f %f,%f M%f,%f %f,%f" ,
width -3.0 , height /2.0 ,
width +offset , height /2.0 ,
offset +3.0 , height /2.0 ,
width +offset , 0. ,
offset +3.0 , height /2.0 ,
width +offset , height ,
)
} else {
childPathEl .D = fmt .Sprintf ("M%f,%f %f,%f M%f,%f %f,%f" ,
width -3.0 , height /2.0 ,
width +offset , height /2.0 ,
offset *2.0 , 0. ,
offset *2.0 , height ,
)
}
gEl := d2themes .NewThemableElement ("g" , inlineTheme )
if !isTarget {
gEl .Transform = fmt .Sprintf ("scale(-1) translate(-%f, -%f)" , width , height )
}
gEl .Fill = d2target .BG_COLOR
gEl .Stroke = connection .Stroke
gEl .ClassName = "connection"
gEl .Attributes = fmt .Sprintf (`stroke-width="%d"` , connection .StrokeWidth )
gEl .Content = fmt .Sprintf ("%s%s" ,
modifierEl .Render (), childPathEl .Render (),
)
path = gEl .Render ()
default :
return ""
}
var refX float64
refY := height / 2
switch arrowhead {
case d2target .DiamondArrowhead :
if isTarget {
refX = width - 0.6 *strokeWidth
} else {
refX = width /8 + 0.6 *strokeWidth
}
width *= 1.1
default :
if isTarget {
refX = width - 1.5 *strokeWidth
} else {
refX = 1.5 * strokeWidth
}
}
return strings .Join ([]string {
fmt .Sprintf (`<marker id="%s" markerWidth="%f" markerHeight="%f" refX="%f" refY="%f"` ,
id , width , height , refX , refY ,
),
fmt .Sprintf (`viewBox="%f %f %f %f"` , 0. , 0. , width , height ),
`orient="auto" markerUnits="userSpaceOnUse">` ,
path ,
"</marker>" ,
}, " " )
}
func arrowheadAdjustment(start , end *geo .Point , arrowhead d2target .Arrowhead , edgeStrokeWidth , shapeStrokeWidth int ) *geo .Point {
distance := (float64 (edgeStrokeWidth ) + float64 (shapeStrokeWidth )) / 2.0
if arrowhead != d2target .NoArrowhead {
distance += float64 (edgeStrokeWidth )
}
v := geo .NewVector (end .X -start .X , end .Y -start .Y )
return v .Unit ().Multiply (-distance ).ToPoint ()
}
func getArrowheadAdjustments(connection d2target .Connection , idToShape map [string ]d2target .Shape ) (srcAdj , dstAdj *geo .Point ) {
route := connection .Route
srcShape := idToShape [connection .Src ]
dstShape := idToShape [connection .Dst ]
sourceAdjustment := arrowheadAdjustment (route [1 ], route [0 ], connection .SrcArrow , connection .StrokeWidth , srcShape .StrokeWidth )
targetAdjustment := arrowheadAdjustment (route [len (route )-2 ], route [len (route )-1 ], connection .DstArrow , connection .StrokeWidth , dstShape .StrokeWidth )
return sourceAdjustment , targetAdjustment
}
func pathData(connection d2target .Connection , srcAdj , dstAdj *geo .Point ) string {
var path []string
route := connection .Route
path = append (path , fmt .Sprintf ("M %f %f" ,
route [0 ].X +srcAdj .X ,
route [0 ].Y +srcAdj .Y ,
))
if connection .IsCurve {
i := 1
for ; i < len (route )-3 ; i += 3 {
path = append (path , fmt .Sprintf ("C %f %f %f %f %f %f" ,
route [i ].X , route [i ].Y ,
route [i +1 ].X , route [i +1 ].Y ,
route [i +2 ].X , route [i +2 ].Y ,
))
}
path = append (path , fmt .Sprintf ("C %f %f %f %f %f %f" ,
route [i ].X , route [i ].Y ,
route [i +1 ].X , route [i +1 ].Y ,
route [i +2 ].X +dstAdj .X ,
route [i +2 ].Y +dstAdj .Y ,
))
} else {
for i := 1 ; i < len (route )-1 ; i ++ {
prevSource := route [i -1 ]
prevTarget := route [i ]
currTarget := route [i +1 ]
prevVector := prevSource .VectorTo (prevTarget )
currVector := prevTarget .VectorTo (currTarget )
dist := geo .EuclideanDistance (prevTarget .X , prevTarget .Y , currTarget .X , currTarget .Y )
connectionBorderRadius := connection .BorderRadius
units := math .Min (connectionBorderRadius , dist /2 )
prevTranslations := prevVector .Unit ().Multiply (units ).ToPoint ()
currTranslations := currVector .Unit ().Multiply (units ).ToPoint ()
path = append (path , fmt .Sprintf ("L %f %f" ,
prevTarget .X -prevTranslations .X ,
prevTarget .Y -prevTranslations .Y ,
))
if units < connectionBorderRadius && i < len (route )-2 {
nextTarget := route [i +2 ]
nextVector := geo .NewVector (nextTarget .X -currTarget .X , nextTarget .Y -currTarget .Y )
i ++
nextTranslations := nextVector .Unit ().Multiply (units ).ToPoint ()
path = append (path , fmt .Sprintf ("C %f %f %f %f %f %f" ,
prevTarget .X +prevTranslations .X ,
prevTarget .Y +prevTranslations .Y ,
currTarget .X -nextTranslations .X ,
currTarget .Y -nextTranslations .Y ,
currTarget .X +nextTranslations .X ,
currTarget .Y +nextTranslations .Y ,
))
} else {
path = append (path , fmt .Sprintf ("S %f %f %f %f" ,
prevTarget .X ,
prevTarget .Y ,
prevTarget .X +currTranslations .X ,
prevTarget .Y +currTranslations .Y ,
))
}
}
lastPoint := route [len (route )-1 ]
path = append (path , fmt .Sprintf ("L %f %f" ,
lastPoint .X +dstAdj .X ,
lastPoint .Y +dstAdj .Y ,
))
}
return strings .Join (path , " " )
}
func makeLabelMask(labelTL *geo .Point , width , height int , opacity float64 ) string {
fill := "black"
if opacity != 1 {
fill = fmt .Sprintf ("rgba(0,0,0,%.2f)" , opacity )
}
return fmt .Sprintf (`<rect x="%f" y="%f" width="%d" height="%d" fill="%s"></rect>` ,
labelTL .X , labelTL .Y ,
width ,
height ,
fill ,
)
}
func drawConnection(writer io .Writer , diagramHash string , connection d2target .Connection , markers map [string ]struct {}, idToShape map [string ]d2target .Shape , jsRunner jsrunner .JSRunner , inlineTheme *d2themes .Theme ) (labelMask string , _ error ) {
opacityStyle := ""
if connection .Opacity != 1.0 {
opacityStyle = fmt .Sprintf (" style='opacity:%f'" , connection .Opacity )
}
classes := []string {base64 .URLEncoding .EncodeToString ([]byte (svg .EscapeText (connection .ID )))}
classes = append (classes , connection .Classes ...)
classStr := fmt .Sprintf (` class="%s"` , strings .Join (classes , " " ))
fmt .Fprintf (writer , `<g%s%s>` , classStr , opacityStyle )
var markerStart string
if connection .SrcArrow != d2target .NoArrowhead {
id := arrowheadMarkerID (diagramHash , false , connection )
if _ , in := markers [id ]; !in {
marker := arrowheadMarker (false , id , connection , inlineTheme )
if marker == "" {
panic (fmt .Sprintf ("received empty arrow head marker for: %#v" , connection ))
}
fmt .Fprint (writer , marker )
markers [id ] = struct {}{}
}
markerStart = fmt .Sprintf (`marker-start="url(#%s)" ` , id )
}
var markerEnd string
if connection .DstArrow != d2target .NoArrowhead {
id := arrowheadMarkerID (diagramHash , true , connection )
if _ , in := markers [id ]; !in {
marker := arrowheadMarker (true , id , connection , inlineTheme )
if marker == "" {
panic (fmt .Sprintf ("received empty arrow head marker for: %#v" , connection ))
}
fmt .Fprint (writer , marker )
markers [id ] = struct {}{}
}
markerEnd = fmt .Sprintf (`marker-end="url(#%s)" ` , id )
}
var labelTL *geo .Point
if connection .Label != "" {
labelTL = connection .GetLabelTopLeft ()
labelTL .X = math .Round (labelTL .X )
labelTL .Y = math .Round (labelTL .Y )
if label .FromString (connection .LabelPosition ).IsOnEdge () {
labelMask = makeLabelMask (labelTL , connection .LabelWidth , connection .LabelHeight , 1 )
} else {
labelMask = makeLabelMask (labelTL , connection .LabelWidth , connection .LabelHeight , 0.75 )
}
}
srcAdj , dstAdj := getArrowheadAdjustments (connection , idToShape )
path := pathData (connection , srcAdj , dstAdj )
mask := fmt .Sprintf (`mask="url(#%s)"` , diagramHash )
if jsRunner != nil {
out , err := d2sketch .Connection (jsRunner , connection , path , mask )
if err != nil {
return "" , err
}
fmt .Fprint (writer , out )
arrowPaths , err := d2sketch .Arrowheads (jsRunner , connection , srcAdj , dstAdj )
if err != nil {
return "" , err
}
fmt .Fprint (writer , arrowPaths )
} else {
animatedClass := ""
if connection .Animated {
animatedClass = " animated-connection"
}
if connection .Animated && ((connection .DstArrow == d2target .NoArrowhead && connection .SrcArrow == d2target .NoArrowhead ) || (connection .DstArrow != d2target .NoArrowhead && connection .SrcArrow != d2target .NoArrowhead )) {
path1 , path2 , err := svg .SplitPath (path , 0.5 )
if err != nil {
return "" , err
}
pathEl1 := d2themes .NewThemableElement ("path" , inlineTheme )
pathEl1 .D = path1
pathEl1 .Fill = color .None
pathEl1 .Stroke = connection .Stroke
pathEl1 .ClassName = fmt .Sprintf ("connection%s" , animatedClass )
pathEl1 .Style = connection .CSSStyle ()
pathEl1 .Style += "animation-direction: reverse;"
pathEl1 .Attributes = fmt .Sprintf ("%s%s" , markerStart , mask )
fmt .Fprint (writer , pathEl1 .Render ())
pathEl2 := d2themes .NewThemableElement ("path" , inlineTheme )
pathEl2 .D = path2
pathEl2 .Fill = color .None
pathEl2 .Stroke = connection .Stroke
pathEl2 .ClassName = fmt .Sprintf ("connection%s" , animatedClass )
pathEl2 .Style = connection .CSSStyle ()
pathEl2 .Attributes = fmt .Sprintf ("%s%s" , markerEnd , mask )
fmt .Fprint (writer , pathEl2 .Render ())
} else {
pathEl := d2themes .NewThemableElement ("path" , inlineTheme )
pathEl .D = path
pathEl .Fill = color .None
pathEl .Stroke = connection .Stroke
pathEl .ClassName = fmt .Sprintf ("connection%s" , animatedClass )
pathEl .Style = connection .CSSStyle ()
pathEl .Attributes = fmt .Sprintf ("%s%s%s" , markerStart , markerEnd , mask )
fmt .Fprint (writer , pathEl .Render ())
}
}
if connection .Label != "" {
fontClass := "text"
if connection .FontFamily == "mono" {
fontClass = "text-mono"
}
if connection .Bold {
fontClass += "-bold"
} else if connection .Italic {
fontClass += "-italic"
}
if connection .Underline {
fontClass += " text-underline"
}
if connection .Fill != color .Empty {
rectEl := d2themes .NewThemableElement ("rect" , inlineTheme )
rectEl .Rx = 10
rectEl .X , rectEl .Y = labelTL .X -4 , labelTL .Y -3
rectEl .Width , rectEl .Height = float64 (connection .LabelWidth )+8 , float64 (connection .LabelHeight )+6
rectEl .Fill = connection .Fill
fmt .Fprint (writer , rectEl .Render ())
}
textEl := d2themes .NewThemableElement ("text" , inlineTheme )
textEl .X = labelTL .X + float64 (connection .LabelWidth )/2
textEl .Y = labelTL .Y + float64 (connection .FontSize )
textEl .ClassName = fontClass
textEl .Style = fmt .Sprintf ("text-anchor:%s;font-size:%vpx" , "middle" , connection .FontSize )
textEl .Content = RenderText (connection .Label , textEl .X , float64 (connection .LabelHeight ))
if connection .Link != "" {
textEl .ClassName += " text-underline text-link"
fmt .Fprintf (writer , `<a href="%s" xlink:href="%[1]s">` , svg .EscapeText (connection .Link ))
} else {
textEl .Fill = connection .GetFontColor ()
}
fmt .Fprint (writer , textEl .Render ())
if connection .Link != "" {
fmt .Fprintf (writer , "</a>" )
}
}
if connection .SrcLabel != nil && connection .SrcLabel .Label != "" {
fmt .Fprint (writer , renderArrowheadLabel (connection , connection .SrcLabel .Label , false , inlineTheme ))
}
if connection .DstLabel != nil && connection .DstLabel .Label != "" {
fmt .Fprint (writer , renderArrowheadLabel (connection , connection .DstLabel .Label , true , inlineTheme ))
}
fmt .Fprintf (writer , `</g>` )
return
}
func renderArrowheadLabel(connection d2target .Connection , text string , isDst bool , inlineTheme *d2themes .Theme ) string {
var width , height float64
if isDst {
width = float64 (connection .DstLabel .LabelWidth )
height = float64 (connection .DstLabel .LabelHeight )
} else {
width = float64 (connection .SrcLabel .LabelWidth )
height = float64 (connection .SrcLabel .LabelHeight )
}
labelTL := connection .GetArrowheadLabelPosition (isDst )
baselineCenter := geo .Point {
X : labelTL .X + width /2. ,
Y : labelTL .Y + float64 (connection .FontSize ),
}
textEl := d2themes .NewThemableElement ("text" , inlineTheme )
textEl .X = baselineCenter .X
textEl .Y = baselineCenter .Y
textEl .Fill = d2target .FG_COLOR
if isDst {
if connection .DstLabel .Color != "" {
textEl .Fill = connection .DstLabel .Color
}
} else {
if connection .SrcLabel .Color != "" {
textEl .Fill = connection .SrcLabel .Color
}
}
textEl .ClassName = "text-italic"
textEl .Style = fmt .Sprintf ("text-anchor:middle;font-size:%vpx" , connection .FontSize )
textEl .Content = RenderText (text , textEl .X , height )
return textEl .Render ()
}
func renderOval(tl *geo .Point , width , height float64 , fill , fillPattern , stroke , style string , inlineTheme *d2themes .Theme ) string {
el := d2themes .NewThemableElement ("ellipse" , inlineTheme )
el .Rx = width / 2
el .Ry = height / 2
el .Cx = tl .X + el .Rx
el .Cy = tl .Y + el .Ry
el .Fill , el .Stroke = fill , stroke
el .FillPattern = fillPattern
el .ClassName = "shape"
el .Style = style
return el .Render ()
}
func renderDoubleOval(tl *geo .Point , width , height float64 , fill , fillStroke , stroke , style string , inlineTheme *d2themes .Theme ) string {
var innerTL *geo .Point = tl .AddVector (geo .NewVector (d2target .INNER_BORDER_OFFSET , d2target .INNER_BORDER_OFFSET ))
return renderOval (tl , width , height , fill , fillStroke , stroke , style , inlineTheme ) + renderOval (innerTL , width -10 , height -10 , fill , "" , stroke , style , inlineTheme )
}
func defineGradients(writer io .Writer , cssGradient string ) {
gradient , _ := color .ParseGradient (cssGradient )
fmt .Fprint (writer , fmt .Sprintf (`<defs>%s</defs>` , color .GradientToSVG (gradient )))
}
func defineShadowFilter(writer io .Writer ) {
fmt .Fprint (writer , `<defs>
<filter id="shadow-filter" width="200%" height="200%" x="-50%" y="-50%">
<feGaussianBlur stdDeviation="1.7 " in="SourceGraphic"></feGaussianBlur>
<feFlood flood-color="#3d4574" flood-opacity="0.4" result="ShadowFeFlood" in="SourceGraphic"></feFlood>
<feComposite in="ShadowFeFlood" in2="SourceAlpha" operator="in" result="ShadowFeComposite"></feComposite>
<feOffset dx="3" dy="5" result="ShadowFeOffset" in="ShadowFeComposite"></feOffset>
<feBlend in="SourceGraphic" in2="ShadowFeOffset" mode="normal" result="ShadowFeBlend"></feBlend>
</filter>
</defs>` )
}
func render3DRect(diagramHash string , targetShape d2target .Shape , inlineTheme *d2themes .Theme ) string {
moveTo := func (p d2target .Point ) string {
return fmt .Sprintf ("M%d,%d" , p .X +targetShape .Pos .X , p .Y +targetShape .Pos .Y )
}
lineTo := func (p d2target .Point ) string {
return fmt .Sprintf ("L%d,%d" , p .X +targetShape .Pos .X , p .Y +targetShape .Pos .Y )
}
var borderSegments []string
borderSegments = append (borderSegments ,
moveTo (d2target .Point {X : 0 , Y : 0 }),
)
for _ , v := range []d2target .Point {
{X : d2target .THREE_DEE_OFFSET , Y : -d2target .THREE_DEE_OFFSET },
{X : targetShape .Width + d2target .THREE_DEE_OFFSET , Y : -d2target .THREE_DEE_OFFSET },
{X : targetShape .Width + d2target .THREE_DEE_OFFSET , Y : targetShape .Height - d2target .THREE_DEE_OFFSET },
{X : targetShape .Width , Y : targetShape .Height },
{X : 0 , Y : targetShape .Height },
{X : 0 , Y : 0 },
{X : targetShape .Width , Y : 0 },
{X : targetShape .Width , Y : targetShape .Height },
} {
borderSegments = append (borderSegments , lineTo (v ))
}
borderSegments = append (borderSegments ,
moveTo (d2target .Point {X : targetShape .Width , Y : 0 }),
)
borderSegments = append (borderSegments ,
lineTo (d2target .Point {X : targetShape .Width + d2target .THREE_DEE_OFFSET , Y : -d2target .THREE_DEE_OFFSET }),
)
border := d2themes .NewThemableElement ("path" , inlineTheme )
border .D = strings .Join (borderSegments , " " )
border .Fill = color .None
_ , borderStroke := d2themes .ShapeTheme (targetShape )
border .Stroke = borderStroke
borderStyle := targetShape .CSSStyle ()
border .Style = borderStyle
renderedBorder := border .Render ()
maskID := fmt .Sprintf ("border-mask-%v-%v" , diagramHash , svg .EscapeText (targetShape .ID ))
borderMask := strings .Join ([]string {
fmt .Sprintf (`<defs><mask id="%s" maskUnits="userSpaceOnUse" x="%d" y="%d" width="%d" height="%d">` ,
maskID , targetShape .Pos .X , targetShape .Pos .Y -d2target .THREE_DEE_OFFSET , targetShape .Width +d2target .THREE_DEE_OFFSET , targetShape .Height +d2target .THREE_DEE_OFFSET ,
),
fmt .Sprintf (`<rect x="%d" y="%d" width="%d" height="%d" fill="white"></rect>` ,
targetShape .Pos .X , targetShape .Pos .Y -d2target .THREE_DEE_OFFSET , targetShape .Width +d2target .THREE_DEE_OFFSET , targetShape .Height +d2target .THREE_DEE_OFFSET ,
),
fmt .Sprintf (`<path d="%s" style="%s;stroke:#000;fill:none;opacity:1;"/></mask></defs>` ,
strings .Join (borderSegments , "" ), borderStyle ),
}, "\n" )
mainShape := d2themes .NewThemableElement ("rect" , inlineTheme )
mainShape .X = float64 (targetShape .Pos .X )
mainShape .Y = float64 (targetShape .Pos .Y )
mainShape .Width = float64 (targetShape .Width )
mainShape .Height = float64 (targetShape .Height )
mainShape .SetMaskUrl (maskID )
mainShapeFill , _ := d2themes .ShapeTheme (targetShape )
mainShape .Fill = mainShapeFill
mainShape .FillPattern = targetShape .FillPattern
mainShape .Stroke = color .None
mainShape .Style = targetShape .CSSStyle ()
mainShapeRendered := mainShape .Render ()
var sidePoints []string
for _ , v := range []d2target .Point {
{X : 0 , Y : 0 },
{X : d2target .THREE_DEE_OFFSET , Y : -d2target .THREE_DEE_OFFSET },
{X : targetShape .Width + d2target .THREE_DEE_OFFSET , Y : -d2target .THREE_DEE_OFFSET },
{X : targetShape .Width + d2target .THREE_DEE_OFFSET , Y : targetShape .Height - d2target .THREE_DEE_OFFSET },
{X : targetShape .Width , Y : targetShape .Height },
{X : targetShape .Width , Y : 0 },
} {
sidePoints = append (sidePoints ,
fmt .Sprintf ("%d,%d" , v .X +targetShape .Pos .X , v .Y +targetShape .Pos .Y ),
)
}
darkerColor , err := color .Darken (targetShape .Fill )
if err != nil {
darkerColor = targetShape .Fill
}
sideShape := d2themes .NewThemableElement ("polygon" , inlineTheme )
sideShape .Fill = darkerColor
sideShape .Points = strings .Join (sidePoints , " " )
sideShape .SetMaskUrl (maskID )
sideShape .Style = targetShape .CSSStyle ()
renderedSides := sideShape .Render ()
return borderMask + mainShapeRendered + renderedSides + renderedBorder
}
func render3DHexagon(diagramHash string , targetShape d2target .Shape , inlineTheme *d2themes .Theme ) string {
moveTo := func (p d2target .Point ) string {
return fmt .Sprintf ("M%d,%d" , p .X +targetShape .Pos .X , p .Y +targetShape .Pos .Y )
}
lineTo := func (p d2target .Point ) string {
return fmt .Sprintf ("L%d,%d" , p .X +targetShape .Pos .X , p .Y +targetShape .Pos .Y )
}
scale := func (n int , f float64 ) int {
return int (float64 (n ) * f )
}
halfYFactor := 43.6 / 87.3
var borderSegments []string
borderSegments = append (borderSegments ,
moveTo (d2target .Point {X : scale (targetShape .Width , 0.25 ), Y : 0 }),
)
Y_OFFSET := d2target .THREE_DEE_OFFSET / 2
for _ , v := range []d2target .Point {
{X : scale (targetShape .Width , 0.25 ) + d2target .THREE_DEE_OFFSET , Y : -Y_OFFSET },
{X : scale (targetShape .Width , 0.75 ) + d2target .THREE_DEE_OFFSET , Y : -Y_OFFSET },
{X : targetShape .Width + d2target .THREE_DEE_OFFSET , Y : scale (targetShape .Height , halfYFactor ) - Y_OFFSET },
{X : scale (targetShape .Width , 0.75 ) + d2target .THREE_DEE_OFFSET , Y : targetShape .Height - Y_OFFSET },
{X : scale (targetShape .Width , 0.75 ), Y : targetShape .Height },
{X : scale (targetShape .Width , 0.25 ), Y : targetShape .Height },
{X : 0 , Y : scale (targetShape .Height , halfYFactor )},
{X : scale (targetShape .Width , 0.25 ), Y : 0 },
{X : scale (targetShape .Width , 0.75 ), Y : 0 },
{X : targetShape .Width , Y : scale (targetShape .Height , halfYFactor )},
{X : scale (targetShape .Width , 0.75 ), Y : targetShape .Height },
} {
borderSegments = append (borderSegments , lineTo (v ))
}
for _ , v := range []d2target .Point {
{X : scale (targetShape .Width , 0.75 ), Y : 0 },
{X : targetShape .Width , Y : scale (targetShape .Height , halfYFactor )},
{X : scale (targetShape .Width , 0.75 ), Y : targetShape .Height },
} {
borderSegments = append (borderSegments , moveTo (v ))
borderSegments = append (borderSegments , lineTo (
d2target .Point {X : v .X + d2target .THREE_DEE_OFFSET , Y : v .Y - Y_OFFSET },
))
}
border := d2themes .NewThemableElement ("path" , inlineTheme )
border .D = strings .Join (borderSegments , " " )
border .Fill = color .None
_ , borderStroke := d2themes .ShapeTheme (targetShape )
border .Stroke = borderStroke
borderStyle := targetShape .CSSStyle ()
border .Style = borderStyle
renderedBorder := border .Render ()
var mainPoints []string
for _ , v := range []d2target .Point {
{X : scale (targetShape .Width , 0.25 ), Y : 0 },
{X : scale (targetShape .Width , 0.75 ), Y : 0 },
{X : targetShape .Width , Y : scale (targetShape .Height , halfYFactor )},
{X : scale (targetShape .Width , 0.75 ), Y : targetShape .Height },
{X : scale (targetShape .Width , 0.25 ), Y : targetShape .Height },
{X : 0 , Y : scale (targetShape .Height , halfYFactor )},
} {
mainPoints = append (mainPoints ,
fmt .Sprintf ("%d,%d" , v .X +targetShape .Pos .X , v .Y +targetShape .Pos .Y ),
)
}
mainPointsPoly := strings .Join (mainPoints , " " )
maskID := fmt .Sprintf ("border-mask-%v-%v" , diagramHash , svg .EscapeText (targetShape .ID ))
borderMask := strings .Join ([]string {
fmt .Sprintf (`<defs><mask id="%s" maskUnits="userSpaceOnUse" x="%d" y="%d" width="%d" height="%d">` ,
maskID , targetShape .Pos .X , targetShape .Pos .Y -d2target .THREE_DEE_OFFSET , targetShape .Width +d2target .THREE_DEE_OFFSET , targetShape .Height +d2target .THREE_DEE_OFFSET ,
),
fmt .Sprintf (`<rect x="%d" y="%d" width="%d" height="%d" fill="white"></rect>` ,
targetShape .Pos .X , targetShape .Pos .Y -d2target .THREE_DEE_OFFSET , targetShape .Width +d2target .THREE_DEE_OFFSET , targetShape .Height +d2target .THREE_DEE_OFFSET ,
),
fmt .Sprintf (`<path d="%s" style="%s;stroke:#000;fill:none;opacity:1;"/></mask></defs>` ,
strings .Join (borderSegments , "" ), borderStyle ),
}, "\n" )
mainShape := d2themes .NewThemableElement ("polygon" , inlineTheme )
mainShape .X = float64 (targetShape .Pos .X )
mainShape .Y = float64 (targetShape .Pos .Y )
mainShape .Points = mainPointsPoly
mainShape .SetMaskUrl (maskID )
mainShapeFill , _ := d2themes .ShapeTheme (targetShape )
mainShape .FillPattern = targetShape .FillPattern
mainShape .Fill = mainShapeFill
mainShape .Stroke = color .None
mainShape .Style = targetShape .CSSStyle ()
mainShapeRendered := mainShape .Render ()
var sidePoints []string
for _ , v := range []d2target .Point {
{X : scale (targetShape .Width , 0.25 ) + d2target .THREE_DEE_OFFSET , Y : -Y_OFFSET },
{X : scale (targetShape .Width , 0.75 ) + d2target .THREE_DEE_OFFSET , Y : -Y_OFFSET },
{X : targetShape .Width + d2target .THREE_DEE_OFFSET , Y : scale (targetShape .Height , halfYFactor ) - Y_OFFSET },
{X : scale (targetShape .Width , 0.75 ) + d2target .THREE_DEE_OFFSET , Y : targetShape .Height - Y_OFFSET },
{X : scale (targetShape .Width , 0.75 ), Y : targetShape .Height },
{X : targetShape .Width , Y : scale (targetShape .Height , halfYFactor )},
{X : scale (targetShape .Width , 0.75 ), Y : 0 },
{X : scale (targetShape .Width , 0.25 ), Y : 0 },
} {
sidePoints = append (sidePoints ,
fmt .Sprintf ("%d,%d" , v .X +targetShape .Pos .X , v .Y +targetShape .Pos .Y ),
)
}
darkerColor , err := color .Darken (targetShape .Fill )
if err != nil {
darkerColor = targetShape .Fill
}
sideShape := d2themes .NewThemableElement ("polygon" , inlineTheme )
sideShape .Fill = darkerColor
sideShape .Points = strings .Join (sidePoints , " " )
sideShape .SetMaskUrl (maskID )
sideShape .Style = targetShape .CSSStyle ()
renderedSides := sideShape .Render ()
return borderMask + mainShapeRendered + renderedSides + renderedBorder
}
func drawShape(writer , appendixWriter io .Writer , diagramHash string , targetShape d2target .Shape , jsRunner jsrunner .JSRunner , inlineTheme *d2themes .Theme ) (labelMask string , err error ) {
closingTag := "</g>"
if targetShape .Link != "" {
fmt .Fprintf (writer , `<a href="%s" xlink:href="%[1]s">` , svg .EscapeText (targetShape .Link ))
closingTag += "</a>"
}
opacityStyle := ""
if targetShape .Opacity != 1.0 {
opacityStyle = fmt .Sprintf (" style='opacity:%f'" , targetShape .Opacity )
}
if targetShape .BorderRadius != 0 && (targetShape .Type == d2target .ShapeClass || targetShape .Type == d2target .ShapeSQLTable ) {
fmt .Fprint (writer , clipPathForBorderRadius (diagramHash , targetShape ))
}
classes := []string {base64 .URLEncoding .EncodeToString ([]byte (svg .EscapeText (targetShape .ID )))}
if targetShape .Animated {
classes = append (classes , "animated-shape" )
}
classes = append (classes , targetShape .Classes ...)
classStr := fmt .Sprintf (` class="%s"` , strings .Join (classes , " " ))
fmt .Fprintf (writer , `<g%s%s>` , classStr , opacityStyle )
tl := geo .NewPoint (float64 (targetShape .Pos .X ), float64 (targetShape .Pos .Y ))
width := float64 (targetShape .Width )
height := float64 (targetShape .Height )
fill , stroke := d2themes .ShapeTheme (targetShape )
style := targetShape .CSSStyle ()
shapeType := d2target .DSL_SHAPE_TO_SHAPE_TYPE [targetShape .Type ]
s := shape .NewShape (shapeType , geo .NewBox (tl , width , height ))
if shapeType == shape .CLOUD_TYPE && targetShape .ContentAspectRatio != nil {
s .SetInnerBoxAspectRatio (*targetShape .ContentAspectRatio )
}
var shadowAttr string
if targetShape .Shadow {
switch targetShape .Type {
case d2target .ShapeText ,
d2target .ShapeCode ,
d2target .ShapeClass ,
d2target .ShapeSQLTable :
default :
shadowAttr = `filter="url(#shadow-filter)" `
}
}
var blendModeClass string
if targetShape .Blend {
blendModeClass = " blend"
}
fmt .Fprintf (writer , `<g class="shape%s" %s>` , blendModeClass , shadowAttr )
var multipleTL *geo .Point
if targetShape .Multiple {
multipleTL = tl .AddVector (multipleOffset )
}
switch targetShape .Type {
case d2target .ShapeClass :
if jsRunner != nil {
out , err := d2sketch .Class (jsRunner , targetShape )
if err != nil {
return "" , err
}
fmt .Fprint (writer , out )
} else {
drawClass (writer , diagramHash , targetShape , inlineTheme )
}
addAppendixItems (appendixWriter , diagramHash , targetShape , s )
fmt .Fprint (writer , `</g>` )
fmt .Fprint (writer , closingTag )
return labelMask , nil
case d2target .ShapeSQLTable :
if jsRunner != nil {
out , err := d2sketch .Table (jsRunner , targetShape )
if err != nil {
return "" , err
}
fmt .Fprint (writer , out )
} else {
drawTable (writer , diagramHash , targetShape , inlineTheme )
}
addAppendixItems (appendixWriter , diagramHash , targetShape , s )
fmt .Fprint (writer , `</g>` )
fmt .Fprint (writer , closingTag )
return labelMask , nil
case d2target .ShapeOval :
if targetShape .DoubleBorder {
if targetShape .Multiple {
fmt .Fprint (writer , renderDoubleOval (multipleTL , width , height , fill , "" , stroke , style , inlineTheme ))
}
if jsRunner != nil {
out , err := d2sketch .DoubleOval (jsRunner , targetShape )
if err != nil {
return "" , err
}
fmt .Fprint (writer , out )
} else {
fmt .Fprint (writer , renderDoubleOval (tl , width , height , fill , targetShape .FillPattern , stroke , style , inlineTheme ))
}
} else {
if targetShape .Multiple {
fmt .Fprint (writer , renderOval (multipleTL , width , height , fill , "" , stroke , style , inlineTheme ))
}
if jsRunner != nil {
out , err := d2sketch .Oval (jsRunner , targetShape )
if err != nil {
return "" , err
}
fmt .Fprint (writer , out )
} else {
fmt .Fprint (writer , renderOval (tl , width , height , fill , targetShape .FillPattern , stroke , style , inlineTheme ))
}
}
case d2target .ShapeImage :
el := d2themes .NewThemableElement ("image" , inlineTheme )
el .X = float64 (targetShape .Pos .X )
el .Y = float64 (targetShape .Pos .Y )
el .Width = float64 (targetShape .Width )
el .Height = float64 (targetShape .Height )
el .Href = html .EscapeString (targetShape .Icon .String ())
el .Fill = fill
el .Stroke = stroke
el .Style = style
fmt .Fprint (writer , el .Render ())
case d2target .ShapeRectangle , d2target .ShapeSequenceDiagram , d2target .ShapeHierarchy , "" :
borderRadius := math .MaxFloat64
if targetShape .BorderRadius != 0 {
borderRadius = float64 (targetShape .BorderRadius )
}
if targetShape .ThreeDee {
fmt .Fprint (writer , render3DRect (diagramHash , targetShape , inlineTheme ))
} else {
if !targetShape .DoubleBorder {
if targetShape .Multiple {
el := d2themes .NewThemableElement ("rect" , inlineTheme )
el .X = float64 (targetShape .Pos .X + 10 )
el .Y = float64 (targetShape .Pos .Y - 10 )
el .Width = float64 (targetShape .Width )
el .Height = float64 (targetShape .Height )
el .Fill = fill
el .Stroke = stroke
el .Style = style
el .Rx = borderRadius
fmt .Fprint (writer , el .Render ())
}
if jsRunner != nil {
out , err := d2sketch .Rect (jsRunner , targetShape )
if err != nil {
return "" , err
}
fmt .Fprint (writer , out )
} else {
el := d2themes .NewThemableElement ("rect" , inlineTheme )
el .X = float64 (targetShape .Pos .X )
el .Y = float64 (targetShape .Pos .Y )
el .Width = float64 (targetShape .Width )
el .Height = float64 (targetShape .Height )
el .Fill = fill
el .FillPattern = targetShape .FillPattern
el .Stroke = stroke
el .Style = style
el .Rx = borderRadius
fmt .Fprint (writer , el .Render ())
}
} else {
if targetShape .Multiple {
el := d2themes .NewThemableElement ("rect" , inlineTheme )
el .X = float64 (targetShape .Pos .X + 10 )
el .Y = float64 (targetShape .Pos .Y - 10 )
el .Width = float64 (targetShape .Width )
el .Height = float64 (targetShape .Height )
el .Fill = fill
el .FillPattern = targetShape .FillPattern
el .Stroke = stroke
el .Style = style
el .Rx = borderRadius
fmt .Fprint (writer , el .Render ())
el = d2themes .NewThemableElement ("rect" , inlineTheme )
el .X = float64 (targetShape .Pos .X + 10 + d2target .INNER_BORDER_OFFSET )
el .Y = float64 (targetShape .Pos .Y - 10 + d2target .INNER_BORDER_OFFSET )
el .Width = float64 (targetShape .Width - 2 *d2target .INNER_BORDER_OFFSET )
el .Height = float64 (targetShape .Height - 2 *d2target .INNER_BORDER_OFFSET )
el .Fill = fill
el .Stroke = stroke
el .Style = style
el .Rx = borderRadius
fmt .Fprint (writer , el .Render ())
}
if jsRunner != nil {
out , err := d2sketch .DoubleRect (jsRunner , targetShape )
if err != nil {
return "" , err
}
fmt .Fprint (writer , out )
} else {
el := d2themes .NewThemableElement ("rect" , inlineTheme )
el .X = float64 (targetShape .Pos .X )
el .Y = float64 (targetShape .Pos .Y )
el .Width = float64 (targetShape .Width )
el .Height = float64 (targetShape .Height )
el .Fill = fill
el .FillPattern = targetShape .FillPattern
el .Stroke = stroke
el .Style = style
el .Rx = borderRadius
fmt .Fprint (writer , el .Render ())
el = d2themes .NewThemableElement ("rect" , inlineTheme )
el .X = float64 (targetShape .Pos .X + d2target .INNER_BORDER_OFFSET )
el .Y = float64 (targetShape .Pos .Y + d2target .INNER_BORDER_OFFSET )
el .Width = float64 (targetShape .Width - 2 *d2target .INNER_BORDER_OFFSET )
el .Height = float64 (targetShape .Height - 2 *d2target .INNER_BORDER_OFFSET )
el .Fill = "transparent"
el .Stroke = stroke
el .Style = style
el .Rx = borderRadius
fmt .Fprint (writer , el .Render ())
}
}
}
case d2target .ShapeHexagon :
if targetShape .ThreeDee {
fmt .Fprint (writer , render3DHexagon (diagramHash , targetShape , inlineTheme ))
} else {
if targetShape .Multiple {
multiplePathData := shape .NewShape (shapeType , geo .NewBox (multipleTL , width , height )).GetSVGPathData ()
el := d2themes .NewThemableElement ("path" , inlineTheme )
el .Fill = fill
el .Stroke = stroke
el .Style = style
for _ , pathData := range multiplePathData {
el .D = pathData
fmt .Fprint (writer , el .Render ())
}
}
if jsRunner != nil {
out , err := d2sketch .Paths (jsRunner , targetShape , s .GetSVGPathData ())
if err != nil {
return "" , err
}
fmt .Fprint (writer , out )
} else {
el := d2themes .NewThemableElement ("path" , inlineTheme )
el .Fill = fill
el .FillPattern = targetShape .FillPattern
el .Stroke = stroke
el .Style = style
for _ , pathData := range s .GetSVGPathData () {
el .D = pathData
fmt .Fprint (writer , el .Render ())
}
}
}
case d2target .ShapeText , d2target .ShapeCode :
default :
if targetShape .Multiple {
multiplePathData := shape .NewShape (shapeType , geo .NewBox (multipleTL , width , height )).GetSVGPathData ()
el := d2themes .NewThemableElement ("path" , inlineTheme )
el .Fill = fill
el .Stroke = stroke
el .Style = style
for _ , pathData := range multiplePathData {
el .D = pathData
fmt .Fprint (writer , el .Render ())
}
}
if jsRunner != nil {
out , err := d2sketch .Paths (jsRunner , targetShape , s .GetSVGPathData ())
if err != nil {
return "" , err
}
fmt .Fprint (writer , out )
} else {
el := d2themes .NewThemableElement ("path" , inlineTheme )
el .Fill = fill
el .FillPattern = targetShape .FillPattern
el .Stroke = stroke
el .Style = style
for _ , pathData := range s .GetSVGPathData () {
el .D = pathData
fmt .Fprint (writer , el .Render ())
}
}
}
fmt .Fprint (writer , `</g>` )
if targetShape .Icon != nil && targetShape .Type != d2target .ShapeImage && targetShape .Opacity != 0 {
iconPosition := label .FromString (targetShape .IconPosition )
var box *geo .Box
if iconPosition .IsOutside () {
box = s .GetBox ()
} else {
box = s .GetInnerBox ()
}
iconSize := d2target .GetIconSize (box , targetShape .IconPosition )
tl := iconPosition .GetPointOnBox (box , label .PADDING , float64 (iconSize ), float64 (iconSize ))
fmt .Fprintf (writer , `<image href="%s" x="%f" y="%f" width="%d" height="%d" />` ,
html .EscapeString (targetShape .Icon .String ()),
tl .X ,
tl .Y ,
iconSize ,
iconSize ,
)
}
if targetShape .Label != "" && targetShape .Opacity != 0 {
labelPosition := label .FromString (targetShape .LabelPosition )
var box *geo .Box
if labelPosition .IsOutside () {
box = s .GetBox ().Copy ()
if targetShape .ThreeDee {
offsetY := d2target .THREE_DEE_OFFSET
if targetShape .Type == d2target .ShapeHexagon {
offsetY /= 2
}
box .TopLeft .Y -= float64 (offsetY )
box .Height += float64 (offsetY )
box .Width += d2target .THREE_DEE_OFFSET
} else if targetShape .Multiple {
box .TopLeft .Y -= d2target .MULTIPLE_OFFSET
box .Height += d2target .MULTIPLE_OFFSET
box .Width += d2target .MULTIPLE_OFFSET
}
} else {
box = s .GetInnerBox ()
}
labelTL := labelPosition .GetPointOnBox (box , label .PADDING ,
float64 (targetShape .LabelWidth ),
float64 (targetShape .LabelHeight ),
)
labelMask = makeLabelMask (labelTL , targetShape .LabelWidth , targetShape .LabelHeight , 0.75 )
fontClass := "text"
if targetShape .FontFamily == "mono" {
fontClass = "text-mono"
}
if targetShape .Bold {
fontClass += "-bold"
} else if targetShape .Italic {
fontClass += "-italic"
}
if targetShape .Underline {
fontClass += " text-underline"
}
if targetShape .Type == d2target .ShapeCode {
lexer := lexers .Get (targetShape .Language )
if lexer == nil {
lexer = lexers .Fallback
}
for _ , isLight := range []bool {true , false } {
theme := "github"
if !isLight {
theme = "catppuccin-mocha"
}
style := styles .Get (theme )
if style == nil {
return labelMask , errors .New (`code snippet style "github" not found` )
}
formatter := formatters .Get ("svg" )
if formatter == nil {
return labelMask , errors .New (`code snippet formatter "svg" not found` )
}
iterator , err := lexer .Tokenise (nil , targetShape .Label )
if err != nil {
return labelMask , err
}
svgStyles := styleToSVG (style )
class := "light-code"
if !isLight {
class = "dark-code"
}
var fontSize string
if targetShape .FontSize != d2fonts .FONT_SIZE_M {
fontSize = fmt .Sprintf (` style="font-size:%v"` , targetShape .FontSize )
}
fmt .Fprintf (writer , `<g transform="translate(%f %f)" class="%s"%s>` ,
box .TopLeft .X , box .TopLeft .Y , class , fontSize ,
)
rectEl := d2themes .NewThemableElement ("rect" , inlineTheme )
rectEl .Width = float64 (targetShape .Width )
rectEl .Height = float64 (targetShape .Height )
rectEl .Stroke = targetShape .Stroke
rectEl .ClassName = "shape"
rectEl .Style = fmt .Sprintf (`fill:%s;stroke-width:%d;` ,
style .Get (chroma .Background ).Background .String (),
targetShape .StrokeWidth ,
)
fmt .Fprint (writer , rectEl .Render ())
padding := float64 (targetShape .FontSize ) / 2.
fmt .Fprintf (writer , `<g transform="translate(%f %f)">` , padding , padding )
lineHeight := textmeasure .CODE_LINE_HEIGHT
for index , tokens := range chroma .SplitTokensIntoLines (iterator .Tokens ()) {
fmt .Fprintf (writer , "<text class=\"text-mono\" x=\"0\" y=\"%fem\">" , 1 +float64 (index )*lineHeight )
for _ , token := range tokens {
text := svgEscaper .Replace (token .String ())
attr := styleAttr (svgStyles , token .Type )
if attr != "" {
text = fmt .Sprintf ("<tspan %s>%s</tspan>" , attr , text )
}
fmt .Fprint (writer , text )
}
fmt .Fprint (writer , "</text>" )
}
fmt .Fprint (writer , "</g></g>" )
}
} else if targetShape .Type == d2target .ShapeText && targetShape .Language == "latex" {
render , err := d2latex .Render (targetShape .Label )
if err != nil {
return labelMask , err
}
gEl := d2themes .NewThemableElement ("g" , inlineTheme )
gEl .SetTranslate (float64 (box .TopLeft .X ), float64 (box .TopLeft .Y ))
gEl .Color = targetShape .Stroke
gEl .Content = render
fmt .Fprint (writer , gEl .Render ())
} else if targetShape .Type == d2target .ShapeText && targetShape .Language != "" {
render , err := textmeasure .RenderMarkdown (targetShape .Label )
if err != nil {
return labelMask , err
}
fmt .Fprintf (writer , `<g><foreignObject requiredFeatures="http://www.w3.org/TR/SVG11/feature#Extensibility" x="%f" y="%f" width="%d" height="%d">` ,
box .TopLeft .X , box .TopLeft .Y , targetShape .Width , targetShape .Height ,
)
render = strings .ReplaceAll (render , "<hr>" , "<hr />" )
mdEl := d2themes .NewThemableElement ("div" , inlineTheme )
mdEl .ClassName = "md"
mdEl .Content = render
var styles []string
if targetShape .FontSize != textmeasure .MarkdownFontSize {
styles = append (styles , fmt .Sprintf ("font-size:%vpx" , targetShape .FontSize ))
}
if targetShape .Fill != "" && targetShape .Fill != "transparent" {
styles = append (styles , fmt .Sprintf (`background-color:%s` , targetShape .Fill ))
}
if !color .IsThemeColor (targetShape .Color ) {
styles = append (styles , fmt .Sprintf (`color:%s` , targetShape .Color ))
}
mdEl .Style = strings .Join (styles , ";" )
fmt .Fprint (writer , mdEl .Render ())
fmt .Fprint (writer , `</foreignObject></g>` )
} else {
if targetShape .LabelFill != "" {
rectEl := d2themes .NewThemableElement ("rect" , inlineTheme )
rectEl .X = labelTL .X
rectEl .Y = labelTL .Y
rectEl .Width = float64 (targetShape .LabelWidth )
rectEl .Height = float64 (targetShape .LabelHeight )
rectEl .Fill = targetShape .LabelFill
fmt .Fprint (writer , rectEl .Render ())
}
textEl := d2themes .NewThemableElement ("text" , inlineTheme )
textEl .X = labelTL .X + float64 (targetShape .LabelWidth )/2
textEl .Y = labelTL .Y + float64 (targetShape .FontSize )
textEl .Fill = targetShape .GetFontColor ()
textEl .ClassName = fontClass
textEl .Style = fmt .Sprintf ("text-anchor:%s;font-size:%vpx" , "middle" , targetShape .FontSize )
textEl .Content = RenderText (targetShape .Label , textEl .X , float64 (targetShape .LabelHeight ))
fmt .Fprint (writer , textEl .Render ())
if targetShape .Blend {
labelMask = makeLabelMask (labelTL , targetShape .LabelWidth , targetShape .LabelHeight -d2graph .INNER_LABEL_PADDING , 1 )
}
}
}
if targetShape .Tooltip != "" {
fmt .Fprintf (writer , `<title>%s</title>` ,
svg .EscapeText (targetShape .Tooltip ),
)
}
addAppendixItems (appendixWriter , diagramHash , targetShape , s )
fmt .Fprint (writer , closingTag )
return labelMask , nil
}
func addAppendixItems(writer io .Writer , diagramHash string , targetShape d2target .Shape , s shape .Shape ) {
var p1 , p2 *geo .Point
if targetShape .Tooltip != "" || targetShape .Link != "" {
bothIcons := targetShape .Tooltip != "" && targetShape .Link != ""
corner := geo .NewPoint (float64 (targetShape .Pos .X +targetShape .Width ), float64 (targetShape .Pos .Y ))
center := geo .NewPoint (
float64 (targetShape .Pos .X )+float64 (targetShape .Width )/2. ,
float64 (targetShape .Pos .Y )+float64 (targetShape .Height )/2. ,
)
offset := geo .Vector {-2 * appendixIconRadius , 0 }
var leftOnShape bool
switch s .GetType () {
case shape .STEP_TYPE , shape .HEXAGON_TYPE , shape .QUEUE_TYPE , shape .PAGE_TYPE :
center .Y = float64 (targetShape .Pos .Y )
case shape .PACKAGE_TYPE :
center .X = float64 (targetShape .Pos .X + targetShape .Width )
case shape .CIRCLE_TYPE , shape .OVAL_TYPE , shape .DIAMOND_TYPE ,
shape .PERSON_TYPE , shape .CLOUD_TYPE , shape .CYLINDER_TYPE :
if bothIcons {
leftOnShape = true
corner = corner .AddVector (offset )
}
}
v1 := center .VectorTo (corner )
p1 = shape .TraceToShapeBorder (s , corner , corner .AddVector (v1 ))
if bothIcons {
if leftOnShape {
p2 = p1 .AddVector (offset .Reverse ())
p1 , p2 = p2 , p1
} else {
p2 = p1 .AddVector (offset )
}
}
}
if targetShape .Tooltip != "" {
x := int (math .Ceil (p1 .X ))
y := int (math .Ceil (p1 .Y ))
fmt .Fprintf (writer , `<g transform="translate(%d %d)" class="appendix-icon"><title>%s</title>%s</g>` ,
x -appendixIconRadius ,
y -appendixIconRadius ,
svg .EscapeText (targetShape .Tooltip ),
fmt .Sprintf (TooltipIcon , diagramHash , svg .SVGID (targetShape .ID )),
)
}
if targetShape .Link != "" {
if p2 == nil {
p2 = p1
}
x := int (math .Ceil (p2 .X ))
y := int (math .Ceil (p2 .Y ))
fmt .Fprintf (writer , `<g transform="translate(%d %d)" class="appendix-icon">%s</g>` ,
x -appendixIconRadius ,
y -appendixIconRadius ,
fmt .Sprintf (LinkIcon , diagramHash , svg .SVGID (targetShape .ID )),
)
}
}
func RenderText (text string , x , height float64 ) string {
if !strings .Contains (text , "\n" ) {
return svg .EscapeText (text )
}
rendered := []string {}
lines := strings .Split (text , "\n" )
for i , line := range lines {
dy := height / float64 (len (lines ))
if i == 0 {
dy = 0
}
escaped := svg .EscapeText (line )
if escaped == "" {
escaped = " "
}
rendered = append (rendered , fmt .Sprintf (`<tspan x="%f" dy="%f">%s</tspan>` , x , dy , escaped ))
}
return strings .Join (rendered , "" )
}
func EmbedFonts (buf *bytes .Buffer , diagramHash , source string , fontFamily *d2fonts .FontFamily , corpus string ) {
fmt .Fprint (buf , `<style type="text/css"><![CDATA[` )
appendOnTrigger (
buf ,
source ,
[]string {
`class="text"` ,
`class="text ` ,
`class="md"` ,
},
fmt .Sprintf (`
.%s .text {
font-family: "%s-font-regular";
}
@font-face {
font-family: %s-font-regular;
src: url("%s");
}` ,
diagramHash ,
diagramHash ,
diagramHash ,
fontFamily .Font (0 , d2fonts .FONT_STYLE_REGULAR ).GetEncodedSubset (corpus ),
),
)
appendOnTrigger (
buf ,
source ,
[]string {`class="md"` },
fmt .Sprintf (`
@font-face {
font-family: %s-font-semibold;
src: url("%s");
}` ,
diagramHash ,
fontFamily .Font (0 , d2fonts .FONT_STYLE_SEMIBOLD ).GetEncodedSubset (corpus ),
),
)
appendOnTrigger (
buf ,
source ,
[]string {
`text-underline` ,
},
`
.text-underline {
text-decoration: underline;
}` ,
)
appendOnTrigger (
buf ,
source ,
[]string {
`text-link` ,
},
`
.text-link {
fill: blue;
}
.text-link:visited {
fill: purple;
}` ,
)
appendOnTrigger (
buf ,
source ,
[]string {
`animated-connection` ,
},
`
@keyframes dashdraw {
from {
stroke-dashoffset: 0;
}
}
` ,
)
appendOnTrigger (
buf ,
source ,
[]string {
`animated-shape` ,
},
`
@keyframes shapeappear {
0%, 100% { transform: translateY(0); filter: drop-shadow(0px 0px 0px rgba(0,0,0,0)); }
50% { transform: translateY(-4px); filter: drop-shadow(0px 12.6px 25.2px rgba(50,50,93,0.25)) drop-shadow(0px 7.56px 15.12px rgba(0,0,0,0.1)); }
}
.animated-shape {
animation: shapeappear 1s linear infinite;
}
` ,
)
appendOnTrigger (
buf ,
source ,
[]string {
`appendix-icon` ,
},
`
.appendix-icon {
filter: drop-shadow(0px 0px 32px rgba(31, 36, 58, 0.1));
}` ,
)
appendOnTrigger (
buf ,
source ,
[]string {
`class="text-bold` ,
`<b>` ,
`<strong>` ,
},
fmt .Sprintf (`
.%s .text-bold {
font-family: "%s-font-bold";
}
@font-face {
font-family: %s-font-bold;
src: url("%s");
}` ,
diagramHash ,
diagramHash ,
diagramHash ,
fontFamily .Font (0 , d2fonts .FONT_STYLE_BOLD ).GetEncodedSubset (corpus ),
),
)
appendOnTrigger (
buf ,
source ,
[]string {
`class="text-italic` ,
`<em>` ,
`<dfn>` ,
},
fmt .Sprintf (`
.%s .text-italic {
font-family: "%s-font-italic";
}
@font-face {
font-family: %s-font-italic;
src: url("%s");
}` ,
diagramHash ,
diagramHash ,
diagramHash ,
fontFamily .Font (0 , d2fonts .FONT_STYLE_ITALIC ).GetEncodedSubset (corpus ),
),
)
appendOnTrigger (
buf ,
source ,
[]string {
`class="text-mono` ,
`<pre>` ,
`<code>` ,
`<kbd>` ,
`<samp>` ,
},
fmt .Sprintf (`
.%s .text-mono {
font-family: "%s-font-mono";
}
@font-face {
font-family: %s-font-mono;
src: url("%s");
}` ,
diagramHash ,
diagramHash ,
diagramHash ,
d2fonts .SourceCodePro .Font (0 , d2fonts .FONT_STYLE_REGULAR ).GetEncodedSubset (corpus ),
),
)
appendOnTrigger (
buf ,
source ,
[]string {
`class="text-mono-bold` ,
},
fmt .Sprintf (`
.%s .text-mono-bold {
font-family: "%s-font-mono-bold";
}
@font-face {
font-family: %s-font-mono-bold;
src: url("%s");
}` ,
diagramHash ,
diagramHash ,
diagramHash ,
d2fonts .SourceCodePro .Font (0 , d2fonts .FONT_STYLE_BOLD ).GetEncodedSubset (corpus ),
),
)
appendOnTrigger (
buf ,
source ,
[]string {
`class="text-mono-italic` ,
},
fmt .Sprintf (`
.%s .text-mono-italic {
font-family: "%s-font-mono-italic";
}
@font-face {
font-family: %s-font-mono-italic;
src: url("%s");
}` ,
diagramHash ,
diagramHash ,
diagramHash ,
d2fonts .SourceCodePro .Font (0 , d2fonts .FONT_STYLE_ITALIC ).GetEncodedSubset (corpus ),
),
)
appendOnTrigger (
buf ,
source ,
[]string {
`sketch-overlay-bright` ,
},
fmt .Sprintf (`
.sketch-overlay-bright {
fill: url(#streaks-bright-%s);
mix-blend-mode: darken;
}` , diagramHash ),
)
appendOnTrigger (
buf ,
source ,
[]string {
`sketch-overlay-normal` ,
},
fmt .Sprintf (`
.sketch-overlay-normal {
fill: url(#streaks-normal-%s);
mix-blend-mode: color-burn;
}` , diagramHash ),
)
appendOnTrigger (
buf ,
source ,
[]string {
`sketch-overlay-dark` ,
},
fmt .Sprintf (`
.sketch-overlay-dark {
fill: url(#streaks-dark-%s);
mix-blend-mode: overlay;
}` , diagramHash ),
)
appendOnTrigger (
buf ,
source ,
[]string {
`sketch-overlay-darker` ,
},
fmt .Sprintf (`
.sketch-overlay-darker {
fill: url(#streaks-darker-%s);
mix-blend-mode: lighten;
}` , diagramHash ),
)
fmt .Fprint (buf , `]]></style>` )
}
func appendOnTrigger(buf *bytes .Buffer , source string , triggers []string , newContent string ) {
for _ , trigger := range triggers {
if strings .Contains (source , trigger ) {
fmt .Fprint (buf , newContent )
break
}
}
}
var DEFAULT_DARK_THEME *int64 = nil
func Render (diagram *d2target .Diagram , opts *RenderOpts ) ([]byte , error ) {
var jsRunner jsrunner .JSRunner
pad := DEFAULT_PADDING
themeID := d2themescatalog .NeutralDefault .ID
darkThemeID := DEFAULT_DARK_THEME
var scale *float64
if opts != nil {
if opts .Pad != nil {
pad = int (*opts .Pad )
}
if opts .Sketch != nil && *opts .Sketch {
jsRunner = jsrunner .NewJSRunner ()
err := d2sketch .LoadJS (jsRunner )
if err != nil {
return nil , err
}
}
if opts .ThemeID != nil {
themeID = *opts .ThemeID
}
darkThemeID = opts .DarkThemeID
scale = opts .Scale
} else {
opts = &RenderOpts {}
}
buf := &bytes .Buffer {}
for _ , s := range diagram .Shapes {
if s .Shadow {
defineShadowFilter (buf )
break
}
}
if color .IsGradient (diagram .Root .Fill ) {
defineGradients (buf , diagram .Root .Fill )
}
if color .IsGradient (diagram .Root .Stroke ) {
defineGradients (buf , diagram .Root .Stroke )
}
for _ , s := range diagram .Shapes {
if color .IsGradient (s .Fill ) {
defineGradients (buf , s .Fill )
}
if color .IsGradient (s .Stroke ) {
defineGradients (buf , s .Stroke )
}
if color .IsGradient (s .Color ) {
defineGradients (buf , s .Color )
}
}
for _ , c := range diagram .Connections {
if color .IsGradient (c .Stroke ) {
defineGradients (buf , c .Stroke )
}
if color .IsGradient (c .Fill ) {
defineGradients (buf , c .Fill )
}
}
diagramHash , err := diagram .HashID (opts .Salt )
if err != nil {
return nil , err
}
isolatedDiagramHash := diagramHash
if opts != nil && opts .MasterID != "" {
diagramHash = opts .MasterID
}
idToShape := make (map [string ]d2target .Shape )
allObjects := make ([]DiagramObject , 0 , len (diagram .Shapes )+len (diagram .Connections ))
for _ , s := range diagram .Shapes {
idToShape [s .ID ] = s
allObjects = append (allObjects , s )
}
for _ , c := range diagram .Connections {
allObjects = append (allObjects , c )
}
sortObjects (allObjects )
appendixItemBuf := &bytes .Buffer {}
var labelMasks []string
markers := map [string ]struct {}{}
var inlineTheme *d2themes .Theme
if darkThemeID == nil {
inlineTheme = go2 .Pointer (d2themescatalog .Find (themeID ))
inlineTheme .ApplyOverrides (opts .ThemeOverrides )
}
for _ , obj := range allObjects {
if c , is := obj .(d2target .Connection ); is {
labelMask , err := drawConnection (buf , isolatedDiagramHash , c , markers , idToShape , jsRunner , inlineTheme )
if err != nil {
return nil , err
}
if labelMask != "" {
labelMasks = append (labelMasks , labelMask )
}
} else if s , is := obj .(d2target .Shape ); is {
labelMask , err := drawShape (buf , appendixItemBuf , diagramHash , s , jsRunner , inlineTheme )
if err != nil {
return nil , err
} else if labelMask != "" {
labelMasks = append (labelMasks , labelMask )
}
} else {
return nil , fmt .Errorf ("unknown object of type %T" , obj )
}
}
fmt .Fprint (buf , appendixItemBuf )
left , top , w , h := dimensions (diagram , pad )
fmt .Fprint (buf , strings .Join ([]string {
fmt .Sprintf (`<mask id="%s" maskUnits="userSpaceOnUse" x="%d" y="%d" width="%d" height="%d">` ,
isolatedDiagramHash , left , top , w , h ,
),
fmt .Sprintf (`<rect x="%d" y="%d" width="%d" height="%d" fill="white"></rect>` ,
left , top , w , h ,
),
strings .Join (labelMasks , "\n" ),
`</mask>` ,
}, "\n" ))
upperBuf := &bytes .Buffer {}
if opts .MasterID == "" {
EmbedFonts (upperBuf , diagramHash , buf .String (), diagram .FontFamily , diagram .GetCorpus ())
themeStylesheet , err := ThemeCSS (diagramHash , &themeID , darkThemeID , opts .ThemeOverrides , opts .DarkThemeOverrides )
if err != nil {
return nil , err
}
fmt .Fprintf (upperBuf , `<style type="text/css"><![CDATA[%s%s]]></style>` , BaseStylesheet , themeStylesheet )
hasMarkdown := false
for _ , s := range diagram .Shapes {
if s .Label != "" && s .Type == d2target .ShapeText {
hasMarkdown = true
break
}
}
if hasMarkdown {
css := MarkdownCSS
css = strings .ReplaceAll (css , ".md" , fmt .Sprintf (".%s .md" , diagramHash ))
css = strings .ReplaceAll (css , "font-italic" , fmt .Sprintf ("%s-font-italic" , diagramHash ))
css = strings .ReplaceAll (css , "font-bold" , fmt .Sprintf ("%s-font-bold" , diagramHash ))
css = strings .ReplaceAll (css , "font-mono" , fmt .Sprintf ("%s-font-mono" , diagramHash ))
css = strings .ReplaceAll (css , "font-regular" , fmt .Sprintf ("%s-font-regular" , diagramHash ))
css = strings .ReplaceAll (css , "font-semibold" , fmt .Sprintf ("%s-font-semibold" , diagramHash ))
fmt .Fprintf (upperBuf , `<style type="text/css">%s</style>` , css )
}
if jsRunner != nil {
d2sketch .DefineFillPatterns (upperBuf , diagramHash )
}
}
left -= int (math .Ceil (float64 (diagram .Root .StrokeWidth ) / 2. ))
top -= int (math .Ceil (float64 (diagram .Root .StrokeWidth ) / 2. ))
w += int (math .Ceil (float64 (diagram .Root .StrokeWidth )/2. ) * 2. )
h += int (math .Ceil (float64 (diagram .Root .StrokeWidth )/2. ) * 2. )
backgroundEl := d2themes .NewThemableElement ("rect" , inlineTheme )
backgroundEl .X = float64 (left )
backgroundEl .Y = float64 (top )
backgroundEl .Width = float64 (w )
backgroundEl .Height = float64 (h )
backgroundEl .Fill = diagram .Root .Fill
backgroundEl .Stroke = diagram .Root .Stroke
backgroundEl .FillPattern = diagram .Root .FillPattern
backgroundEl .Rx = float64 (diagram .Root .BorderRadius )
if diagram .Root .StrokeDash != 0 {
dashSize , gapSize := svg .GetStrokeDashAttributes (float64 (diagram .Root .StrokeWidth ), diagram .Root .StrokeDash )
backgroundEl .StrokeDashArray = fmt .Sprintf ("%f, %f" , dashSize , gapSize )
}
backgroundEl .Attributes = fmt .Sprintf (`stroke-width="%d"` , diagram .Root .StrokeWidth )
left -= int (math .Ceil (float64 (diagram .Root .StrokeWidth ) / 2. ))
top -= int (math .Ceil (float64 (diagram .Root .StrokeWidth ) / 2. ))
w += int (math .Ceil (float64 (diagram .Root .StrokeWidth )/2. ) * 2. )
h += int (math .Ceil (float64 (diagram .Root .StrokeWidth )/2. ) * 2. )
doubleBorderElStr := ""
if diagram .Root .DoubleBorder {
offset := d2target .INNER_BORDER_OFFSET
left -= int (math .Ceil (float64 (diagram .Root .StrokeWidth )/2. )) + offset
top -= int (math .Ceil (float64 (diagram .Root .StrokeWidth )/2. )) + offset
w += int (math .Ceil (float64 (diagram .Root .StrokeWidth )/2. )*2. ) + 2 *offset
h += int (math .Ceil (float64 (diagram .Root .StrokeWidth )/2. )*2. ) + 2 *offset
backgroundEl2 := backgroundEl .Copy ()
backgroundEl .Fill = "transparent"
backgroundEl2 .X = float64 (left )
backgroundEl2 .Y = float64 (top )
backgroundEl2 .Width = float64 (w )
backgroundEl2 .Height = float64 (h )
doubleBorderElStr = backgroundEl2 .Render ()
left -= int (math .Ceil (float64 (diagram .Root .StrokeWidth ) / 2. ))
top -= int (math .Ceil (float64 (diagram .Root .StrokeWidth ) / 2. ))
w += int (math .Ceil (float64 (diagram .Root .StrokeWidth )/2. ) * 2. )
h += int (math .Ceil (float64 (diagram .Root .StrokeWidth )/2. ) * 2. )
}
bufStr := buf .String ()
patternDefs := ""
for _ , pattern := range d2ast .FillPatterns {
if strings .Contains (bufStr , fmt .Sprintf ("%s-overlay" , pattern )) || diagram .Root .FillPattern == pattern {
if patternDefs == "" {
fmt .Fprint (upperBuf , `<style type="text/css"><![CDATA[` )
}
switch pattern {
case "dots" :
patternDefs += fmt .Sprintf (dots , diagramHash )
case "lines" :
patternDefs += fmt .Sprintf (lines , diagramHash )
case "grain" :
patternDefs += fmt .Sprintf (grain , diagramHash )
case "paper" :
patternDefs += fmt .Sprintf (paper , diagramHash )
}
fmt .Fprintf (upperBuf , `
.%s-overlay {
fill: url(#%s-%s);
mix-blend-mode: multiply;
}` , pattern , pattern , diagramHash )
}
}
if patternDefs != "" {
fmt .Fprint (upperBuf , `]]></style>` )
fmt .Fprint (upperBuf , "<defs>" )
fmt .Fprint (upperBuf , patternDefs )
fmt .Fprint (upperBuf , "</defs>" )
}
var dimensions string
if scale != nil {
dimensions = fmt .Sprintf (` width="%d" height="%d"` ,
int (math .Ceil ((*scale )*float64 (w ))),
int (math .Ceil ((*scale )*float64 (h ))),
)
}
alignment := "xMinYMin"
if opts .Center != nil && *opts .Center {
alignment = "xMidYMid"
}
fitToScreenWrapperOpening := ""
xmlTag := ""
fitToScreenWrapperClosing := ""
idAttr := ""
tag := "g"
if opts .MasterID == "" {
fitToScreenWrapperOpening = fmt .Sprintf (`<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" data-d2-version="%s" preserveAspectRatio="%s meet" viewBox="0 0 %d %d"%s>` ,
version .Version ,
alignment ,
w , h ,
dimensions ,
)
if opts .NoXMLTag == nil || !*opts .NoXMLTag {
xmlTag = `<?xml version="1.0" encoding="utf-8"?>`
}
fitToScreenWrapperClosing = "</svg>"
idAttr = `d2-svg`
tag = "svg"
}
docRendered := fmt .Sprintf (`%s%s<%s class="%s" width="%d" height="%d" viewBox="%d %d %d %d">%s%s%s%s</%s>%s` ,
xmlTag ,
fitToScreenWrapperOpening ,
tag ,
strings .Join ([]string {diagramHash , idAttr }, " " ),
w , h , left , top , w , h ,
doubleBorderElStr ,
backgroundEl .Render (),
upperBuf .String (),
buf .String (),
tag ,
fitToScreenWrapperClosing ,
)
return []byte (docRendered ), nil
}
func ThemeCSS (diagramHash string , themeID *int64 , darkThemeID *int64 , overrides , darkOverrides *d2target .ThemeOverrides ) (stylesheet string , err error ) {
if themeID == nil {
themeID = &d2themescatalog .NeutralDefault .ID
}
out , err := singleThemeRulesets (diagramHash , *themeID , overrides )
if err != nil {
return "" , err
}
if darkThemeID != nil {
darkOut , err := singleThemeRulesets (diagramHash , *darkThemeID , darkOverrides )
if err != nil {
return "" , err
}
out += fmt .Sprintf ("@media screen and (prefers-color-scheme:dark){%s}" , darkOut )
}
return out , nil
}
func singleThemeRulesets(diagramHash string , themeID int64 , overrides *d2target .ThemeOverrides ) (rulesets string , err error ) {
out := ""
theme := d2themescatalog .Find (themeID )
theme .ApplyOverrides (overrides )
for _ , property := range []string {"fill" , "stroke" , "background-color" , "color" } {
out += fmt .Sprintf (`
.%s .%s-N1{%s:%s;}
.%s .%s-N2{%s:%s;}
.%s .%s-N3{%s:%s;}
.%s .%s-N4{%s:%s;}
.%s .%s-N5{%s:%s;}
.%s .%s-N6{%s:%s;}
.%s .%s-N7{%s:%s;}
.%s .%s-B1{%s:%s;}
.%s .%s-B2{%s:%s;}
.%s .%s-B3{%s:%s;}
.%s .%s-B4{%s:%s;}
.%s .%s-B5{%s:%s;}
.%s .%s-B6{%s:%s;}
.%s .%s-AA2{%s:%s;}
.%s .%s-AA4{%s:%s;}
.%s .%s-AA5{%s:%s;}
.%s .%s-AB4{%s:%s;}
.%s .%s-AB5{%s:%s;}` ,
diagramHash ,
property , property , theme .Colors .Neutrals .N1 ,
diagramHash ,
property , property , theme .Colors .Neutrals .N2 ,
diagramHash ,
property , property , theme .Colors .Neutrals .N3 ,
diagramHash ,
property , property , theme .Colors .Neutrals .N4 ,
diagramHash ,
property , property , theme .Colors .Neutrals .N5 ,
diagramHash ,
property , property , theme .Colors .Neutrals .N6 ,
diagramHash ,
property , property , theme .Colors .Neutrals .N7 ,
diagramHash ,
property , property , theme .Colors .B1 ,
diagramHash ,
property , property , theme .Colors .B2 ,
diagramHash ,
property , property , theme .Colors .B3 ,
diagramHash ,
property , property , theme .Colors .B4 ,
diagramHash ,
property , property , theme .Colors .B5 ,
diagramHash ,
property , property , theme .Colors .B6 ,
diagramHash ,
property , property , theme .Colors .AA2 ,
diagramHash ,
property , property , theme .Colors .AA4 ,
diagramHash ,
property , property , theme .Colors .AA5 ,
diagramHash ,
property , property , theme .Colors .AB4 ,
diagramHash ,
property , property , theme .Colors .AB5 ,
)
}
out += fmt .Sprintf (".appendix text.text{fill:%s}" , theme .Colors .Neutrals .N1 )
out += fmt .Sprintf (".md{--color-fg-default:%s;--color-fg-muted:%s;--color-fg-subtle:%s;--color-canvas-default:%s;--color-canvas-subtle:%s;--color-border-default:%s;--color-border-muted:%s;--color-neutral-muted:%s;--color-accent-fg:%s;--color-accent-emphasis:%s;--color-attention-subtle:%s;--color-danger-fg:%s;}" ,
theme .Colors .Neutrals .N1 , theme .Colors .Neutrals .N2 , theme .Colors .Neutrals .N3 ,
theme .Colors .Neutrals .N7 , theme .Colors .Neutrals .N6 ,
theme .Colors .B1 , theme .Colors .B2 ,
theme .Colors .Neutrals .N6 ,
theme .Colors .B2 , theme .Colors .B2 ,
theme .Colors .Neutrals .N2 ,
"red" ,
)
lc , err := color .LuminanceCategory (theme .Colors .B1 )
if err != nil {
return "" , err
}
out += fmt .Sprintf (".sketch-overlay-%s{fill:url(#streaks-%s-%s);mix-blend-mode:%s}" , color .B1 , lc , diagramHash , blendMode (lc ))
lc , err = color .LuminanceCategory (theme .Colors .B2 )
if err != nil {
return "" , err
}
out += fmt .Sprintf (".sketch-overlay-%s{fill:url(#streaks-%s-%s);mix-blend-mode:%s}" , color .B2 , lc , diagramHash , blendMode (lc ))
lc , err = color .LuminanceCategory (theme .Colors .B3 )
if err != nil {
return "" , err
}
out += fmt .Sprintf (".sketch-overlay-%s{fill:url(#streaks-%s-%s);mix-blend-mode:%s}" , color .B3 , lc , diagramHash , blendMode (lc ))
lc , err = color .LuminanceCategory (theme .Colors .B4 )
if err != nil {
return "" , err
}
out += fmt .Sprintf (".sketch-overlay-%s{fill:url(#streaks-%s-%s);mix-blend-mode:%s}" , color .B4 , lc , diagramHash , blendMode (lc ))
lc , err = color .LuminanceCategory (theme .Colors .B5 )
if err != nil {
return "" , err
}
out += fmt .Sprintf (".sketch-overlay-%s{fill:url(#streaks-%s-%s);mix-blend-mode:%s}" , color .B5 , lc , diagramHash , blendMode (lc ))
lc , err = color .LuminanceCategory (theme .Colors .B6 )
if err != nil {
return "" , err
}
out += fmt .Sprintf (".sketch-overlay-%s{fill:url(#streaks-%s-%s);mix-blend-mode:%s}" , color .B6 , lc , diagramHash , blendMode (lc ))
lc , err = color .LuminanceCategory (theme .Colors .AA2 )
if err != nil {
return "" , err
}
out += fmt .Sprintf (".sketch-overlay-%s{fill:url(#streaks-%s-%s);mix-blend-mode:%s}" , color .AA2 , lc , diagramHash , blendMode (lc ))
lc , err = color .LuminanceCategory (theme .Colors .AA4 )
if err != nil {
return "" , err
}
out += fmt .Sprintf (".sketch-overlay-%s{fill:url(#streaks-%s-%s);mix-blend-mode:%s}" , color .AA4 , lc , diagramHash , blendMode (lc ))
lc , err = color .LuminanceCategory (theme .Colors .AA5 )
if err != nil {
return "" , err
}
out += fmt .Sprintf (".sketch-overlay-%s{fill:url(#streaks-%s-%s);mix-blend-mode:%s}" , color .AA5 , lc , diagramHash , blendMode (lc ))
lc , err = color .LuminanceCategory (theme .Colors .AB4 )
if err != nil {
return "" , err
}
out += fmt .Sprintf (".sketch-overlay-%s{fill:url(#streaks-%s-%s);mix-blend-mode:%s}" , color .AB4 , lc , diagramHash , blendMode (lc ))
lc , err = color .LuminanceCategory (theme .Colors .AB5 )
if err != nil {
return "" , err
}
out += fmt .Sprintf (".sketch-overlay-%s{fill:url(#streaks-%s-%s);mix-blend-mode:%s}" , color .AB5 , lc , diagramHash , blendMode (lc ))
lc , err = color .LuminanceCategory (theme .Colors .Neutrals .N1 )
if err != nil {
return "" , err
}
out += fmt .Sprintf (".sketch-overlay-%s{fill:url(#streaks-%s-%s);mix-blend-mode:%s}" , color .N1 , lc , diagramHash , blendMode (lc ))
lc , err = color .LuminanceCategory (theme .Colors .Neutrals .N2 )
if err != nil {
return "" , err
}
out += fmt .Sprintf (".sketch-overlay-%s{fill:url(#streaks-%s-%s);mix-blend-mode:%s}" , color .N2 , lc , diagramHash , blendMode (lc ))
lc , err = color .LuminanceCategory (theme .Colors .Neutrals .N3 )
if err != nil {
return "" , err
}
out += fmt .Sprintf (".sketch-overlay-%s{fill:url(#streaks-%s-%s);mix-blend-mode:%s}" , color .N3 , lc , diagramHash , blendMode (lc ))
lc , err = color .LuminanceCategory (theme .Colors .Neutrals .N4 )
if err != nil {
return "" , err
}
out += fmt .Sprintf (".sketch-overlay-%s{fill:url(#streaks-%s-%s);mix-blend-mode:%s}" , color .N4 , lc , diagramHash , blendMode (lc ))
lc , err = color .LuminanceCategory (theme .Colors .Neutrals .N5 )
if err != nil {
return "" , err
}
out += fmt .Sprintf (".sketch-overlay-%s{fill:url(#streaks-%s-%s);mix-blend-mode:%s}" , color .N5 , lc , diagramHash , blendMode (lc ))
lc , err = color .LuminanceCategory (theme .Colors .Neutrals .N6 )
if err != nil {
return "" , err
}
out += fmt .Sprintf (".sketch-overlay-%s{fill:url(#streaks-%s-%s);mix-blend-mode:%s}" , color .N6 , lc , diagramHash , blendMode (lc ))
lc , err = color .LuminanceCategory (theme .Colors .Neutrals .N7 )
if err != nil {
return "" , err
}
out += fmt .Sprintf (".sketch-overlay-%s{fill:url(#streaks-%s-%s);mix-blend-mode:%s}" , color .N7 , lc , diagramHash , blendMode (lc ))
if theme .IsDark () {
out += ".light-code{display: none}"
out += ".dark-code{display: block}"
} else {
out += ".light-code{display: block}"
out += ".dark-code{display: none}"
}
return out , nil
}
func blendMode(lc string ) string {
switch lc {
case "bright" :
return "darken"
case "normal" :
return "color-burn"
case "dark" :
return "overlay"
case "darker" :
return "lighten"
}
panic ("invalid luminance category" )
}
type DiagramObject interface {
GetID () string
GetZIndex () int
}
func sortObjects(allObjects []DiagramObject ) {
sort .SliceStable (allObjects , func (i , j int ) bool {
iZIndex := allObjects [i ].GetZIndex ()
jZIndex := allObjects [j ].GetZIndex ()
if iZIndex != jZIndex {
return iZIndex < jZIndex
}
iShape , iIsShape := allObjects [i ].(d2target .Shape )
jShape , jIsShape := allObjects [j ].(d2target .Shape )
if iIsShape && jIsShape {
return iShape .Level < jShape .Level
}
_ , jIsConnection := allObjects [j ].(d2target .Connection )
return iIsShape && jIsConnection
})
}
func hash(s string ) string {
const secret = "lalalas"
h := fnv .New32a ()
h .Write ([]byte (fmt .Sprintf ("%s%s" , s , secret )))
return fmt .Sprint (h .Sum32 ())
}
func RenderMultiboard (diagram *d2target .Diagram , opts *RenderOpts ) ([][]byte , error ) {
var boards [][]byte
for _ , dl := range diagram .Layers {
childrenBoards , err := RenderMultiboard (dl , opts )
if err != nil {
return nil , err
}
boards = append (boards , childrenBoards ...)
}
for _ , dl := range diagram .Scenarios {
childrenBoards , err := RenderMultiboard (dl , opts )
if err != nil {
return nil , err
}
boards = append (boards , childrenBoards ...)
}
for _ , dl := range diagram .Steps {
childrenBoards , err := RenderMultiboard (dl , opts )
if err != nil {
return nil , err
}
boards = append (boards , childrenBoards ...)
}
if !diagram .IsFolderOnly {
out , err := Render (diagram , opts )
if err != nil {
return boards , err
}
boards = append ([][]byte {out }, boards ...)
return boards , nil
}
return boards , nil
}
The pages are generated with Golds v0.8.2 . (GOOS=linux GOARCH=amd64)
Golds is a Go 101 project developed by Tapir Liu .
PR and bug reports are welcome and can be submitted to the issue list .
Please follow @zigo_101 (reachable from the left QR code) to get the latest news of Golds .