2022-11-03 13:54:49 +00:00
// d2svg implements an SVG renderer for d2 diagrams.
// The input is d2exporter's output
package d2svg
import (
"bytes"
_ "embed"
2025-01-26 20:55:51 +00:00
"encoding/base64"
2022-11-03 13:54:49 +00:00
"errors"
"fmt"
"hash/fnv"
2022-12-24 20:45:12 +00:00
"html"
2022-11-03 13:54:49 +00:00
"io"
2022-11-29 22:21:23 +00:00
"sort"
2022-11-03 13:54:49 +00:00
"strings"
"math"
2023-03-04 04:08:13 +00:00
"github.com/alecthomas/chroma/v2"
"github.com/alecthomas/chroma/v2/formatters"
"github.com/alecthomas/chroma/v2/lexers"
"github.com/alecthomas/chroma/v2/styles"
2022-11-03 13:54:49 +00:00
2024-09-15 16:43:10 +00:00
"oss.terrastruct.com/d2/d2ast"
2022-12-05 20:48:03 +00:00
"oss.terrastruct.com/d2/d2graph"
2022-11-03 13:54:49 +00:00
"oss.terrastruct.com/d2/d2renderers/d2fonts"
2022-11-27 18:11:14 +00:00
"oss.terrastruct.com/d2/d2renderers/d2latex"
2022-12-21 07:43:45 +00:00
"oss.terrastruct.com/d2/d2renderers/d2sketch"
2022-11-03 13:54:49 +00:00
"oss.terrastruct.com/d2/d2target"
2023-02-19 12:00:01 +00:00
"oss.terrastruct.com/d2/d2themes"
2023-01-09 21:16:22 +00:00
"oss.terrastruct.com/d2/d2themes/d2themescatalog"
2022-11-10 19:21:14 +00:00
"oss.terrastruct.com/d2/lib/color"
2022-11-03 13:54:49 +00:00
"oss.terrastruct.com/d2/lib/geo"
2025-01-15 23:30:17 +00:00
"oss.terrastruct.com/d2/lib/jsrunner"
2022-11-03 13:54:49 +00:00
"oss.terrastruct.com/d2/lib/label"
"oss.terrastruct.com/d2/lib/shape"
2022-12-21 07:43:45 +00:00
"oss.terrastruct.com/d2/lib/svg"
2022-12-01 13:46:45 +00:00
"oss.terrastruct.com/d2/lib/textmeasure"
2023-03-11 16:34:31 +00:00
"oss.terrastruct.com/d2/lib/version"
2024-10-09 18:09:46 +00:00
"oss.terrastruct.com/util-go/go2"
2022-11-03 13:54:49 +00:00
)
const (
2023-04-17 19:06:17 +00:00
DEFAULT_PADDING = 100
2022-12-27 07:56:23 +00:00
2022-12-29 00:19:30 +00:00
appendixIconRadius = 16
2025-03-11 21:30:05 +00:00
// Legend constants
LEGEND_PADDING = 20
LEGEND_ITEM_SPACING = 15
LEGEND_ICON_SIZE = 24
LEGEND_FONT_SIZE = 14
LEGEND_CORNER_PADDING = 10
2022-11-03 13:54:49 +00:00
)
2023-01-21 04:04:59 +00:00
var multipleOffset = geo . NewVector ( d2target . MULTIPLE_OFFSET , - d2target . MULTIPLE_OFFSET )
2023-01-19 07:12:26 +00:00
2022-12-27 07:56:23 +00:00
//go:embed tooltip.svg
2022-12-28 20:07:01 +00:00
var TooltipIcon string
2022-12-27 07:56:23 +00:00
2022-12-29 00:19:30 +00:00
//go:embed link.svg
var LinkIcon string
2022-11-12 18:29:21 +00:00
//go:embed style.css
2023-03-23 20:37:28 +00:00
var BaseStylesheet string
2022-11-12 18:29:21 +00:00
2022-11-03 13:54:49 +00:00
//go:embed github-markdown.css
2023-03-23 20:37:28 +00:00
var MarkdownCSS string
2022-11-03 13:54:49 +00:00
2023-03-14 06:01:33 +00:00
//go:embed dots.txt
var dots string
2023-03-16 05:53:12 +00:00
//go:embed lines.txt
var lines string
//go:embed grain.txt
var grain string
2023-03-18 22:43:28 +00:00
//go:embed paper.txt
var paper string
2022-12-21 07:43:45 +00:00
type RenderOpts struct {
2023-12-13 20:17:22 +00:00
Pad * int64
Sketch * bool
Center * bool
ThemeID * int64
DarkThemeID * int64
ThemeOverrides * d2target . ThemeOverrides
DarkThemeOverrides * d2target . ThemeOverrides
Font string
2023-06-15 22:53:23 +00:00
// the svg will be scaled by this factor, if unset the svg will fit to screen
Scale * float64
2023-03-23 20:37:28 +00:00
// MasterID is passed when the diagram should use something other than its own hash for unique targeting
// Currently, that's when multi-boards are collapsed
2025-04-02 19:57:06 +00:00
MasterID string
NoXMLTag * bool
Salt * string
OmitVersion * bool
2022-12-21 07:43:45 +00:00
}
2023-02-19 11:32:44 +00:00
func dimensions ( diagram * d2target . Diagram , pad int ) ( left , top , width , height int ) {
2022-11-03 13:54:49 +00:00
tl , br := diagram . BoundingBox ( )
2023-02-15 19:33:13 +00:00
left = tl . X - pad
top = tl . Y - pad
width = br . X - tl . X + pad * 2
height = br . Y - tl . Y + pad * 2
2022-11-03 13:54:49 +00:00
2023-02-15 19:33:13 +00:00
return left , top , width , height
2022-11-03 13:54:49 +00:00
}
2025-03-11 21:30:05 +00:00
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
}
_ , 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 , nil )
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 , nil )
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 ( ) )
fmt . Fprintf ( buf , ` <text class="text-bold" x="%d" y="%d" style="font-size: %dpx;">Legend</text> ` ,
legendX + LEGEND_PADDING , legendY + LEGEND_PADDING + LEGEND_FONT_SIZE , LEGEND_FONT_SIZE + 2 )
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 , nil )
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 , nil )
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
}
if shapeCount > 0 && len ( diagram . Legend . Connections ) > 0 {
currentY += LEGEND_PADDING / 2
} else {
currentY += LEGEND_PADDING / 4
}
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(%f)"> ` ,
x , y , 1.0 / sizeFactor )
_ , err := drawShape ( buf , appendixBuf , diagramHash , iconShape , 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(%f)"> ` ,
x , y , 1.0 / sizeFactor )
_ , err := drawConnection ( buf , legendHash , legendConn , markers , idToShape , nil , theme )
if err != nil {
return "" , err
}
fmt . Fprint ( finalBuf , buf . String ( ) )
fmt . Fprint ( finalBuf , ` </g> ` )
return finalBuf . String ( ) , nil
}
2025-01-26 21:18:00 +00:00
func arrowheadMarkerID ( diagramHash string , isTarget bool , connection d2target . Connection ) string {
2022-11-03 13:54:49 +00:00
var arrowhead d2target . Arrowhead
if isTarget {
arrowhead = connection . DstArrow
} else {
arrowhead = connection . SrcArrow
}
2025-01-26 21:18:00 +00:00
return fmt . Sprintf ( "mk-%s-%s" , diagramHash , hash ( fmt . Sprintf ( "%s,%t,%d,%s" ,
2023-02-19 13:26:24 +00:00
arrowhead , isTarget , connection . StrokeWidth , connection . Stroke ,
2022-11-03 13:54:49 +00:00
) ) )
}
2024-10-09 18:09:46 +00:00
func arrowheadMarker ( isTarget bool , id string , connection d2target . Connection , inlineTheme * d2themes . Theme ) string {
2022-11-03 13:54:49 +00:00
arrowhead := connection . DstArrow
if ! isTarget {
arrowhead = connection . SrcArrow
}
strokeWidth := float64 ( connection . StrokeWidth )
2023-04-17 19:06:17 +00:00
width , height := arrowhead . Dimensions ( strokeWidth )
2022-11-03 13:54:49 +00:00
var path string
switch arrowhead {
case d2target . ArrowArrowhead :
2024-10-09 18:09:46 +00:00
polygonEl := d2themes . NewThemableElement ( "polygon" , inlineTheme )
2023-02-19 13:26:24 +00:00
polygonEl . Fill = connection . Stroke
polygonEl . ClassName = "connection"
2023-01-09 18:16:28 +00:00
polygonEl . Attributes = fmt . Sprintf ( ` stroke-width="%d" ` , connection . StrokeWidth )
2022-11-03 13:54:49 +00:00
if isTarget {
2023-01-09 18:16:28 +00:00
polygonEl . Points = fmt . Sprintf ( "%f,%f %f,%f %f,%f %f,%f" ,
2022-11-03 13:54:49 +00:00
0. , 0. ,
width , height / 2 ,
0. , height ,
width / 4 , height / 2 ,
)
} else {
2023-01-09 18:16:28 +00:00
polygonEl . Points = fmt . Sprintf ( "%f,%f %f,%f %f,%f %f,%f" ,
2022-11-03 13:54:49 +00:00
0. , height / 2 ,
width , 0. ,
width * 3 / 4 , height / 2 ,
width , height ,
)
}
2023-01-09 18:16:28 +00:00
path = polygonEl . Render ( )
2023-11-08 00:57:43 +00:00
case d2target . UnfilledTriangleArrowhead :
2024-10-09 18:09:46 +00:00
polygonEl := d2themes . NewThemableElement ( "polygon" , inlineTheme )
2023-11-08 00:57:43 +00:00
polygonEl . Fill = d2target . BG_COLOR
polygonEl . Stroke = connection . Stroke
polygonEl . ClassName = "connection"
polygonEl . Attributes = fmt . Sprintf ( ` stroke-width="%d" ` , connection . StrokeWidth )
2023-11-08 01:46:26 +00:00
inset := strokeWidth / 2
2023-11-08 00:57:43 +00:00
if isTarget {
polygonEl . Points = fmt . Sprintf ( "%f,%f %f,%f %f,%f" ,
2023-11-08 01:46:26 +00:00
inset , inset ,
width - inset , height / 2.0 ,
inset , height - inset ,
2023-11-08 00:57:43 +00:00
)
} else {
polygonEl . Points = fmt . Sprintf ( "%f,%f %f,%f %f,%f" ,
2023-11-08 01:46:26 +00:00
width - inset , inset ,
inset , height / 2.0 ,
width - inset , height - inset ,
2023-11-08 00:57:43 +00:00
)
}
path = polygonEl . Render ( )
2022-11-03 13:54:49 +00:00
case d2target . TriangleArrowhead :
2024-10-09 18:09:46 +00:00
polygonEl := d2themes . NewThemableElement ( "polygon" , inlineTheme )
2023-02-19 13:26:24 +00:00
polygonEl . Fill = connection . Stroke
polygonEl . ClassName = "connection"
2023-01-09 18:16:28 +00:00
polygonEl . Attributes = fmt . Sprintf ( ` stroke-width="%d" ` , connection . StrokeWidth )
2022-11-03 13:54:49 +00:00
if isTarget {
2023-01-09 18:16:28 +00:00
polygonEl . Points = fmt . Sprintf ( "%f,%f %f,%f %f,%f" ,
2022-11-03 13:54:49 +00:00
0. , 0. ,
width , height / 2.0 ,
0. , height ,
)
} else {
2023-01-09 18:16:28 +00:00
polygonEl . Points = fmt . Sprintf ( "%f,%f %f,%f %f,%f" ,
2022-11-03 13:54:49 +00:00
width , 0. ,
0. , height / 2.0 ,
width , height ,
)
}
2023-01-09 18:16:28 +00:00
path = polygonEl . Render ( )
2022-11-03 13:54:49 +00:00
case d2target . LineArrowhead :
2024-10-09 18:09:46 +00:00
polylineEl := d2themes . NewThemableElement ( "polyline" , inlineTheme )
2023-01-09 18:16:28 +00:00
polylineEl . Fill = color . None
2023-02-19 13:26:24 +00:00
polylineEl . ClassName = "connection"
polylineEl . Stroke = connection . Stroke
2023-01-09 18:16:28 +00:00
polylineEl . Attributes = fmt . Sprintf ( ` stroke-width="%d" ` , connection . StrokeWidth )
2022-11-03 13:54:49 +00:00
if isTarget {
2023-01-09 18:16:28 +00:00
polylineEl . Points = fmt . Sprintf ( "%f,%f %f,%f %f,%f" ,
2022-11-03 13:54:49 +00:00
strokeWidth / 2 , strokeWidth / 2 ,
width - strokeWidth / 2 , height / 2 ,
strokeWidth / 2 , height - strokeWidth / 2 ,
)
} else {
2023-01-09 18:16:28 +00:00
polylineEl . Points = fmt . Sprintf ( "%f,%f %f,%f %f,%f" ,
2022-11-03 13:54:49 +00:00
width - strokeWidth / 2 , strokeWidth / 2 ,
strokeWidth / 2 , height / 2 ,
width - strokeWidth / 2 , height - strokeWidth / 2 ,
)
}
2023-01-09 18:16:28 +00:00
path = polylineEl . Render ( )
2022-11-03 13:54:49 +00:00
case d2target . FilledDiamondArrowhead :
2024-10-09 18:09:46 +00:00
polygonEl := d2themes . NewThemableElement ( "polygon" , inlineTheme )
2023-02-19 13:26:24 +00:00
polygonEl . ClassName = "connection"
polygonEl . Fill = connection . Stroke
2023-01-09 18:16:28 +00:00
polygonEl . Attributes = fmt . Sprintf ( ` stroke-width="%d" ` , connection . StrokeWidth )
2022-11-03 13:54:49 +00:00
if isTarget {
2023-01-09 18:16:28 +00:00
polygonEl . Points = fmt . Sprintf ( "%f,%f %f,%f %f,%f %f,%f" ,
2022-11-03 13:54:49 +00:00
0. , height / 2.0 ,
width / 2.0 , 0. ,
width , height / 2.0 ,
width / 2.0 , height ,
)
} else {
2023-01-09 18:16:28 +00:00
polygonEl . Points = fmt . Sprintf ( "%f,%f %f,%f %f,%f %f,%f" ,
2022-11-03 13:54:49 +00:00
0. , height / 2.0 ,
width / 2.0 , 0. ,
width , height / 2.0 ,
width / 2.0 , height ,
)
}
2023-01-09 18:16:28 +00:00
path = polygonEl . Render ( )
2022-11-03 13:54:49 +00:00
case d2target . DiamondArrowhead :
2024-10-09 18:09:46 +00:00
polygonEl := d2themes . NewThemableElement ( "polygon" , inlineTheme )
2023-02-19 13:26:24 +00:00
polygonEl . ClassName = "connection"
2023-02-26 19:41:50 +00:00
polygonEl . Fill = d2target . BG_COLOR
2023-02-19 13:26:24 +00:00
polygonEl . Stroke = connection . Stroke
2023-01-09 18:16:28 +00:00
polygonEl . Attributes = fmt . Sprintf ( ` stroke-width="%d" ` , connection . StrokeWidth )
2022-11-03 13:54:49 +00:00
if isTarget {
2023-01-09 18:16:28 +00:00
polygonEl . Points = fmt . Sprintf ( "%f,%f %f,%f %f,%f %f,%f" ,
2022-11-03 13:54:49 +00:00
0. , height / 2.0 ,
width / 2 , height / 8 ,
width , height / 2.0 ,
width / 2.0 , height * 0.9 ,
)
} else {
2023-01-09 18:16:28 +00:00
polygonEl . Points = fmt . Sprintf ( "%f,%f %f,%f %f,%f %f,%f" ,
2022-11-03 13:54:49 +00:00
width / 8 , height / 2.0 ,
width * 0.6 , height / 8 ,
width * 1.1 , height / 2.0 ,
width * 0.6 , height * 7 / 8 ,
)
}
2023-01-09 18:16:28 +00:00
path = polygonEl . Render ( )
2023-01-10 03:44:45 +00:00
case d2target . FilledCircleArrowhead :
2023-01-19 19:19:29 +00:00
radius := width / 2
2023-01-27 20:08:01 +00:00
2024-10-09 18:09:46 +00:00
circleEl := d2themes . NewThemableElement ( "circle" , inlineTheme )
2023-01-27 20:08:01 +00:00
circleEl . Cy = radius
2023-02-22 17:50:06 +00:00
circleEl . R = radius - strokeWidth / 2
2023-01-27 20:08:01 +00:00
circleEl . Fill = connection . Stroke
2023-02-19 13:26:24 +00:00
circleEl . ClassName = "connection"
2023-01-27 20:08:01 +00:00
circleEl . Attributes = fmt . Sprintf ( ` stroke-width="%d" ` , connection . StrokeWidth )
2023-01-10 03:44:45 +00:00
if isTarget {
2023-01-27 20:08:01 +00:00
circleEl . Cx = radius + strokeWidth / 2
2023-01-10 03:44:45 +00:00
} else {
2023-02-22 17:50:06 +00:00
circleEl . Cx = radius - strokeWidth / 2
2023-01-10 03:44:45 +00:00
}
2023-01-27 20:08:01 +00:00
path = circleEl . Render ( )
2023-01-10 03:44:45 +00:00
case d2target . CircleArrowhead :
2023-01-19 19:19:29 +00:00
radius := width / 2
2023-01-27 20:08:01 +00:00
2024-10-09 18:09:46 +00:00
circleEl := d2themes . NewThemableElement ( "circle" , inlineTheme )
2023-01-27 20:08:01 +00:00
circleEl . Cy = radius
circleEl . R = radius - strokeWidth
2023-02-26 19:41:50 +00:00
circleEl . Fill = d2target . BG_COLOR
2023-01-27 20:08:01 +00:00
circleEl . Stroke = connection . Stroke
circleEl . Attributes = fmt . Sprintf ( ` stroke-width="%d" ` , connection . StrokeWidth )
2023-01-10 03:44:45 +00:00
if isTarget {
2023-01-27 20:08:01 +00:00
circleEl . Cx = radius + strokeWidth / 2
2023-01-10 03:44:45 +00:00
} else {
2023-01-27 20:08:01 +00:00
circleEl . Cx = radius - strokeWidth / 2
2023-01-10 03:44:45 +00:00
}
2023-01-27 20:08:01 +00:00
path = circleEl . Render ( )
2024-12-18 17:40:01 +00:00
case d2target . FilledBoxArrowhead :
polygonEl := d2themes . NewThemableElement ( "polygon" , inlineTheme )
polygonEl . ClassName = "connection"
polygonEl . Fill = connection . Stroke
polygonEl . Attributes = fmt . Sprintf ( ` stroke-width="%d" ` , connection . StrokeWidth )
if isTarget {
polygonEl . Points = fmt . Sprintf ( "%f,%f %f,%f %f,%f %f,%f" ,
0. , 0. ,
0. , height ,
width , height ,
width , 0. ,
)
} else {
polygonEl . Points = fmt . Sprintf ( "%f,%f %f,%f %f,%f %f,%f" ,
0. , 0. ,
0. , height ,
width , height ,
width , 0. ,
)
}
2025-02-04 13:25:28 +00:00
2024-12-18 17:40:01 +00:00
path = polygonEl . Render ( )
case d2target . BoxArrowhead :
polygonEl := d2themes . NewThemableElement ( "polygon" , inlineTheme )
polygonEl . ClassName = "connection"
polygonEl . Fill = d2target . BG_COLOR
polygonEl . Stroke = connection . Stroke
polygonEl . Attributes = fmt . Sprintf ( ` stroke-width="%d" ` , connection . StrokeWidth )
2025-02-04 13:25:28 +00:00
polygonEl . Style = fmt . Sprintf ( "%sstroke-linejoin:miter;" , polygonEl . Style )
2024-12-18 17:40:01 +00:00
2025-02-04 13:25:28 +00:00
inset := strokeWidth / 2
2024-12-18 17:40:01 +00:00
if isTarget {
polygonEl . Points = fmt . Sprintf ( "%f,%f %f,%f %f,%f %f,%f" ,
2025-02-04 13:25:28 +00:00
inset , inset ,
inset , height - inset ,
width - inset , height - inset ,
width - inset , inset ,
2024-12-18 17:40:01 +00:00
)
} else {
polygonEl . Points = fmt . Sprintf ( "%f,%f %f,%f %f,%f %f,%f" ,
2025-02-04 13:25:28 +00:00
inset , inset ,
inset , height - inset ,
width - inset , height - inset ,
width - inset , inset ,
2024-12-18 17:40:01 +00:00
)
}
path = polygonEl . Render ( )
2022-12-31 16:50:40 +00:00
case d2target . CfOne , d2target . CfMany , d2target . CfOneRequired , d2target . CfManyRequired :
2023-02-10 08:57:15 +00:00
offset := 3.0 + float64 ( connection . StrokeWidth ) * 1.8
2023-01-09 18:16:28 +00:00
2023-02-19 12:00:01 +00:00
var modifierEl * d2themes . ThemableElement
2022-12-31 17:56:34 +00:00
if arrowhead == d2target . CfOneRequired || arrowhead == d2target . CfManyRequired {
2024-10-09 18:09:46 +00:00
modifierEl = d2themes . NewThemableElement ( "path" , inlineTheme )
2023-01-09 18:16:28 +00:00
modifierEl . D = fmt . Sprintf ( "M%f,%f %f,%f" ,
2022-12-31 10:59:40 +00:00
offset , 0. ,
offset , height ,
)
2023-02-26 19:41:50 +00:00
modifierEl . Fill = d2target . BG_COLOR
2023-02-19 13:26:24 +00:00
modifierEl . Stroke = connection . Stroke
modifierEl . ClassName = "connection"
2023-01-16 21:19:19 +00:00
modifierEl . Attributes = fmt . Sprintf ( ` stroke-width="%d" ` , connection . StrokeWidth )
2022-12-31 10:59:40 +00:00
} else {
2024-10-09 18:09:46 +00:00
modifierEl = d2themes . NewThemableElement ( "circle" , inlineTheme )
2023-02-19 11:32:44 +00:00
modifierEl . Cx = offset / 2.0 + 2.0
2023-01-09 18:16:28 +00:00
modifierEl . Cy = height / 2.0
modifierEl . R = offset / 2.0
2023-02-26 19:41:50 +00:00
modifierEl . Fill = d2target . BG_COLOR
2023-02-19 13:26:24 +00:00
modifierEl . Stroke = connection . Stroke
modifierEl . ClassName = "connection"
2023-01-16 21:19:19 +00:00
modifierEl . Attributes = fmt . Sprintf ( ` stroke-width="%d" ` , connection . StrokeWidth )
2022-12-31 10:59:40 +00:00
}
2023-01-09 18:16:28 +00:00
2024-10-09 18:09:46 +00:00
childPathEl := d2themes . NewThemableElement ( "path" , inlineTheme )
2022-12-31 17:56:34 +00:00
if arrowhead == d2target . CfMany || arrowhead == d2target . CfManyRequired {
2023-01-09 18:16:28 +00:00
childPathEl . D = fmt . Sprintf ( "M%f,%f %f,%f M%f,%f %f,%f M%f,%f %f,%f" ,
2023-01-01 10:31:45 +00:00
width - 3.0 , height / 2.0 ,
2022-12-31 10:59:40 +00:00
width + offset , height / 2.0 ,
2023-02-10 08:57:15 +00:00
offset + 3.0 , height / 2.0 ,
2022-12-31 10:59:40 +00:00
width + offset , 0. ,
2023-02-10 08:57:15 +00:00
offset + 3.0 , height / 2.0 ,
2022-12-31 10:59:40 +00:00
width + offset , height ,
)
} else {
2023-01-09 18:16:28 +00:00
childPathEl . D = fmt . Sprintf ( "M%f,%f %f,%f M%f,%f %f,%f" ,
2023-01-01 10:31:45 +00:00
width - 3.0 , height / 2.0 ,
2022-12-31 10:59:40 +00:00
width + offset , height / 2.0 ,
2023-02-10 08:57:15 +00:00
offset * 2.0 , 0. ,
offset * 2.0 , height ,
2022-12-31 10:59:40 +00:00
)
}
2023-01-09 18:16:28 +00:00
2024-10-09 18:09:46 +00:00
gEl := d2themes . NewThemableElement ( "g" , inlineTheme )
2023-01-09 18:16:28 +00:00
if ! isTarget {
gEl . Transform = fmt . Sprintf ( "scale(-1) translate(-%f, -%f)" , width , height )
}
2023-02-26 19:41:50 +00:00
gEl . Fill = d2target . BG_COLOR
2023-02-19 13:26:24 +00:00
gEl . Stroke = connection . Stroke
gEl . ClassName = "connection"
2023-01-16 21:19:19 +00:00
gEl . Attributes = fmt . Sprintf ( ` stroke-width="%d" ` , connection . StrokeWidth )
2023-01-09 18:16:28 +00:00
gEl . Content = fmt . Sprintf ( "%s%s" ,
modifierEl . Render ( ) , childPathEl . Render ( ) ,
)
path = gEl . Render ( )
2022-11-03 13:54:49 +00:00
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 {
2022-11-09 17:37:28 +00:00
refX = width - 1.5 * strokeWidth
2022-11-03 13:54:49 +00:00
} else {
2022-11-09 17:37:28 +00:00
refX = 1.5 * strokeWidth
2022-11-03 13:54:49 +00:00
}
}
return strings . Join ( [ ] string {
fmt . Sprintf ( ` <marker id="%s" markerWidth="%f" markerHeight="%f" refX="%f" refY="%f" ` ,
id , width , height , refX , refY ,
) ,
fmt . Sprintf ( ` viewBox="%f %f %f %f" ` , 0. , 0. , width , height ) ,
` orient="auto" markerUnits="userSpaceOnUse"> ` ,
path ,
"</marker>" ,
} , " " )
}
// compute the (dx, dy) adjustment to apply to get the arrowhead-adjusted end point
2022-11-09 17:37:28 +00:00
func arrowheadAdjustment ( start , end * geo . Point , arrowhead d2target . Arrowhead , edgeStrokeWidth , shapeStrokeWidth int ) * geo . Point {
distance := ( float64 ( edgeStrokeWidth ) + float64 ( shapeStrokeWidth ) ) / 2.0
2022-11-03 13:54:49 +00:00
if arrowhead != d2target . NoArrowhead {
2022-11-09 17:37:28 +00:00
distance += float64 ( edgeStrokeWidth )
2022-11-03 13:54:49 +00:00
}
v := geo . NewVector ( end . X - start . X , end . Y - start . Y )
return v . Unit ( ) . Multiply ( - distance ) . ToPoint ( )
}
2023-01-13 04:41:25 +00:00
func getArrowheadAdjustments ( connection d2target . Connection , idToShape map [ string ] d2target . Shape ) ( srcAdj , dstAdj * geo . Point ) {
2022-11-03 13:54:49 +00:00
route := connection . Route
2022-11-09 17:37:28 +00:00
srcShape := idToShape [ connection . Src ]
dstShape := idToShape [ connection . Dst ]
2022-11-03 13:54:49 +00:00
2023-01-13 04:41:25 +00:00
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
}
// returns the path's d attribute for the given connection
func pathData ( connection d2target . Connection , srcAdj , dstAdj * geo . Point ) string {
var path [ ] string
route := connection . Route
2022-11-03 13:54:49 +00:00
path = append ( path , fmt . Sprintf ( "M %f %f" ,
2023-01-13 04:41:25 +00:00
route [ 0 ] . X + srcAdj . X ,
route [ 0 ] . Y + srcAdj . Y ,
2022-11-03 13:54:49 +00:00
) )
if connection . IsCurve {
i := 1
for ; i < len ( route ) - 3 ; i += 3 {
path = append ( path , fmt . Sprintf ( "C %f %f %f %f %f %f" ,
route [ i ] . X , route [ i ] . Y ,
route [ i + 1 ] . X , route [ i + 1 ] . Y ,
route [ i + 2 ] . X , route [ i + 2 ] . Y ,
) )
}
// final curve target adjustment
path = append ( path , fmt . Sprintf ( "C %f %f %f %f %f %f" ,
route [ i ] . X , route [ i ] . Y ,
route [ i + 1 ] . X , route [ i + 1 ] . Y ,
2023-01-13 04:41:25 +00:00
route [ i + 2 ] . X + dstAdj . X ,
route [ i + 2 ] . Y + dstAdj . Y ,
2022-11-03 13:54:49 +00:00
) )
} 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 )
2023-02-28 04:06:24 +00:00
connectionBorderRadius := connection . BorderRadius
units := math . Min ( connectionBorderRadius , dist / 2 )
2022-11-03 13:54:49 +00:00
prevTranslations := prevVector . Unit ( ) . Multiply ( units ) . ToPoint ( )
currTranslations := currVector . Unit ( ) . Multiply ( units ) . ToPoint ( )
path = append ( path , fmt . Sprintf ( "L %f %f" ,
prevTarget . X - prevTranslations . X ,
prevTarget . Y - prevTranslations . Y ,
) )
// If the segment length is too small, instead of drawing 2 arcs, just skip this segment and bezier curve to the next one
2023-02-28 04:06:24 +00:00
if units < connectionBorderRadius && i < len ( route ) - 2 {
2022-11-03 13:54:49 +00:00
nextTarget := route [ i + 2 ]
nextVector := geo . NewVector ( nextTarget . X - currTarget . X , nextTarget . Y - currTarget . Y )
i ++
nextTranslations := nextVector . Unit ( ) . Multiply ( units ) . ToPoint ( )
// These 2 bezier control points aren't just at the corner -- they are reflected at the corner, which causes the curve to be ~tangent to the corner,
// which matches how the two arcs look
path = append ( path , fmt . Sprintf ( "C %f %f %f %f %f %f" ,
// Control point
prevTarget . X + prevTranslations . X ,
prevTarget . Y + prevTranslations . Y ,
// Control point
currTarget . X - nextTranslations . X ,
currTarget . Y - nextTranslations . Y ,
// Where curve ends
currTarget . X + nextTranslations . X ,
currTarget . Y + nextTranslations . Y ,
) )
} else {
path = append ( path , fmt . Sprintf ( "S %f %f %f %f" ,
prevTarget . X ,
prevTarget . Y ,
prevTarget . X + currTranslations . X ,
prevTarget . Y + currTranslations . Y ,
) )
}
}
lastPoint := route [ len ( route ) - 1 ]
path = append ( path , fmt . Sprintf ( "L %f %f" ,
2023-01-13 04:41:25 +00:00
lastPoint . X + dstAdj . X ,
lastPoint . Y + dstAdj . Y ,
2022-11-03 13:54:49 +00:00
) )
}
return strings . Join ( path , " " )
}
2023-06-08 22:54:17 +00:00
func makeLabelMask ( labelTL * geo . Point , width , height int , opacity float64 ) string {
fill := "black"
if opacity != 1 {
fill = fmt . Sprintf ( "rgba(0,0,0,%.2f)" , opacity )
}
return fmt . Sprintf ( ` <rect x="%f" y="%f" width="%d" height="%d" fill="%s"></rect> ` ,
2025-03-24 22:35:11 +00:00
labelTL . X - 2 , labelTL . Y ,
width + 4 ,
2022-12-05 19:57:16 +00:00
height ,
2023-06-08 22:54:17 +00:00
fill ,
2022-12-03 06:47:54 +00:00
)
2022-11-03 13:54:49 +00:00
}
2025-01-26 21:18:00 +00:00
func drawConnection ( writer io . Writer , diagramHash string , connection d2target . Connection , markers map [ string ] struct { } , idToShape map [ string ] d2target . Shape , jsRunner jsrunner . JSRunner , inlineTheme * d2themes . Theme ) ( labelMask string , _ error ) {
2023-01-19 08:46:30 +00:00
opacityStyle := ""
if connection . Opacity != 1.0 {
opacityStyle = fmt . Sprintf ( " style='opacity:%f'" , connection . Opacity )
}
2023-02-06 21:32:08 +00:00
2025-02-04 17:23:26 +00:00
classes := [ ] string { base64 . URLEncoding . EncodeToString ( [ ] byte ( svg . EscapeText ( connection . ID ) ) ) }
classes = append ( classes , connection . Classes ... )
classStr := fmt . Sprintf ( ` class="%s" ` , strings . Join ( classes , " " ) )
2025-02-04 17:25:03 +00:00
fmt . Fprintf ( writer , ` <g%s%s> ` , classStr , opacityStyle )
2022-11-03 13:54:49 +00:00
var markerStart string
if connection . SrcArrow != d2target . NoArrowhead {
2025-01-26 21:18:00 +00:00
id := arrowheadMarkerID ( diagramHash , false , connection )
2022-11-03 13:54:49 +00:00
if _ , in := markers [ id ] ; ! in {
2024-10-09 18:09:46 +00:00
marker := arrowheadMarker ( false , id , connection , inlineTheme )
2022-11-03 13:54:49 +00:00
if marker == "" {
panic ( fmt . Sprintf ( "received empty arrow head marker for: %#v" , connection ) )
}
fmt . Fprint ( writer , marker )
markers [ id ] = struct { } { }
}
markerStart = fmt . Sprintf ( ` marker-start="url(#%s)" ` , id )
}
var markerEnd string
if connection . DstArrow != d2target . NoArrowhead {
2025-01-26 21:18:00 +00:00
id := arrowheadMarkerID ( diagramHash , true , connection )
2022-11-03 13:54:49 +00:00
if _ , in := markers [ id ] ; ! in {
2024-10-09 18:09:46 +00:00
marker := arrowheadMarker ( true , id , connection , inlineTheme )
2022-11-03 13:54:49 +00:00
if marker == "" {
panic ( fmt . Sprintf ( "received empty arrow head marker for: %#v" , connection ) )
}
fmt . Fprint ( writer , marker )
markers [ id ] = struct { } { }
}
markerEnd = fmt . Sprintf ( ` marker-end="url(#%s)" ` , id )
}
2025-02-17 04:49:49 +00:00
if connection . Icon != nil {
iconPos := connection . GetIconPosition ( )
if iconPos != nil {
2025-03-29 09:09:40 +00:00
connectionIconClipPath := ""
2025-03-19 17:32:49 +00:00
if connection . IconBorderRadius != 0 {
2025-03-29 09:09:40 +00:00
connectionIconClipPath = fmt . Sprintf ( ` clip-path="inset(0 round %fpx)" ` , connection . IconBorderRadius )
2025-03-19 17:32:49 +00:00
}
2025-03-29 09:09:40 +00:00
fmt . Fprintf ( writer , ` <image href="%s" x="%f" y="%f" width="%d" height="%d"%s /> ` ,
html . EscapeString ( connection . Icon . String ( ) ) ,
iconPos . X ,
iconPos . Y ,
d2target . DEFAULT_ICON_SIZE ,
d2target . DEFAULT_ICON_SIZE ,
connectionIconClipPath ,
)
2025-02-17 04:49:49 +00:00
}
}
2022-11-03 13:54:49 +00:00
var labelTL * geo . Point
if connection . Label != "" {
labelTL = connection . GetLabelTopLeft ( )
labelTL . X = math . Round ( labelTL . X )
labelTL . Y = math . Round ( labelTL . Y )
2025-02-17 04:49:49 +00:00
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 )
}
2023-07-17 21:21:36 +00:00
if label . FromString ( connection . LabelPosition ) . IsOnEdge ( ) {
2025-02-17 04:49:49 +00:00
labelMask = makeLabelMask ( maskTL , width , height , 1 )
2022-12-15 03:29:09 +00:00
} else {
2025-02-17 04:49:49 +00:00
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 )
}
2022-11-03 13:54:49 +00:00
}
}
2023-01-13 04:41:25 +00:00
srcAdj , dstAdj := getArrowheadAdjustments ( connection , idToShape )
path := pathData ( connection , srcAdj , dstAdj )
2025-01-26 21:18:00 +00:00
mask := fmt . Sprintf ( ` mask="url(#%s)" ` , diagramHash )
2024-05-14 21:08:25 +00:00
2025-01-15 23:30:17 +00:00
if jsRunner != nil {
out , err := d2sketch . Connection ( jsRunner , connection , path , mask )
2022-12-21 07:43:45 +00:00
if err != nil {
return "" , err
}
2023-01-09 18:16:28 +00:00
fmt . Fprint ( writer , out )
2023-01-12 20:01:49 +00:00
// render sketch arrowheads separately
2025-01-15 23:30:17 +00:00
arrowPaths , err := d2sketch . Arrowheads ( jsRunner , connection , srcAdj , dstAdj )
2023-01-13 02:27:53 +00:00
if err != nil {
return "" , err
2023-01-12 20:01:49 +00:00
}
2023-01-13 02:27:53 +00:00
fmt . Fprint ( writer , arrowPaths )
2022-12-21 07:43:45 +00:00
} else {
2023-01-16 11:15:59 +00:00
animatedClass := ""
if connection . Animated {
animatedClass = " animated-connection"
}
2024-05-18 16:52:04 +00:00
// If connection is animated and bidirectional
if connection . Animated && ( ( connection . DstArrow == d2target . NoArrowhead && connection . SrcArrow == d2target . NoArrowhead ) || ( connection . DstArrow != d2target . NoArrowhead && connection . SrcArrow != d2target . NoArrowhead ) ) {
// There is no pure CSS way to animate bidirectional connections in two directions, so we split it up
2024-05-20 11:39:06 +00:00
path1 , path2 , err := svg . SplitPath ( path , 0.5 )
2024-05-17 03:14:15 +00:00
if err != nil {
return "" , err
}
2024-05-14 20:28:44 +00:00
2024-10-09 18:09:46 +00:00
pathEl1 := d2themes . NewThemableElement ( "path" , inlineTheme )
2024-05-14 20:28:44 +00:00
pathEl1 . D = path1
pathEl1 . Fill = color . None
pathEl1 . Stroke = connection . Stroke
pathEl1 . ClassName = fmt . Sprintf ( "connection%s" , animatedClass )
pathEl1 . Style = connection . CSSStyle ( )
pathEl1 . Style += "animation-direction: reverse;"
pathEl1 . Attributes = fmt . Sprintf ( "%s%s" , markerStart , mask )
fmt . Fprint ( writer , pathEl1 . Render ( ) )
2024-10-09 18:09:46 +00:00
pathEl2 := d2themes . NewThemableElement ( "path" , inlineTheme )
2024-05-14 20:28:44 +00:00
pathEl2 . D = path2
pathEl2 . Fill = color . None
pathEl2 . Stroke = connection . Stroke
pathEl2 . ClassName = fmt . Sprintf ( "connection%s" , animatedClass )
pathEl2 . Style = connection . CSSStyle ( )
pathEl2 . Attributes = fmt . Sprintf ( "%s%s" , markerEnd , mask )
fmt . Fprint ( writer , pathEl2 . Render ( ) )
2024-05-18 16:52:04 +00:00
} else {
2024-10-09 18:09:46 +00:00
pathEl := d2themes . NewThemableElement ( "path" , inlineTheme )
2024-05-18 16:52:04 +00:00
pathEl . D = path
pathEl . Fill = color . None
pathEl . Stroke = connection . Stroke
pathEl . ClassName = fmt . Sprintf ( "connection%s" , animatedClass )
pathEl . Style = connection . CSSStyle ( )
pathEl . Attributes = fmt . Sprintf ( "%s%s%s" , markerStart , markerEnd , mask )
fmt . Fprint ( writer , pathEl . Render ( ) )
2024-05-14 20:28:44 +00:00
}
2022-12-21 07:43:45 +00:00
}
2022-11-03 13:54:49 +00:00
if connection . Label != "" {
fontClass := "text"
2023-03-13 23:14:50 +00:00
if connection . FontFamily == "mono" {
fontClass = "text-mono"
}
2022-11-03 13:54:49 +00:00
if connection . Bold {
fontClass += "-bold"
} else if connection . Italic {
fontClass += "-italic"
}
2024-02-15 00:46:47 +00:00
if connection . Underline {
fontClass += " text-underline"
}
2023-01-09 18:16:28 +00:00
if connection . Fill != color . Empty {
2024-10-09 18:09:46 +00:00
rectEl := d2themes . NewThemableElement ( "rect" , inlineTheme )
2024-09-14 23:38:47 +00:00
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
2023-01-09 18:16:28 +00:00
rectEl . Fill = connection . Fill
fmt . Fprint ( writer , rectEl . Render ( ) )
2022-12-16 23:44:26 +00:00
}
2023-01-09 18:16:28 +00:00
2024-10-09 18:09:46 +00:00
textEl := d2themes . NewThemableElement ( "text" , inlineTheme )
2023-01-09 18:16:28 +00:00
textEl . X = labelTL . X + float64 ( connection . LabelWidth ) / 2
textEl . Y = labelTL . Y + float64 ( connection . FontSize )
2023-02-19 13:26:24 +00:00
textEl . ClassName = fontClass
2023-01-09 18:16:28 +00:00
textEl . Style = fmt . Sprintf ( "text-anchor:%s;font-size:%vpx" , "middle" , connection . FontSize )
textEl . Content = RenderText ( connection . Label , textEl . X , float64 ( connection . LabelHeight ) )
2024-05-30 01:05:52 +00:00
if connection . Link != "" {
2024-05-30 01:45:36 +00:00
textEl . ClassName += " text-underline text-link"
2024-05-30 01:05:52 +00:00
fmt . Fprintf ( writer , ` <a href="%s" xlink:href="%[1]s"> ` , svg . EscapeText ( connection . Link ) )
2024-05-30 01:45:36 +00:00
} else {
textEl . Fill = connection . GetFontColor ( )
2024-05-30 01:05:52 +00:00
}
2023-01-09 18:16:28 +00:00
fmt . Fprint ( writer , textEl . Render ( ) )
2024-05-30 01:05:52 +00:00
if connection . Link != "" {
fmt . Fprintf ( writer , "</a>" )
}
2022-11-03 13:54:49 +00:00
}
2022-11-24 04:14:46 +00:00
2023-04-14 18:51:48 +00:00
if connection . SrcLabel != nil && connection . SrcLabel . Label != "" {
2024-10-09 18:09:46 +00:00
fmt . Fprint ( writer , renderArrowheadLabel ( connection , connection . SrcLabel . Label , false , inlineTheme ) )
2022-11-24 04:14:46 +00:00
}
2023-04-14 18:51:48 +00:00
if connection . DstLabel != nil && connection . DstLabel . Label != "" {
2024-10-09 18:09:46 +00:00
fmt . Fprint ( writer , renderArrowheadLabel ( connection , connection . DstLabel . Label , true , inlineTheme ) )
2022-11-24 04:14:46 +00:00
}
2022-11-26 03:31:54 +00:00
fmt . Fprintf ( writer , ` </g> ` )
2022-12-03 06:47:54 +00:00
return
2022-11-03 13:54:49 +00:00
}
2024-10-09 18:09:46 +00:00
func renderArrowheadLabel ( connection d2target . Connection , text string , isDst bool , inlineTheme * d2themes . Theme ) string {
2023-04-15 02:08:29 +00:00
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 )
}
2023-04-17 19:06:17 +00:00
labelTL := connection . GetArrowheadLabelPosition ( isDst )
2023-04-15 02:08:29 +00:00
// svg text is positioned with the center of its baseline
baselineCenter := geo . Point {
X : labelTL . X + width / 2. ,
Y : labelTL . Y + float64 ( connection . FontSize ) ,
}
2022-11-24 04:40:24 +00:00
2024-10-09 18:09:46 +00:00
textEl := d2themes . NewThemableElement ( "text" , inlineTheme )
2023-04-17 18:06:36 +00:00
textEl . X = baselineCenter . X
textEl . Y = baselineCenter . Y
2023-02-26 19:41:50 +00:00
textEl . Fill = d2target . FG_COLOR
2023-09-11 19:19:26 +00:00
if isDst {
if connection . DstLabel . Color != "" {
textEl . Fill = connection . DstLabel . Color
}
} else {
if connection . SrcLabel . Color != "" {
textEl . Fill = connection . SrcLabel . Color
}
}
2023-02-19 13:26:24 +00:00
textEl . ClassName = "text-italic"
2023-04-15 02:08:29 +00:00
textEl . Style = fmt . Sprintf ( "text-anchor:middle;font-size:%vpx" , connection . FontSize )
2023-01-09 18:16:28 +00:00
textEl . Content = RenderText ( text , textEl . X , height )
return textEl . Render ( )
2022-11-24 04:40:24 +00:00
}
2024-10-09 18:09:46 +00:00
func renderOval ( tl * geo . Point , width , height float64 , fill , fillPattern , stroke , style string , inlineTheme * d2themes . Theme ) string {
el := d2themes . NewThemableElement ( "ellipse" , inlineTheme )
2023-01-09 18:16:28 +00:00
el . Rx = width / 2
el . Ry = height / 2
el . Cx = tl . X + el . Rx
el . Cy = tl . Y + el . Ry
2023-01-27 21:30:44 +00:00
el . Fill , el . Stroke = fill , stroke
2023-03-14 06:01:33 +00:00
el . FillPattern = fillPattern
2023-02-19 13:26:24 +00:00
el . ClassName = "shape"
2023-01-09 18:16:28 +00:00
el . Style = style
return el . Render ( )
2022-11-09 03:40:20 +00:00
}
2024-10-09 18:09:46 +00:00
func renderDoubleOval ( tl * geo . Point , width , height float64 , fill , fillStroke , stroke , style string , inlineTheme * d2themes . Theme ) string {
2023-01-22 10:40:47 +00:00
var innerTL * geo . Point = tl . AddVector ( geo . NewVector ( d2target . INNER_BORDER_OFFSET , d2target . INNER_BORDER_OFFSET ) )
2024-10-09 18:09:46 +00:00
return renderOval ( tl , width , height , fill , fillStroke , stroke , style , inlineTheme ) + renderOval ( innerTL , width - 10 , height - 10 , fill , "" , stroke , style , inlineTheme )
2022-12-30 09:14:44 +00:00
}
2024-09-27 21:49:10 +00:00
func defineGradients ( writer io . Writer , cssGradient string ) {
gradient , _ := color . ParseGradient ( cssGradient )
fmt . Fprint ( writer , fmt . Sprintf ( ` <defs>%s</defs> ` , color . GradientToSVG ( gradient ) ) )
}
2022-11-09 19:10:51 +00:00
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 >
2022-11-09 19:14:31 +00:00
< 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 >
2022-11-09 19:10:51 +00:00
< / filter >
< / defs > ` )
}
2025-01-26 21:11:53 +00:00
func render3DRect ( diagramHash string , targetShape d2target . Shape , inlineTheme * d2themes . Theme ) string {
2022-11-28 22:04:45 +00:00
moveTo := func ( p d2target . Point ) string {
2022-11-30 00:39:38 +00:00
return fmt . Sprintf ( "M%d,%d" , p . X + targetShape . Pos . X , p . Y + targetShape . Pos . Y )
2022-11-28 22:04:45 +00:00
}
lineTo := func ( p d2target . Point ) string {
2022-11-30 00:39:38 +00:00
return fmt . Sprintf ( "L%d,%d" , p . X + targetShape . Pos . X , p . Y + targetShape . Pos . Y )
2022-11-28 22:04:45 +00:00
}
// draw border all in one path to prevent overlapping sections
var borderSegments [ ] string
borderSegments = append ( borderSegments ,
moveTo ( d2target . Point { X : 0 , Y : 0 } ) ,
)
for _ , v := range [ ] d2target . Point {
2023-01-19 19:51:30 +00:00
{ 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 } ,
2022-11-28 22:04:45 +00:00
{ 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 ) )
}
// move to top right to draw last segment without overlapping
borderSegments = append ( borderSegments ,
moveTo ( d2target . Point { X : targetShape . Width , Y : 0 } ) ,
)
borderSegments = append ( borderSegments ,
2023-01-19 19:51:30 +00:00
lineTo ( d2target . Point { X : targetShape . Width + d2target . THREE_DEE_OFFSET , Y : - d2target . THREE_DEE_OFFSET } ) ,
2022-11-28 22:04:45 +00:00
)
2024-10-09 18:09:46 +00:00
border := d2themes . NewThemableElement ( "path" , inlineTheme )
2023-01-09 18:16:28 +00:00
border . D = strings . Join ( borderSegments , " " )
2023-01-09 21:50:17 +00:00
border . Fill = color . None
2023-02-19 12:00:01 +00:00
_ , borderStroke := d2themes . ShapeTheme ( targetShape )
2023-01-09 18:16:28 +00:00
border . Stroke = borderStroke
2023-01-15 20:36:43 +00:00
borderStyle := targetShape . CSSStyle ( )
2023-01-09 18:16:28 +00:00
border . Style = borderStyle
renderedBorder := border . Render ( )
2022-11-28 22:04:45 +00:00
// create mask from border stroke, to cut away from the shape fills
2025-01-26 21:11:53 +00:00
maskID := fmt . Sprintf ( "border-mask-%v-%v" , diagramHash , svg . EscapeText ( targetShape . ID ) )
2022-11-28 22:04:45 +00:00
borderMask := strings . Join ( [ ] string {
fmt . Sprintf ( ` <defs><mask id="%s" maskUnits="userSpaceOnUse" x="%d" y="%d" width="%d" height="%d"> ` ,
2023-01-19 19:51:30 +00:00
maskID , targetShape . Pos . X , targetShape . Pos . Y - d2target . THREE_DEE_OFFSET , targetShape . Width + d2target . THREE_DEE_OFFSET , targetShape . Height + d2target . THREE_DEE_OFFSET ,
2022-11-28 22:04:45 +00:00
) ,
fmt . Sprintf ( ` <rect x="%d" y="%d" width="%d" height="%d" fill="white"></rect> ` ,
2023-01-19 19:51:30 +00:00
targetShape . Pos . X , targetShape . Pos . Y - d2target . THREE_DEE_OFFSET , targetShape . Width + d2target . THREE_DEE_OFFSET , targetShape . Height + d2target . THREE_DEE_OFFSET ,
2022-11-28 22:04:45 +00:00
) ,
fmt . Sprintf ( ` <path d="%s" style="%s;stroke:#000;fill:none;opacity:1;"/></mask></defs> ` ,
strings . Join ( borderSegments , "" ) , borderStyle ) ,
} , "\n" )
// render the main rectangle without stroke and the border mask
2024-10-09 18:09:46 +00:00
mainShape := d2themes . NewThemableElement ( "rect" , inlineTheme )
2023-01-09 18:16:28 +00:00
mainShape . X = float64 ( targetShape . Pos . X )
mainShape . Y = float64 ( targetShape . Pos . Y )
mainShape . Width = float64 ( targetShape . Width )
mainShape . Height = float64 ( targetShape . Height )
2023-02-19 13:26:24 +00:00
mainShape . SetMaskUrl ( maskID )
2023-02-19 12:00:01 +00:00
mainShapeFill , _ := d2themes . ShapeTheme ( targetShape )
2023-01-09 18:16:28 +00:00
mainShape . Fill = mainShapeFill
2023-03-14 06:01:33 +00:00
mainShape . FillPattern = targetShape . FillPattern
2023-01-09 21:50:17 +00:00
mainShape . Stroke = color . None
2023-01-15 20:36:43 +00:00
mainShape . Style = targetShape . CSSStyle ( )
2023-01-09 18:16:28 +00:00
mainShapeRendered := mainShape . Render ( )
2022-11-28 22:04:45 +00:00
// render the side shapes in the darkened color without stroke and the border mask
var sidePoints [ ] string
for _ , v := range [ ] d2target . Point {
{ X : 0 , Y : 0 } ,
2023-01-19 19:51:30 +00:00
{ 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 } ,
2022-11-28 22:04:45 +00:00
{ 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 ) ,
)
}
2023-02-25 03:16:30 +00:00
darkerColor , err := color . Darken ( targetShape . Fill )
if err != nil {
darkerColor = targetShape . Fill
}
2024-10-09 18:09:46 +00:00
sideShape := d2themes . NewThemableElement ( "polygon" , inlineTheme )
2023-02-25 03:16:30 +00:00
sideShape . Fill = darkerColor
sideShape . Points = strings . Join ( sidePoints , " " )
sideShape . SetMaskUrl ( maskID )
sideShape . Style = targetShape . CSSStyle ( )
renderedSides := sideShape . Render ( )
return borderMask + mainShapeRendered + renderedSides + renderedBorder
}
2025-01-26 21:11:53 +00:00
func render3DHexagon ( diagramHash string , targetShape d2target . Shape , inlineTheme * d2themes . Theme ) string {
2023-02-25 03:16:30 +00:00
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
// draw border all in one path to prevent overlapping sections
var borderSegments [ ] string
// start from the top-left
borderSegments = append ( borderSegments ,
moveTo ( d2target . Point { X : scale ( targetShape . Width , 0.25 ) , Y : 0 } ) ,
)
Y_OFFSET := d2target . THREE_DEE_OFFSET / 2
// The following iterates through the sidepoints in clockwise order from top-left, then the main points in clockwise order from bottom-right
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 } ,
) )
}
2024-10-09 18:09:46 +00:00
border := d2themes . NewThemableElement ( "path" , inlineTheme )
2023-02-25 03:16:30 +00:00
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 , " " )
// create mask from border stroke, to cut away from the shape fills
2025-01-26 21:11:53 +00:00
maskID := fmt . Sprintf ( "border-mask-%v-%v" , diagramHash , svg . EscapeText ( targetShape . ID ) )
2023-02-25 03:16:30 +00:00
borderMask := strings . Join ( [ ] string {
fmt . Sprintf ( ` <defs><mask id="%s" maskUnits="userSpaceOnUse" x="%d" y="%d" width="%d" height="%d"> ` ,
maskID , targetShape . Pos . X , targetShape . Pos . Y - d2target . THREE_DEE_OFFSET , targetShape . Width + d2target . THREE_DEE_OFFSET , targetShape . Height + d2target . THREE_DEE_OFFSET ,
) ,
fmt . Sprintf ( ` <rect x="%d" y="%d" width="%d" height="%d" fill="white"></rect> ` ,
targetShape . Pos . X , targetShape . Pos . Y - d2target . THREE_DEE_OFFSET , targetShape . Width + d2target . THREE_DEE_OFFSET , targetShape . Height + d2target . THREE_DEE_OFFSET ,
) ,
fmt . Sprintf ( ` <path d="%s" style="%s;stroke:#000;fill:none;opacity:1;"/></mask></defs> ` ,
strings . Join ( borderSegments , "" ) , borderStyle ) ,
} , "\n" )
// render the main hexagon without stroke and the border mask
2024-10-09 18:09:46 +00:00
mainShape := d2themes . NewThemableElement ( "polygon" , inlineTheme )
2023-02-25 03:16:30 +00:00
mainShape . X = float64 ( targetShape . Pos . X )
mainShape . Y = float64 ( targetShape . Pos . Y )
mainShape . Points = mainPointsPoly
mainShape . SetMaskUrl ( maskID )
mainShapeFill , _ := d2themes . ShapeTheme ( targetShape )
2023-03-14 06:01:33 +00:00
mainShape . FillPattern = targetShape . FillPattern
2023-02-25 03:16:30 +00:00
mainShape . Fill = mainShapeFill
mainShape . Stroke = color . None
mainShape . Style = targetShape . CSSStyle ( )
mainShapeRendered := mainShape . Render ( )
// render the side shapes in the darkened color without stroke and the border mask
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 ) ,
)
}
2023-01-09 21:50:17 +00:00
// TODO make darker color part of the theme? or just keep this bypass
darkerColor , err := color . Darken ( targetShape . Fill )
if err != nil {
darkerColor = targetShape . Fill
}
2024-10-09 18:09:46 +00:00
sideShape := d2themes . NewThemableElement ( "polygon" , inlineTheme )
2022-11-28 22:04:45 +00:00
sideShape . Fill = darkerColor
2023-01-09 18:16:28 +00:00
sideShape . Points = strings . Join ( sidePoints , " " )
2023-02-19 13:26:24 +00:00
sideShape . SetMaskUrl ( maskID )
2023-01-15 20:36:43 +00:00
sideShape . Style = targetShape . CSSStyle ( )
2023-01-09 21:50:17 +00:00
renderedSides := sideShape . Render ( )
2022-11-28 22:04:45 +00:00
2023-01-09 18:16:28 +00:00
return borderMask + mainShapeRendered + renderedSides + renderedBorder
2022-11-28 22:04:45 +00:00
}
2025-01-15 23:30:17 +00:00
func drawShape ( writer , appendixWriter io . Writer , diagramHash string , targetShape d2target . Shape , jsRunner jsrunner . JSRunner , inlineTheme * d2themes . Theme ) ( labelMask string , err error ) {
2022-12-29 00:19:30 +00:00
closingTag := "</g>"
if targetShape . Link != "" {
2023-03-07 04:07:26 +00:00
fmt . Fprintf ( writer , ` <a href="%s" xlink:href="%[1]s"> ` , svg . EscapeText ( targetShape . Link ) )
2022-12-29 00:19:30 +00:00
closingTag += "</a>"
}
2023-01-19 08:46:30 +00:00
// Opacity is a unique style, it applies to everything for a shape
opacityStyle := ""
if targetShape . Opacity != 1.0 {
opacityStyle = fmt . Sprintf ( " style='opacity:%f'" , targetShape . Opacity )
}
2023-03-06 05:00:40 +00:00
// this clipPath must be defined outside `g` element
2023-03-09 01:41:26 +00:00
if targetShape . BorderRadius != 0 && ( targetShape . Type == d2target . ShapeClass || targetShape . Type == d2target . ShapeSQLTable ) {
2023-03-10 01:54:35 +00:00
fmt . Fprint ( writer , clipPathForBorderRadius ( diagramHash , targetShape ) )
2023-03-06 05:00:40 +00:00
}
2025-03-30 10:31:56 +00:00
var iconClipPathID string
if targetShape . IconBorderRadius != 0 && ( targetShape . Type == d2target . ShapeImage ) {
2025-03-30 14:15:17 +00:00
// Set the icon's border-radius to half of it's smaller dimension in case it exceeds that
2025-03-30 10:31:56 +00:00
// https://www.w3.org/Style/CSS/Tracker/issues/29?changelog
targetShape . IconBorderRadius = min ( targetShape . IconBorderRadius , min ( targetShape . Width , targetShape . Height ) / 2 )
iconClipPathID = fmt . Sprintf ( "%v-%v-icon" , diagramHash , svg . SVGID ( targetShape . ID ) )
fmt . Fprint ( writer , applyIconBorderRadius ( iconClipPathID , targetShape ) )
}
2025-02-04 17:23:26 +00:00
classes := [ ] string { base64 . URLEncoding . EncodeToString ( [ ] byte ( svg . EscapeText ( targetShape . ID ) ) ) }
2024-12-15 02:35:18 +00:00
if targetShape . Animated {
2025-02-04 17:23:26 +00:00
classes = append ( classes , "animated-shape" )
2023-02-06 21:32:08 +00:00
}
2025-02-04 17:23:26 +00:00
classes = append ( classes , targetShape . Classes ... )
classStr := fmt . Sprintf ( ` class="%s" ` , strings . Join ( classes , " " ) )
2025-02-04 17:25:03 +00:00
fmt . Fprintf ( writer , ` <g%s%s> ` , classStr , opacityStyle )
2022-11-03 13:54:49 +00:00
tl := geo . NewPoint ( float64 ( targetShape . Pos . X ) , float64 ( targetShape . Pos . Y ) )
width := float64 ( targetShape . Width )
height := float64 ( targetShape . Height )
2023-02-19 12:00:01 +00:00
fill , stroke := d2themes . ShapeTheme ( targetShape )
2023-01-12 19:22:53 +00:00
style := targetShape . CSSStyle ( )
2022-11-03 13:54:49 +00:00
shapeType := d2target . DSL_SHAPE_TO_SHAPE_TYPE [ targetShape . Type ]
s := shape . NewShape ( shapeType , geo . NewBox ( tl , width , height ) )
2023-11-17 02:08:33 +00:00
if shapeType == shape . CLOUD_TYPE && targetShape . ContentAspectRatio != nil {
s . SetInnerBoxAspectRatio ( * targetShape . ContentAspectRatio )
}
2022-11-03 13:54:49 +00:00
2022-11-10 03:24:27 +00:00
var shadowAttr string
2022-11-10 19:21:14 +00:00
if targetShape . Shadow {
2022-11-10 03:24:27 +00:00
switch targetShape . Type {
case d2target . ShapeText ,
d2target . ShapeCode ,
d2target . ShapeClass ,
d2target . ShapeSQLTable :
default :
shadowAttr = ` filter="url(#shadow-filter)" `
}
}
2022-12-05 19:22:16 +00:00
var blendModeClass string
if targetShape . Blend {
blendModeClass = " blend"
}
fmt . Fprintf ( writer , ` <g class="shape%s" %s> ` , blendModeClass , shadowAttr )
2022-11-10 03:24:27 +00:00
2022-11-09 03:40:20 +00:00
var multipleTL * geo . Point
if targetShape . Multiple {
2023-01-21 04:04:59 +00:00
multipleTL = tl . AddVector ( multipleOffset )
2022-11-09 03:40:20 +00:00
}
2022-11-03 13:54:49 +00:00
switch targetShape . Type {
case d2target . ShapeClass :
2025-01-15 23:30:17 +00:00
if jsRunner != nil {
out , err := d2sketch . Class ( jsRunner , targetShape )
2022-12-22 19:32:41 +00:00
if err != nil {
return "" , err
}
2023-01-09 18:16:28 +00:00
fmt . Fprint ( writer , out )
2022-12-22 19:32:41 +00:00
} else {
2024-10-09 18:09:46 +00:00
drawClass ( writer , diagramHash , targetShape , inlineTheme )
2022-12-22 19:32:41 +00:00
}
2025-01-28 16:16:14 +00:00
addAppendixItems ( appendixWriter , diagramHash , targetShape , s )
2023-01-21 04:04:59 +00:00
fmt . Fprint ( writer , ` </g> ` )
2023-01-09 18:16:28 +00:00
fmt . Fprint ( writer , closingTag )
2022-12-05 19:57:16 +00:00
return labelMask , nil
2022-11-03 13:54:49 +00:00
case d2target . ShapeSQLTable :
2025-01-15 23:30:17 +00:00
if jsRunner != nil {
out , err := d2sketch . Table ( jsRunner , targetShape )
2022-12-22 19:06:57 +00:00
if err != nil {
return "" , err
}
2023-01-09 18:16:28 +00:00
fmt . Fprint ( writer , out )
2022-12-22 19:06:57 +00:00
} else {
2024-10-09 18:09:46 +00:00
drawTable ( writer , diagramHash , targetShape , inlineTheme )
2022-12-22 19:06:57 +00:00
}
2025-01-28 16:16:14 +00:00
addAppendixItems ( appendixWriter , diagramHash , targetShape , s )
2023-01-21 04:04:59 +00:00
fmt . Fprint ( writer , ` </g> ` )
2023-01-09 18:16:28 +00:00
fmt . Fprint ( writer , closingTag )
2022-12-05 19:57:16 +00:00
return labelMask , nil
2022-11-03 13:54:49 +00:00
case d2target . ShapeOval :
2023-01-19 07:12:26 +00:00
if targetShape . DoubleBorder {
2022-12-31 07:26:38 +00:00
if targetShape . Multiple {
2024-10-09 18:09:46 +00:00
fmt . Fprint ( writer , renderDoubleOval ( multipleTL , width , height , fill , "" , stroke , style , inlineTheme ) )
2022-12-21 07:43:45 +00:00
}
2025-01-15 23:30:17 +00:00
if jsRunner != nil {
out , err := d2sketch . DoubleOval ( jsRunner , targetShape )
2022-12-31 07:26:38 +00:00
if err != nil {
return "" , err
}
2023-01-21 04:04:59 +00:00
fmt . Fprint ( writer , out )
2022-12-31 07:26:38 +00:00
} else {
2024-10-09 18:09:46 +00:00
fmt . Fprint ( writer , renderDoubleOval ( tl , width , height , fill , targetShape . FillPattern , stroke , style , inlineTheme ) )
2022-12-21 07:43:45 +00:00
}
} else {
2022-12-31 07:26:38 +00:00
if targetShape . Multiple {
2024-10-09 18:09:46 +00:00
fmt . Fprint ( writer , renderOval ( multipleTL , width , height , fill , "" , stroke , style , inlineTheme ) )
2022-12-31 07:26:38 +00:00
}
2025-01-15 23:30:17 +00:00
if jsRunner != nil {
out , err := d2sketch . Oval ( jsRunner , targetShape )
2022-12-31 07:26:38 +00:00
if err != nil {
return "" , err
}
2023-01-21 04:04:59 +00:00
fmt . Fprint ( writer , out )
2022-12-31 07:26:38 +00:00
} else {
2024-10-09 18:09:46 +00:00
fmt . Fprint ( writer , renderOval ( tl , width , height , fill , targetShape . FillPattern , stroke , style , inlineTheme ) )
2022-12-31 07:26:38 +00:00
}
2022-12-21 07:43:45 +00:00
}
2022-11-03 13:54:49 +00:00
case d2target . ShapeImage :
2024-10-09 18:09:46 +00:00
el := d2themes . NewThemableElement ( "image" , inlineTheme )
2023-01-09 18:16:28 +00:00
el . X = float64 ( targetShape . Pos . X )
el . Y = float64 ( targetShape . Pos . Y )
el . Width = float64 ( targetShape . Width )
el . Height = float64 ( targetShape . Height )
el . Href = html . EscapeString ( targetShape . Icon . String ( ) )
el . Fill = fill
el . Stroke = stroke
el . Style = style
2025-03-29 09:09:40 +00:00
if targetShape . IconBorderRadius != 0 {
2025-03-30 10:31:56 +00:00
el . ClipPath = iconClipPathID
2025-03-18 17:47:28 +00:00
}
2023-01-09 18:16:28 +00:00
fmt . Fprint ( writer , el . Render ( ) )
2022-11-10 03:24:27 +00:00
// TODO should standardize "" to rectangle
2024-03-15 17:00:40 +00:00
case d2target . ShapeRectangle , d2target . ShapeSequenceDiagram , d2target . ShapeHierarchy , "" :
2023-04-06 16:53:54 +00:00
borderRadius := math . MaxFloat64
if targetShape . BorderRadius != 0 {
2023-04-13 14:20:31 +00:00
borderRadius = float64 ( targetShape . BorderRadius )
2023-04-06 16:53:54 +00:00
}
2022-11-10 19:21:14 +00:00
if targetShape . ThreeDee {
2025-01-26 21:11:53 +00:00
fmt . Fprint ( writer , render3DRect ( diagramHash , targetShape , inlineTheme ) )
2022-11-28 22:04:45 +00:00
} else {
2022-12-31 07:57:22 +00:00
if ! targetShape . DoubleBorder {
if targetShape . Multiple {
2024-10-09 18:09:46 +00:00
el := d2themes . NewThemableElement ( "rect" , inlineTheme )
2023-01-27 21:30:44 +00:00
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
2023-04-06 16:53:54 +00:00
el . Rx = borderRadius
2023-01-27 21:30:44 +00:00
fmt . Fprint ( writer , el . Render ( ) )
2022-12-31 07:57:22 +00:00
}
2025-01-15 23:30:17 +00:00
if jsRunner != nil {
out , err := d2sketch . Rect ( jsRunner , targetShape )
2022-12-31 07:57:22 +00:00
if err != nil {
return "" , err
}
2023-01-27 21:30:44 +00:00
fmt . Fprint ( writer , out )
2022-12-31 07:57:22 +00:00
} else {
2024-10-09 18:09:46 +00:00
el := d2themes . NewThemableElement ( "rect" , inlineTheme )
2023-01-27 21:30:44 +00:00
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
2023-03-14 06:01:33 +00:00
el . FillPattern = targetShape . FillPattern
2023-01-27 21:30:44 +00:00
el . Stroke = stroke
el . Style = style
2023-04-06 16:53:54 +00:00
el . Rx = borderRadius
2023-01-27 21:30:44 +00:00
fmt . Fprint ( writer , el . Render ( ) )
2022-12-21 07:43:45 +00:00
}
} else {
2022-12-31 07:57:22 +00:00
if targetShape . Multiple {
2024-10-09 18:09:46 +00:00
el := d2themes . NewThemableElement ( "rect" , inlineTheme )
2023-01-27 21:30:44 +00:00
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
2023-03-14 06:01:33 +00:00
el . FillPattern = targetShape . FillPattern
2023-01-27 21:30:44 +00:00
el . Stroke = stroke
el . Style = style
2023-04-06 16:53:54 +00:00
el . Rx = borderRadius
2023-01-27 21:30:44 +00:00
fmt . Fprint ( writer , el . Render ( ) )
2024-10-09 18:09:46 +00:00
el = d2themes . NewThemableElement ( "rect" , inlineTheme )
2023-01-27 21:30:44 +00:00
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
2023-04-06 16:53:54 +00:00
el . Rx = borderRadius
2023-01-27 21:30:44 +00:00
fmt . Fprint ( writer , el . Render ( ) )
2022-12-31 07:57:22 +00:00
}
2025-01-15 23:30:17 +00:00
if jsRunner != nil {
out , err := d2sketch . DoubleRect ( jsRunner , targetShape )
2022-12-31 07:57:22 +00:00
if err != nil {
return "" , err
}
2023-01-27 21:30:44 +00:00
fmt . Fprint ( writer , out )
2022-12-31 07:57:22 +00:00
} else {
2024-10-09 18:09:46 +00:00
el := d2themes . NewThemableElement ( "rect" , inlineTheme )
2023-01-27 21:30:44 +00:00
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
2023-03-14 06:01:33 +00:00
el . FillPattern = targetShape . FillPattern
2023-01-27 21:30:44 +00:00
el . Stroke = stroke
el . Style = style
2023-04-06 16:53:54 +00:00
el . Rx = borderRadius
2023-01-27 21:30:44 +00:00
fmt . Fprint ( writer , el . Render ( ) )
2024-10-09 18:09:46 +00:00
el = d2themes . NewThemableElement ( "rect" , inlineTheme )
2023-01-27 21:30:44 +00:00
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 )
2023-03-14 06:12:41 +00:00
el . Fill = "transparent"
2023-01-27 21:30:44 +00:00
el . Stroke = stroke
el . Style = style
2023-04-06 16:53:54 +00:00
el . Rx = borderRadius
2023-01-27 21:30:44 +00:00
fmt . Fprint ( writer , el . Render ( ) )
2022-12-31 07:57:22 +00:00
}
2022-12-21 07:43:45 +00:00
}
2022-11-09 03:40:20 +00:00
}
2023-02-25 03:16:30 +00:00
case d2target . ShapeHexagon :
if targetShape . ThreeDee {
2025-01-26 21:11:53 +00:00
fmt . Fprint ( writer , render3DHexagon ( diagramHash , targetShape , inlineTheme ) )
2023-02-25 03:16:30 +00:00
} else {
if targetShape . Multiple {
multiplePathData := shape . NewShape ( shapeType , geo . NewBox ( multipleTL , width , height ) ) . GetSVGPathData ( )
2024-10-09 18:09:46 +00:00
el := d2themes . NewThemableElement ( "path" , inlineTheme )
2023-02-25 03:16:30 +00:00
el . Fill = fill
el . Stroke = stroke
el . Style = style
for _ , pathData := range multiplePathData {
el . D = pathData
fmt . Fprint ( writer , el . Render ( ) )
}
}
2025-01-15 23:30:17 +00:00
if jsRunner != nil {
out , err := d2sketch . Paths ( jsRunner , targetShape , s . GetSVGPathData ( ) )
2023-02-25 03:16:30 +00:00
if err != nil {
return "" , err
}
fmt . Fprint ( writer , out )
} else {
2024-10-09 18:09:46 +00:00
el := d2themes . NewThemableElement ( "path" , inlineTheme )
2023-02-25 03:16:30 +00:00
el . Fill = fill
2023-03-14 06:01:33 +00:00
el . FillPattern = targetShape . FillPattern
2023-02-25 03:16:30 +00:00
el . Stroke = stroke
el . Style = style
for _ , pathData := range s . GetSVGPathData ( ) {
el . D = pathData
fmt . Fprint ( writer , el . Render ( ) )
}
}
}
2022-11-10 03:24:27 +00:00
case d2target . ShapeText , d2target . ShapeCode :
2022-11-03 13:54:49 +00:00
default :
2022-11-09 03:40:20 +00:00
if targetShape . Multiple {
multiplePathData := shape . NewShape ( shapeType , geo . NewBox ( multipleTL , width , height ) ) . GetSVGPathData ( )
2024-10-09 18:09:46 +00:00
el := d2themes . NewThemableElement ( "path" , inlineTheme )
2023-01-09 18:16:28 +00:00
el . Fill = fill
el . Stroke = stroke
el . Style = style
2022-11-09 03:40:20 +00:00
for _ , pathData := range multiplePathData {
2023-01-09 18:16:28 +00:00
el . D = pathData
fmt . Fprint ( writer , el . Render ( ) )
2022-11-09 03:40:20 +00:00
}
}
2022-11-09 19:45:26 +00:00
2025-01-15 23:30:17 +00:00
if jsRunner != nil {
out , err := d2sketch . Paths ( jsRunner , targetShape , s . GetSVGPathData ( ) )
2022-12-21 07:43:45 +00:00
if err != nil {
return "" , err
}
2023-01-09 18:16:28 +00:00
fmt . Fprint ( writer , out )
2022-12-21 07:43:45 +00:00
} else {
2024-10-09 18:09:46 +00:00
el := d2themes . NewThemableElement ( "path" , inlineTheme )
2023-01-09 18:16:28 +00:00
el . Fill = fill
2023-03-14 06:01:33 +00:00
el . FillPattern = targetShape . FillPattern
2023-01-09 18:16:28 +00:00
el . Stroke = stroke
el . Style = style
2022-12-21 07:43:45 +00:00
for _ , pathData := range s . GetSVGPathData ( ) {
2023-01-09 18:16:28 +00:00
el . D = pathData
fmt . Fprint ( writer , el . Render ( ) )
2022-12-21 07:43:45 +00:00
}
2022-11-03 13:54:49 +00:00
}
}
2023-11-16 04:02:22 +00:00
// // to examine shape's innerBox
// innerBox := s.GetInnerBox()
2024-10-09 18:09:46 +00:00
// el := d2themes.NewThemableElement("rect", inlineTheme)
2023-11-16 04:02:22 +00:00
// el.X = float64(innerBox.TopLeft.X)
// el.Y = float64(innerBox.TopLeft.Y)
// el.Width = float64(innerBox.Width)
// el.Height = float64(innerBox.Height)
// el.Style = "fill:rgba(255,0,0,0.5);"
// fmt.Fprint(writer, el.Render())
2023-02-11 00:19:19 +00:00
2022-12-29 00:19:30 +00:00
// Closes the class=shape
2023-01-21 04:04:59 +00:00
fmt . Fprint ( writer , ` </g> ` )
2022-11-10 03:24:27 +00:00
2024-05-15 03:36:41 +00:00
if targetShape . Icon != nil && targetShape . Type != d2target . ShapeImage && targetShape . Opacity != 0 {
2023-07-17 21:21:36 +00:00
iconPosition := label . FromString ( targetShape . IconPosition )
2022-11-03 13:54:49 +00:00
var box * geo . Box
if iconPosition . IsOutside ( ) {
box = s . GetBox ( )
} else {
box = s . GetInnerBox ( )
}
2023-02-13 18:42:47 +00:00
iconSize := d2target . GetIconSize ( box , targetShape . IconPosition )
2022-11-03 13:54:49 +00:00
tl := iconPosition . GetPointOnBox ( box , label . PADDING , float64 ( iconSize ) , float64 ( iconSize ) )
2025-03-29 09:09:40 +00:00
shapeIconClipPath := ""
if targetShape . IconBorderRadius != 0 {
shapeIconClipPath = fmt . Sprintf ( ` clip-path="inset(0 round %dpx)" ` , targetShape . IconBorderRadius )
2025-03-18 17:33:35 +00:00
}
2025-03-29 09:09:40 +00:00
fmt . Fprintf ( writer , ` <image href="%s" x="%f" y="%f" width="%d" height="%d"%s /> ` ,
html . EscapeString ( targetShape . Icon . String ( ) ) ,
tl . X ,
tl . Y ,
iconSize ,
iconSize ,
shapeIconClipPath ,
)
2022-11-03 13:54:49 +00:00
}
2024-05-15 03:36:41 +00:00
if targetShape . Label != "" && targetShape . Opacity != 0 {
2023-07-17 21:21:36 +00:00
labelPosition := label . FromString ( targetShape . LabelPosition )
2022-11-03 13:54:49 +00:00
var box * geo . Box
if labelPosition . IsOutside ( ) {
2023-05-25 02:39:39 +00:00
box = s . GetBox ( ) . Copy ( )
// if it is 3d/multiple, place label using box around those
2023-05-24 02:25:27 +00:00
if targetShape . ThreeDee {
2023-05-25 02:39:39 +00:00
offsetY := d2target . THREE_DEE_OFFSET
if targetShape . Type == d2target . ShapeHexagon {
offsetY /= 2
}
box . TopLeft . Y -= float64 ( offsetY )
2023-07-13 02:04:42 +00:00
box . Height += float64 ( offsetY )
2023-05-24 02:25:27 +00:00
box . Width += d2target . THREE_DEE_OFFSET
} else if targetShape . Multiple {
box . TopLeft . Y -= d2target . MULTIPLE_OFFSET
2023-07-13 02:04:42 +00:00
box . Height += d2target . MULTIPLE_OFFSET
2023-05-24 02:25:27 +00:00
box . Width += d2target . MULTIPLE_OFFSET
}
2022-11-03 13:54:49 +00:00
} else {
box = s . GetInnerBox ( )
}
2023-01-21 04:04:59 +00:00
labelTL := labelPosition . GetPointOnBox ( box , label . PADDING ,
float64 ( targetShape . LabelWidth ) ,
2023-02-03 21:13:10 +00:00
float64 ( targetShape . LabelHeight ) ,
2023-01-21 04:04:59 +00:00
)
2023-06-09 02:28:34 +00:00
labelMask = makeLabelMask ( labelTL , targetShape . LabelWidth , targetShape . LabelHeight , 0.75 )
2022-11-03 13:54:49 +00:00
fontClass := "text"
2023-03-07 06:21:23 +00:00
if targetShape . FontFamily == "mono" {
fontClass = "text-mono"
2023-07-12 05:04:01 +00:00
}
if targetShape . Bold {
fontClass += "-bold"
} else if targetShape . Italic {
fontClass += "-italic"
2022-11-03 13:54:49 +00:00
}
2022-12-26 05:49:26 +00:00
if targetShape . Underline {
fontClass += " text-underline"
}
2022-11-03 13:54:49 +00:00
2024-08-02 14:01:26 +00:00
if targetShape . Language == "latex" {
render , err := d2latex . Render ( targetShape . Label )
if err != nil {
return labelMask , err
}
gEl := d2themes . NewThemableElement ( "g" , inlineTheme )
2025-03-02 05:22:33 +00:00
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 )
2024-08-02 14:01:26 +00:00
gEl . Color = targetShape . Stroke
gEl . Content = render
fmt . Fprint ( writer , gEl . Render ( ) )
} else if targetShape . Language == "markdown" {
render , err := textmeasure . RenderMarkdown ( targetShape . Label )
if err != nil {
return labelMask , err
}
2025-03-02 05:22:33 +00:00
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 ) ,
)
2024-08-02 14:01:26 +00:00
fmt . Fprintf ( writer , ` <g><foreignObject requiredFeatures="http://www.w3.org/TR/SVG11/feature#Extensibility" x="%f" y="%f" width="%d" height="%d"> ` ,
2025-03-02 05:22:33 +00:00
labelTL . X , labelTL . Y , targetShape . LabelWidth , targetShape . LabelHeight ,
2024-08-02 14:01:26 +00:00
)
2025-03-02 05:22:33 +00:00
2024-08-02 14:01:26 +00:00
// we need the self closing form in this svg/xhtml context
render = strings . ReplaceAll ( render , "<hr>" , "<hr />" )
2024-11-08 21:58:03 +00:00
mdEl := d2themes . NewThemableElement ( "div" , inlineTheme )
2024-08-02 14:01:26 +00:00
mdEl . Content = render
// We have to set with styles since within foreignObject, we're in html
// land and not SVG attributes
var styles [ ] string
2025-03-16 15:49:00 +00:00
var classes [ ] string = [ ] string { "md" }
2024-08-02 14:01:26 +00:00
if targetShape . FontSize != textmeasure . MarkdownFontSize {
styles = append ( styles , fmt . Sprintf ( "font-size:%vpx" , targetShape . FontSize ) )
}
2025-03-16 15:49:00 +00:00
2024-08-02 14:01:26 +00:00
if targetShape . Fill != "" && targetShape . Fill != "transparent" {
2025-03-16 15:49:00 +00:00
if color . IsThemeColor ( targetShape . Fill ) {
classes = append ( classes , fmt . Sprintf ( "fill-%s" , targetShape . Fill ) )
} else {
styles = append ( styles , fmt . Sprintf ( ` background-color:%s ` , targetShape . Fill ) )
}
2024-08-02 14:01:26 +00:00
}
2025-03-16 15:49:00 +00:00
2024-08-02 14:01:26 +00:00
if ! color . IsThemeColor ( targetShape . Color ) {
styles = append ( styles , fmt . Sprintf ( ` color:%s ` , targetShape . Color ) )
2025-03-04 01:33:00 +00:00
} else {
2025-03-16 15:49:00 +00:00
classes = append ( classes , fmt . Sprintf ( "color-%s" , targetShape . Color ) )
2024-08-02 14:01:26 +00:00
}
2025-03-16 15:49:00 +00:00
mdEl . ClassName = strings . Join ( classes , " " )
2025-03-12 01:53:45 +00:00
// When using dark theme, inlineTheme is nil and we rely on CSS variables
2024-08-02 14:01:26 +00:00
mdEl . Style = strings . Join ( styles , ";" )
fmt . Fprint ( writer , mdEl . Render ( ) )
fmt . Fprint ( writer , ` </foreignObject></g> ` )
} else if targetShape . Language != "" {
2022-11-27 21:54:41 +00:00
lexer := lexers . Get ( targetShape . Language )
if lexer == nil {
2023-03-04 05:25:06 +00:00
lexer = lexers . Fallback
2022-11-27 21:54:41 +00:00
}
2023-03-04 05:02:02 +00:00
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
}
2022-11-27 21:54:41 +00:00
2023-03-04 05:02:02 +00:00
svgStyles := styleToSVG ( style )
class := "light-code"
if ! isLight {
class = "dark-code"
}
2023-09-25 19:16:32 +00:00
var fontSize string
if targetShape . FontSize != d2fonts . FONT_SIZE_M {
fontSize = fmt . Sprintf ( ` style="font-size:%v" ` , targetShape . FontSize )
}
fmt . Fprintf ( writer , ` <g transform="translate(%f %f)" class="%s"%s> ` ,
box . TopLeft . X , box . TopLeft . Y , class , fontSize ,
2023-09-25 19:10:09 +00:00
)
2024-10-09 18:09:46 +00:00
rectEl := d2themes . NewThemableElement ( "rect" , inlineTheme )
2023-03-04 05:02:02 +00:00
rectEl . Width = float64 ( targetShape . Width )
rectEl . Height = float64 ( targetShape . Height )
rectEl . Stroke = targetShape . Stroke
rectEl . ClassName = "shape"
2023-06-19 21:17:34 +00:00
rectEl . Style = fmt . Sprintf ( ` fill:%s;stroke-width:%d; ` ,
style . Get ( chroma . Background ) . Background . String ( ) ,
targetShape . StrokeWidth ,
)
2023-03-04 05:02:02 +00:00
fmt . Fprint ( writer , rectEl . Render ( ) )
2023-06-19 21:17:34 +00:00
// Padding = 0.5em
padding := float64 ( targetShape . FontSize ) / 2.
fmt . Fprintf ( writer , ` <g transform="translate(%f %f)"> ` , padding , padding )
2023-03-04 05:02:02 +00:00
2023-06-19 22:25:14 +00:00
lineHeight := textmeasure . CODE_LINE_HEIGHT
2023-03-04 05:02:02 +00:00
for index , tokens := range chroma . SplitTokensIntoLines ( iterator . Tokens ( ) ) {
2023-06-19 22:25:14 +00:00
fmt . Fprintf ( writer , "<text class=\"text-mono\" x=\"0\" y=\"%fem\">" , 1 + float64 ( index ) * lineHeight )
2023-03-04 05:02:02 +00:00
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 )
2022-11-27 21:54:41 +00:00
}
2023-03-04 05:02:02 +00:00
fmt . Fprint ( writer , "</text>" )
2022-11-27 21:54:41 +00:00
}
2023-03-04 05:02:02 +00:00
fmt . Fprint ( writer , "</g></g>" )
2022-11-27 21:54:41 +00:00
}
2022-12-26 05:49:26 +00:00
} else {
2023-02-09 22:14:31 +00:00
if targetShape . LabelFill != "" {
2024-10-09 18:09:46 +00:00
rectEl := d2themes . NewThemableElement ( "rect" , inlineTheme )
2023-02-19 11:32:44 +00:00
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 ( ) )
2023-02-09 22:14:31 +00:00
}
2024-10-09 18:09:46 +00:00
textEl := d2themes . NewThemableElement ( "text" , inlineTheme )
2023-01-09 18:16:28 +00:00
textEl . X = labelTL . X + float64 ( targetShape . LabelWidth ) / 2
2022-11-03 13:54:49 +00:00
// text is vertically positioned at its baseline which is at labelTL+FontSize
2023-01-09 18:16:28 +00:00
textEl . Y = labelTL . Y + float64 ( targetShape . FontSize )
2023-02-25 04:26:40 +00:00
textEl . Fill = targetShape . GetFontColor ( )
2023-02-19 13:26:24 +00:00
textEl . ClassName = fontClass
2023-01-09 18:16:28 +00:00
textEl . Style = fmt . Sprintf ( "text-anchor:%s;font-size:%vpx" , "middle" , targetShape . FontSize )
textEl . Content = RenderText ( targetShape . Label , textEl . X , float64 ( targetShape . LabelHeight ) )
fmt . Fprint ( writer , textEl . Render ( ) )
2022-12-05 23:53:43 +00:00
if targetShape . Blend {
2023-06-08 22:54:17 +00:00
labelMask = makeLabelMask ( labelTL , targetShape . LabelWidth , targetShape . LabelHeight - d2graph . INNER_LABEL_PADDING , 1 )
2022-12-05 21:32:33 +00:00
}
2022-11-03 13:54:49 +00:00
}
}
2023-07-27 05:46:28 +00:00
if targetShape . Tooltip != "" {
fmt . Fprintf ( writer , ` <title>%s</title> ` ,
svg . EscapeText ( targetShape . Tooltip ) ,
)
}
2025-01-28 16:16:14 +00:00
addAppendixItems ( appendixWriter , diagramHash , targetShape , s )
2023-01-13 16:16:18 +00:00
2023-01-16 14:15:01 +00:00
fmt . Fprint ( writer , closingTag )
2023-01-13 16:16:18 +00:00
return labelMask , nil
}
2025-03-29 16:39:11 +00:00
func applyIconBorderRadius ( clipPathID string , shape d2target . Shape ) string {
2025-03-29 09:09:40 +00:00
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
2025-03-29 16:39:11 +00:00
out := fmt . Sprintf ( ` <clipPath id="%s"> ` , clipPathID )
2025-03-29 09:09:40 +00:00
out += fmt . Sprintf ( ` <path d="M %f %f L %f %f S %f %f %f %f ` , box . TopLeft . X , box . TopLeft . Y + float64 ( shape . IconBorderRadius ) , box . TopLeft . X , box . TopLeft . Y + float64 ( shape . IconBorderRadius ) , box . TopLeft . X , box . TopLeft . Y , box . TopLeft . X + float64 ( shape . IconBorderRadius ) , box . TopLeft . Y )
out += fmt . Sprintf ( ` L %f %f L %f %f ` , box . TopLeft . X + box . Width - float64 ( shape . IconBorderRadius ) , box . TopLeft . Y , topX - float64 ( shape . IconBorderRadius ) , topY )
out += fmt . Sprintf ( ` S %f %f %f %f ` , topX , topY , topX , topY + float64 ( shape . IconBorderRadius ) )
out += fmt . Sprintf ( ` L %f %f ` , topX , topY + box . Height - float64 ( shape . IconBorderRadius ) )
out += fmt . Sprintf ( ` S %f % f %f %f ` , topX , topY + box . Height , topX - float64 ( shape . IconBorderRadius ) , topY + box . Height )
out += fmt . Sprintf ( ` L %f %f ` , box . TopLeft . X + float64 ( shape . IconBorderRadius ) , box . TopLeft . Y + box . Height )
out += fmt . Sprintf ( ` S %f %f %f %f ` , box . TopLeft . X , box . TopLeft . Y + box . Height , box . TopLeft . X , box . TopLeft . Y + box . Height - float64 ( shape . IconBorderRadius ) )
out += fmt . Sprintf ( ` L %f %f ` , box . TopLeft . X , box . TopLeft . Y + float64 ( shape . IconBorderRadius ) )
out += fmt . Sprintf ( ` Z %f %f" ` , box . TopLeft . X , box . TopLeft . Y )
return out + ` fill="none" /> </clipPath> `
}
2025-01-28 16:16:14 +00:00
func addAppendixItems ( writer io . Writer , diagramHash string , targetShape d2target . Shape , s shape . Shape ) {
2023-07-03 19:56:46 +00:00
var p1 , p2 * geo . Point
if targetShape . Tooltip != "" || targetShape . Link != "" {
2023-07-04 01:50:20 +00:00
bothIcons := targetShape . Tooltip != "" && targetShape . Link != ""
2023-07-03 19:56:46 +00:00
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. ,
)
2023-07-04 01:50:20 +00:00
offset := geo . Vector { - 2 * appendixIconRadius , 0 }
var leftOnShape bool
2023-07-04 01:15:57 +00:00
switch s . GetType ( ) {
2023-07-04 01:50:20 +00:00
case shape . STEP_TYPE , shape . HEXAGON_TYPE , shape . QUEUE_TYPE , shape . PAGE_TYPE :
2023-07-04 01:15:57 +00:00
// trace straight left for these
center . Y = float64 ( targetShape . Pos . Y )
case shape . PACKAGE_TYPE :
// trace straight down
center . X = float64 ( targetShape . Pos . X + targetShape . Width )
2023-07-04 01:50:20 +00:00
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 )
}
2023-07-04 01:15:57 +00:00
}
2023-07-03 19:56:46 +00:00
v1 := center . VectorTo ( corner )
p1 = shape . TraceToShapeBorder ( s , corner , corner . AddVector ( v1 ) )
2023-07-04 01:50:20 +00:00
if bothIcons {
if leftOnShape {
// these shapes should have p1 on shape border
p2 = p1 . AddVector ( offset . Reverse ( ) )
p1 , p2 = p2 , p1
} else {
p2 = p1 . AddVector ( offset )
}
2023-07-03 19:56:46 +00:00
}
}
if targetShape . Tooltip != "" {
x := int ( math . Ceil ( p1 . X ) )
y := int ( math . Ceil ( p1 . Y ) )
2023-07-27 05:46:28 +00:00
fmt . Fprintf ( writer , ` <g transform="translate(%d %d)" class="appendix-icon"><title>%s</title>%s</g> ` ,
2023-07-03 19:56:46 +00:00
x - appendixIconRadius ,
y - appendixIconRadius ,
2023-07-27 05:46:28 +00:00
svg . EscapeText ( targetShape . Tooltip ) ,
2025-01-28 16:16:14 +00:00
fmt . Sprintf ( TooltipIcon , diagramHash , svg . SVGID ( targetShape . ID ) ) ,
2022-12-27 07:56:23 +00:00
)
}
2023-07-03 19:56:46 +00:00
if targetShape . Link != "" {
if p2 == nil {
p2 = p1
}
x := int ( math . Ceil ( p2 . X ) )
y := int ( math . Ceil ( p2 . Y ) )
2022-12-29 00:19:30 +00:00
fmt . Fprintf ( writer , ` <g transform="translate(%d %d)" class="appendix-icon">%s</g> ` ,
2023-07-03 19:56:46 +00:00
x - appendixIconRadius ,
y - appendixIconRadius ,
2025-01-28 17:21:42 +00:00
fmt . Sprintf ( LinkIcon , diagramHash , svg . SVGID ( targetShape . ID ) ) ,
2022-12-29 00:19:30 +00:00
)
}
2022-11-03 13:54:49 +00:00
}
2022-12-28 04:29:51 +00:00
func RenderText ( text string , x , height float64 ) string {
2022-11-03 13:54:49 +00:00
if ! strings . Contains ( text , "\n" ) {
2022-12-22 19:06:57 +00:00
return svg . EscapeText ( text )
2022-11-03 13:54:49 +00:00
}
rendered := [ ] string { }
lines := strings . Split ( text , "\n" )
for i , line := range lines {
dy := height / float64 ( len ( lines ) )
if i == 0 {
dy = 0
}
2022-12-22 19:06:57 +00:00
escaped := svg . EscapeText ( line )
2022-11-03 13:54:49 +00:00
if escaped == "" {
// if there are multiple newlines in a row we still need text for the tspan to render
escaped = " "
}
rendered = append ( rendered , fmt . Sprintf ( ` <tspan x="%f" dy="%f">%s</tspan> ` , x , dy , escaped ) )
}
return strings . Join ( rendered , "" )
}
2023-03-29 17:03:44 +00:00
func EmbedFonts ( buf * bytes . Buffer , diagramHash , source string , fontFamily * d2fonts . FontFamily , corpus string ) {
2023-02-19 13:26:24 +00:00
fmt . Fprint ( buf , ` <style type="text/css"><![CDATA[ ` )
appendOnTrigger (
buf ,
source ,
[ ] string {
` class="text" ` ,
` class="text ` ,
` class="md" ` ,
2025-03-16 15:49:00 +00:00
` class="md ` ,
2023-02-19 13:26:24 +00:00
} ,
fmt . Sprintf ( `
2023-03-12 01:20:59 +00:00
. % s . text {
font - family : "%s-font-regular" ;
2023-02-21 08:33:13 +00:00
}
@ font - face {
2023-03-12 01:20:59 +00:00
font - family : % s - font - regular ;
2023-02-21 08:33:13 +00:00
src : url ( "%s" ) ;
} ` ,
2023-03-12 01:20:59 +00:00
diagramHash ,
diagramHash ,
diagramHash ,
2023-03-29 17:03:44 +00:00
fontFamily . Font ( 0 , d2fonts . FONT_STYLE_REGULAR ) . GetEncodedSubset ( corpus ) ,
2023-02-19 13:26:24 +00:00
) ,
)
2023-01-12 18:51:26 +00:00
2023-04-10 21:38:34 +00:00
appendOnTrigger (
buf ,
source ,
2025-03-16 15:49:00 +00:00
[ ] string {
` class="md" ` ,
` class="md ` ,
} ,
2023-04-10 21:38:34 +00:00
fmt . Sprintf ( `
2023-04-10 20:55:52 +00:00
@ font - face {
font - family : % s - font - semibold ;
src : url ( "%s" ) ;
} ` ,
2023-04-10 21:38:34 +00:00
diagramHash ,
fontFamily . Font ( 0 , d2fonts . FONT_STYLE_SEMIBOLD ) . GetEncodedSubset ( corpus ) ,
) ,
)
2023-04-10 20:55:52 +00:00
2023-02-19 13:26:24 +00:00
appendOnTrigger (
buf ,
source ,
[ ] string {
` text-underline ` ,
} ,
`
2023-02-21 08:33:13 +00:00
. text - underline {
text - decoration : underline ;
} ` ,
2023-02-19 13:26:24 +00:00
)
2022-12-27 07:56:23 +00:00
2024-05-30 01:45:36 +00:00
appendOnTrigger (
buf ,
source ,
[ ] string {
` text-link ` ,
} ,
`
2024-05-31 04:57:31 +00:00
. text - link {
fill : blue ;
}
2024-05-31 04:44:40 +00:00
2024-05-31 04:57:31 +00:00
. text - link : visited {
fill : purple ;
} ` ,
2024-05-30 01:45:36 +00:00
)
2023-02-19 13:26:24 +00:00
appendOnTrigger (
buf ,
source ,
[ ] string {
` animated-connection ` ,
} ,
`
2023-02-21 08:33:13 +00:00
@ keyframes dashdraw {
from {
stroke - dashoffset : 0 ;
}
}
` ,
2023-02-19 13:26:24 +00:00
)
2022-11-03 13:54:49 +00:00
2024-12-15 02:35:18 +00:00
appendOnTrigger (
buf ,
source ,
[ ] string {
` animated-shape ` ,
} ,
`
@ keyframes shapeappear {
0 % , 100 % { transform : translateY ( 0 ) ; filter : drop - shadow ( 0 px 0 px 0 px rgba ( 0 , 0 , 0 , 0 ) ) ; }
50 % { transform : translateY ( - 4 px ) ; filter : drop - shadow ( 0 px 12.6 px 25.2 px rgba ( 50 , 50 , 93 , 0.25 ) ) drop - shadow ( 0 px 7.56 px 15.12 px rgba ( 0 , 0 , 0 , 0.1 ) ) ; }
}
. animated - shape {
animation : shapeappear 1 s linear infinite ;
}
` ,
)
2023-02-19 13:26:24 +00:00
appendOnTrigger (
buf ,
source ,
[ ] string {
` appendix-icon ` ,
} ,
`
2023-02-21 08:33:13 +00:00
. appendix - icon {
filter : drop - shadow ( 0 px 0 px 32 px rgba ( 31 , 36 , 58 , 0.1 ) ) ;
} ` ,
2023-02-19 13:26:24 +00:00
)
2022-11-03 13:54:49 +00:00
2023-02-19 13:26:24 +00:00
appendOnTrigger (
buf ,
source ,
[ ] string {
` class="text-bold ` ,
` <b> ` ,
` <strong> ` ,
} ,
fmt . Sprintf ( `
2023-03-12 01:20:59 +00:00
. % s . text - bold {
font - family : "%s-font-bold" ;
2023-02-21 08:33:13 +00:00
}
@ font - face {
2023-03-12 01:20:59 +00:00
font - family : % s - font - bold ;
2023-02-21 08:33:13 +00:00
src : url ( "%s" ) ;
} ` ,
2023-03-12 01:20:59 +00:00
diagramHash ,
diagramHash ,
diagramHash ,
2023-03-29 17:03:44 +00:00
fontFamily . Font ( 0 , d2fonts . FONT_STYLE_BOLD ) . GetEncodedSubset ( corpus ) ,
2023-02-19 13:26:24 +00:00
) ,
)
2022-11-03 13:54:49 +00:00
2023-02-19 13:26:24 +00:00
appendOnTrigger (
buf ,
source ,
[ ] string {
` class="text-italic ` ,
` <em> ` ,
` <dfn> ` ,
} ,
fmt . Sprintf ( `
2023-03-12 01:20:59 +00:00
. % s . text - italic {
font - family : "%s-font-italic" ;
2023-02-21 08:33:13 +00:00
}
@ font - face {
2023-03-12 01:20:59 +00:00
font - family : % s - font - italic ;
2023-02-21 08:33:13 +00:00
src : url ( "%s" ) ;
} ` ,
2023-03-12 01:20:59 +00:00
diagramHash ,
diagramHash ,
diagramHash ,
2023-03-29 17:03:44 +00:00
fontFamily . Font ( 0 , d2fonts . FONT_STYLE_ITALIC ) . GetEncodedSubset ( corpus ) ,
2023-02-19 13:26:24 +00:00
) ,
)
2022-11-07 19:52:01 +00:00
2023-02-19 13:26:24 +00:00
appendOnTrigger (
buf ,
source ,
[ ] string {
` class="text-mono ` ,
` <pre> ` ,
` <code> ` ,
` <kbd> ` ,
` <samp> ` ,
} ,
fmt . Sprintf ( `
2023-03-12 01:20:59 +00:00
. % s . text - mono {
font - family : "%s-font-mono" ;
2023-02-21 08:33:13 +00:00
}
@ font - face {
2023-03-12 01:20:59 +00:00
font - family : % s - font - mono ;
2023-02-21 08:33:13 +00:00
src : url ( "%s" ) ;
} ` ,
2023-03-12 01:20:59 +00:00
diagramHash ,
diagramHash ,
diagramHash ,
2023-03-29 17:03:44 +00:00
d2fonts . SourceCodePro . Font ( 0 , d2fonts . FONT_STYLE_REGULAR ) . GetEncodedSubset ( corpus ) ,
2023-02-19 13:26:24 +00:00
) ,
)
2022-11-07 19:52:01 +00:00
2023-02-19 13:26:24 +00:00
appendOnTrigger (
buf ,
source ,
[ ] string {
2023-03-07 06:21:23 +00:00
` class="text-mono-bold ` ,
2023-02-19 13:26:24 +00:00
} ,
fmt . Sprintf ( `
2023-03-12 01:20:59 +00:00
. % s . text - mono - bold {
font - family : "%s-font-mono-bold" ;
2023-02-21 08:33:13 +00:00
}
@ font - face {
2023-03-12 01:20:59 +00:00
font - family : % s - font - mono - bold ;
2023-02-21 08:33:13 +00:00
src : url ( "%s" ) ;
} ` ,
2023-03-12 01:20:59 +00:00
diagramHash ,
diagramHash ,
diagramHash ,
2023-03-29 17:03:44 +00:00
d2fonts . SourceCodePro . Font ( 0 , d2fonts . FONT_STYLE_BOLD ) . GetEncodedSubset ( corpus ) ,
2023-02-19 13:26:24 +00:00
) ,
)
2023-01-29 22:17:34 +00:00
2023-02-19 13:26:24 +00:00
appendOnTrigger (
buf ,
source ,
[ ] string {
2023-03-07 06:21:23 +00:00
` class="text-mono-italic ` ,
2023-02-19 13:26:24 +00:00
} ,
fmt . Sprintf ( `
2023-03-12 01:20:59 +00:00
. % s . text - mono - italic {
font - family : "%s-font-mono-italic" ;
2023-02-21 08:33:13 +00:00
}
@ font - face {
2023-03-12 01:20:59 +00:00
font - family : % s - font - mono - italic ;
2023-02-21 08:33:13 +00:00
src : url ( "%s" ) ;
} ` ,
2023-03-12 01:20:59 +00:00
diagramHash ,
diagramHash ,
diagramHash ,
2023-03-29 17:03:44 +00:00
d2fonts . SourceCodePro . Font ( 0 , d2fonts . FONT_STYLE_ITALIC ) . GetEncodedSubset ( corpus ) ,
2023-02-19 13:26:24 +00:00
) ,
)
2023-01-29 22:17:34 +00:00
2023-02-19 13:26:24 +00:00
appendOnTrigger (
buf ,
source ,
[ ] string {
` sketch-overlay-bright ` ,
} ,
2025-01-29 00:39:53 +00:00
fmt . Sprintf ( `
2023-02-21 08:33:13 +00:00
. sketch - overlay - bright {
2025-01-29 00:39:53 +00:00
fill : url ( # streaks - bright - % s ) ;
2023-02-21 08:33:13 +00:00
mix - blend - mode : darken ;
2025-01-29 00:39:53 +00:00
} ` , diagramHash ) ,
2023-02-19 13:26:24 +00:00
)
2023-01-29 22:17:34 +00:00
2023-02-19 13:26:24 +00:00
appendOnTrigger (
buf ,
source ,
[ ] string {
` sketch-overlay-normal ` ,
} ,
2025-01-29 00:39:53 +00:00
fmt . Sprintf ( `
2023-02-21 08:33:13 +00:00
. sketch - overlay - normal {
2025-01-29 00:39:53 +00:00
fill : url ( # streaks - normal - % s ) ;
2023-02-21 08:33:13 +00:00
mix - blend - mode : color - burn ;
2025-01-29 00:39:53 +00:00
} ` , diagramHash ) ,
2023-02-19 13:26:24 +00:00
)
2023-01-30 11:22:14 +00:00
2023-02-19 13:26:24 +00:00
appendOnTrigger (
buf ,
source ,
[ ] string {
` sketch-overlay-dark ` ,
} ,
2025-01-29 00:39:53 +00:00
fmt . Sprintf ( `
2023-02-21 08:33:13 +00:00
. sketch - overlay - dark {
2025-01-29 00:39:53 +00:00
fill : url ( # streaks - dark - % s ) ;
2023-02-21 08:33:13 +00:00
mix - blend - mode : overlay ;
2025-01-29 00:39:53 +00:00
} ` , diagramHash ) ,
2023-02-19 13:26:24 +00:00
)
2023-01-30 11:22:14 +00:00
2023-02-19 13:26:24 +00:00
appendOnTrigger (
buf ,
source ,
[ ] string {
` sketch-overlay-darker ` ,
} ,
2025-01-29 00:39:53 +00:00
fmt . Sprintf ( `
2023-02-21 08:33:13 +00:00
. sketch - overlay - darker {
2025-01-29 00:39:53 +00:00
fill : url ( # streaks - darker - % s ) ;
2023-02-21 08:33:13 +00:00
mix - blend - mode : lighten ;
2025-01-29 00:39:53 +00:00
} ` , diagramHash ) ,
2023-02-19 13:26:24 +00:00
)
2023-01-30 11:22:14 +00:00
2023-02-19 13:26:24 +00:00
fmt . Fprint ( buf , ` ]]></style> ` )
2023-01-30 11:22:14 +00:00
}
2023-01-30 11:06:54 +00:00
2023-02-19 13:26:24 +00:00
func appendOnTrigger ( buf * bytes . Buffer , source string , triggers [ ] string , newContent string ) {
for _ , trigger := range triggers {
if strings . Contains ( source , trigger ) {
fmt . Fprint ( buf , newContent )
2023-01-30 11:06:54 +00:00
break
}
}
2022-11-03 13:54:49 +00:00
}
2023-02-19 14:33:40 +00:00
var DEFAULT_DARK_THEME * int64 = nil // no theme selected
2022-12-21 07:43:45 +00:00
func Render ( diagram * d2target . Diagram , opts * RenderOpts ) ( [ ] byte , error ) {
2025-01-15 23:30:17 +00:00
var jsRunner jsrunner . JSRunner
2022-12-21 07:43:45 +00:00
pad := DEFAULT_PADDING
2023-07-14 20:08:26 +00:00
themeID := d2themescatalog . NeutralDefault . ID
2023-01-09 21:16:22 +00:00
darkThemeID := DEFAULT_DARK_THEME
2023-06-15 22:53:23 +00:00
var scale * float64
2022-12-21 07:43:45 +00:00
if opts != nil {
2023-07-14 20:08:26 +00:00
if opts . Pad != nil {
pad = int ( * opts . Pad )
}
if opts . Sketch != nil && * opts . Sketch {
2025-01-15 23:30:17 +00:00
jsRunner = jsrunner . NewJSRunner ( )
err := d2sketch . LoadJS ( jsRunner )
2022-12-21 07:43:45 +00:00
if err != nil {
return nil , err
}
}
2023-07-14 20:08:26 +00:00
if opts . ThemeID != nil {
themeID = * opts . ThemeID
}
2023-01-09 21:16:22 +00:00
darkThemeID = opts . DarkThemeID
2023-06-15 22:53:23 +00:00
scale = opts . Scale
2024-12-29 21:19:32 +00:00
} else {
opts = & RenderOpts { }
2022-12-21 07:43:45 +00:00
}
2022-11-03 13:54:49 +00:00
buf := & bytes . Buffer { }
2022-11-09 19:10:51 +00:00
// only define shadow filter if a shape uses it
for _ , s := range diagram . Shapes {
if s . Shadow {
defineShadowFilter ( buf )
break
}
}
2024-09-27 21:49:10 +00:00
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 )
}
2025-02-03 23:28:07 +00:00
if color . IsGradient ( c . Fill ) {
defineGradients ( buf , c . Fill )
}
2024-09-27 21:49:10 +00:00
}
2023-03-23 20:37:28 +00:00
// Apply hash on IDs for targeting, to be specific for this diagram
2025-01-30 21:48:06 +00:00
diagramHash , err := diagram . HashID ( opts . Salt )
2022-12-06 06:32:23 +00:00
if err != nil {
return nil , err
}
2023-03-23 20:37:28 +00:00
// Some targeting is still per-board, like masks for connections
isolatedDiagramHash := diagramHash
if opts != nil && opts . MasterID != "" {
diagramHash = opts . MasterID
}
2022-12-06 06:32:23 +00:00
2022-11-03 13:54:49 +00:00
// SVG has no notion of z-index. The z-index is effectively the order it's drawn.
// So draw from the least nested to most nested
2022-11-09 17:37:28 +00:00
idToShape := make ( map [ string ] d2target . Shape )
2022-11-30 21:15:33 +00:00
allObjects := make ( [ ] DiagramObject , 0 , len ( diagram . Shapes ) + len ( diagram . Connections ) )
2022-11-03 13:54:49 +00:00
for _ , s := range diagram . Shapes {
2022-11-09 17:37:28 +00:00
idToShape [ s . ID ] = s
2022-11-29 22:21:23 +00:00
allObjects = append ( allObjects , s )
2022-11-03 13:54:49 +00:00
}
2022-11-29 22:21:23 +00:00
for _ , c := range diagram . Connections {
allObjects = append ( allObjects , c )
2022-11-03 13:54:49 +00:00
}
2022-11-30 19:22:43 +00:00
sortObjects ( allObjects )
2022-11-29 22:21:23 +00:00
2023-07-03 23:45:05 +00:00
appendixItemBuf := & bytes . Buffer { }
2022-12-03 06:47:54 +00:00
var labelMasks [ ] string
2022-11-03 13:54:49 +00:00
markers := map [ string ] struct { } { }
2024-10-09 18:09:46 +00:00
var inlineTheme * d2themes . Theme
2024-10-09 21:14:47 +00:00
// We only want to inline when no dark theme is specified, otherwise the inline style will override the CSS
if darkThemeID == nil {
2024-10-09 18:09:46 +00:00
inlineTheme = go2 . Pointer ( d2themescatalog . Find ( themeID ) )
inlineTheme . ApplyOverrides ( opts . ThemeOverrides )
}
2022-11-29 22:21:23 +00:00
for _ , obj := range allObjects {
if c , is := obj . ( d2target . Connection ) ; is {
2025-01-15 23:30:17 +00:00
labelMask , err := drawConnection ( buf , isolatedDiagramHash , c , markers , idToShape , jsRunner , inlineTheme )
2022-12-21 07:43:45 +00:00
if err != nil {
return nil , err
}
2022-12-06 03:50:42 +00:00
if labelMask != "" {
2022-12-03 06:47:54 +00:00
labelMasks = append ( labelMasks , labelMask )
}
2022-11-29 22:21:23 +00:00
} else if s , is := obj . ( d2target . Shape ) ; is {
2025-01-15 23:30:17 +00:00
labelMask , err := drawShape ( buf , appendixItemBuf , diagramHash , s , jsRunner , inlineTheme )
2022-12-06 03:50:42 +00:00
if err != nil {
2022-11-29 22:21:23 +00:00
return nil , err
2022-12-05 19:57:16 +00:00
} else if labelMask != "" {
labelMasks = append ( labelMasks , labelMask )
2022-11-29 22:21:23 +00:00
}
} else {
2022-12-17 01:15:58 +00:00
return nil , fmt . Errorf ( "unknown object of type %T" , obj )
2022-11-29 22:21:23 +00:00
}
2022-11-03 13:54:49 +00:00
}
2023-07-03 23:45:05 +00:00
// add all appendix items afterwards so they are always on top
fmt . Fprint ( buf , appendixItemBuf )
2022-11-03 13:54:49 +00:00
2025-03-11 21:30:05 +00:00
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 )
}
2022-12-17 01:15:37 +00:00
// Note: we always want this since we reference it on connections even if there end up being no masked labels
2023-02-19 11:32:44 +00:00
left , top , w , h := dimensions ( diagram , pad )
2025-03-11 21:30:05 +00:00
if diagram . Legend != nil && ( len ( diagram . Legend . Shapes ) > 0 || len ( diagram . Legend . Connections ) > 0 ) {
tl , br := diagram . BoundingBox ( )
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 , nil )
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 , nil )
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
}
}
}
}
2022-12-17 01:15:37 +00:00
fmt . Fprint ( buf , strings . Join ( [ ] string {
2022-12-21 20:54:02 +00:00
fmt . Sprintf ( ` <mask id="%s" maskUnits="userSpaceOnUse" x="%d" y="%d" width="%d" height="%d"> ` ,
2023-03-23 20:37:28 +00:00
isolatedDiagramHash , left , top , w , h ,
2022-12-17 01:15:37 +00:00
) ,
2022-12-21 20:54:02 +00:00
fmt . Sprintf ( ` <rect x="%d" y="%d" width="%d" height="%d" fill="white"></rect> ` ,
2023-02-15 19:33:13 +00:00
left , top , w , h ,
2022-12-17 01:15:37 +00:00
) ,
strings . Join ( labelMasks , "\n" ) ,
` </mask> ` ,
} , "\n" ) )
2022-12-03 06:47:54 +00:00
2023-02-19 19:34:42 +00:00
// generate style elements that will be appended to the SVG tag
2023-02-19 13:26:24 +00:00
upperBuf := & bytes . Buffer { }
2023-03-23 20:37:28 +00:00
if opts . MasterID == "" {
2023-03-29 20:50:00 +00:00
EmbedFonts ( upperBuf , diagramHash , buf . String ( ) , diagram . FontFamily , diagram . GetCorpus ( ) ) // EmbedFonts *must* run before `d2sketch.DefineFillPatterns`, but after all elements are appended to `buf`
2023-12-13 20:17:22 +00:00
themeStylesheet , err := ThemeCSS ( diagramHash , & themeID , darkThemeID , opts . ThemeOverrides , opts . DarkThemeOverrides )
2023-03-23 20:37:28 +00:00
if err != nil {
return nil , err
}
fmt . Fprintf ( upperBuf , ` <style type="text/css"><![CDATA[%s%s]]></style> ` , BaseStylesheet , themeStylesheet )
2023-01-26 20:40:53 +00:00
2023-03-23 20:37:28 +00:00
hasMarkdown := false
for _ , s := range diagram . Shapes {
2024-08-02 14:01:26 +00:00
if s . Language == "markdown" {
2023-03-23 20:37:28 +00:00
hasMarkdown = true
break
}
}
if hasMarkdown {
css := MarkdownCSS
2023-04-07 23:45:57 +00:00
css = strings . ReplaceAll ( css , ".md" , fmt . Sprintf ( ".%s .md" , diagramHash ) )
2023-03-23 20:37:28 +00:00
css = strings . ReplaceAll ( css , "font-italic" , fmt . Sprintf ( "%s-font-italic" , diagramHash ) )
css = strings . ReplaceAll ( css , "font-bold" , fmt . Sprintf ( "%s-font-bold" , diagramHash ) )
css = strings . ReplaceAll ( css , "font-mono" , fmt . Sprintf ( "%s-font-mono" , diagramHash ) )
css = strings . ReplaceAll ( css , "font-regular" , fmt . Sprintf ( "%s-font-regular" , diagramHash ) )
2023-04-10 20:55:52 +00:00
css = strings . ReplaceAll ( css , "font-semibold" , fmt . Sprintf ( "%s-font-semibold" , diagramHash ) )
2023-03-23 20:37:28 +00:00
fmt . Fprintf ( upperBuf , ` <style type="text/css">%s</style> ` , css )
2023-01-09 18:16:28 +00:00
}
2023-03-14 06:01:33 +00:00
2025-01-15 23:30:17 +00:00
if jsRunner != nil {
2025-01-29 00:39:53 +00:00
d2sketch . DefineFillPatterns ( upperBuf , diagramHash )
2023-03-23 20:37:28 +00:00
}
2023-02-28 03:26:19 +00:00
}
2023-01-09 18:16:28 +00:00
2023-02-26 19:41:50 +00:00
// This shift is for background el to envelop the diagram
left -= int ( math . Ceil ( float64 ( diagram . Root . StrokeWidth ) / 2. ) )
top -= int ( math . Ceil ( float64 ( diagram . Root . StrokeWidth ) / 2. ) )
w += int ( math . Ceil ( float64 ( diagram . Root . StrokeWidth ) / 2. ) * 2. )
h += int ( math . Ceil ( float64 ( diagram . Root . StrokeWidth ) / 2. ) * 2. )
2024-10-09 18:09:46 +00:00
backgroundEl := d2themes . NewThemableElement ( "rect" , inlineTheme )
2023-02-26 19:41:50 +00:00
// We don't want to change the document viewbox, only the background el
2023-02-19 19:34:42 +00:00
backgroundEl . X = float64 ( left )
backgroundEl . Y = float64 ( top )
backgroundEl . Width = float64 ( w )
backgroundEl . Height = float64 ( h )
2023-02-26 19:41:50 +00:00
backgroundEl . Fill = diagram . Root . Fill
backgroundEl . Stroke = diagram . Root . Stroke
2023-03-14 06:01:33 +00:00
backgroundEl . FillPattern = diagram . Root . FillPattern
2023-04-13 14:20:31 +00:00
backgroundEl . Rx = float64 ( diagram . Root . BorderRadius )
2023-02-26 19:41:50 +00:00
if diagram . Root . StrokeDash != 0 {
dashSize , gapSize := svg . GetStrokeDashAttributes ( float64 ( diagram . Root . StrokeWidth ) , diagram . Root . StrokeDash )
backgroundEl . StrokeDashArray = fmt . Sprintf ( "%f, %f" , dashSize , gapSize )
}
backgroundEl . Attributes = fmt . Sprintf ( ` stroke-width="%d" ` , diagram . Root . StrokeWidth )
// This shift is for viewbox to envelop the background el
left -= int ( math . Ceil ( float64 ( diagram . Root . StrokeWidth ) / 2. ) )
top -= int ( math . Ceil ( float64 ( diagram . Root . StrokeWidth ) / 2. ) )
w += int ( math . Ceil ( float64 ( diagram . Root . StrokeWidth ) / 2. ) * 2. )
h += int ( math . Ceil ( float64 ( diagram . Root . StrokeWidth ) / 2. ) * 2. )
doubleBorderElStr := ""
if diagram . Root . DoubleBorder {
offset := d2target . INNER_BORDER_OFFSET
left -= int ( math . Ceil ( float64 ( diagram . Root . StrokeWidth ) / 2. ) ) + offset
top -= int ( math . Ceil ( float64 ( diagram . Root . StrokeWidth ) / 2. ) ) + offset
w += int ( math . Ceil ( float64 ( diagram . Root . StrokeWidth ) / 2. ) * 2. ) + 2 * offset
h += int ( math . Ceil ( float64 ( diagram . Root . StrokeWidth ) / 2. ) * 2. ) + 2 * offset
backgroundEl2 := backgroundEl . Copy ( )
// No need to double-paint
backgroundEl . Fill = "transparent"
backgroundEl2 . X = float64 ( left )
backgroundEl2 . Y = float64 ( top )
backgroundEl2 . Width = float64 ( w )
backgroundEl2 . Height = float64 ( h )
doubleBorderElStr = backgroundEl2 . Render ( )
left -= int ( math . Ceil ( float64 ( diagram . Root . StrokeWidth ) / 2. ) )
top -= int ( math . Ceil ( float64 ( diagram . Root . StrokeWidth ) / 2. ) )
w += int ( math . Ceil ( float64 ( diagram . Root . StrokeWidth ) / 2. ) * 2. )
h += int ( math . Ceil ( float64 ( diagram . Root . StrokeWidth ) / 2. ) * 2. )
}
2023-02-19 19:34:42 +00:00
2023-03-14 06:01:33 +00:00
bufStr := buf . String ( )
2023-03-16 05:53:12 +00:00
patternDefs := ""
2024-09-15 16:43:10 +00:00
for _ , pattern := range d2ast . FillPatterns {
2023-03-18 22:43:28 +00:00
if strings . Contains ( bufStr , fmt . Sprintf ( "%s-overlay" , pattern ) ) || diagram . Root . FillPattern == pattern {
2023-03-16 05:53:12 +00:00
if patternDefs == "" {
fmt . Fprint ( upperBuf , ` <style type="text/css"><![CDATA[ ` )
}
switch pattern {
case "dots" :
2025-01-29 00:23:40 +00:00
patternDefs += fmt . Sprintf ( dots , diagramHash )
2023-03-16 05:53:12 +00:00
case "lines" :
2025-01-29 00:23:40 +00:00
patternDefs += fmt . Sprintf ( lines , diagramHash )
2023-03-16 05:53:12 +00:00
case "grain" :
2025-01-29 00:23:40 +00:00
patternDefs += fmt . Sprintf ( grain , diagramHash )
2023-03-18 22:43:28 +00:00
case "paper" :
2025-01-29 00:23:40 +00:00
patternDefs += fmt . Sprintf ( paper , diagramHash )
2023-03-16 05:53:12 +00:00
}
2023-07-03 23:18:02 +00:00
fmt . Fprintf ( upperBuf , `
2023-03-16 05:53:12 +00:00
. % s - overlay {
2025-01-29 00:23:40 +00:00
fill : url ( # % s - % s ) ;
2023-03-14 06:01:33 +00:00
mix - blend - mode : multiply ;
2025-01-29 00:23:40 +00:00
} ` , pattern , pattern , diagramHash )
2023-03-16 05:53:12 +00:00
}
}
if patternDefs != "" {
2023-03-14 06:01:33 +00:00
fmt . Fprint ( upperBuf , ` ]]></style> ` )
fmt . Fprint ( upperBuf , "<defs>" )
2023-07-03 23:18:02 +00:00
fmt . Fprint ( upperBuf , patternDefs )
2023-03-14 06:01:33 +00:00
fmt . Fprint ( upperBuf , "</defs>" )
}
2023-02-28 03:06:13 +00:00
var dimensions string
2023-06-15 22:53:23 +00:00
if scale != nil {
dimensions = fmt . Sprintf ( ` width="%d" height="%d" ` ,
int ( math . Ceil ( ( * scale ) * float64 ( w ) ) ) ,
int ( math . Ceil ( ( * scale ) * float64 ( h ) ) ) ,
)
2023-02-28 03:06:13 +00:00
}
2023-03-18 05:29:51 +00:00
alignment := "xMinYMin"
2023-07-14 20:08:26 +00:00
if opts . Center != nil && * opts . Center {
2023-03-18 05:29:51 +00:00
alignment = "xMidYMid"
}
2023-03-23 20:37:28 +00:00
fitToScreenWrapperOpening := ""
xmlTag := ""
fitToScreenWrapperClosing := ""
idAttr := ""
tag := "g"
// Many things change when this is rendering for animation
if opts . MasterID == "" {
2025-04-02 19:57:06 +00:00
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 ,
2023-03-23 20:37:28 +00:00
alignment ,
w , h ,
dimensions ,
)
2025-01-24 05:27:34 +00:00
if opts . NoXMLTag == nil || ! * opts . NoXMLTag {
xmlTag = ` <?xml version="1.0" encoding="utf-8"?> `
}
2023-03-23 20:37:28 +00:00
fitToScreenWrapperClosing = "</svg>"
2025-01-27 01:28:28 +00:00
idAttr = ` d2-svg `
2023-03-23 20:37:28 +00:00
tag = "svg"
}
2023-02-19 19:34:42 +00:00
// TODO minify
2025-01-27 01:28:28 +00:00
docRendered := fmt . Sprintf ( ` %s%s<%s class="%s" width="%d" height="%d" viewBox="%d %d %d %d">%s%s%s%s</%s>%s ` ,
2023-03-23 20:37:28 +00:00
xmlTag ,
fitToScreenWrapperOpening ,
tag ,
2025-01-27 01:28:28 +00:00
strings . Join ( [ ] string { diagramHash , idAttr } , " " ) ,
2023-02-19 11:32:44 +00:00
w , h , left , top , w , h ,
2023-02-26 19:41:50 +00:00
doubleBorderElStr ,
backgroundEl . Render ( ) ,
2023-02-19 13:26:24 +00:00
upperBuf . String ( ) ,
2023-01-09 18:16:28 +00:00
buf . String ( ) ,
2023-03-23 20:37:28 +00:00
tag ,
fitToScreenWrapperClosing ,
2023-01-09 18:16:28 +00:00
)
return [ ] byte ( docRendered ) , nil
2022-11-03 13:54:49 +00:00
}
2023-01-11 21:18:14 +00:00
// TODO include only colors that are being used to reduce size
2023-12-13 20:17:22 +00:00
func ThemeCSS ( diagramHash string , themeID * int64 , darkThemeID * int64 , overrides , darkOverrides * d2target . ThemeOverrides ) ( stylesheet string , err error ) {
2023-07-14 20:08:26 +00:00
if themeID == nil {
themeID = & d2themescatalog . NeutralDefault . ID
}
2023-12-13 20:17:22 +00:00
out , err := singleThemeRulesets ( diagramHash , * themeID , overrides )
2023-01-11 21:18:14 +00:00
if err != nil {
return "" , err
}
2023-01-09 21:16:22 +00:00
2023-02-19 14:33:40 +00:00
if darkThemeID != nil {
2023-12-13 20:17:22 +00:00
darkOut , err := singleThemeRulesets ( diagramHash , * darkThemeID , darkOverrides )
2023-01-11 21:18:14 +00:00
if err != nil {
return "" , err
}
out += fmt . Sprintf ( "@media screen and (prefers-color-scheme:dark){%s}" , darkOut )
2023-01-09 21:16:22 +00:00
}
2023-01-11 21:18:14 +00:00
return out , nil
2023-01-09 21:16:22 +00:00
}
2023-12-13 20:17:22 +00:00
func singleThemeRulesets ( diagramHash string , themeID int64 , overrides * d2target . ThemeOverrides ) ( rulesets string , err error ) {
2023-01-09 21:16:22 +00:00
out := ""
theme := d2themescatalog . Find ( themeID )
2023-12-13 20:17:22 +00:00
theme . ApplyOverrides ( overrides )
2023-01-09 23:17:56 +00:00
2023-01-11 21:18:14 +00:00
// Global theme colors
2023-01-09 21:16:22 +00:00
for _ , property := range [ ] string { "fill" , "stroke" , "background-color" , "color" } {
2023-03-12 01:20:59 +00:00
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 ,
2023-01-09 21:16:22 +00:00
property , property , theme . Colors . Neutrals . N1 ,
2023-03-12 01:20:59 +00:00
diagramHash ,
2023-01-09 21:16:22 +00:00
property , property , theme . Colors . Neutrals . N2 ,
2023-03-12 01:20:59 +00:00
diagramHash ,
2023-01-09 21:16:22 +00:00
property , property , theme . Colors . Neutrals . N3 ,
2023-03-12 01:20:59 +00:00
diagramHash ,
2023-01-09 21:16:22 +00:00
property , property , theme . Colors . Neutrals . N4 ,
2023-03-12 01:20:59 +00:00
diagramHash ,
2023-01-09 21:16:22 +00:00
property , property , theme . Colors . Neutrals . N5 ,
2023-03-12 01:20:59 +00:00
diagramHash ,
2023-01-09 21:16:22 +00:00
property , property , theme . Colors . Neutrals . N6 ,
2023-03-12 01:20:59 +00:00
diagramHash ,
2023-01-09 21:16:22 +00:00
property , property , theme . Colors . Neutrals . N7 ,
2023-03-12 01:20:59 +00:00
diagramHash ,
2023-01-09 21:16:22 +00:00
property , property , theme . Colors . B1 ,
2023-03-12 01:20:59 +00:00
diagramHash ,
2023-01-09 21:16:22 +00:00
property , property , theme . Colors . B2 ,
2023-03-12 01:20:59 +00:00
diagramHash ,
2023-01-09 21:16:22 +00:00
property , property , theme . Colors . B3 ,
2023-03-12 01:20:59 +00:00
diagramHash ,
2023-01-09 21:16:22 +00:00
property , property , theme . Colors . B4 ,
2023-03-12 01:20:59 +00:00
diagramHash ,
2023-01-09 21:16:22 +00:00
property , property , theme . Colors . B5 ,
2023-03-12 01:20:59 +00:00
diagramHash ,
2023-01-09 21:16:22 +00:00
property , property , theme . Colors . B6 ,
2023-03-12 01:20:59 +00:00
diagramHash ,
2023-01-09 21:16:22 +00:00
property , property , theme . Colors . AA2 ,
2023-03-12 01:20:59 +00:00
diagramHash ,
2023-01-09 21:16:22 +00:00
property , property , theme . Colors . AA4 ,
2023-03-12 01:20:59 +00:00
diagramHash ,
2023-01-09 21:16:22 +00:00
property , property , theme . Colors . AA5 ,
2023-03-12 01:20:59 +00:00
diagramHash ,
2023-01-09 21:16:22 +00:00
property , property , theme . Colors . AB4 ,
2023-03-12 01:20:59 +00:00
diagramHash ,
2023-01-09 21:16:22 +00:00
property , property , theme . Colors . AB5 ,
)
}
2023-01-09 23:17:56 +00:00
2023-01-28 00:43:24 +00:00
// Appendix
2023-02-20 19:54:32 +00:00
out += fmt . Sprintf ( ".appendix text.text{fill:%s}" , theme . Colors . Neutrals . N1 )
2023-01-28 00:43:24 +00:00
2023-01-11 21:18:14 +00:00
// Markdown specific rulesets
2023-01-09 23:17:56 +00:00
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 ,
2023-01-10 10:34:42 +00:00
theme . Colors . Neutrals . N6 ,
2023-01-09 23:17:56 +00:00
theme . Colors . B2 , theme . Colors . B2 ,
theme . Colors . Neutrals . N2 , // TODO or N3 --color-attention-subtle
"red" ,
)
2023-01-11 21:18:14 +00:00
// Sketch style specific rulesets
2023-01-12 10:19:34 +00:00
// B
2023-01-11 21:18:14 +00:00
lc , err := color . LuminanceCategory ( theme . Colors . B1 )
if err != nil {
return "" , err
}
2025-01-29 00:39:53 +00:00
out += fmt . Sprintf ( ".sketch-overlay-%s{fill:url(#streaks-%s-%s);mix-blend-mode:%s}" , color . B1 , lc , diagramHash , blendMode ( lc ) )
2023-01-11 21:18:14 +00:00
lc , err = color . LuminanceCategory ( theme . Colors . B2 )
if err != nil {
return "" , err
}
2025-01-29 00:39:53 +00:00
out += fmt . Sprintf ( ".sketch-overlay-%s{fill:url(#streaks-%s-%s);mix-blend-mode:%s}" , color . B2 , lc , diagramHash , blendMode ( lc ) )
2023-01-11 21:18:14 +00:00
lc , err = color . LuminanceCategory ( theme . Colors . B3 )
if err != nil {
return "" , err
}
2025-01-29 00:39:53 +00:00
out += fmt . Sprintf ( ".sketch-overlay-%s{fill:url(#streaks-%s-%s);mix-blend-mode:%s}" , color . B3 , lc , diagramHash , blendMode ( lc ) )
2023-01-11 21:18:14 +00:00
lc , err = color . LuminanceCategory ( theme . Colors . B4 )
if err != nil {
return "" , err
}
2025-01-29 00:39:53 +00:00
out += fmt . Sprintf ( ".sketch-overlay-%s{fill:url(#streaks-%s-%s);mix-blend-mode:%s}" , color . B4 , lc , diagramHash , blendMode ( lc ) )
2023-01-11 21:18:14 +00:00
lc , err = color . LuminanceCategory ( theme . Colors . B5 )
if err != nil {
return "" , err
}
2025-01-29 00:39:53 +00:00
out += fmt . Sprintf ( ".sketch-overlay-%s{fill:url(#streaks-%s-%s);mix-blend-mode:%s}" , color . B5 , lc , diagramHash , blendMode ( lc ) )
2023-01-11 21:18:14 +00:00
lc , err = color . LuminanceCategory ( theme . Colors . B6 )
if err != nil {
return "" , err
}
2025-01-29 00:39:53 +00:00
out += fmt . Sprintf ( ".sketch-overlay-%s{fill:url(#streaks-%s-%s);mix-blend-mode:%s}" , color . B6 , lc , diagramHash , blendMode ( lc ) )
2023-01-11 21:18:14 +00:00
2023-01-12 10:19:34 +00:00
// AA
lc , err = color . LuminanceCategory ( theme . Colors . AA2 )
if err != nil {
return "" , err
}
2025-01-29 00:39:53 +00:00
out += fmt . Sprintf ( ".sketch-overlay-%s{fill:url(#streaks-%s-%s);mix-blend-mode:%s}" , color . AA2 , lc , diagramHash , blendMode ( lc ) )
2023-01-12 10:19:34 +00:00
lc , err = color . LuminanceCategory ( theme . Colors . AA4 )
if err != nil {
return "" , err
}
2025-01-29 00:39:53 +00:00
out += fmt . Sprintf ( ".sketch-overlay-%s{fill:url(#streaks-%s-%s);mix-blend-mode:%s}" , color . AA4 , lc , diagramHash , blendMode ( lc ) )
2023-01-12 10:19:34 +00:00
lc , err = color . LuminanceCategory ( theme . Colors . AA5 )
if err != nil {
return "" , err
}
2025-01-29 00:39:53 +00:00
out += fmt . Sprintf ( ".sketch-overlay-%s{fill:url(#streaks-%s-%s);mix-blend-mode:%s}" , color . AA5 , lc , diagramHash , blendMode ( lc ) )
2023-01-12 10:19:34 +00:00
// AB
lc , err = color . LuminanceCategory ( theme . Colors . AB4 )
if err != nil {
return "" , err
}
2025-01-29 00:39:53 +00:00
out += fmt . Sprintf ( ".sketch-overlay-%s{fill:url(#streaks-%s-%s);mix-blend-mode:%s}" , color . AB4 , lc , diagramHash , blendMode ( lc ) )
2023-01-12 10:19:34 +00:00
lc , err = color . LuminanceCategory ( theme . Colors . AB5 )
if err != nil {
return "" , err
}
2025-01-29 00:39:53 +00:00
out += fmt . Sprintf ( ".sketch-overlay-%s{fill:url(#streaks-%s-%s);mix-blend-mode:%s}" , color . AB5 , lc , diagramHash , blendMode ( lc ) )
2023-01-12 10:19:34 +00:00
// Neutrals
2023-01-11 21:18:14 +00:00
lc , err = color . LuminanceCategory ( theme . Colors . Neutrals . N1 )
if err != nil {
return "" , err
}
2025-01-29 00:39:53 +00:00
out += fmt . Sprintf ( ".sketch-overlay-%s{fill:url(#streaks-%s-%s);mix-blend-mode:%s}" , color . N1 , lc , diagramHash , blendMode ( lc ) )
2023-01-11 21:18:14 +00:00
lc , err = color . LuminanceCategory ( theme . Colors . Neutrals . N2 )
if err != nil {
return "" , err
}
2025-01-29 00:39:53 +00:00
out += fmt . Sprintf ( ".sketch-overlay-%s{fill:url(#streaks-%s-%s);mix-blend-mode:%s}" , color . N2 , lc , diagramHash , blendMode ( lc ) )
2023-01-11 21:18:14 +00:00
lc , err = color . LuminanceCategory ( theme . Colors . Neutrals . N3 )
if err != nil {
return "" , err
}
2025-01-29 00:39:53 +00:00
out += fmt . Sprintf ( ".sketch-overlay-%s{fill:url(#streaks-%s-%s);mix-blend-mode:%s}" , color . N3 , lc , diagramHash , blendMode ( lc ) )
2023-01-11 21:18:14 +00:00
lc , err = color . LuminanceCategory ( theme . Colors . Neutrals . N4 )
if err != nil {
return "" , err
}
2025-01-29 00:39:53 +00:00
out += fmt . Sprintf ( ".sketch-overlay-%s{fill:url(#streaks-%s-%s);mix-blend-mode:%s}" , color . N4 , lc , diagramHash , blendMode ( lc ) )
2023-01-11 21:18:14 +00:00
lc , err = color . LuminanceCategory ( theme . Colors . Neutrals . N5 )
if err != nil {
return "" , err
}
2025-01-29 00:39:53 +00:00
out += fmt . Sprintf ( ".sketch-overlay-%s{fill:url(#streaks-%s-%s);mix-blend-mode:%s}" , color . N5 , lc , diagramHash , blendMode ( lc ) )
2023-01-11 21:18:14 +00:00
lc , err = color . LuminanceCategory ( theme . Colors . Neutrals . N6 )
if err != nil {
return "" , err
}
2025-01-29 00:39:53 +00:00
out += fmt . Sprintf ( ".sketch-overlay-%s{fill:url(#streaks-%s-%s);mix-blend-mode:%s}" , color . N6 , lc , diagramHash , blendMode ( lc ) )
2023-01-11 21:18:14 +00:00
lc , err = color . LuminanceCategory ( theme . Colors . Neutrals . N7 )
if err != nil {
return "" , err
}
2025-01-29 00:39:53 +00:00
out += fmt . Sprintf ( ".sketch-overlay-%s{fill:url(#streaks-%s-%s);mix-blend-mode:%s}" , color . N7 , lc , diagramHash , blendMode ( lc ) )
2023-01-11 21:18:14 +00:00
2023-03-04 05:02:02 +00:00
if theme . IsDark ( ) {
2023-07-03 23:18:02 +00:00
out += ".light-code{display: none}"
out += ".dark-code{display: block}"
2023-03-04 05:02:02 +00:00
} else {
2023-07-03 23:18:02 +00:00
out += ".light-code{display: block}"
out += ".dark-code{display: none}"
2023-03-04 05:02:02 +00:00
}
2023-01-11 21:18:14 +00:00
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" )
2023-01-09 21:16:22 +00:00
}
2022-11-30 21:15:33 +00:00
type DiagramObject interface {
GetID ( ) string
GetZIndex ( ) int
}
2022-11-30 19:22:43 +00:00
// sortObjects sorts all diagrams objects (shapes and connections) in the desired drawing order
// the sorting criteria is:
// 1. zIndex, lower comes first
// 2. two shapes with the same zIndex are sorted by their level (container nesting), containers come first
// 3. two shapes with the same zIndex and same level, are sorted in the order they were exported
// 4. shape and edge, shapes come first
2022-11-30 21:15:33 +00:00
func sortObjects ( allObjects [ ] DiagramObject ) {
2022-11-30 19:22:43 +00:00
sort . SliceStable ( allObjects , func ( i , j int ) bool {
// first sort by zIndex
iZIndex := allObjects [ i ] . GetZIndex ( )
jZIndex := allObjects [ j ] . GetZIndex ( )
if iZIndex != jZIndex {
return iZIndex < jZIndex
}
2022-11-30 21:15:33 +00:00
// then, if both are shapes, parents come before their children
2022-11-30 19:22:43 +00:00
iShape , iIsShape := allObjects [ i ] . ( d2target . Shape )
jShape , jIsShape := allObjects [ j ] . ( d2target . Shape )
if iIsShape && jIsShape {
return iShape . Level < jShape . Level
}
// then, shapes come before connections
_ , jIsConnection := allObjects [ j ] . ( d2target . Connection )
return iIsShape && jIsConnection
} )
}
2022-11-03 13:54:49 +00:00
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 ( ) )
}
2023-04-07 21:03:32 +00:00
func RenderMultiboard ( diagram * d2target . Diagram , opts * RenderOpts ) ( [ ] [ ] byte , error ) {
var boards [ ] [ ] byte
for _ , dl := range diagram . Layers {
childrenBoards , err := RenderMultiboard ( dl , opts )
if err != nil {
return nil , err
}
boards = append ( boards , childrenBoards ... )
}
for _ , dl := range diagram . Scenarios {
childrenBoards , err := RenderMultiboard ( dl , opts )
if err != nil {
return nil , err
}
boards = append ( boards , childrenBoards ... )
}
for _ , dl := range diagram . Steps {
childrenBoards , err := RenderMultiboard ( dl , opts )
if err != nil {
return nil , err
}
boards = append ( boards , childrenBoards ... )
}
if ! diagram . IsFolderOnly {
out , err := Render ( diagram , opts )
if err != nil {
return boards , err
}
boards = append ( [ ] [ ] byte { out } , boards ... )
return boards , nil
}
return boards , nil
}