package d2svg
import (
"bytes"
_ "embed"
"encoding/base64"
"errors"
"fmt"
"hash/fnv"
"html"
"io"
"regexp"
"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"
"github.com/pancsta/d2/d2ast"
"github.com/pancsta/d2/d2graph"
"github.com/pancsta/d2/d2renderers/d2fonts"
"github.com/pancsta/d2/d2renderers/d2latex"
"github.com/pancsta/d2/d2renderers/d2sketch"
"github.com/pancsta/d2/d2renderers/internal/patternassets"
"github.com/pancsta/d2/d2target"
"github.com/pancsta/d2/d2themes"
"github.com/pancsta/d2/d2themes/d2themescatalog"
"github.com/pancsta/d2/lib/color"
"github.com/pancsta/d2/lib/geo"
"github.com/pancsta/d2/lib/label"
"github.com/pancsta/d2/lib/shape"
"github.com/pancsta/d2/lib/svg"
"github.com/pancsta/d2/lib/textmeasure"
"github.com/pancsta/d2/lib/version"
"github.com/d2lang/util-go/go2"
)
const (
DEFAULT_PADDING = 100
appendixIconRadius = 16
LEGEND_PADDING = 20
LEGEND_ITEM_SPACING = 15
LEGEND_ICON_SIZE = 24
LEGEND_FONT_SIZE = 14
LEGEND_CORNER_PADDING = 10
)
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
type RenderOpts struct {
Pad *int64
Sketch *bool
Isometric *bool
Center *bool
ThemeID *int64
DarkThemeID *int64
ThemeOverrides *d2target .ThemeOverrides
DarkThemeOverrides *d2target .ThemeOverrides
Font string
Scale *float64
MasterID string
NoXMLTag *bool
Salt *string
OmitVersion *bool
}
func invalidPaddingError(pad int64 ) error {
return fmt .Errorf ("padding %d produces invalid SVG dimensions" , pad )
}
func checkedIntAdd(a , b , minInt , maxInt int64 ) (int64 , bool ) {
if b > 0 && a > maxInt -b || b < 0 && a < minInt -b {
return 0 , false
}
return a + b , true
}
func checkedIntSub(a , b , minInt , maxInt int64 ) (int64 , bool ) {
if b > 0 && a < minInt +b || b < 0 && a > maxInt +b {
return 0 , false
}
return a - b , true
}
func validatePadding(tl , br d2target .Point , pad int64 ) (int , error ) {
maxInt := int64 (^uint (0 ) >> 1 )
minInt := -maxInt - 1
if pad < minInt || pad > maxInt {
return 0 , invalidPaddingError (pad )
}
doublePad , ok := checkedIntAdd (pad , pad , minInt , maxInt )
if !ok {
return 0 , invalidPaddingError (pad )
}
for _ , bounds := range [][2 ]int64 {
{int64 (tl .X ), int64 (br .X )},
{int64 (tl .Y ), int64 (br .Y )},
} {
if _ , ok := checkedIntSub (bounds [0 ], pad , minInt , maxInt ); !ok {
return 0 , invalidPaddingError (pad )
}
size , ok := checkedIntSub (bounds [1 ], bounds [0 ], minInt , maxInt )
if !ok {
return 0 , invalidPaddingError (pad )
}
if _ , ok := checkedIntAdd (size , doublePad , minInt , maxInt ); !ok {
return 0 , invalidPaddingError (pad )
}
}
return int (pad ), nil
}
func expandDimensions(left , top , width , height , amount int ) (int , int , int , int , bool ) {
maxInt := int64 (^uint (0 ) >> 1 )
minInt := -maxInt - 1
amount64 := int64 (amount )
doubleAmount , ok := checkedIntAdd (amount64 , amount64 , minInt , maxInt )
if !ok {
return 0 , 0 , 0 , 0 , false
}
left64 , ok := checkedIntSub (int64 (left ), amount64 , minInt , maxInt )
if !ok {
return 0 , 0 , 0 , 0 , false
}
top64 , ok := checkedIntSub (int64 (top ), amount64 , minInt , maxInt )
if !ok {
return 0 , 0 , 0 , 0 , false
}
width64 , ok := checkedIntAdd (int64 (width ), doubleAmount , minInt , maxInt )
if !ok {
return 0 , 0 , 0 , 0 , false
}
height64 , ok := checkedIntAdd (int64 (height ), doubleAmount , minInt , maxInt )
if !ok {
return 0 , 0 , 0 , 0 , false
}
return int (left64 ), int (top64 ), int (width64 ), int (height64 ), true
}
func dimensions(diagram *d2target .Diagram , pad int , tl , br d2target .Point ) (left , top , width , height int ) {
left = tl .X - pad
top = tl .Y - pad
width = br .X - tl .X + pad *2
height = br .Y - tl .Y + pad *2
if diagram .Legend != nil && (len (diagram .Legend .Shapes ) > 0 || len (diagram .Legend .Connections ) > 0 ) {
totalHeight := LEGEND_PADDING + LEGEND_FONT_SIZE + LEGEND_ITEM_SPACING
maxLabelWidth := 0
itemCount := 0
ruler , err := textmeasure .NewRuler ()
if err == nil && ruler != nil {
for _ , s := range diagram .Legend .Shapes {
if s .Label == "" {
continue
}
mtext := &d2target .MText {
Text : s .Label ,
FontSize : LEGEND_FONT_SIZE ,
}
dims := d2graph .GetTextDimensions (nil , ruler , mtext , fontToFamily (s .FontFamily ))
maxLabelWidth = go2 .IntMax (maxLabelWidth , dims .Width )
totalHeight += go2 .IntMax (dims .Height , LEGEND_ICON_SIZE ) + LEGEND_ITEM_SPACING
itemCount ++
}
for _ , c := range diagram .Legend .Connections {
if c .Label == "" {
continue
}
mtext := &d2target .MText {
Text : c .Label ,
FontSize : LEGEND_FONT_SIZE ,
}
dims := d2graph .GetTextDimensions (nil , ruler , mtext , fontToFamily (c .FontFamily ))
maxLabelWidth = go2 .IntMax (maxLabelWidth , dims .Width )
totalHeight += go2 .IntMax (dims .Height , LEGEND_ICON_SIZE ) + LEGEND_ITEM_SPACING
itemCount ++
}
if itemCount > 0 {
totalHeight -= LEGEND_ITEM_SPACING / 2
}
if itemCount > 0 && len (diagram .Legend .Connections ) > 0 {
totalHeight += LEGEND_PADDING * 1.5
} else {
totalHeight += LEGEND_PADDING * 1.2
}
if totalHeight > 0 && maxLabelWidth > 0 {
legendWidth := LEGEND_PADDING *2 + LEGEND_ICON_SIZE + LEGEND_PADDING + maxLabelWidth
legendY := br .Y - totalHeight
if legendY < tl .Y {
legendY = tl .Y
}
legendRight := br .X + LEGEND_CORNER_PADDING + legendWidth
if left +width < legendRight {
width = legendRight - left + pad /2
}
if legendY < top {
diffY := top - legendY
top -= diffY
height += diffY
}
legendBottom := legendY + totalHeight
if top +height < legendBottom {
height = legendBottom - top + pad /2
}
}
}
}
return left , top , width , height
}
func RenderLegend (buf *bytes .Buffer , diagram *d2target .Diagram , diagramHash string , theme *d2themes .Theme ) error {
if diagram .Legend == nil || (len (diagram .Legend .Shapes ) == 0 && len (diagram .Legend .Connections ) == 0 ) {
return nil
}
for _ , targetShape := range diagram .Legend .Shapes {
if textmeasure .IsDangerousLink (targetShape .Link ) {
return fmt .Errorf ("legend shape %q uses an unsafe link URL scheme" , targetShape .ID )
}
}
_ , br := diagram .BoundingBox ()
ruler , err := textmeasure .NewRuler ()
if err != nil {
return err
}
totalHeight := LEGEND_PADDING + LEGEND_FONT_SIZE + LEGEND_ITEM_SPACING
maxLabelWidth := 0
itemCount := 0
for _ , s := range diagram .Legend .Shapes {
if s .Label == "" {
continue
}
mtext := &d2target .MText {
Text : s .Label ,
FontSize : LEGEND_FONT_SIZE ,
}
dims := d2graph .GetTextDimensions (nil , ruler , mtext , fontToFamily (s .FontFamily ))
maxLabelWidth = go2 .IntMax (maxLabelWidth , dims .Width )
totalHeight += go2 .IntMax (dims .Height , LEGEND_ICON_SIZE ) + LEGEND_ITEM_SPACING
itemCount ++
}
for _ , c := range diagram .Legend .Connections {
if c .Label == "" {
continue
}
mtext := &d2target .MText {
Text : c .Label ,
FontSize : LEGEND_FONT_SIZE ,
}
dims := d2graph .GetTextDimensions (nil , ruler , mtext , fontToFamily (c .FontFamily ))
maxLabelWidth = go2 .IntMax (maxLabelWidth , dims .Width )
totalHeight += go2 .IntMax (dims .Height , LEGEND_ICON_SIZE ) + LEGEND_ITEM_SPACING
itemCount ++
}
if itemCount > 0 {
totalHeight -= LEGEND_ITEM_SPACING / 2
}
if itemCount > 0 && len (diagram .Legend .Connections ) > 0 {
totalHeight += LEGEND_PADDING * 1.5
} else {
totalHeight += LEGEND_PADDING * 1.2
}
legendWidth := LEGEND_PADDING *2 + LEGEND_ICON_SIZE + LEGEND_PADDING + maxLabelWidth
legendX := br .X + LEGEND_CORNER_PADDING
tl , _ := diagram .BoundingBox ()
legendY := br .Y - totalHeight
if legendY < tl .Y {
legendY = tl .Y
}
shadowEl := d2themes .NewThemableElement ("rect" , theme )
shadowEl .Fill = "#F7F7FA"
shadowEl .Stroke = "#DEE1EB"
shadowEl .Style = "stroke-width: 1px; filter: drop-shadow(0px 2px 3px rgba(0, 0, 0, 0.1))"
shadowEl .X = float64 (legendX )
shadowEl .Y = float64 (legendY )
shadowEl .Width = float64 (legendWidth )
shadowEl .Height = float64 (totalHeight )
shadowEl .Rx = 4
fmt .Fprint (buf , shadowEl .Render ())
legendEl := d2themes .NewThemableElement ("rect" , theme )
legendEl .Fill = "#ffffff"
legendEl .Stroke = "#DEE1EB"
legendEl .Style = "stroke-width: 1px"
legendEl .X = float64 (legendX )
legendEl .Y = float64 (legendY )
legendEl .Width = float64 (legendWidth )
legendEl .Height = float64 (totalHeight )
legendEl .Rx = 4
fmt .Fprint (buf , legendEl .Render ())
legendLabel := "Legend"
if diagram .Legend .Label != "" {
legendLabel = diagram .Legend .Label
}
fmt .Fprintf (buf , `<text class="text-bold" x="%d" y="%d" style="font-size: %dpx;">%s</text>` ,
legendX +LEGEND_PADDING , legendY +LEGEND_PADDING +LEGEND_FONT_SIZE , LEGEND_FONT_SIZE +2 , html .EscapeString (legendLabel ))
currentY := legendY + LEGEND_PADDING *2 + LEGEND_FONT_SIZE
shapeCount := 0
for _ , s := range diagram .Legend .Shapes {
if s .Label == "" {
continue
}
iconX := legendX + LEGEND_PADDING
iconY := currentY
shapeIcon , err := renderLegendShapeIcon (s , iconX , iconY , diagramHash , theme )
if err != nil {
return err
}
fmt .Fprint (buf , shapeIcon )
mtext := &d2target .MText {
Text : s .Label ,
FontSize : LEGEND_FONT_SIZE ,
}
dims := d2graph .GetTextDimensions (nil , ruler , mtext , fontToFamily (s .FontFamily ))
rowHeight := go2 .IntMax (dims .Height , LEGEND_ICON_SIZE )
textY := currentY + rowHeight /2 + int (float64 (dims .Height )*0.3 )
fmt .Fprintf (buf , `<text class="text" x="%d" y="%d" style="font-size: %dpx;">%s</text>` ,
iconX +LEGEND_ICON_SIZE +LEGEND_PADDING , textY , LEGEND_FONT_SIZE ,
html .EscapeString (s .Label ))
currentY += rowHeight + LEGEND_ITEM_SPACING
shapeCount ++
}
if shapeCount > 0 && len (diagram .Legend .Connections ) > 0 {
currentY += LEGEND_ITEM_SPACING / 2
separatorEl := d2themes .NewThemableElement ("line" , theme )
separatorEl .X1 = float64 (legendX + LEGEND_PADDING )
separatorEl .Y1 = float64 (currentY )
separatorEl .X2 = float64 (legendX + legendWidth - LEGEND_PADDING )
separatorEl .Y2 = float64 (currentY )
separatorEl .Stroke = "#DEE1EB"
separatorEl .StrokeDashArray = "2,2"
fmt .Fprint (buf , separatorEl .Render ())
currentY += LEGEND_ITEM_SPACING
}
for _ , c := range diagram .Legend .Connections {
if c .Label == "" {
continue
}
iconX := legendX + LEGEND_PADDING
iconY := currentY + LEGEND_ICON_SIZE /2
connIcon , err := renderLegendConnectionIcon (c , iconX , iconY , theme )
if err != nil {
return err
}
fmt .Fprint (buf , connIcon )
mtext := &d2target .MText {
Text : c .Label ,
FontSize : LEGEND_FONT_SIZE ,
}
dims := d2graph .GetTextDimensions (nil , ruler , mtext , fontToFamily (c .FontFamily ))
rowHeight := go2 .IntMax (dims .Height , LEGEND_ICON_SIZE )
textY := currentY + rowHeight /2 + int (float64 (dims .Height )*0.2 )
fmt .Fprintf (buf , `<text class="text" x="%d" y="%d" style="font-size: %dpx;">%s</text>` ,
iconX +LEGEND_ICON_SIZE +LEGEND_PADDING , textY , LEGEND_FONT_SIZE ,
html .EscapeString (c .Label ))
currentY += rowHeight + LEGEND_ITEM_SPACING
}
return nil
}
func renderLegendShapeIcon(s d2target .Shape , x , y int , diagramHash string , theme *d2themes .Theme ) (string , error ) {
iconShape := s
const sizeFactor = 5
iconShape .Pos .X = 0
iconShape .Pos .Y = 0
iconShape .Width = LEGEND_ICON_SIZE * sizeFactor
iconShape .Height = LEGEND_ICON_SIZE * sizeFactor
iconShape .Label = ""
buf := &bytes .Buffer {}
appendixBuf := &bytes .Buffer {}
finalBuf := &bytes .Buffer {}
fmt .Fprintf (finalBuf , `<g transform="translate(%d, %d) scale(%s)">` ,
x , y , svg .FormatFloat (1.0 /sizeFactor ))
_ , err := drawShape (buf , appendixBuf , diagramHash , iconShape , false , theme , newMarkdownRenderer (nil , nil , theme ))
if err != nil {
return "" , err
}
fmt .Fprint (finalBuf , buf .String ())
fmt .Fprint (finalBuf , `</g>` )
return finalBuf .String (), nil
}
func renderLegendConnectionIcon(c d2target .Connection , x , y int , theme *d2themes .Theme ) (string , error ) {
finalBuf := &bytes .Buffer {}
buf := &bytes .Buffer {}
const sizeFactor = 2
legendConn := *d2target .BaseConnection ()
legendConn .ID = c .ID
legendConn .SrcArrow = c .SrcArrow
legendConn .DstArrow = c .DstArrow
legendConn .StrokeDash = c .StrokeDash
legendConn .StrokeWidth = c .StrokeWidth
legendConn .Stroke = c .Stroke
legendConn .Fill = c .Fill
legendConn .BorderRadius = c .BorderRadius
legendConn .Opacity = c .Opacity
legendConn .Animated = c .Animated
startX := 0.0
midY := 0.0
width := float64 (LEGEND_ICON_SIZE * sizeFactor )
legendConn .Route = []*geo .Point {
{X : startX , Y : midY },
{X : startX + width , Y : midY },
}
legendHash := fmt .Sprintf ("legend-%s" , hash (fmt .Sprintf ("%s-%d-%d" , c .ID , x , y )))
markers := make (map [string ]struct {})
idToShape := make (map [string ]d2target .Shape )
fmt .Fprintf (finalBuf , `<g transform="translate(%d, %d) scale(%s)">` ,
x , y , svg .FormatFloat (1.0 /sizeFactor ))
_ , err := drawConnection (buf , legendHash , legendConn , markers , idToShape , false , theme , newMarkdownRenderer (nil , nil , theme ))
if err != nil {
return "" , err
}
fmt .Fprint (finalBuf , buf .String ())
fmt .Fprint (finalBuf , `</g>` )
return finalBuf .String (), nil
}
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 .AddAttributes (svg .IntAttr ("stroke-width" , connection .StrokeWidth ))
if isTarget {
polygonEl .Points = fmt .Sprintf ("%s,%s %s,%s %s,%s %s,%s" ,
svg .FormatFloat (0. ), svg .FormatFloat (0. ),
svg .FormatFloat (width ), svg .FormatFloat (height /2 ),
svg .FormatFloat (0. ), svg .FormatFloat (height ),
svg .FormatFloat (width /4 ), svg .FormatFloat (height /2 ),
)
} else {
polygonEl .Points = fmt .Sprintf ("%s,%s %s,%s %s,%s %s,%s" ,
svg .FormatFloat (0. ), svg .FormatFloat (height /2 ),
svg .FormatFloat (width ), svg .FormatFloat (0. ),
svg .FormatFloat (width *3 /4 ), svg .FormatFloat (height /2 ),
svg .FormatFloat (width ), svg .FormatFloat (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 .AddAttributes (svg .IntAttr ("stroke-width" , connection .StrokeWidth ))
inset := strokeWidth / 2
if isTarget {
polygonEl .Points = fmt .Sprintf ("%s,%s %s,%s %s,%s" ,
svg .FormatFloat (inset ), svg .FormatFloat (inset ),
svg .FormatFloat (width -inset ), svg .FormatFloat (height /2.0 ),
svg .FormatFloat (inset ), svg .FormatFloat (height -inset ),
)
} else {
polygonEl .Points = fmt .Sprintf ("%s,%s %s,%s %s,%s" ,
svg .FormatFloat (width -inset ), svg .FormatFloat (inset ),
svg .FormatFloat (inset ), svg .FormatFloat (height /2.0 ),
svg .FormatFloat (width -inset ), svg .FormatFloat (height -inset ),
)
}
path = polygonEl .Render ()
case d2target .TriangleArrowhead :
polygonEl := d2themes .NewThemableElement ("polygon" , inlineTheme )
polygonEl .Fill = connection .Stroke
polygonEl .ClassName = "connection"
polygonEl .AddAttributes (svg .IntAttr ("stroke-width" , connection .StrokeWidth ))
if isTarget {
polygonEl .Points = fmt .Sprintf ("%s,%s %s,%s %s,%s" ,
svg .FormatFloat (0. ), svg .FormatFloat (0. ),
svg .FormatFloat (width ), svg .FormatFloat (height /2.0 ),
svg .FormatFloat (0. ), svg .FormatFloat (height ),
)
} else {
polygonEl .Points = fmt .Sprintf ("%s,%s %s,%s %s,%s" ,
svg .FormatFloat (width ), svg .FormatFloat (0. ),
svg .FormatFloat (0. ), svg .FormatFloat (height /2.0 ),
svg .FormatFloat (width ), svg .FormatFloat (height ),
)
}
path = polygonEl .Render ()
case d2target .LineArrowhead :
polylineEl := d2themes .NewThemableElement ("polyline" , inlineTheme )
polylineEl .Fill = color .None
polylineEl .ClassName = "connection"
polylineEl .Stroke = connection .Stroke
polylineEl .AddAttributes (svg .IntAttr ("stroke-width" , connection .StrokeWidth ))
if isTarget {
polylineEl .Points = fmt .Sprintf ("%s,%s %s,%s %s,%s" ,
svg .FormatFloat (strokeWidth /2 ), svg .FormatFloat (strokeWidth /2 ),
svg .FormatFloat (width -strokeWidth /2 ), svg .FormatFloat (height /2 ),
svg .FormatFloat (strokeWidth /2 ), svg .FormatFloat (height -strokeWidth /2 ),
)
} else {
polylineEl .Points = fmt .Sprintf ("%s,%s %s,%s %s,%s" ,
svg .FormatFloat (width -strokeWidth /2 ), svg .FormatFloat (strokeWidth /2 ),
svg .FormatFloat (strokeWidth /2 ), svg .FormatFloat (height /2 ),
svg .FormatFloat (width -strokeWidth /2 ), svg .FormatFloat (height -strokeWidth /2 ),
)
}
path = polylineEl .Render ()
case d2target .FilledDiamondArrowhead :
polygonEl := d2themes .NewThemableElement ("polygon" , inlineTheme )
polygonEl .ClassName = "connection"
polygonEl .Fill = connection .Stroke
polygonEl .AddAttributes (svg .IntAttr ("stroke-width" , connection .StrokeWidth ))
if isTarget {
polygonEl .Points = fmt .Sprintf ("%s,%s %s,%s %s,%s %s,%s" ,
svg .FormatFloat (0. ), svg .FormatFloat (height /2.0 ),
svg .FormatFloat (width /2.0 ), svg .FormatFloat (0. ),
svg .FormatFloat (width ), svg .FormatFloat (height /2.0 ),
svg .FormatFloat (width /2.0 ), svg .FormatFloat (height ),
)
} else {
polygonEl .Points = fmt .Sprintf ("%s,%s %s,%s %s,%s %s,%s" ,
svg .FormatFloat (0. ), svg .FormatFloat (height /2.0 ),
svg .FormatFloat (width /2.0 ), svg .FormatFloat (0. ),
svg .FormatFloat (width ), svg .FormatFloat (height /2.0 ),
svg .FormatFloat (width /2.0 ), svg .FormatFloat (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 .AddAttributes (svg .IntAttr ("stroke-width" , connection .StrokeWidth ))
if isTarget {
polygonEl .Points = fmt .Sprintf ("%s,%s %s,%s %s,%s %s,%s" ,
svg .FormatFloat (0. ), svg .FormatFloat (height /2.0 ),
svg .FormatFloat (width /2 ), svg .FormatFloat (height /8 ),
svg .FormatFloat (width ), svg .FormatFloat (height /2.0 ),
svg .FormatFloat (width /2.0 ), svg .FormatFloat (height *0.9 ),
)
} else {
polygonEl .Points = fmt .Sprintf ("%s,%s %s,%s %s,%s %s,%s" ,
svg .FormatFloat (width /8 ), svg .FormatFloat (height /2.0 ),
svg .FormatFloat (width *0.6 ), svg .FormatFloat (height /8 ),
svg .FormatFloat (width *1.1 ), svg .FormatFloat (height /2.0 ),
svg .FormatFloat (width *0.6 ), svg .FormatFloat (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 .AddAttributes (svg .IntAttr ("stroke-width" , 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 .AddAttributes (svg .IntAttr ("stroke-width" , connection .StrokeWidth ))
if isTarget {
circleEl .Cx = radius + strokeWidth /2
} else {
circleEl .Cx = radius - strokeWidth /2
}
path = circleEl .Render ()
case d2target .CrossArrowhead :
inset := strokeWidth / 8
rotationAngle := math .Pi / 4
origin := geo .NewPoint (width /2 , height /2 )
newOrigin := geo .NewPoint (math .Cos (rotationAngle )*origin .X -math .Sin (rotationAngle )*origin .Y , math .Sin (rotationAngle )*origin .X +math .Cos (rotationAngle )*origin .Y )
crossEl := d2themes .NewThemableElement ("polygon" , inlineTheme )
crossEl .Points = fmt .Sprintf ("%s,%s %s,%s %s,%s %s,%s, %s,%s %s,%s %s,%s %s,%s %s,%s %s,%s %s,%s %s,%s" ,
svg .FormatFloat (0. ), svg .FormatFloat (height /2 +inset ),
svg .FormatFloat (width /2 -inset ), svg .FormatFloat (height /2 +inset ),
svg .FormatFloat (width /2 -inset ), svg .FormatFloat (height ),
svg .FormatFloat (width /2 +inset ), svg .FormatFloat (height ),
svg .FormatFloat (width /2 +inset ), svg .FormatFloat (height /2 +inset ),
svg .FormatFloat (width ), svg .FormatFloat (height /2 +inset ),
svg .FormatFloat (width ), svg .FormatFloat (height /2 -inset ),
svg .FormatFloat (width /2 +inset ), svg .FormatFloat (height /2 -inset ),
svg .FormatFloat (width /2 +inset ), svg .FormatFloat (0. ),
svg .FormatFloat (width /2 -inset ), svg .FormatFloat (0. ),
svg .FormatFloat (width /2 -inset ), svg .FormatFloat (height /2 -inset ),
svg .FormatFloat (0. ), svg .FormatFloat (height /2 -inset ),
)
crossEl .Transform = fmt .Sprintf ("translate(%s, %s) rotate(45)" , svg .FormatFloat (-newOrigin .X +width /2 ), svg .FormatFloat (-newOrigin .Y +height /2 ))
childPathEl := d2themes .NewThemableElement ("path" , inlineTheme )
if isTarget {
childPathEl .D = fmt .Sprintf ("M%s,%s %s,%s" ,
svg .FormatFloat (width /2 ), svg .FormatFloat (height /2 ),
svg .FormatFloat (width ), svg .FormatFloat (height /2 ),
)
} else {
childPathEl .D = fmt .Sprintf ("M%s,%s %s,%s" ,
svg .FormatFloat (width /2 ), svg .FormatFloat (height /2 ),
svg .FormatFloat (0. ), svg .FormatFloat (height /2 ),
)
}
gEl := d2themes .NewThemableElement ("g" , inlineTheme )
gEl .Fill = d2target .BG_COLOR
gEl .Stroke = connection .Stroke
gEl .ClassName = "connection"
gEl .AddAttributes (svg .IntAttr ("stroke-width" , connection .StrokeWidth ))
gEl .SetInnerSVG (svg .Join (svg .TrustedFragment (crossEl .Render ()), svg .TrustedFragment (childPathEl .Render ())))
path = gEl .Render ()
case d2target .FilledBoxArrowhead :
polygonEl := d2themes .NewThemableElement ("polygon" , inlineTheme )
polygonEl .ClassName = "connection"
polygonEl .Fill = connection .Stroke
polygonEl .AddAttributes (svg .IntAttr ("stroke-width" , connection .StrokeWidth ))
if isTarget {
polygonEl .Points = fmt .Sprintf ("%s,%s %s,%s %s,%s %s,%s" ,
svg .FormatFloat (0. ), svg .FormatFloat (0. ),
svg .FormatFloat (0. ), svg .FormatFloat (height ),
svg .FormatFloat (width ), svg .FormatFloat (height ),
svg .FormatFloat (width ), svg .FormatFloat (0. ),
)
} else {
polygonEl .Points = fmt .Sprintf ("%s,%s %s,%s %s,%s %s,%s" ,
svg .FormatFloat (0. ), svg .FormatFloat (0. ),
svg .FormatFloat (0. ), svg .FormatFloat (height ),
svg .FormatFloat (width ), svg .FormatFloat (height ),
svg .FormatFloat (width ), svg .FormatFloat (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 .AddAttributes (svg .IntAttr ("stroke-width" , connection .StrokeWidth ))
polygonEl .Style = fmt .Sprintf ("%sstroke-linejoin:miter;" , polygonEl .Style )
inset := strokeWidth / 2
if isTarget {
polygonEl .Points = fmt .Sprintf ("%s,%s %s,%s %s,%s %s,%s" ,
svg .FormatFloat (inset ), svg .FormatFloat (inset ),
svg .FormatFloat (inset ), svg .FormatFloat (height -inset ),
svg .FormatFloat (width -inset ), svg .FormatFloat (height -inset ),
svg .FormatFloat (width -inset ), svg .FormatFloat (inset ),
)
} else {
polygonEl .Points = fmt .Sprintf ("%s,%s %s,%s %s,%s %s,%s" ,
svg .FormatFloat (inset ), svg .FormatFloat (inset ),
svg .FormatFloat (inset ), svg .FormatFloat (height -inset ),
svg .FormatFloat (width -inset ), svg .FormatFloat (height -inset ),
svg .FormatFloat (width -inset ), svg .FormatFloat (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%s,%s %s,%s" ,
svg .FormatFloat (offset ), svg .FormatFloat (0. ),
svg .FormatFloat (offset ), svg .FormatFloat (height ),
)
modifierEl .Fill = d2target .BG_COLOR
modifierEl .Stroke = connection .Stroke
modifierEl .ClassName = "connection"
modifierEl .AddAttributes (svg .IntAttr ("stroke-width" , 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 .AddAttributes (svg .IntAttr ("stroke-width" , connection .StrokeWidth ))
}
childPathEl := d2themes .NewThemableElement ("path" , inlineTheme )
if arrowhead == d2target .CfMany || arrowhead == d2target .CfManyRequired {
childPathEl .D = fmt .Sprintf ("M%s,%s %s,%s M%s,%s %s,%s M%s,%s %s,%s" ,
svg .FormatFloat (width -3.0 ), svg .FormatFloat (height /2.0 ),
svg .FormatFloat (width +offset ), svg .FormatFloat (height /2.0 ),
svg .FormatFloat (offset +3.0 ), svg .FormatFloat (height /2.0 ),
svg .FormatFloat (width +offset ), svg .FormatFloat (0. ),
svg .FormatFloat (offset +3.0 ), svg .FormatFloat (height /2.0 ),
svg .FormatFloat (width +offset ), svg .FormatFloat (height ),
)
} else {
childPathEl .D = fmt .Sprintf ("M%s,%s %s,%s M%s,%s %s,%s" ,
svg .FormatFloat (width -3.0 ), svg .FormatFloat (height /2.0 ),
svg .FormatFloat (width +offset ), svg .FormatFloat (height /2.0 ),
svg .FormatFloat (offset *2.0 ), svg .FormatFloat (0. ),
svg .FormatFloat (offset *2.0 ), svg .FormatFloat (height ),
)
}
gEl := d2themes .NewThemableElement ("g" , inlineTheme )
if !isTarget {
gEl .Transform = fmt .Sprintf ("scale(-1) translate(-%s, -%s)" , svg .FormatFloat (width ), svg .FormatFloat (height ))
}
gEl .Fill = d2target .BG_COLOR
gEl .Stroke = connection .Stroke
gEl .ClassName = "connection"
gEl .AddAttributes (svg .IntAttr ("stroke-width" , connection .StrokeWidth ))
gEl .SetInnerSVG (svg .Join (svg .TrustedFragment (modifierEl .Render ()), svg .TrustedFragment (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="%s" markerHeight="%s" refX="%s" refY="%s"` ,
id , svg .FormatFloat (width ), svg .FormatFloat (height ), svg .FormatFloat (refX ), svg .FormatFloat (refY ),
),
fmt .Sprintf (`viewBox="%s %s %s %s"` , svg .FormatFloat (0. ), svg .FormatFloat (0. ), svg .FormatFloat (width ), svg .FormatFloat (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 %s %s" ,
svg .FormatFloat (route [0 ].X +srcAdj .X ),
svg .FormatFloat (route [0 ].Y +srcAdj .Y ),
))
if connection .IsCurve {
i := 1
for ; i < len (route )-3 ; i += 3 {
path = append (path , fmt .Sprintf ("C %s %s %s %s %s %s" ,
svg .FormatFloat (route [i ].X ), svg .FormatFloat (route [i ].Y ),
svg .FormatFloat (route [i +1 ].X ), svg .FormatFloat (route [i +1 ].Y ),
svg .FormatFloat (route [i +2 ].X ), svg .FormatFloat (route [i +2 ].Y ),
))
}
path = append (path , fmt .Sprintf ("C %s %s %s %s %s %s" ,
svg .FormatFloat (route [i ].X ), svg .FormatFloat (route [i ].Y ),
svg .FormatFloat (route [i +1 ].X ), svg .FormatFloat (route [i +1 ].Y ),
svg .FormatFloat (route [i +2 ].X +dstAdj .X ),
svg .FormatFloat (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 %s %s" ,
svg .FormatFloat (prevTarget .X -prevTranslations .X ),
svg .FormatFloat (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 %s %s %s %s %s %s" ,
svg .FormatFloat (prevTarget .X +prevTranslations .X ),
svg .FormatFloat (prevTarget .Y +prevTranslations .Y ),
svg .FormatFloat (currTarget .X -nextTranslations .X ),
svg .FormatFloat (currTarget .Y -nextTranslations .Y ),
svg .FormatFloat (currTarget .X +nextTranslations .X ),
svg .FormatFloat (currTarget .Y +nextTranslations .Y ),
))
} else {
path = append (path , fmt .Sprintf ("S %s %s %s %s" ,
svg .FormatFloat (prevTarget .X ),
svg .FormatFloat (prevTarget .Y ),
svg .FormatFloat (prevTarget .X +currTranslations .X ),
svg .FormatFloat (prevTarget .Y +currTranslations .Y ),
))
}
}
lastPoint := route [len (route )-1 ]
path = append (path , fmt .Sprintf ("L %s %s" ,
svg .FormatFloat (lastPoint .X +dstAdj .X ),
svg .FormatFloat (lastPoint .Y +dstAdj .Y ),
))
}
return strings .Join (path , " " )
}
func makeLabelMask(labelTL *geo .Point , width , height int , opacity float64 ) string {
return makeLabelMaskWithTransform (labelTL , width , height , opacity , nil )
}
func makeLabelMaskWithTransform(labelTL *geo .Point , width , height int , opacity float64 , shapePos *geo .Point ) string {
fill := "black"
if opacity != 1 {
fill = fmt .Sprintf ("rgba(0,0,0,%.2f)" , opacity )
}
x := labelTL .X - 2
y := labelTL .Y
if shapePos != nil {
x -= shapePos .X
y -= shapePos .Y
}
return fmt .Sprintf (`<rect x="%s" y="%s" width="%d" height="%d" fill="%s"></rect>` ,
svg .FormatFloat (x ), svg .FormatFloat (y ),
width +4 ,
height ,
fill ,
)
}
func makeBorderLabelMask(labelPosition label .Position , labelTL *geo .Point , labelWidth , labelHeight int , shapeBox *geo .Box , strokeWidth int , opacity float64 , shapePos *geo .Point ) string {
fill := "black"
if opacity != 1 {
fill = fmt .Sprintf ("rgba(0,0,0,%.2f)" , opacity )
}
effectiveStrokeWidth := float64 (strokeWidth )
if shapePos != nil {
effectiveStrokeWidth *= 3
}
var maskX , maskY , maskWidth , maskHeight float64
switch labelPosition {
case label .BorderTopLeft , label .BorderTopCenter , label .BorderTopRight :
maskX = labelTL .X - 2
maskY = shapeBox .TopLeft .Y - effectiveStrokeWidth /2
maskWidth = float64 (labelWidth + 4 )
maskHeight = effectiveStrokeWidth
case label .BorderBottomLeft , label .BorderBottomCenter , label .BorderBottomRight :
maskX = labelTL .X - 2
maskY = shapeBox .TopLeft .Y + shapeBox .Height - effectiveStrokeWidth /2
maskWidth = float64 (labelWidth + 4 )
maskHeight = effectiveStrokeWidth
case label .BorderLeftTop , label .BorderLeftMiddle , label .BorderLeftBottom :
maskX = shapeBox .TopLeft .X - effectiveStrokeWidth /2
maskY = labelTL .Y - 2
maskWidth = effectiveStrokeWidth
maskHeight = float64 (labelHeight + 4 )
case label .BorderRightTop , label .BorderRightMiddle , label .BorderRightBottom :
maskX = shapeBox .TopLeft .X + shapeBox .Width - effectiveStrokeWidth /2
maskY = labelTL .Y - 2
maskWidth = effectiveStrokeWidth
maskHeight = float64 (labelHeight + 4 )
default :
return ""
}
if shapePos != nil {
maskX -= shapePos .X
maskY -= shapePos .Y
}
return fmt .Sprintf (`<rect x="%s" y="%s" width="%s" height="%s" fill="%s"></rect>` ,
svg .FormatFloat (maskX ), svg .FormatFloat (maskY ), svg .FormatFloat (maskWidth ), svg .FormatFloat (maskHeight ), fill ,
)
}
func drawConnection(writer io .Writer , diagramHash string , connection d2target .Connection , markers map [string ]struct {}, idToShape map [string ]d2target .Shape , sketch bool , inlineTheme *d2themes .Theme , markdown *markdownRenderer ) (labelMask string , _ error ) {
classes := []string {base64 .URLEncoding .EncodeToString ([]byte (svg .EscapeText (connection .ID )))}
classes = append (classes , connection .Classes ...)
groupAttributes := []svg .Attribute {svg .Attr ("class" , strings .Join (classes , " " ))}
opacityStyle := ""
if connection .Opacity != 1.0 {
opacityStyle = fmt .Sprintf (" style='opacity:%s'" , svg .FormatFloat (connection .Opacity ))
}
fmt .Fprint (writer , strings .TrimSuffix (svg .OpenElement ("g" , groupAttributes ...), ">" )+opacityStyle +">" )
var markerStart []svg .Attribute
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 = []svg .Attribute {svg .Attr ("marker-start" , svg .LocalIRI (svg .LiteralID (id )))}
}
var markerEnd []svg .Attribute
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 = []svg .Attribute {svg .Attr ("marker-end" , svg .LocalIRI (svg .LiteralID (id )))}
}
if connection .Icon != nil {
iconPos := connection .GetIconPosition ()
if iconPos != nil {
attributes := []svg .Attribute {
svg .Attr ("href" , connection .Icon .String ()),
svg .FloatAttr ("x" , iconPos .X ),
svg .FloatAttr ("y" , iconPos .Y ),
svg .IntAttr ("width" , d2target .DEFAULT_ICON_SIZE ),
svg .IntAttr ("height" , d2target .DEFAULT_ICON_SIZE ),
}
if connection .IconBorderRadius != 0 {
attributes = append (attributes , svg .Attr ("clip-path" , "inset(0 round " +svg .FormatFloat (connection .IconBorderRadius )+"px)" ))
}
fmt .Fprint (writer , svg .EmptyElement ("image" , attributes ...))
}
}
var labelTL *geo .Point
if connection .Label != "" {
labelTL = connection .GetLabelTopLeft ()
labelTL .X = math .Round (labelTL .X )
labelTL .Y = math .Round (labelTL .Y )
maskTL := labelTL .Copy ()
width := connection .LabelWidth
height := connection .LabelHeight
if connection .Icon != nil {
width += d2target .CONNECTION_ICON_LABEL_GAP + d2target .DEFAULT_ICON_SIZE
maskTL .X -= float64 (d2target .CONNECTION_ICON_LABEL_GAP + d2target .DEFAULT_ICON_SIZE )
}
if label .FromString (connection .LabelPosition ).IsOnEdge () {
labelMask = makeLabelMask (maskTL , width , height , 1 )
} else {
labelMask = makeLabelMask (maskTL , width , height , 0.75 )
}
} else if connection .Icon != nil {
iconPos := connection .GetIconPosition ()
if iconPos != nil {
maskTL := &geo .Point {
X : iconPos .X ,
Y : iconPos .Y ,
}
if label .FromString (connection .IconPosition ).IsOnEdge () {
labelMask = makeLabelMask (maskTL , d2target .DEFAULT_ICON_SIZE , d2target .DEFAULT_ICON_SIZE , 1 )
} else {
labelMask = makeLabelMask (maskTL , d2target .DEFAULT_ICON_SIZE , d2target .DEFAULT_ICON_SIZE , 0.75 )
}
}
}
srcAdj , dstAdj := getArrowheadAdjustments (connection , idToShape )
path := pathData (connection , srcAdj , dstAdj )
mask := svg .Attr ("mask" , svg .LocalIRI (svg .LiteralID (diagramHash )))
if sketch {
out , err := d2sketch .ConnectionWithAttributes (connection , path , []svg .Attribute {mask })
if err != nil {
return "" , err
}
fmt .Fprint (writer , out )
arrowPaths , err := d2sketch .Arrowheads (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 .AddAttributes (append (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 .AddAttributes (append (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 ()
pathAttributes := append (append ([]svg .Attribute {}, markerStart ...), markerEnd ...)
pathEl .AddAttributes (append (pathAttributes , mask )...)
fmt .Fprint (writer , pathEl .Render ())
}
}
if connection .Label != "" {
richLabelLink := connection .Link != "" && connection .Language != ""
if richLabelLink {
fmt .Fprint (writer , svg .OpenElement ("a" , svg .HrefAttributes (connection .Link )...))
}
if connection .Language == "latex" {
render , err := d2latex .Render (connection .Label )
if err != nil {
return labelMask , err
}
xmlDecl := `<?xml version="1.0" encoding="UTF-8"?>`
doctype := `<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.0//EN" "http://www.w3.org/TR/2001/REC-SVG-20010904/DTD/svg10.dtd">`
render = strings .ReplaceAll (render , xmlDecl , "" )
render = strings .ReplaceAll (render , doctype , "" )
gEl := d2themes .NewThemableElement ("g" , inlineTheme )
gEl .SetTranslate (labelTL .X , labelTL .Y )
gEl .Color = connection .Color
gEl .SetInnerSVG (svg .TrustedFragment (render ))
fmt .Fprint (writer , gEl .Render ())
} else if connection .Language == "markdown" {
layout , err := markdown .layout (connection .Label , connection .FontFamily , connection .FontSize )
if err != nil {
return labelMask , err
}
fmt .Fprint (writer , markdown .render (
layout ,
labelTL .X ,
labelTL .Y ,
connection .LabelWidth ,
connection .LabelHeight ,
connection .GetFontColor (),
connection .Fill ,
connection .Link != "" ,
connection .Underline ,
))
} else if connection .Language != "" {
lexer := lexers .Get (connection .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` )
}
iterator , err := lexer .Tokenise (nil , connection .Label )
if err != nil {
return labelMask , err
}
svgStyles := styleToSVG (style )
class := "light-code"
if !isLight {
class = "dark-code"
}
var fontSize string
if connection .FontSize != d2fonts .FONT_SIZE_M {
fontSize = fmt .Sprintf (` style="font-size:%v"` , connection .FontSize )
}
fmt .Fprintf (writer , `<g transform="translate(%s %s)" class="%s"%s>` ,
svg .FormatFloat (labelTL .X ), svg .FormatFloat (labelTL .Y ), class , fontSize ,
)
lineHeight := textmeasure .CODE_LINE_HEIGHT
for index , tokens := range chroma .SplitTokensIntoLines (iterator .Tokens ()) {
fmt .Fprintf (writer , "<text class=\"text-mono\" x=\"0\" y=\"%sem\">" , svg .FormatFloat (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>" )
}
} else {
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 .SetInnerSVG (svg .TrustedFragment (RenderText (connection .Label , textEl .X , float64 (connection .LabelHeight ))))
if connection .Link != "" {
textEl .ClassName += " text-underline text-link"
fmt .Fprint (writer , svg .OpenElement ("a" , svg .HrefAttributes (connection .Link )...))
} else {
textEl .Fill = connection .GetFontColor ()
}
fmt .Fprint (writer , textEl .Render ())
if connection .Link != "" {
fmt .Fprintf (writer , "</a>" )
}
}
if richLabelLink {
fmt .Fprint (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 .SetInnerSVG (svg .TrustedFragment (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 := svg .ScopedID ("border-mask-" +diagramHash +"-" , targetShape .ID , "" )
borderMask := strings .Join ([]string {
svg .OpenElement ("defs" ) + svg .OpenElement ("mask" ,
svg .Attr ("id" , maskID .String ()),
svg .Attr ("maskUnits" , "userSpaceOnUse" ),
svg .IntAttr ("x" , targetShape .Pos .X ),
svg .IntAttr ("y" , targetShape .Pos .Y -d2target .THREE_DEE_OFFSET ),
svg .IntAttr ("width" , targetShape .Width +d2target .THREE_DEE_OFFSET ),
svg .IntAttr ("height" , targetShape .Height +d2target .THREE_DEE_OFFSET ),
),
svg .OpenElement ("rect" ,
svg .IntAttr ("x" , targetShape .Pos .X ),
svg .IntAttr ("y" , targetShape .Pos .Y -d2target .THREE_DEE_OFFSET ),
svg .IntAttr ("width" , targetShape .Width +d2target .THREE_DEE_OFFSET ),
svg .IntAttr ("height" , targetShape .Height +d2target .THREE_DEE_OFFSET ),
svg .Attr ("fill" , "white" ),
) + svg .CloseElement ("rect" ),
svg .CompactEmptyElement ("path" ,
svg .Attr ("d" , strings .Join (borderSegments , "" )),
svg .Attr ("style" , borderStyle +";stroke:#000;fill:none;opacity:1;" ),
) + svg .CloseElement ("mask" ) + svg .CloseElement ("defs" ),
}, "\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 .SetMaskID (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 .SetMaskID (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 := svg .ScopedID ("border-mask-" +diagramHash +"-" , targetShape .ID , "" )
borderMask := strings .Join ([]string {
svg .OpenElement ("defs" ) + svg .OpenElement ("mask" ,
svg .Attr ("id" , maskID .String ()),
svg .Attr ("maskUnits" , "userSpaceOnUse" ),
svg .IntAttr ("x" , targetShape .Pos .X ),
svg .IntAttr ("y" , targetShape .Pos .Y -d2target .THREE_DEE_OFFSET ),
svg .IntAttr ("width" , targetShape .Width +d2target .THREE_DEE_OFFSET ),
svg .IntAttr ("height" , targetShape .Height +d2target .THREE_DEE_OFFSET ),
),
svg .OpenElement ("rect" ,
svg .IntAttr ("x" , targetShape .Pos .X ),
svg .IntAttr ("y" , targetShape .Pos .Y -d2target .THREE_DEE_OFFSET ),
svg .IntAttr ("width" , targetShape .Width +d2target .THREE_DEE_OFFSET ),
svg .IntAttr ("height" , targetShape .Height +d2target .THREE_DEE_OFFSET ),
svg .Attr ("fill" , "white" ),
) + svg .CloseElement ("rect" ),
svg .CompactEmptyElement ("path" ,
svg .Attr ("d" , strings .Join (borderSegments , "" )),
svg .Attr ("style" , borderStyle +";stroke:#000;fill:none;opacity:1;" ),
) + svg .CloseElement ("mask" ) + svg .CloseElement ("defs" ),
}, "\n" )
mainShape := d2themes .NewThemableElement ("polygon" , inlineTheme )
mainShape .X = float64 (targetShape .Pos .X )
mainShape .Y = float64 (targetShape .Pos .Y )
mainShape .Points = mainPointsPoly
mainShape .SetMaskID (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 .SetMaskID (maskID )
sideShape .Style = targetShape .CSSStyle ()
renderedSides := sideShape .Render ()
return borderMask + mainShapeRendered + renderedSides + renderedBorder
}
func drawShape(writer , appendixWriter io .Writer , diagramHash string , targetShape d2target .Shape , sketch bool , inlineTheme *d2themes .Theme , markdown *markdownRenderer ) (labelMask string , err error ) {
closingTag := "</g>"
if targetShape .Link != "" {
fmt .Fprint (writer , svg .OpenElement ("a" , svg .HrefAttributes (targetShape .Link )...))
closingTag += "</a>"
}
if targetShape .BorderRadius != 0 && (targetShape .Type == d2target .ShapeClass || targetShape .Type == d2target .ShapeSQLTable ) {
fmt .Fprint (writer , clipPathForBorderRadius (diagramHash , targetShape ))
}
var iconClipPathID svg .ID
if targetShape .IconBorderRadius != 0 && (targetShape .Type == d2target .ShapeImage ) {
targetShape .IconBorderRadius = min (targetShape .IconBorderRadius , min (targetShape .Width , targetShape .Height )/2 )
iconClipPathID = svg .LiteralID (fmt .Sprintf ("%v-%v-icon" , diagramHash , svg .SVGID (targetShape .ID )))
fmt .Fprint (writer , applyIconBorderRadius (iconClipPathID , targetShape ))
}
classes := []string {base64 .URLEncoding .EncodeToString ([]byte (svg .EscapeText (targetShape .ID )))}
if targetShape .Animated {
classes = append (classes , "animated-shape" )
}
classes = append (classes , targetShape .Classes ...)
groupAttributes := []svg .Attribute {svg .Attr ("class" , strings .Join (classes , " " ))}
opacityStyle := ""
if targetShape .Opacity != 1.0 {
opacityStyle = fmt .Sprintf (" style='opacity:%s'" , svg .FormatFloat (targetShape .Opacity ))
}
fmt .Fprint (writer , strings .TrimSuffix (svg .OpenElement ("g" , groupAttributes ...), ">" )+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 sketch {
out , err := d2sketch .Class (targetShape )
if err != nil {
return "" , err
}
fmt .Fprint (writer , out )
} else {
drawClass (writer , diagramHash , targetShape , inlineTheme )
}
err := addAppendixItems (appendixWriter , diagramHash , targetShape , s , markdown )
if err != nil {
return "" , err
}
fmt .Fprint (writer , `</g>` )
fmt .Fprint (writer , closingTag )
return labelMask , nil
case d2target .ShapeSQLTable :
if sketch {
out , err := d2sketch .Table (targetShape )
if err != nil {
return "" , err
}
fmt .Fprint (writer , out )
} else {
drawTable (writer , diagramHash , targetShape , inlineTheme )
}
err := addAppendixItems (appendixWriter , diagramHash , targetShape , s , markdown )
if err != nil {
return "" , err
}
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 sketch {
out , err := d2sketch .DoubleOval (targetShape , diagramHash )
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 sketch {
out , err := d2sketch .Oval (targetShape , diagramHash )
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 = targetShape .Icon .String ()
el .Fill = fill
el .Stroke = stroke
el .Style = style
if targetShape .IconBorderRadius != 0 {
el .SetClipPathID (iconClipPathID )
}
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 sketch {
out , err := d2sketch .Rect (targetShape , diagramHash )
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
if targetShape .Label != "" && label .FromString (targetShape .LabelPosition ).IsBorder () {
el .SetMaskID (svg .LiteralID (diagramHash ))
}
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 sketch {
out , err := d2sketch .DoubleRect (targetShape , diagramHash )
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 sketch {
out , err := d2sketch .Paths (targetShape , diagramHash , 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 sketch {
out , err := d2sketch .Paths (targetShape , diagramHash , 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
if targetShape .Label != "" && label .FromString (targetShape .LabelPosition ).IsBorder () {
el .SetMaskID (svg .LiteralID (diagramHash ))
}
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 ))
attributes := []svg .Attribute {
svg .Attr ("href" , targetShape .Icon .String ()),
svg .FloatAttr ("x" , tl .X ),
svg .FloatAttr ("y" , tl .Y ),
svg .IntAttr ("width" , iconSize ),
svg .IntAttr ("height" , iconSize ),
}
if targetShape .IconBorderRadius != 0 {
attributes = append (attributes , svg .Attr ("clip-path" , fmt .Sprintf ("inset(0 round %dpx)" , targetShape .IconBorderRadius )))
}
fmt .Fprint (writer , svg .EmptyElement ("image" , attributes ...))
}
if targetShape .Label != "" && targetShape .Opacity != 0 {
labelPosition := label .FromString (targetShape .LabelPosition )
if targetShape .Language == "markdown" && labelPosition == label .Unset {
labelPosition = label .InsideMiddleCenter
}
var box *geo .Box
if labelPosition .IsOutside () || labelPosition .IsBorder () {
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 ),
)
if labelPosition .IsBorder () {
if sketch {
labelMask = makeBorderLabelMask (labelPosition , labelTL , targetShape .LabelWidth , targetShape .LabelHeight , box , targetShape .StrokeWidth , 1.0 , tl )
} else {
labelMask = makeBorderLabelMask (labelPosition , labelTL , targetShape .LabelWidth , targetShape .LabelHeight , box , targetShape .StrokeWidth , 1.0 , nil )
}
}
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 .Language == "latex" {
render , err := d2latex .Render (targetShape .Label )
if err != nil {
return labelMask , err
}
xmlDecl := `<?xml version="1.0" encoding="UTF-8"?>`
doctype := `<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.0//EN" "http://www.w3.org/TR/2001/REC-SVG-20010904/DTD/svg10.dtd">`
render = strings .ReplaceAll (render , xmlDecl , "" )
render = strings .ReplaceAll (render , doctype , "" )
gEl := d2themes .NewThemableElement ("g" , inlineTheme )
labelPosition := label .FromString (targetShape .LabelPosition )
if labelPosition == label .Unset {
labelPosition = label .InsideMiddleCenter
}
var box *geo .Box
if labelPosition .IsOutside () {
box = s .GetBox ()
} else {
box = s .GetInnerBox ()
}
labelTL := labelPosition .GetPointOnBox (box , label .PADDING ,
float64 (targetShape .LabelWidth ),
float64 (targetShape .LabelHeight ),
)
gEl .SetTranslate (labelTL .X , labelTL .Y )
gEl .Color = targetShape .Stroke
gEl .SetInnerSVG (svg .TrustedFragment (render ))
fmt .Fprint (writer , gEl .Render ())
} else if targetShape .Language == "markdown" {
layout , err := markdown .layout (targetShape .Label , targetShape .FontFamily , targetShape .FontSize )
if err != nil {
return labelMask , err
}
fmt .Fprint (writer , markdown .render (
layout ,
labelTL .X ,
labelTL .Y ,
targetShape .LabelWidth ,
targetShape .LabelHeight ,
targetShape .GetFontColor (),
targetShape .Fill ,
targetShape .Link != "" ,
targetShape .Underline ,
))
} else if targetShape .Language != "" {
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(%s %s)" class="%s"%s>` ,
svg .FormatFloat (box .TopLeft .X ), svg .FormatFloat (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(%s %s)">` , svg .FormatFloat (padding ), svg .FormatFloat (padding ))
lineHeight := textmeasure .CODE_LINE_HEIGHT
for index , tokens := range chroma .SplitTokensIntoLines (iterator .Tokens ()) {
fmt .Fprintf (writer , "<text class=\"text-mono\" x=\"0\" y=\"%sem\">" , svg .FormatFloat (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 .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 .SetInnerSVG (svg .TrustedFragment (RenderText (targetShape .Label , textEl .X , float64 (targetShape .LabelHeight ))))
fmt .Fprint (writer , textEl .Render ())
}
}
if targetShape .Tooltip != "" && targetShape .TooltipPosition == "" {
fmt .Fprintf (writer , `<title>%s</title>` ,
svg .EscapeText (targetShape .Tooltip ),
)
}
err = addAppendixItems (appendixWriter , diagramHash , targetShape , s , markdown )
if err != nil {
return "" , err
}
fmt .Fprint (writer , closingTag )
return labelMask , nil
}
func applyIconBorderRadius(clipPathID svg .ID , 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 := svg .OpenElement ("clipPath" , svg .Attr ("id" , clipPathID .String ()))
out += fmt .Sprintf (`<path d="M %s %s L %s %s S %s %s %s %s ` , svg .FormatFloat (box .TopLeft .X ), svg .FormatFloat (box .TopLeft .Y +float64 (shape .IconBorderRadius )), svg .FormatFloat (box .TopLeft .X ), svg .FormatFloat (box .TopLeft .Y +float64 (shape .IconBorderRadius )), svg .FormatFloat (box .TopLeft .X ), svg .FormatFloat (box .TopLeft .Y ), svg .FormatFloat (box .TopLeft .X +float64 (shape .IconBorderRadius )), svg .FormatFloat (box .TopLeft .Y ))
out += fmt .Sprintf (`L %s %s L %s %s ` , svg .FormatFloat (box .TopLeft .X +box .Width -float64 (shape .IconBorderRadius )), svg .FormatFloat (box .TopLeft .Y ), svg .FormatFloat (topX -float64 (shape .IconBorderRadius )), svg .FormatFloat (topY ))
out += fmt .Sprintf (`S %s %s %s %s ` , svg .FormatFloat (topX ), svg .FormatFloat (topY ), svg .FormatFloat (topX ), svg .FormatFloat (topY +float64 (shape .IconBorderRadius )))
out += fmt .Sprintf (`L %s %s ` , svg .FormatFloat (topX ), svg .FormatFloat (topY +box .Height -float64 (shape .IconBorderRadius )))
out += fmt .Sprintf (`S %s %s %s %s ` , svg .FormatFloat (topX ), svg .FormatFloat (topY +box .Height ), svg .FormatFloat (topX -float64 (shape .IconBorderRadius )), svg .FormatFloat (topY +box .Height ))
out += fmt .Sprintf (`L %s %s ` , svg .FormatFloat (box .TopLeft .X +float64 (shape .IconBorderRadius )), svg .FormatFloat (box .TopLeft .Y +box .Height ))
out += fmt .Sprintf (`S %s %s %s %s` , svg .FormatFloat (box .TopLeft .X ), svg .FormatFloat (box .TopLeft .Y +box .Height ), svg .FormatFloat (box .TopLeft .X ), svg .FormatFloat (box .TopLeft .Y +box .Height -float64 (shape .IconBorderRadius )))
out += fmt .Sprintf (`L %s %s` , svg .FormatFloat (box .TopLeft .X ), svg .FormatFloat (box .TopLeft .Y +float64 (shape .IconBorderRadius )))
out += fmt .Sprintf (`Z %s %s" ` , svg .FormatFloat (box .TopLeft .X ), svg .FormatFloat (box .TopLeft .Y ))
return out + `fill="none" /> </clipPath>`
}
func addAppendixItems(writer io .Writer , diagramHash string , targetShape d2target .Shape , s shape .Shape , markdown *markdownRenderer ) error {
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 != "" {
if targetShape .TooltipPosition != "" {
tt , err := renderPositionedTooltip (targetShape , targetShape .TooltipPosition , markdown )
if err != nil {
return err
}
fmt .Fprint (writer , tt )
} else {
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 )),
)
}
return nil
}
func calculateTooltipPosition(targetShape d2target .Shape , tooltipPosition string , tooltipWidth , tooltipHeight int ) (x , y float64 , tailDirection string , tailX , tailY float64 ) {
shapeX := float64 (targetShape .Pos .X )
shapeY := float64 (targetShape .Pos .Y )
shapeWidth := float64 (targetShape .Width )
shapeHeight := float64 (targetShape .Height )
x , y = d2target .CalculateTooltipPosition (shapeX , shapeY , shapeWidth , shapeHeight , tooltipWidth , tooltipHeight , tooltipPosition )
switch tooltipPosition {
case "top-left" :
tailDirection = "bottom"
tailX = 20
tailY = float64 (tooltipHeight )
case "top-center" :
tailDirection = "bottom"
tailX = float64 (tooltipWidth ) / 2
tailY = float64 (tooltipHeight )
case "top-right" :
tailDirection = "bottom"
tailX = float64 (tooltipWidth ) - 20
tailY = float64 (tooltipHeight )
case "center-left" :
tailDirection = "right"
tailX = float64 (tooltipWidth )
tailY = float64 (tooltipHeight ) / 2
case "center-right" :
tailDirection = "left"
tailX = 0
tailY = float64 (tooltipHeight ) / 2
case "bottom-left" :
tailDirection = "top"
tailX = 20
tailY = 0
case "bottom-center" :
tailDirection = "top"
tailX = float64 (tooltipWidth ) / 2
tailY = 0
case "bottom-right" :
tailDirection = "top"
tailX = float64 (tooltipWidth ) - 20
tailY = 0
default :
tailDirection = "bottom"
tailX = float64 (tooltipWidth ) / 2
tailY = float64 (tooltipHeight )
}
return x , y , tailDirection , tailX , tailY
}
func renderTooltipTail(tailDirection string , tailX , tailY float64 , inlineTheme *d2themes .Theme ) string {
tailSize := 8.0
var path string
switch tailDirection {
case "top" :
path = fmt .Sprintf (`M %s %s L %s %s L %s %s Z` ,
svg .FormatFloat (tailX -tailSize /2 ), svg .FormatFloat (tailY ),
svg .FormatFloat (tailX +tailSize /2 ), svg .FormatFloat (tailY ),
svg .FormatFloat (tailX ), svg .FormatFloat (tailY -tailSize ))
case "bottom" :
path = fmt .Sprintf (`M %s %s L %s %s L %s %s Z` ,
svg .FormatFloat (tailX -tailSize /2 ), svg .FormatFloat (tailY ),
svg .FormatFloat (tailX +tailSize /2 ), svg .FormatFloat (tailY ),
svg .FormatFloat (tailX ), svg .FormatFloat (tailY +tailSize ))
case "left" :
path = fmt .Sprintf (`M %s %s L %s %s L %s %s Z` ,
svg .FormatFloat (tailX ), svg .FormatFloat (tailY -tailSize /2 ),
svg .FormatFloat (tailX ), svg .FormatFloat (tailY +tailSize /2 ),
svg .FormatFloat (tailX -tailSize ), svg .FormatFloat (tailY ))
case "right" :
path = fmt .Sprintf (`M %s %s L %s %s L %s %s Z` ,
svg .FormatFloat (tailX ), svg .FormatFloat (tailY -tailSize /2 ),
svg .FormatFloat (tailX ), svg .FormatFloat (tailY +tailSize /2 ),
svg .FormatFloat (tailX +tailSize ), svg .FormatFloat (tailY ))
default :
return ""
}
tail := d2themes .NewThemableElement ("path" , inlineTheme )
tail .D = path
tail .Fill = color .N7
tail .Stroke = color .N5
tail .AddAttributes (svg .IntAttr ("stroke-width" , 1 ))
return tail .Render ()
}
func renderPositionedTooltip(targetShape d2target .Shape , tooltipPosition string , markdown *markdownRenderer ) (string , error ) {
if targetShape .Tooltip == "" || tooltipPosition == "" {
return "" , nil
}
layout , err := markdown .layout (targetShape .Tooltip , targetShape .FontFamily , d2fonts .FONT_SIZE_M )
if err != nil {
return "" , err
}
tooltipWidth := layout .Width + 20
tooltipHeight := layout .Height + 20
x , y , tailDirection , tailX , tailY := calculateTooltipPosition (targetShape , tooltipPosition , tooltipWidth , tooltipHeight )
tooltipContent := markdown .render (
layout ,
x +10 ,
y +10 ,
layout .Width ,
layout .Height ,
color .N1 ,
"" ,
false ,
false ,
)
tooltipBox := d2themes .NewThemableElement ("rect" , markdown .inlineTheme )
tooltipBox .X = x
tooltipBox .Y = y
tooltipBox .Width = float64 (tooltipWidth )
tooltipBox .Height = float64 (tooltipHeight )
tooltipBox .Rx = 4
tooltipBox .Ry = 4
tooltipBox .Fill = color .N7
tooltipBox .Stroke = color .N5
tooltipBox .AddAttributes (svg .IntAttr ("stroke-width" , 1 ))
tail := renderTooltipTail (tailDirection , tailX +x , tailY +y , markdown .inlineTheme )
return fmt .Sprintf (`<g class="positioned-tooltip">%s%s%s</g>` , tooltipBox .Render (), tail , tooltipContent ), nil
}
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="%s" dy="%s">%s</tspan>` , svg .FormatFloat (x ), svg .FormatFloat (dy ), escaped ))
}
return strings .Join (rendered , "" )
}
func EmbedFonts (buf *bytes .Buffer , diagramHash , source string , fontFamily *d2fonts .FontFamily , monoFontFamily *d2fonts .FontFamily , corpus string ) {
embedFonts (buf , diagramHash , source , fontFamily , monoFontFamily , corpus , nil )
}
func embedFonts(buf *bytes .Buffer , diagramHash , source string , fontFamily *d2fonts .FontFamily , monoFontFamily *d2fonts .FontFamily , corpus string , corpora fontCorpora ) {
if fontFamily == nil || *fontFamily == "" {
family := d2fonts .SourceSansPro
fontFamily = &family
}
if monoFontFamily == nil || *monoFontFamily == "" {
family := d2fonts .SourceCodePro
monoFontFamily = &family
}
for _ , match := range nativeMarkdownTextPattern .FindAllStringSubmatch (source , -1 ) {
corpus += html .UnescapeString (match [1 ])
}
corpora = restrictFontCorpora (corpora , corpus )
fmt .Fprint (buf , `<style type="text/css"><![CDATA[` )
appendFontOnTrigger (
buf ,
source ,
[]string {
`class="text"` ,
`class="text ` ,
`class="md"` ,
`class="md ` ,
},
corpora , "text" , corpus ,
func (corpus string ) string {
return 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 ),
)
},
)
appendFontOnTrigger (
buf ,
source ,
[]string {
`text-semibold` ,
},
corpora , "text-semibold" , corpus ,
func (corpus string ) string {
return fmt .Sprintf (`
.%s .text-semibold {
font-family: "%s-font-semibold";
}
@font-face {
font-family: %s-font-semibold;
src: url("%s");
}` ,
diagramHash ,
diagramHash ,
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));
}` ,
)
appendFontOnTrigger (
buf ,
source ,
[]string {
`text-bold` ,
`<b>` ,
`<strong>` ,
},
corpora , "text-bold" , corpus ,
func (corpus string ) string {
return 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 ),
)
},
)
appendFontOnTrigger (
buf ,
source ,
[]string {
`text-italic` ,
`<em>` ,
`<dfn>` ,
},
corpora , "text-italic" , corpus ,
func (corpus string ) string {
return 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 ),
)
},
)
appendFontOnTrigger (
buf ,
source ,
[]string {
`text-mono` ,
`<pre>` ,
`<code>` ,
`<kbd>` ,
`<samp>` ,
},
corpora , "text-mono" , corpus ,
func (corpus string ) string {
return fmt .Sprintf (`
.%s .text-mono {
font-family: "%s-font-mono";
}
@font-face {
font-family: %s-font-mono;
src: url("%s");
}` ,
diagramHash ,
diagramHash ,
diagramHash ,
monoFontFamily .Font (0 , d2fonts .FONT_STYLE_REGULAR ).GetEncodedSubset (corpus ),
)
},
)
appendFontOnTrigger (
buf ,
source ,
[]string {
`text-mono-semibold` ,
},
corpora , "text-mono-semibold" , corpus ,
func (corpus string ) string {
return fmt .Sprintf (`
.%s .text-mono-semibold {
font-family: "%s-font-mono-semibold";
}
@font-face {
font-family: %s-font-mono-semibold;
src: url("%s");
}` ,
diagramHash ,
diagramHash ,
diagramHash ,
monoFontFamily .Font (0 , d2fonts .FONT_STYLE_SEMIBOLD ).GetEncodedSubset (corpus ),
)
},
)
appendFontOnTrigger (
buf ,
source ,
[]string {
`text-mono-bold` ,
},
corpora , "text-mono-bold" , corpus ,
func (corpus string ) string {
return 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 ,
monoFontFamily .Font (0 , d2fonts .FONT_STYLE_BOLD ).GetEncodedSubset (corpus ),
)
},
)
appendFontOnTrigger (
buf ,
source ,
[]string {
`text-mono-italic` ,
},
corpora , "text-mono-italic" , corpus ,
func (corpus string ) string {
return 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 ,
monoFontFamily .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>` )
}
var nativeMarkdownTextPattern = regexp .MustCompile (`<text\b[^>]*\bclass="[^"]*\bmd-text\b[^"]*"[^>]*>([^<]*)</text>` )
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
}
}
}
func appendOnTriggerLazy(buf *bytes .Buffer , source string , triggers []string , newContent func () 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 ) {
if diagram == nil {
return nil , fmt .Errorf ("render target is nil" )
}
if err := validateRenderLinks (diagram ); err != nil {
return nil , err
}
if err := d2target .ValidateRenderTarget (diagram ); err != nil {
return nil , err
}
return renderValidated (diagram , opts )
}
func validateRenderLinks(diagram *d2target .Diagram ) error {
for _ , targetShape := range diagram .Shapes {
if textmeasure .IsDangerousLink (targetShape .Link ) {
return fmt .Errorf ("shape %q uses an unsafe link URL scheme" , targetShape .ID )
}
}
for _ , connection := range diagram .Connections {
if textmeasure .IsDangerousLink (connection .Link ) {
return fmt .Errorf ("connection %q uses an unsafe link URL scheme" , connection .ID )
}
}
return nil
}
func renderValidated(diagram *d2target .Diagram , opts *RenderOpts ) ([]byte , error ) {
if err := validateRenderPaints (diagram , opts ); err != nil {
return nil , err
}
if opts != nil && opts .Isometric != nil && *opts .Isometric {
return renderIsometric (diagram , opts )
}
sketch := false
pad := DEFAULT_PADDING
tl , br := diagram .BoundingBox ()
themeID := d2themescatalog .NeutralDefault .ID
darkThemeID := DEFAULT_DARK_THEME
var scale *float64
if opts != nil {
if opts .Pad != nil {
var err error
pad , err = validatePadding (tl , br , *opts .Pad )
if err != nil {
return nil , err
}
}
if opts .Sketch != nil && *opts .Sketch {
sketch = true
}
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 )
}
markdown := newMarkdownRenderer (diagram .FontFamily , diagram .MonoFontFamily , inlineTheme )
for _ , obj := range allObjects {
if c , is := obj .(d2target .Connection ); is {
labelMask , err := drawConnection (buf , isolatedDiagramHash , c , markers , idToShape , sketch , inlineTheme , markdown )
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 , sketch , inlineTheme , markdown )
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 )
if diagram .Legend != nil && (len (diagram .Legend .Shapes ) > 0 || len (diagram .Legend .Connections ) > 0 ) {
legendBuf := &bytes .Buffer {}
err := RenderLegend (legendBuf , diagram , diagramHash , inlineTheme )
if err != nil {
return nil , err
}
fmt .Fprint (buf , legendBuf )
}
left , top , w , h := dimensions (diagram , pad , tl , br )
if diagram .Legend != nil && (len (diagram .Legend .Shapes ) > 0 || len (diagram .Legend .Connections ) > 0 ) {
totalHeight := LEGEND_PADDING + LEGEND_FONT_SIZE + LEGEND_ITEM_SPACING
maxLabelWidth := 0
itemCount := 0
ruler , _ := textmeasure .NewRuler ()
if ruler != nil {
for _ , s := range diagram .Legend .Shapes {
if s .Label == "" {
continue
}
mtext := &d2target .MText {
Text : s .Label ,
FontSize : LEGEND_FONT_SIZE ,
}
dims := d2graph .GetTextDimensions (nil , ruler , mtext , fontToFamily (s .FontFamily ))
maxLabelWidth = go2 .IntMax (maxLabelWidth , dims .Width )
totalHeight += go2 .IntMax (dims .Height , LEGEND_ICON_SIZE ) + LEGEND_ITEM_SPACING
itemCount ++
}
for _ , c := range diagram .Legend .Connections {
if c .Label == "" {
continue
}
mtext := &d2target .MText {
Text : c .Label ,
FontSize : LEGEND_FONT_SIZE ,
}
dims := d2graph .GetTextDimensions (nil , ruler , mtext , fontToFamily (c .FontFamily ))
maxLabelWidth = go2 .IntMax (maxLabelWidth , dims .Width )
totalHeight += go2 .IntMax (dims .Height , LEGEND_ICON_SIZE ) + LEGEND_ITEM_SPACING
itemCount ++
}
if itemCount > 0 {
totalHeight -= LEGEND_ITEM_SPACING / 2
}
totalHeight += LEGEND_PADDING
if totalHeight > 0 && maxLabelWidth > 0 {
legendWidth := LEGEND_PADDING *2 + LEGEND_ICON_SIZE + LEGEND_PADDING + maxLabelWidth
legendY := br .Y - totalHeight
if legendY < tl .Y {
legendY = tl .Y
}
legendRight := br .X + LEGEND_CORNER_PADDING + legendWidth
if left +w < legendRight {
w = legendRight - left + pad /2
}
if legendY < top {
diffY := top - legendY
top -= diffY
h += diffY
}
legendBottom := legendY + totalHeight
if top +h < legendBottom {
h = legendBottom - top + pad /2
}
}
}
}
if w < 0 || h < 0 {
return nil , invalidPaddingError (int64 (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" ))
var themeStylesheet string
if opts .MasterID == "" {
var err error
themeStylesheet , err = ThemeCSS (diagramHash , &themeID , darkThemeID , opts .ThemeOverrides , opts .DarkThemeOverrides )
if err != nil {
return nil , err
}
}
strokePadding := int (math .Ceil (float64 (diagram .Root .StrokeWidth ) / 2. ))
left , top , w , h , ok := expandDimensions (left , top , w , h , strokePadding )
if !ok {
return nil , invalidPaddingError (int64 (pad ))
}
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 ("%s, %s" , svg .FormatFloat (dashSize ), svg .FormatFloat (gapSize ))
}
backgroundEl .AddAttributes (svg .IntAttr ("stroke-width" , diagram .Root .StrokeWidth ))
left , top , w , h , ok = expandDimensions (left , top , w , h , strokePadding )
if !ok {
return nil , invalidPaddingError (int64 (pad ))
}
doubleBorderElStr := ""
if diagram .Root .DoubleBorder {
offset := d2target .INNER_BORDER_OFFSET
left , top , w , h , ok = expandDimensions (left , top , w , h , strokePadding +offset )
if !ok {
return nil , invalidPaddingError (int64 (pad ))
}
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 , top , w , h , ok = expandDimensions (left , top , w , h , strokePadding )
if !ok {
return nil , invalidPaddingError (int64 (pad ))
}
}
backgroundStr := backgroundEl .Render ()
upperBuf := &bytes .Buffer {}
if opts .MasterID == "" {
corpora , classes , ok := collectFontCorporaAndClasses (buf .String ())
if !ok {
corpora = nil
}
for _ , shape := range diagram .Shapes {
if shape .Tooltip != "" || shape .Link != "" || shape .PrettyLink != "" {
corpora = nil
break
}
}
embedFonts (upperBuf , diagramHash , buf .String (), diagram .FontFamily , diagram .MonoFontFamily , diagram .GetCorpus ()+markdown .generatedCorpus (), corpora )
if classes == nil {
themeStylesheet = pruneThemeCSS (themeStylesheet , buf .String (), backgroundStr , doubleBorderElStr )
} else {
themeStylesheet = pruneThemeCSSWithClasses (themeStylesheet , classes , backgroundStr , doubleBorderElStr )
}
fmt .Fprintf (upperBuf , `<style type="text/css"><![CDATA[%s%s]]></style>` , BaseStylesheet , themeStylesheet )
if sketch {
d2sketch .DefineFillPatterns (upperBuf , diagramHash )
}
}
bufStr := buf .String ()
patternDefs := ""
for _ , pattern := range d2ast .FillPatterns {
if strings .Contains (bufStr , fmt .Sprintf ("%s-overlay" , pattern )) || strings .EqualFold (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 (patternassets .GrainSVG (), diagramHash )
case "paper" :
patternDefs += fmt .Sprintf (patternassets .PaperSVG (), 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 == "" {
dataD2Version := ""
if opts .OmitVersion == nil || !*opts .OmitVersion {
dataD2Version = fmt .Sprintf (`data-d2-version="%s"` , version .Version )
}
fitToScreenWrapperOpening = fmt .Sprintf (`<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" %s preserveAspectRatio="%s meet" viewBox="0 0 %d %d"%s>` ,
dataD2Version ,
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 ,
backgroundStr ,
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 err := validateThemeOverrides (overrides , "theme override" ); err != nil {
return "" , err
}
if err := validateThemeOverrides (darkOverrides , "dark theme override" ); err != nil {
return "" , err
}
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 fontToFamily(fontFamily string ) *d2fonts .FontFamily {
ff , ok := d2fonts .D2_FONT_TO_FAMILY [fontFamily ]
if !ok {
return nil
}
return &ff
}
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 ) {
if err := d2target .ValidateRenderTarget (diagram ); err != nil {
return nil , err
}
return renderMultiboard (diagram , opts )
}
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 {
if err := validateRenderLinks (diagram ); err != nil {
return boards , err
}
out , err := renderValidated (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.7 . (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 .