...

Source file src/oss.terrastruct.com/d2/d2renderers/d2svg/d2svg.go

Documentation: oss.terrastruct.com/d2/d2renderers/d2svg

     1  // d2svg implements an SVG renderer for d2 diagrams.
     2  // The input is d2exporter's output
     3  package d2svg
     4  
     5  import (
     6  	"bytes"
     7  	_ "embed"
     8  	"errors"
     9  	"fmt"
    10  	"hash/fnv"
    11  	"html"
    12  	"io"
    13  	"sort"
    14  	"strings"
    15  
    16  	"math"
    17  
    18  	"github.com/alecthomas/chroma/v2"
    19  	"github.com/alecthomas/chroma/v2/formatters"
    20  	"github.com/alecthomas/chroma/v2/lexers"
    21  	"github.com/alecthomas/chroma/v2/styles"
    22  
    23  	"oss.terrastruct.com/d2/d2graph"
    24  	"oss.terrastruct.com/d2/d2renderers/d2fonts"
    25  	"oss.terrastruct.com/d2/d2renderers/d2latex"
    26  	"oss.terrastruct.com/d2/d2renderers/d2sketch"
    27  	"oss.terrastruct.com/d2/d2target"
    28  	"oss.terrastruct.com/d2/d2themes"
    29  	"oss.terrastruct.com/d2/d2themes/d2themescatalog"
    30  	"oss.terrastruct.com/d2/lib/color"
    31  	"oss.terrastruct.com/d2/lib/geo"
    32  	"oss.terrastruct.com/d2/lib/label"
    33  	"oss.terrastruct.com/d2/lib/shape"
    34  	"oss.terrastruct.com/d2/lib/svg"
    35  	"oss.terrastruct.com/d2/lib/textmeasure"
    36  	"oss.terrastruct.com/d2/lib/version"
    37  )
    38  
    39  const (
    40  	DEFAULT_PADDING = 100
    41  
    42  	appendixIconRadius = 16
    43  )
    44  
    45  var multipleOffset = geo.NewVector(d2target.MULTIPLE_OFFSET, -d2target.MULTIPLE_OFFSET)
    46  
    47  //go:embed tooltip.svg
    48  var TooltipIcon string
    49  
    50  //go:embed link.svg
    51  var LinkIcon string
    52  
    53  //go:embed style.css
    54  var BaseStylesheet string
    55  
    56  //go:embed github-markdown.css
    57  var MarkdownCSS string
    58  
    59  //go:embed dots.txt
    60  var dots string
    61  
    62  //go:embed lines.txt
    63  var lines string
    64  
    65  //go:embed grain.txt
    66  var grain string
    67  
    68  //go:embed paper.txt
    69  var paper string
    70  
    71  type RenderOpts struct {
    72  	Pad                *int64
    73  	Sketch             *bool
    74  	Center             *bool
    75  	ThemeID            *int64
    76  	DarkThemeID        *int64
    77  	ThemeOverrides     *d2target.ThemeOverrides
    78  	DarkThemeOverrides *d2target.ThemeOverrides
    79  	Font               string
    80  	// the svg will be scaled by this factor, if unset the svg will fit to screen
    81  	Scale *float64
    82  
    83  	// MasterID is passed when the diagram should use something other than its own hash for unique targeting
    84  	// Currently, that's when multi-boards are collapsed
    85  	MasterID string
    86  }
    87  
    88  func dimensions(diagram *d2target.Diagram, pad int) (left, top, width, height int) {
    89  	tl, br := diagram.BoundingBox()
    90  	left = tl.X - pad
    91  	top = tl.Y - pad
    92  	width = br.X - tl.X + pad*2
    93  	height = br.Y - tl.Y + pad*2
    94  
    95  	return left, top, width, height
    96  }
    97  
    98  func arrowheadMarkerID(isTarget bool, connection d2target.Connection) string {
    99  	var arrowhead d2target.Arrowhead
   100  	if isTarget {
   101  		arrowhead = connection.DstArrow
   102  	} else {
   103  		arrowhead = connection.SrcArrow
   104  	}
   105  
   106  	return fmt.Sprintf("mk-%s", hash(fmt.Sprintf("%s,%t,%d,%s",
   107  		arrowhead, isTarget, connection.StrokeWidth, connection.Stroke,
   108  	)))
   109  }
   110  
   111  func arrowheadMarker(isTarget bool, id string, connection d2target.Connection) string {
   112  	arrowhead := connection.DstArrow
   113  	if !isTarget {
   114  		arrowhead = connection.SrcArrow
   115  	}
   116  	strokeWidth := float64(connection.StrokeWidth)
   117  	width, height := arrowhead.Dimensions(strokeWidth)
   118  
   119  	var path string
   120  	switch arrowhead {
   121  	case d2target.ArrowArrowhead:
   122  		polygonEl := d2themes.NewThemableElement("polygon")
   123  		polygonEl.Fill = connection.Stroke
   124  		polygonEl.ClassName = "connection"
   125  		polygonEl.Attributes = fmt.Sprintf(`stroke-width="%d"`, connection.StrokeWidth)
   126  
   127  		if isTarget {
   128  			polygonEl.Points = fmt.Sprintf("%f,%f %f,%f %f,%f %f,%f",
   129  				0., 0.,
   130  				width, height/2,
   131  				0., height,
   132  				width/4, height/2,
   133  			)
   134  		} else {
   135  			polygonEl.Points = fmt.Sprintf("%f,%f %f,%f %f,%f %f,%f",
   136  				0., height/2,
   137  				width, 0.,
   138  				width*3/4, height/2,
   139  				width, height,
   140  			)
   141  		}
   142  		path = polygonEl.Render()
   143  	case d2target.UnfilledTriangleArrowhead:
   144  		polygonEl := d2themes.NewThemableElement("polygon")
   145  		polygonEl.Fill = d2target.BG_COLOR
   146  		polygonEl.Stroke = connection.Stroke
   147  		polygonEl.ClassName = "connection"
   148  		polygonEl.Attributes = fmt.Sprintf(`stroke-width="%d"`, connection.StrokeWidth)
   149  
   150  		inset := strokeWidth / 2
   151  		if isTarget {
   152  			polygonEl.Points = fmt.Sprintf("%f,%f %f,%f %f,%f",
   153  				inset, inset,
   154  				width-inset, height/2.0,
   155  				inset, height-inset,
   156  			)
   157  		} else {
   158  			polygonEl.Points = fmt.Sprintf("%f,%f %f,%f %f,%f",
   159  				width-inset, inset,
   160  				inset, height/2.0,
   161  				width-inset, height-inset,
   162  			)
   163  		}
   164  		path = polygonEl.Render()
   165  
   166  	case d2target.TriangleArrowhead:
   167  		polygonEl := d2themes.NewThemableElement("polygon")
   168  		polygonEl.Fill = connection.Stroke
   169  		polygonEl.ClassName = "connection"
   170  		polygonEl.Attributes = fmt.Sprintf(`stroke-width="%d"`, connection.StrokeWidth)
   171  
   172  		if isTarget {
   173  			polygonEl.Points = fmt.Sprintf("%f,%f %f,%f %f,%f",
   174  				0., 0.,
   175  				width, height/2.0,
   176  				0., height,
   177  			)
   178  		} else {
   179  			polygonEl.Points = fmt.Sprintf("%f,%f %f,%f %f,%f",
   180  				width, 0.,
   181  				0., height/2.0,
   182  				width, height,
   183  			)
   184  		}
   185  		path = polygonEl.Render()
   186  	case d2target.LineArrowhead:
   187  		polylineEl := d2themes.NewThemableElement("polyline")
   188  		polylineEl.Fill = color.None
   189  		polylineEl.ClassName = "connection"
   190  		polylineEl.Stroke = connection.Stroke
   191  		polylineEl.Attributes = fmt.Sprintf(`stroke-width="%d"`, connection.StrokeWidth)
   192  
   193  		if isTarget {
   194  			polylineEl.Points = fmt.Sprintf("%f,%f %f,%f %f,%f",
   195  				strokeWidth/2, strokeWidth/2,
   196  				width-strokeWidth/2, height/2,
   197  				strokeWidth/2, height-strokeWidth/2,
   198  			)
   199  		} else {
   200  			polylineEl.Points = fmt.Sprintf("%f,%f %f,%f %f,%f",
   201  				width-strokeWidth/2, strokeWidth/2,
   202  				strokeWidth/2, height/2,
   203  				width-strokeWidth/2, height-strokeWidth/2,
   204  			)
   205  		}
   206  		path = polylineEl.Render()
   207  	case d2target.FilledDiamondArrowhead:
   208  		polygonEl := d2themes.NewThemableElement("polygon")
   209  		polygonEl.ClassName = "connection"
   210  		polygonEl.Fill = connection.Stroke
   211  		polygonEl.Attributes = fmt.Sprintf(`stroke-width="%d"`, connection.StrokeWidth)
   212  
   213  		if isTarget {
   214  			polygonEl.Points = fmt.Sprintf("%f,%f %f,%f %f,%f %f,%f",
   215  				0., height/2.0,
   216  				width/2.0, 0.,
   217  				width, height/2.0,
   218  				width/2.0, height,
   219  			)
   220  		} else {
   221  			polygonEl.Points = fmt.Sprintf("%f,%f %f,%f %f,%f %f,%f",
   222  				0., height/2.0,
   223  				width/2.0, 0.,
   224  				width, height/2.0,
   225  				width/2.0, height,
   226  			)
   227  		}
   228  		path = polygonEl.Render()
   229  	case d2target.DiamondArrowhead:
   230  		polygonEl := d2themes.NewThemableElement("polygon")
   231  		polygonEl.ClassName = "connection"
   232  		polygonEl.Fill = d2target.BG_COLOR
   233  		polygonEl.Stroke = connection.Stroke
   234  		polygonEl.Attributes = fmt.Sprintf(`stroke-width="%d"`, connection.StrokeWidth)
   235  
   236  		if isTarget {
   237  			polygonEl.Points = fmt.Sprintf("%f,%f %f,%f %f,%f %f,%f",
   238  				0., height/2.0,
   239  				width/2, height/8,
   240  				width, height/2.0,
   241  				width/2.0, height*0.9,
   242  			)
   243  		} else {
   244  			polygonEl.Points = fmt.Sprintf("%f,%f %f,%f %f,%f %f,%f",
   245  				width/8, height/2.0,
   246  				width*0.6, height/8,
   247  				width*1.1, height/2.0,
   248  				width*0.6, height*7/8,
   249  			)
   250  		}
   251  		path = polygonEl.Render()
   252  	case d2target.FilledCircleArrowhead:
   253  		radius := width / 2
   254  
   255  		circleEl := d2themes.NewThemableElement("circle")
   256  		circleEl.Cy = radius
   257  		circleEl.R = radius - strokeWidth/2
   258  		circleEl.Fill = connection.Stroke
   259  		circleEl.ClassName = "connection"
   260  		circleEl.Attributes = fmt.Sprintf(`stroke-width="%d"`, connection.StrokeWidth)
   261  
   262  		if isTarget {
   263  			circleEl.Cx = radius + strokeWidth/2
   264  		} else {
   265  			circleEl.Cx = radius - strokeWidth/2
   266  		}
   267  
   268  		path = circleEl.Render()
   269  	case d2target.CircleArrowhead:
   270  		radius := width / 2
   271  
   272  		circleEl := d2themes.NewThemableElement("circle")
   273  		circleEl.Cy = radius
   274  		circleEl.R = radius - strokeWidth
   275  		circleEl.Fill = d2target.BG_COLOR
   276  		circleEl.Stroke = connection.Stroke
   277  		circleEl.Attributes = fmt.Sprintf(`stroke-width="%d"`, connection.StrokeWidth)
   278  
   279  		if isTarget {
   280  			circleEl.Cx = radius + strokeWidth/2
   281  		} else {
   282  			circleEl.Cx = radius - strokeWidth/2
   283  		}
   284  
   285  		path = circleEl.Render()
   286  	case d2target.CfOne, d2target.CfMany, d2target.CfOneRequired, d2target.CfManyRequired:
   287  		offset := 3.0 + float64(connection.StrokeWidth)*1.8
   288  
   289  		var modifierEl *d2themes.ThemableElement
   290  		if arrowhead == d2target.CfOneRequired || arrowhead == d2target.CfManyRequired {
   291  			modifierEl = d2themes.NewThemableElement("path")
   292  			modifierEl.D = fmt.Sprintf("M%f,%f %f,%f",
   293  				offset, 0.,
   294  				offset, height,
   295  			)
   296  			modifierEl.Fill = d2target.BG_COLOR
   297  			modifierEl.Stroke = connection.Stroke
   298  			modifierEl.ClassName = "connection"
   299  			modifierEl.Attributes = fmt.Sprintf(`stroke-width="%d"`, connection.StrokeWidth)
   300  		} else {
   301  			modifierEl = d2themes.NewThemableElement("circle")
   302  			modifierEl.Cx = offset/2.0 + 2.0
   303  			modifierEl.Cy = height / 2.0
   304  			modifierEl.R = offset / 2.0
   305  			modifierEl.Fill = d2target.BG_COLOR
   306  			modifierEl.Stroke = connection.Stroke
   307  			modifierEl.ClassName = "connection"
   308  			modifierEl.Attributes = fmt.Sprintf(`stroke-width="%d"`, connection.StrokeWidth)
   309  		}
   310  
   311  		childPathEl := d2themes.NewThemableElement("path")
   312  		if arrowhead == d2target.CfMany || arrowhead == d2target.CfManyRequired {
   313  			childPathEl.D = fmt.Sprintf("M%f,%f %f,%f M%f,%f %f,%f M%f,%f %f,%f",
   314  				width-3.0, height/2.0,
   315  				width+offset, height/2.0,
   316  				offset+3.0, height/2.0,
   317  				width+offset, 0.,
   318  				offset+3.0, height/2.0,
   319  				width+offset, height,
   320  			)
   321  		} else {
   322  			childPathEl.D = fmt.Sprintf("M%f,%f %f,%f M%f,%f %f,%f",
   323  				width-3.0, height/2.0,
   324  				width+offset, height/2.0,
   325  				offset*2.0, 0.,
   326  				offset*2.0, height,
   327  			)
   328  		}
   329  
   330  		gEl := d2themes.NewThemableElement("g")
   331  		if !isTarget {
   332  			gEl.Transform = fmt.Sprintf("scale(-1) translate(-%f, -%f)", width, height)
   333  		}
   334  		gEl.Fill = d2target.BG_COLOR
   335  		gEl.Stroke = connection.Stroke
   336  		gEl.ClassName = "connection"
   337  		gEl.Attributes = fmt.Sprintf(`stroke-width="%d"`, connection.StrokeWidth)
   338  		gEl.Content = fmt.Sprintf("%s%s",
   339  			modifierEl.Render(), childPathEl.Render(),
   340  		)
   341  		path = gEl.Render()
   342  	default:
   343  		return ""
   344  	}
   345  
   346  	var refX float64
   347  	refY := height / 2
   348  	switch arrowhead {
   349  	case d2target.DiamondArrowhead:
   350  		if isTarget {
   351  			refX = width - 0.6*strokeWidth
   352  		} else {
   353  			refX = width/8 + 0.6*strokeWidth
   354  		}
   355  		width *= 1.1
   356  	default:
   357  		if isTarget {
   358  			refX = width - 1.5*strokeWidth
   359  		} else {
   360  			refX = 1.5 * strokeWidth
   361  		}
   362  	}
   363  
   364  	return strings.Join([]string{
   365  		fmt.Sprintf(`<marker id="%s" markerWidth="%f" markerHeight="%f" refX="%f" refY="%f"`,
   366  			id, width, height, refX, refY,
   367  		),
   368  		fmt.Sprintf(`viewBox="%f %f %f %f"`, 0., 0., width, height),
   369  		`orient="auto" markerUnits="userSpaceOnUse">`,
   370  		path,
   371  		"</marker>",
   372  	}, " ")
   373  }
   374  
   375  // compute the (dx, dy) adjustment to apply to get the arrowhead-adjusted end point
   376  func arrowheadAdjustment(start, end *geo.Point, arrowhead d2target.Arrowhead, edgeStrokeWidth, shapeStrokeWidth int) *geo.Point {
   377  	distance := (float64(edgeStrokeWidth) + float64(shapeStrokeWidth)) / 2.0
   378  	if arrowhead != d2target.NoArrowhead {
   379  		distance += float64(edgeStrokeWidth)
   380  	}
   381  
   382  	v := geo.NewVector(end.X-start.X, end.Y-start.Y)
   383  	return v.Unit().Multiply(-distance).ToPoint()
   384  }
   385  
   386  func getArrowheadAdjustments(connection d2target.Connection, idToShape map[string]d2target.Shape) (srcAdj, dstAdj *geo.Point) {
   387  	route := connection.Route
   388  	srcShape := idToShape[connection.Src]
   389  	dstShape := idToShape[connection.Dst]
   390  
   391  	sourceAdjustment := arrowheadAdjustment(route[1], route[0], connection.SrcArrow, connection.StrokeWidth, srcShape.StrokeWidth)
   392  
   393  	targetAdjustment := arrowheadAdjustment(route[len(route)-2], route[len(route)-1], connection.DstArrow, connection.StrokeWidth, dstShape.StrokeWidth)
   394  	return sourceAdjustment, targetAdjustment
   395  }
   396  
   397  // returns the path's d attribute for the given connection
   398  func pathData(connection d2target.Connection, srcAdj, dstAdj *geo.Point) string {
   399  	var path []string
   400  	route := connection.Route
   401  
   402  	path = append(path, fmt.Sprintf("M %f %f",
   403  		route[0].X+srcAdj.X,
   404  		route[0].Y+srcAdj.Y,
   405  	))
   406  
   407  	if connection.IsCurve {
   408  		i := 1
   409  		for ; i < len(route)-3; i += 3 {
   410  			path = append(path, fmt.Sprintf("C %f %f %f %f %f %f",
   411  				route[i].X, route[i].Y,
   412  				route[i+1].X, route[i+1].Y,
   413  				route[i+2].X, route[i+2].Y,
   414  			))
   415  		}
   416  		// final curve target adjustment
   417  		path = append(path, fmt.Sprintf("C %f %f %f %f %f %f",
   418  			route[i].X, route[i].Y,
   419  			route[i+1].X, route[i+1].Y,
   420  			route[i+2].X+dstAdj.X,
   421  			route[i+2].Y+dstAdj.Y,
   422  		))
   423  	} else {
   424  		for i := 1; i < len(route)-1; i++ {
   425  			prevSource := route[i-1]
   426  			prevTarget := route[i]
   427  			currTarget := route[i+1]
   428  			prevVector := prevSource.VectorTo(prevTarget)
   429  			currVector := prevTarget.VectorTo(currTarget)
   430  
   431  			dist := geo.EuclideanDistance(prevTarget.X, prevTarget.Y, currTarget.X, currTarget.Y)
   432  
   433  			connectionBorderRadius := connection.BorderRadius
   434  			units := math.Min(connectionBorderRadius, dist/2)
   435  
   436  			prevTranslations := prevVector.Unit().Multiply(units).ToPoint()
   437  			currTranslations := currVector.Unit().Multiply(units).ToPoint()
   438  
   439  			path = append(path, fmt.Sprintf("L %f %f",
   440  				prevTarget.X-prevTranslations.X,
   441  				prevTarget.Y-prevTranslations.Y,
   442  			))
   443  
   444  			// If the segment length is too small, instead of drawing 2 arcs, just skip this segment and bezier curve to the next one
   445  			if units < connectionBorderRadius && i < len(route)-2 {
   446  				nextTarget := route[i+2]
   447  				nextVector := geo.NewVector(nextTarget.X-currTarget.X, nextTarget.Y-currTarget.Y)
   448  				i++
   449  				nextTranslations := nextVector.Unit().Multiply(units).ToPoint()
   450  
   451  				// 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,
   452  				// which matches how the two arcs look
   453  				path = append(path, fmt.Sprintf("C %f %f %f %f %f %f",
   454  					// Control point
   455  					prevTarget.X+prevTranslations.X,
   456  					prevTarget.Y+prevTranslations.Y,
   457  					// Control point
   458  					currTarget.X-nextTranslations.X,
   459  					currTarget.Y-nextTranslations.Y,
   460  					// Where curve ends
   461  					currTarget.X+nextTranslations.X,
   462  					currTarget.Y+nextTranslations.Y,
   463  				))
   464  			} else {
   465  				path = append(path, fmt.Sprintf("S %f %f %f %f",
   466  					prevTarget.X,
   467  					prevTarget.Y,
   468  					prevTarget.X+currTranslations.X,
   469  					prevTarget.Y+currTranslations.Y,
   470  				))
   471  			}
   472  		}
   473  
   474  		lastPoint := route[len(route)-1]
   475  		path = append(path, fmt.Sprintf("L %f %f",
   476  			lastPoint.X+dstAdj.X,
   477  			lastPoint.Y+dstAdj.Y,
   478  		))
   479  	}
   480  
   481  	return strings.Join(path, " ")
   482  }
   483  
   484  func makeLabelMask(labelTL *geo.Point, width, height int, opacity float64) string {
   485  	fill := "black"
   486  	if opacity != 1 {
   487  		fill = fmt.Sprintf("rgba(0,0,0,%.2f)", opacity)
   488  	}
   489  	return fmt.Sprintf(`<rect x="%f" y="%f" width="%d" height="%d" fill="%s"></rect>`,
   490  		labelTL.X, labelTL.Y,
   491  		width,
   492  		height,
   493  		fill,
   494  	)
   495  }
   496  
   497  func drawConnection(writer io.Writer, labelMaskID string, connection d2target.Connection, markers map[string]struct{}, idToShape map[string]d2target.Shape, sketchRunner *d2sketch.Runner) (labelMask string, _ error) {
   498  	opacityStyle := ""
   499  	if connection.Opacity != 1.0 {
   500  		opacityStyle = fmt.Sprintf(" style='opacity:%f'", connection.Opacity)
   501  	}
   502  
   503  	classStr := ""
   504  	if len(connection.Classes) > 0 {
   505  		classStr = fmt.Sprintf(` class="%s"`, strings.Join(connection.Classes, " "))
   506  	}
   507  	fmt.Fprintf(writer, `<g id="%s"%s%s>`, svg.EscapeText(connection.ID), opacityStyle, classStr)
   508  	var markerStart string
   509  	if connection.SrcArrow != d2target.NoArrowhead {
   510  		id := arrowheadMarkerID(false, connection)
   511  		if _, in := markers[id]; !in {
   512  			marker := arrowheadMarker(false, id, connection)
   513  			if marker == "" {
   514  				panic(fmt.Sprintf("received empty arrow head marker for: %#v", connection))
   515  			}
   516  			fmt.Fprint(writer, marker)
   517  			markers[id] = struct{}{}
   518  		}
   519  		markerStart = fmt.Sprintf(`marker-start="url(#%s)" `, id)
   520  	}
   521  
   522  	var markerEnd string
   523  	if connection.DstArrow != d2target.NoArrowhead {
   524  		id := arrowheadMarkerID(true, connection)
   525  		if _, in := markers[id]; !in {
   526  			marker := arrowheadMarker(true, id, connection)
   527  			if marker == "" {
   528  				panic(fmt.Sprintf("received empty arrow head marker for: %#v", connection))
   529  			}
   530  			fmt.Fprint(writer, marker)
   531  			markers[id] = struct{}{}
   532  		}
   533  		markerEnd = fmt.Sprintf(`marker-end="url(#%s)" `, id)
   534  	}
   535  
   536  	var labelTL *geo.Point
   537  	if connection.Label != "" {
   538  		labelTL = connection.GetLabelTopLeft()
   539  		labelTL.X = math.Round(labelTL.X)
   540  		labelTL.Y = math.Round(labelTL.Y)
   541  
   542  		if label.FromString(connection.LabelPosition).IsOnEdge() {
   543  			labelMask = makeLabelMask(labelTL, connection.LabelWidth, connection.LabelHeight, 1)
   544  		} else {
   545  			labelMask = makeLabelMask(labelTL, connection.LabelWidth, connection.LabelHeight, 0.75)
   546  		}
   547  	}
   548  
   549  	srcAdj, dstAdj := getArrowheadAdjustments(connection, idToShape)
   550  	path := pathData(connection, srcAdj, dstAdj)
   551  	mask := fmt.Sprintf(`mask="url(#%s)"`, labelMaskID)
   552  	if sketchRunner != nil {
   553  		out, err := d2sketch.Connection(sketchRunner, connection, path, mask)
   554  		if err != nil {
   555  			return "", err
   556  		}
   557  		fmt.Fprint(writer, out)
   558  
   559  		// render sketch arrowheads separately
   560  		arrowPaths, err := d2sketch.Arrowheads(sketchRunner, connection, srcAdj, dstAdj)
   561  		if err != nil {
   562  			return "", err
   563  		}
   564  		fmt.Fprint(writer, arrowPaths)
   565  	} else {
   566  		animatedClass := ""
   567  		if connection.Animated {
   568  			animatedClass = " animated-connection"
   569  		}
   570  
   571  		pathEl := d2themes.NewThemableElement("path")
   572  		pathEl.D = path
   573  		pathEl.Fill = color.None
   574  		pathEl.Stroke = connection.Stroke
   575  		pathEl.ClassName = fmt.Sprintf("connection%s", animatedClass)
   576  		pathEl.Style = connection.CSSStyle()
   577  		pathEl.Attributes = fmt.Sprintf("%s%s%s", markerStart, markerEnd, mask)
   578  		fmt.Fprint(writer, pathEl.Render())
   579  	}
   580  
   581  	if connection.Label != "" {
   582  		fontClass := "text"
   583  		if connection.FontFamily == "mono" {
   584  			fontClass = "text-mono"
   585  		}
   586  		if connection.Bold {
   587  			fontClass += "-bold"
   588  		} else if connection.Italic {
   589  			fontClass += "-italic"
   590  		}
   591  		if connection.Fill != color.Empty {
   592  			rectEl := d2themes.NewThemableElement("rect")
   593  			rectEl.X, rectEl.Y = labelTL.X, labelTL.Y
   594  			rectEl.Width, rectEl.Height = float64(connection.LabelWidth), float64(connection.LabelHeight)
   595  			rectEl.Fill = connection.Fill
   596  			fmt.Fprint(writer, rectEl.Render())
   597  		}
   598  
   599  		textEl := d2themes.NewThemableElement("text")
   600  		textEl.X = labelTL.X + float64(connection.LabelWidth)/2
   601  		textEl.Y = labelTL.Y + float64(connection.FontSize)
   602  		textEl.Fill = connection.GetFontColor()
   603  		textEl.ClassName = fontClass
   604  		textEl.Style = fmt.Sprintf("text-anchor:%s;font-size:%vpx", "middle", connection.FontSize)
   605  		textEl.Content = RenderText(connection.Label, textEl.X, float64(connection.LabelHeight))
   606  		fmt.Fprint(writer, textEl.Render())
   607  	}
   608  
   609  	if connection.SrcLabel != nil && connection.SrcLabel.Label != "" {
   610  		fmt.Fprint(writer, renderArrowheadLabel(connection, connection.SrcLabel.Label, false))
   611  	}
   612  	if connection.DstLabel != nil && connection.DstLabel.Label != "" {
   613  		fmt.Fprint(writer, renderArrowheadLabel(connection, connection.DstLabel.Label, true))
   614  	}
   615  	fmt.Fprintf(writer, `</g>`)
   616  	return
   617  }
   618  
   619  func renderArrowheadLabel(connection d2target.Connection, text string, isDst bool) string {
   620  	var width, height float64
   621  	if isDst {
   622  		width = float64(connection.DstLabel.LabelWidth)
   623  		height = float64(connection.DstLabel.LabelHeight)
   624  	} else {
   625  		width = float64(connection.SrcLabel.LabelWidth)
   626  		height = float64(connection.SrcLabel.LabelHeight)
   627  	}
   628  
   629  	labelTL := connection.GetArrowheadLabelPosition(isDst)
   630  
   631  	// svg text is positioned with the center of its baseline
   632  	baselineCenter := geo.Point{
   633  		X: labelTL.X + width/2.,
   634  		Y: labelTL.Y + float64(connection.FontSize),
   635  	}
   636  
   637  	textEl := d2themes.NewThemableElement("text")
   638  	textEl.X = baselineCenter.X
   639  	textEl.Y = baselineCenter.Y
   640  	textEl.Fill = d2target.FG_COLOR
   641  	if isDst {
   642  		if connection.DstLabel.Color != "" {
   643  			textEl.Fill = connection.DstLabel.Color
   644  		}
   645  	} else {
   646  		if connection.SrcLabel.Color != "" {
   647  			textEl.Fill = connection.SrcLabel.Color
   648  		}
   649  	}
   650  	textEl.ClassName = "text-italic"
   651  	textEl.Style = fmt.Sprintf("text-anchor:middle;font-size:%vpx", connection.FontSize)
   652  	textEl.Content = RenderText(text, textEl.X, height)
   653  	return textEl.Render()
   654  }
   655  
   656  func renderOval(tl *geo.Point, width, height float64, fill, fillPattern, stroke, style string) string {
   657  	el := d2themes.NewThemableElement("ellipse")
   658  	el.Rx = width / 2
   659  	el.Ry = height / 2
   660  	el.Cx = tl.X + el.Rx
   661  	el.Cy = tl.Y + el.Ry
   662  	el.Fill, el.Stroke = fill, stroke
   663  	el.FillPattern = fillPattern
   664  	el.ClassName = "shape"
   665  	el.Style = style
   666  	return el.Render()
   667  }
   668  
   669  func renderDoubleOval(tl *geo.Point, width, height float64, fill, fillStroke, stroke, style string) string {
   670  	var innerTL *geo.Point = tl.AddVector(geo.NewVector(d2target.INNER_BORDER_OFFSET, d2target.INNER_BORDER_OFFSET))
   671  	return renderOval(tl, width, height, fill, fillStroke, stroke, style) + renderOval(innerTL, width-10, height-10, fill, "", stroke, style)
   672  }
   673  
   674  func defineShadowFilter(writer io.Writer) {
   675  	fmt.Fprint(writer, `<defs>
   676  	<filter id="shadow-filter" width="200%" height="200%" x="-50%" y="-50%">
   677  		<feGaussianBlur stdDeviation="1.7 " in="SourceGraphic"></feGaussianBlur>
   678  		<feFlood flood-color="#3d4574" flood-opacity="0.4" result="ShadowFeFlood" in="SourceGraphic"></feFlood>
   679  		<feComposite in="ShadowFeFlood" in2="SourceAlpha" operator="in" result="ShadowFeComposite"></feComposite>
   680  		<feOffset dx="3" dy="5" result="ShadowFeOffset" in="ShadowFeComposite"></feOffset>
   681  		<feBlend in="SourceGraphic" in2="ShadowFeOffset" mode="normal" result="ShadowFeBlend"></feBlend>
   682  	</filter>
   683  </defs>`)
   684  }
   685  
   686  func render3DRect(targetShape d2target.Shape) string {
   687  	moveTo := func(p d2target.Point) string {
   688  		return fmt.Sprintf("M%d,%d", p.X+targetShape.Pos.X, p.Y+targetShape.Pos.Y)
   689  	}
   690  	lineTo := func(p d2target.Point) string {
   691  		return fmt.Sprintf("L%d,%d", p.X+targetShape.Pos.X, p.Y+targetShape.Pos.Y)
   692  	}
   693  
   694  	// draw border all in one path to prevent overlapping sections
   695  	var borderSegments []string
   696  	borderSegments = append(borderSegments,
   697  		moveTo(d2target.Point{X: 0, Y: 0}),
   698  	)
   699  	for _, v := range []d2target.Point{
   700  		{X: d2target.THREE_DEE_OFFSET, Y: -d2target.THREE_DEE_OFFSET},
   701  		{X: targetShape.Width + d2target.THREE_DEE_OFFSET, Y: -d2target.THREE_DEE_OFFSET},
   702  		{X: targetShape.Width + d2target.THREE_DEE_OFFSET, Y: targetShape.Height - d2target.THREE_DEE_OFFSET},
   703  		{X: targetShape.Width, Y: targetShape.Height},
   704  		{X: 0, Y: targetShape.Height},
   705  		{X: 0, Y: 0},
   706  		{X: targetShape.Width, Y: 0},
   707  		{X: targetShape.Width, Y: targetShape.Height},
   708  	} {
   709  		borderSegments = append(borderSegments, lineTo(v))
   710  	}
   711  	// move to top right to draw last segment without overlapping
   712  	borderSegments = append(borderSegments,
   713  		moveTo(d2target.Point{X: targetShape.Width, Y: 0}),
   714  	)
   715  	borderSegments = append(borderSegments,
   716  		lineTo(d2target.Point{X: targetShape.Width + d2target.THREE_DEE_OFFSET, Y: -d2target.THREE_DEE_OFFSET}),
   717  	)
   718  	border := d2themes.NewThemableElement("path")
   719  	border.D = strings.Join(borderSegments, " ")
   720  	border.Fill = color.None
   721  	_, borderStroke := d2themes.ShapeTheme(targetShape)
   722  	border.Stroke = borderStroke
   723  	borderStyle := targetShape.CSSStyle()
   724  	border.Style = borderStyle
   725  	renderedBorder := border.Render()
   726  
   727  	// create mask from border stroke, to cut away from the shape fills
   728  	maskID := fmt.Sprintf("border-mask-%v", svg.EscapeText(targetShape.ID))
   729  	borderMask := strings.Join([]string{
   730  		fmt.Sprintf(`<defs><mask id="%s" maskUnits="userSpaceOnUse" x="%d" y="%d" width="%d" height="%d">`,
   731  			maskID, targetShape.Pos.X, targetShape.Pos.Y-d2target.THREE_DEE_OFFSET, targetShape.Width+d2target.THREE_DEE_OFFSET, targetShape.Height+d2target.THREE_DEE_OFFSET,
   732  		),
   733  		fmt.Sprintf(`<rect x="%d" y="%d" width="%d" height="%d" fill="white"></rect>`,
   734  			targetShape.Pos.X, targetShape.Pos.Y-d2target.THREE_DEE_OFFSET, targetShape.Width+d2target.THREE_DEE_OFFSET, targetShape.Height+d2target.THREE_DEE_OFFSET,
   735  		),
   736  		fmt.Sprintf(`<path d="%s" style="%s;stroke:#000;fill:none;opacity:1;"/></mask></defs>`,
   737  			strings.Join(borderSegments, ""), borderStyle),
   738  	}, "\n")
   739  
   740  	// render the main rectangle without stroke and the border mask
   741  	mainShape := d2themes.NewThemableElement("rect")
   742  	mainShape.X = float64(targetShape.Pos.X)
   743  	mainShape.Y = float64(targetShape.Pos.Y)
   744  	mainShape.Width = float64(targetShape.Width)
   745  	mainShape.Height = float64(targetShape.Height)
   746  	mainShape.SetMaskUrl(maskID)
   747  	mainShapeFill, _ := d2themes.ShapeTheme(targetShape)
   748  	mainShape.Fill = mainShapeFill
   749  	mainShape.FillPattern = targetShape.FillPattern
   750  	mainShape.Stroke = color.None
   751  	mainShape.Style = targetShape.CSSStyle()
   752  	mainShapeRendered := mainShape.Render()
   753  
   754  	// render the side shapes in the darkened color without stroke and the border mask
   755  	var sidePoints []string
   756  	for _, v := range []d2target.Point{
   757  		{X: 0, Y: 0},
   758  		{X: d2target.THREE_DEE_OFFSET, Y: -d2target.THREE_DEE_OFFSET},
   759  		{X: targetShape.Width + d2target.THREE_DEE_OFFSET, Y: -d2target.THREE_DEE_OFFSET},
   760  		{X: targetShape.Width + d2target.THREE_DEE_OFFSET, Y: targetShape.Height - d2target.THREE_DEE_OFFSET},
   761  		{X: targetShape.Width, Y: targetShape.Height},
   762  		{X: targetShape.Width, Y: 0},
   763  	} {
   764  		sidePoints = append(sidePoints,
   765  			fmt.Sprintf("%d,%d", v.X+targetShape.Pos.X, v.Y+targetShape.Pos.Y),
   766  		)
   767  	}
   768  	darkerColor, err := color.Darken(targetShape.Fill)
   769  	if err != nil {
   770  		darkerColor = targetShape.Fill
   771  	}
   772  	sideShape := d2themes.NewThemableElement("polygon")
   773  	sideShape.Fill = darkerColor
   774  	sideShape.Points = strings.Join(sidePoints, " ")
   775  	sideShape.SetMaskUrl(maskID)
   776  	sideShape.Style = targetShape.CSSStyle()
   777  	renderedSides := sideShape.Render()
   778  
   779  	return borderMask + mainShapeRendered + renderedSides + renderedBorder
   780  }
   781  
   782  func render3DHexagon(targetShape d2target.Shape) string {
   783  	moveTo := func(p d2target.Point) string {
   784  		return fmt.Sprintf("M%d,%d", p.X+targetShape.Pos.X, p.Y+targetShape.Pos.Y)
   785  	}
   786  	lineTo := func(p d2target.Point) string {
   787  		return fmt.Sprintf("L%d,%d", p.X+targetShape.Pos.X, p.Y+targetShape.Pos.Y)
   788  	}
   789  	scale := func(n int, f float64) int {
   790  		return int(float64(n) * f)
   791  	}
   792  	halfYFactor := 43.6 / 87.3
   793  
   794  	// draw border all in one path to prevent overlapping sections
   795  	var borderSegments []string
   796  	// start from the top-left
   797  	borderSegments = append(borderSegments,
   798  		moveTo(d2target.Point{X: scale(targetShape.Width, 0.25), Y: 0}),
   799  	)
   800  	Y_OFFSET := d2target.THREE_DEE_OFFSET / 2
   801  	// The following iterates through the sidepoints in clockwise order from top-left, then the main points in clockwise order from bottom-right
   802  	for _, v := range []d2target.Point{
   803  		{X: scale(targetShape.Width, 0.25) + d2target.THREE_DEE_OFFSET, Y: -Y_OFFSET},
   804  		{X: scale(targetShape.Width, 0.75) + d2target.THREE_DEE_OFFSET, Y: -Y_OFFSET},
   805  		{X: targetShape.Width + d2target.THREE_DEE_OFFSET, Y: scale(targetShape.Height, halfYFactor) - Y_OFFSET},
   806  		{X: scale(targetShape.Width, 0.75) + d2target.THREE_DEE_OFFSET, Y: targetShape.Height - Y_OFFSET},
   807  		{X: scale(targetShape.Width, 0.75), Y: targetShape.Height},
   808  		{X: scale(targetShape.Width, 0.25), Y: targetShape.Height},
   809  		{X: 0, Y: scale(targetShape.Height, halfYFactor)},
   810  		{X: scale(targetShape.Width, 0.25), Y: 0},
   811  		{X: scale(targetShape.Width, 0.75), Y: 0},
   812  		{X: targetShape.Width, Y: scale(targetShape.Height, halfYFactor)},
   813  		{X: scale(targetShape.Width, 0.75), Y: targetShape.Height},
   814  	} {
   815  		borderSegments = append(borderSegments, lineTo(v))
   816  	}
   817  	for _, v := range []d2target.Point{
   818  		{X: scale(targetShape.Width, 0.75), Y: 0},
   819  		{X: targetShape.Width, Y: scale(targetShape.Height, halfYFactor)},
   820  		{X: scale(targetShape.Width, 0.75), Y: targetShape.Height},
   821  	} {
   822  		borderSegments = append(borderSegments, moveTo(v))
   823  		borderSegments = append(borderSegments, lineTo(
   824  			d2target.Point{X: v.X + d2target.THREE_DEE_OFFSET, Y: v.Y - Y_OFFSET},
   825  		))
   826  	}
   827  	border := d2themes.NewThemableElement("path")
   828  	border.D = strings.Join(borderSegments, " ")
   829  	border.Fill = color.None
   830  	_, borderStroke := d2themes.ShapeTheme(targetShape)
   831  	border.Stroke = borderStroke
   832  	borderStyle := targetShape.CSSStyle()
   833  	border.Style = borderStyle
   834  	renderedBorder := border.Render()
   835  
   836  	var mainPoints []string
   837  	for _, v := range []d2target.Point{
   838  		{X: scale(targetShape.Width, 0.25), Y: 0},
   839  		{X: scale(targetShape.Width, 0.75), Y: 0},
   840  		{X: targetShape.Width, Y: scale(targetShape.Height, halfYFactor)},
   841  		{X: scale(targetShape.Width, 0.75), Y: targetShape.Height},
   842  		{X: scale(targetShape.Width, 0.25), Y: targetShape.Height},
   843  		{X: 0, Y: scale(targetShape.Height, halfYFactor)},
   844  	} {
   845  		mainPoints = append(mainPoints,
   846  			fmt.Sprintf("%d,%d", v.X+targetShape.Pos.X, v.Y+targetShape.Pos.Y),
   847  		)
   848  	}
   849  
   850  	mainPointsPoly := strings.Join(mainPoints, " ")
   851  	// create mask from border stroke, to cut away from the shape fills
   852  	maskID := fmt.Sprintf("border-mask-%v", svg.EscapeText(targetShape.ID))
   853  	borderMask := strings.Join([]string{
   854  		fmt.Sprintf(`<defs><mask id="%s" maskUnits="userSpaceOnUse" x="%d" y="%d" width="%d" height="%d">`,
   855  			maskID, targetShape.Pos.X, targetShape.Pos.Y-d2target.THREE_DEE_OFFSET, targetShape.Width+d2target.THREE_DEE_OFFSET, targetShape.Height+d2target.THREE_DEE_OFFSET,
   856  		),
   857  		fmt.Sprintf(`<rect x="%d" y="%d" width="%d" height="%d" fill="white"></rect>`,
   858  			targetShape.Pos.X, targetShape.Pos.Y-d2target.THREE_DEE_OFFSET, targetShape.Width+d2target.THREE_DEE_OFFSET, targetShape.Height+d2target.THREE_DEE_OFFSET,
   859  		),
   860  		fmt.Sprintf(`<path d="%s" style="%s;stroke:#000;fill:none;opacity:1;"/></mask></defs>`,
   861  			strings.Join(borderSegments, ""), borderStyle),
   862  	}, "\n")
   863  	// render the main hexagon without stroke and the border mask
   864  	mainShape := d2themes.NewThemableElement("polygon")
   865  	mainShape.X = float64(targetShape.Pos.X)
   866  	mainShape.Y = float64(targetShape.Pos.Y)
   867  	mainShape.Points = mainPointsPoly
   868  	mainShape.SetMaskUrl(maskID)
   869  	mainShapeFill, _ := d2themes.ShapeTheme(targetShape)
   870  	mainShape.FillPattern = targetShape.FillPattern
   871  	mainShape.Fill = mainShapeFill
   872  	mainShape.Stroke = color.None
   873  	mainShape.Style = targetShape.CSSStyle()
   874  	mainShapeRendered := mainShape.Render()
   875  
   876  	// render the side shapes in the darkened color without stroke and the border mask
   877  	var sidePoints []string
   878  	for _, v := range []d2target.Point{
   879  		{X: scale(targetShape.Width, 0.25) + d2target.THREE_DEE_OFFSET, Y: -Y_OFFSET},
   880  		{X: scale(targetShape.Width, 0.75) + d2target.THREE_DEE_OFFSET, Y: -Y_OFFSET},
   881  		{X: targetShape.Width + d2target.THREE_DEE_OFFSET, Y: scale(targetShape.Height, halfYFactor) - Y_OFFSET},
   882  		{X: scale(targetShape.Width, 0.75) + d2target.THREE_DEE_OFFSET, Y: targetShape.Height - Y_OFFSET},
   883  		{X: scale(targetShape.Width, 0.75), Y: targetShape.Height},
   884  		{X: targetShape.Width, Y: scale(targetShape.Height, halfYFactor)},
   885  		{X: scale(targetShape.Width, 0.75), Y: 0},
   886  		{X: scale(targetShape.Width, 0.25), Y: 0},
   887  	} {
   888  		sidePoints = append(sidePoints,
   889  			fmt.Sprintf("%d,%d", v.X+targetShape.Pos.X, v.Y+targetShape.Pos.Y),
   890  		)
   891  	}
   892  	// TODO make darker color part of the theme? or just keep this bypass
   893  	darkerColor, err := color.Darken(targetShape.Fill)
   894  	if err != nil {
   895  		darkerColor = targetShape.Fill
   896  	}
   897  	sideShape := d2themes.NewThemableElement("polygon")
   898  	sideShape.Fill = darkerColor
   899  	sideShape.Points = strings.Join(sidePoints, " ")
   900  	sideShape.SetMaskUrl(maskID)
   901  	sideShape.Style = targetShape.CSSStyle()
   902  	renderedSides := sideShape.Render()
   903  
   904  	return borderMask + mainShapeRendered + renderedSides + renderedBorder
   905  }
   906  
   907  func drawShape(writer, appendixWriter io.Writer, diagramHash string, targetShape d2target.Shape, sketchRunner *d2sketch.Runner) (labelMask string, err error) {
   908  	closingTag := "</g>"
   909  	if targetShape.Link != "" {
   910  
   911  		fmt.Fprintf(writer, `<a href="%s" xlink:href="%[1]s">`, svg.EscapeText(targetShape.Link))
   912  		closingTag += "</a>"
   913  	}
   914  	// Opacity is a unique style, it applies to everything for a shape
   915  	opacityStyle := ""
   916  	if targetShape.Opacity != 1.0 {
   917  		opacityStyle = fmt.Sprintf(" style='opacity:%f'", targetShape.Opacity)
   918  	}
   919  
   920  	// this clipPath must be defined outside `g` element
   921  	if targetShape.BorderRadius != 0 && (targetShape.Type == d2target.ShapeClass || targetShape.Type == d2target.ShapeSQLTable) {
   922  		fmt.Fprint(writer, clipPathForBorderRadius(diagramHash, targetShape))
   923  	}
   924  	classStr := ""
   925  	if len(targetShape.Classes) > 0 {
   926  		classStr = fmt.Sprintf(` class="%s"`, strings.Join(targetShape.Classes, " "))
   927  	}
   928  	fmt.Fprintf(writer, `<g id="%s"%s%s>`, svg.EscapeText(targetShape.ID), opacityStyle, classStr)
   929  	tl := geo.NewPoint(float64(targetShape.Pos.X), float64(targetShape.Pos.Y))
   930  	width := float64(targetShape.Width)
   931  	height := float64(targetShape.Height)
   932  	fill, stroke := d2themes.ShapeTheme(targetShape)
   933  	style := targetShape.CSSStyle()
   934  	shapeType := d2target.DSL_SHAPE_TO_SHAPE_TYPE[targetShape.Type]
   935  
   936  	s := shape.NewShape(shapeType, geo.NewBox(tl, width, height))
   937  	if shapeType == shape.CLOUD_TYPE && targetShape.ContentAspectRatio != nil {
   938  		s.SetInnerBoxAspectRatio(*targetShape.ContentAspectRatio)
   939  	}
   940  
   941  	var shadowAttr string
   942  	if targetShape.Shadow {
   943  		switch targetShape.Type {
   944  		case d2target.ShapeText,
   945  			d2target.ShapeCode,
   946  			d2target.ShapeClass,
   947  			d2target.ShapeSQLTable:
   948  		default:
   949  			shadowAttr = `filter="url(#shadow-filter)" `
   950  		}
   951  	}
   952  
   953  	var blendModeClass string
   954  	if targetShape.Blend {
   955  		blendModeClass = " blend"
   956  	}
   957  
   958  	fmt.Fprintf(writer, `<g class="shape%s" %s>`, blendModeClass, shadowAttr)
   959  
   960  	var multipleTL *geo.Point
   961  	if targetShape.Multiple {
   962  		multipleTL = tl.AddVector(multipleOffset)
   963  	}
   964  
   965  	switch targetShape.Type {
   966  	case d2target.ShapeClass:
   967  		if sketchRunner != nil {
   968  			out, err := d2sketch.Class(sketchRunner, targetShape)
   969  			if err != nil {
   970  				return "", err
   971  			}
   972  			fmt.Fprint(writer, out)
   973  		} else {
   974  			drawClass(writer, diagramHash, targetShape)
   975  		}
   976  		addAppendixItems(appendixWriter, targetShape, s)
   977  		fmt.Fprint(writer, `</g>`)
   978  		fmt.Fprint(writer, closingTag)
   979  		return labelMask, nil
   980  	case d2target.ShapeSQLTable:
   981  		if sketchRunner != nil {
   982  			out, err := d2sketch.Table(sketchRunner, targetShape)
   983  			if err != nil {
   984  				return "", err
   985  			}
   986  			fmt.Fprint(writer, out)
   987  		} else {
   988  			drawTable(writer, diagramHash, targetShape)
   989  		}
   990  		addAppendixItems(appendixWriter, targetShape, s)
   991  		fmt.Fprint(writer, `</g>`)
   992  		fmt.Fprint(writer, closingTag)
   993  		return labelMask, nil
   994  	case d2target.ShapeOval:
   995  		if targetShape.DoubleBorder {
   996  			if targetShape.Multiple {
   997  				fmt.Fprint(writer, renderDoubleOval(multipleTL, width, height, fill, "", stroke, style))
   998  			}
   999  			if sketchRunner != nil {
  1000  				out, err := d2sketch.DoubleOval(sketchRunner, targetShape)
  1001  				if err != nil {
  1002  					return "", err
  1003  				}
  1004  				fmt.Fprint(writer, out)
  1005  			} else {
  1006  				fmt.Fprint(writer, renderDoubleOval(tl, width, height, fill, targetShape.FillPattern, stroke, style))
  1007  			}
  1008  		} else {
  1009  			if targetShape.Multiple {
  1010  				fmt.Fprint(writer, renderOval(multipleTL, width, height, fill, "", stroke, style))
  1011  			}
  1012  			if sketchRunner != nil {
  1013  				out, err := d2sketch.Oval(sketchRunner, targetShape)
  1014  				if err != nil {
  1015  					return "", err
  1016  				}
  1017  				fmt.Fprint(writer, out)
  1018  			} else {
  1019  				fmt.Fprint(writer, renderOval(tl, width, height, fill, targetShape.FillPattern, stroke, style))
  1020  			}
  1021  		}
  1022  
  1023  	case d2target.ShapeImage:
  1024  		el := d2themes.NewThemableElement("image")
  1025  		el.X = float64(targetShape.Pos.X)
  1026  		el.Y = float64(targetShape.Pos.Y)
  1027  		el.Width = float64(targetShape.Width)
  1028  		el.Height = float64(targetShape.Height)
  1029  		el.Href = html.EscapeString(targetShape.Icon.String())
  1030  		el.Fill = fill
  1031  		el.Stroke = stroke
  1032  		el.Style = style
  1033  		fmt.Fprint(writer, el.Render())
  1034  
  1035  	// TODO should standardize "" to rectangle
  1036  	case d2target.ShapeRectangle, d2target.ShapeSequenceDiagram, "":
  1037  		borderRadius := math.MaxFloat64
  1038  		if targetShape.BorderRadius != 0 {
  1039  			borderRadius = float64(targetShape.BorderRadius)
  1040  		}
  1041  		if targetShape.ThreeDee {
  1042  			fmt.Fprint(writer, render3DRect(targetShape))
  1043  		} else {
  1044  			if !targetShape.DoubleBorder {
  1045  				if targetShape.Multiple {
  1046  					el := d2themes.NewThemableElement("rect")
  1047  					el.X = float64(targetShape.Pos.X + 10)
  1048  					el.Y = float64(targetShape.Pos.Y - 10)
  1049  					el.Width = float64(targetShape.Width)
  1050  					el.Height = float64(targetShape.Height)
  1051  					el.Fill = fill
  1052  					el.Stroke = stroke
  1053  					el.Style = style
  1054  					el.Rx = borderRadius
  1055  					fmt.Fprint(writer, el.Render())
  1056  				}
  1057  				if sketchRunner != nil {
  1058  					out, err := d2sketch.Rect(sketchRunner, targetShape)
  1059  					if err != nil {
  1060  						return "", err
  1061  					}
  1062  					fmt.Fprint(writer, out)
  1063  				} else {
  1064  					el := d2themes.NewThemableElement("rect")
  1065  					el.X = float64(targetShape.Pos.X)
  1066  					el.Y = float64(targetShape.Pos.Y)
  1067  					el.Width = float64(targetShape.Width)
  1068  					el.Height = float64(targetShape.Height)
  1069  					el.Fill = fill
  1070  					el.FillPattern = targetShape.FillPattern
  1071  					el.Stroke = stroke
  1072  					el.Style = style
  1073  					el.Rx = borderRadius
  1074  					fmt.Fprint(writer, el.Render())
  1075  				}
  1076  			} else {
  1077  				if targetShape.Multiple {
  1078  					el := d2themes.NewThemableElement("rect")
  1079  					el.X = float64(targetShape.Pos.X + 10)
  1080  					el.Y = float64(targetShape.Pos.Y - 10)
  1081  					el.Width = float64(targetShape.Width)
  1082  					el.Height = float64(targetShape.Height)
  1083  					el.Fill = fill
  1084  					el.FillPattern = targetShape.FillPattern
  1085  					el.Stroke = stroke
  1086  					el.Style = style
  1087  					el.Rx = borderRadius
  1088  					fmt.Fprint(writer, el.Render())
  1089  
  1090  					el = d2themes.NewThemableElement("rect")
  1091  					el.X = float64(targetShape.Pos.X + 10 + d2target.INNER_BORDER_OFFSET)
  1092  					el.Y = float64(targetShape.Pos.Y - 10 + d2target.INNER_BORDER_OFFSET)
  1093  					el.Width = float64(targetShape.Width - 2*d2target.INNER_BORDER_OFFSET)
  1094  					el.Height = float64(targetShape.Height - 2*d2target.INNER_BORDER_OFFSET)
  1095  					el.Fill = fill
  1096  					el.Stroke = stroke
  1097  					el.Style = style
  1098  					el.Rx = borderRadius
  1099  					fmt.Fprint(writer, el.Render())
  1100  				}
  1101  				if sketchRunner != nil {
  1102  					out, err := d2sketch.DoubleRect(sketchRunner, targetShape)
  1103  					if err != nil {
  1104  						return "", err
  1105  					}
  1106  					fmt.Fprint(writer, out)
  1107  				} else {
  1108  					el := d2themes.NewThemableElement("rect")
  1109  					el.X = float64(targetShape.Pos.X)
  1110  					el.Y = float64(targetShape.Pos.Y)
  1111  					el.Width = float64(targetShape.Width)
  1112  					el.Height = float64(targetShape.Height)
  1113  					el.Fill = fill
  1114  					el.FillPattern = targetShape.FillPattern
  1115  					el.Stroke = stroke
  1116  					el.Style = style
  1117  					el.Rx = borderRadius
  1118  					fmt.Fprint(writer, el.Render())
  1119  
  1120  					el = d2themes.NewThemableElement("rect")
  1121  					el.X = float64(targetShape.Pos.X + d2target.INNER_BORDER_OFFSET)
  1122  					el.Y = float64(targetShape.Pos.Y + d2target.INNER_BORDER_OFFSET)
  1123  					el.Width = float64(targetShape.Width - 2*d2target.INNER_BORDER_OFFSET)
  1124  					el.Height = float64(targetShape.Height - 2*d2target.INNER_BORDER_OFFSET)
  1125  					el.Fill = "transparent"
  1126  					el.Stroke = stroke
  1127  					el.Style = style
  1128  					el.Rx = borderRadius
  1129  					fmt.Fprint(writer, el.Render())
  1130  				}
  1131  			}
  1132  		}
  1133  	case d2target.ShapeHexagon:
  1134  		if targetShape.ThreeDee {
  1135  			fmt.Fprint(writer, render3DHexagon(targetShape))
  1136  		} else {
  1137  			if targetShape.Multiple {
  1138  				multiplePathData := shape.NewShape(shapeType, geo.NewBox(multipleTL, width, height)).GetSVGPathData()
  1139  				el := d2themes.NewThemableElement("path")
  1140  				el.Fill = fill
  1141  				el.Stroke = stroke
  1142  				el.Style = style
  1143  				for _, pathData := range multiplePathData {
  1144  					el.D = pathData
  1145  					fmt.Fprint(writer, el.Render())
  1146  				}
  1147  			}
  1148  
  1149  			if sketchRunner != nil {
  1150  				out, err := d2sketch.Paths(sketchRunner, targetShape, s.GetSVGPathData())
  1151  				if err != nil {
  1152  					return "", err
  1153  				}
  1154  				fmt.Fprint(writer, out)
  1155  			} else {
  1156  				el := d2themes.NewThemableElement("path")
  1157  				el.Fill = fill
  1158  				el.FillPattern = targetShape.FillPattern
  1159  				el.Stroke = stroke
  1160  				el.Style = style
  1161  				for _, pathData := range s.GetSVGPathData() {
  1162  					el.D = pathData
  1163  					fmt.Fprint(writer, el.Render())
  1164  				}
  1165  			}
  1166  		}
  1167  	case d2target.ShapeText, d2target.ShapeCode:
  1168  	default:
  1169  		if targetShape.Multiple {
  1170  			multiplePathData := shape.NewShape(shapeType, geo.NewBox(multipleTL, width, height)).GetSVGPathData()
  1171  			el := d2themes.NewThemableElement("path")
  1172  			el.Fill = fill
  1173  			el.Stroke = stroke
  1174  			el.Style = style
  1175  			for _, pathData := range multiplePathData {
  1176  				el.D = pathData
  1177  				fmt.Fprint(writer, el.Render())
  1178  			}
  1179  		}
  1180  
  1181  		if sketchRunner != nil {
  1182  			out, err := d2sketch.Paths(sketchRunner, targetShape, s.GetSVGPathData())
  1183  			if err != nil {
  1184  				return "", err
  1185  			}
  1186  			fmt.Fprint(writer, out)
  1187  		} else {
  1188  			el := d2themes.NewThemableElement("path")
  1189  			el.Fill = fill
  1190  			el.FillPattern = targetShape.FillPattern
  1191  			el.Stroke = stroke
  1192  			el.Style = style
  1193  			for _, pathData := range s.GetSVGPathData() {
  1194  				el.D = pathData
  1195  				fmt.Fprint(writer, el.Render())
  1196  			}
  1197  		}
  1198  	}
  1199  
  1200  	// // to examine shape's innerBox
  1201  	// innerBox := s.GetInnerBox()
  1202  	// el := d2themes.NewThemableElement("rect")
  1203  	// el.X = float64(innerBox.TopLeft.X)
  1204  	// el.Y = float64(innerBox.TopLeft.Y)
  1205  	// el.Width = float64(innerBox.Width)
  1206  	// el.Height = float64(innerBox.Height)
  1207  	// el.Style = "fill:rgba(255,0,0,0.5);"
  1208  	// fmt.Fprint(writer, el.Render())
  1209  
  1210  	// Closes the class=shape
  1211  	fmt.Fprint(writer, `</g>`)
  1212  
  1213  	if targetShape.Icon != nil && targetShape.Type != d2target.ShapeImage {
  1214  		iconPosition := label.FromString(targetShape.IconPosition)
  1215  		var box *geo.Box
  1216  		if iconPosition.IsOutside() {
  1217  			box = s.GetBox()
  1218  		} else {
  1219  			box = s.GetInnerBox()
  1220  		}
  1221  		iconSize := d2target.GetIconSize(box, targetShape.IconPosition)
  1222  
  1223  		tl := iconPosition.GetPointOnBox(box, label.PADDING, float64(iconSize), float64(iconSize))
  1224  
  1225  		fmt.Fprintf(writer, `<image href="%s" x="%f" y="%f" width="%d" height="%d" />`,
  1226  			html.EscapeString(targetShape.Icon.String()),
  1227  			tl.X,
  1228  			tl.Y,
  1229  			iconSize,
  1230  			iconSize,
  1231  		)
  1232  	}
  1233  
  1234  	if targetShape.Label != "" {
  1235  		labelPosition := label.FromString(targetShape.LabelPosition)
  1236  		var box *geo.Box
  1237  		if labelPosition.IsOutside() {
  1238  			box = s.GetBox().Copy()
  1239  			// if it is 3d/multiple, place label using box around those
  1240  			if targetShape.ThreeDee {
  1241  				offsetY := d2target.THREE_DEE_OFFSET
  1242  				if targetShape.Type == d2target.ShapeHexagon {
  1243  					offsetY /= 2
  1244  				}
  1245  				box.TopLeft.Y -= float64(offsetY)
  1246  				box.Height += float64(offsetY)
  1247  				box.Width += d2target.THREE_DEE_OFFSET
  1248  			} else if targetShape.Multiple {
  1249  				box.TopLeft.Y -= d2target.MULTIPLE_OFFSET
  1250  				box.Height += d2target.MULTIPLE_OFFSET
  1251  				box.Width += d2target.MULTIPLE_OFFSET
  1252  			}
  1253  		} else {
  1254  			box = s.GetInnerBox()
  1255  		}
  1256  		labelTL := labelPosition.GetPointOnBox(box, label.PADDING,
  1257  			float64(targetShape.LabelWidth),
  1258  			float64(targetShape.LabelHeight),
  1259  		)
  1260  		labelMask = makeLabelMask(labelTL, targetShape.LabelWidth, targetShape.LabelHeight, 0.75)
  1261  
  1262  		fontClass := "text"
  1263  		if targetShape.FontFamily == "mono" {
  1264  			fontClass = "text-mono"
  1265  		}
  1266  		if targetShape.Bold {
  1267  			fontClass += "-bold"
  1268  		} else if targetShape.Italic {
  1269  			fontClass += "-italic"
  1270  		}
  1271  		if targetShape.Underline {
  1272  			fontClass += " text-underline"
  1273  		}
  1274  
  1275  		if targetShape.Type == d2target.ShapeCode {
  1276  			lexer := lexers.Get(targetShape.Language)
  1277  			if lexer == nil {
  1278  				lexer = lexers.Fallback
  1279  			}
  1280  			for _, isLight := range []bool{true, false} {
  1281  				theme := "github"
  1282  				if !isLight {
  1283  					theme = "catppuccin-mocha"
  1284  				}
  1285  				style := styles.Get(theme)
  1286  				if style == nil {
  1287  					return labelMask, errors.New(`code snippet style "github" not found`)
  1288  				}
  1289  				formatter := formatters.Get("svg")
  1290  				if formatter == nil {
  1291  					return labelMask, errors.New(`code snippet formatter "svg" not found`)
  1292  				}
  1293  				iterator, err := lexer.Tokenise(nil, targetShape.Label)
  1294  				if err != nil {
  1295  					return labelMask, err
  1296  				}
  1297  
  1298  				svgStyles := styleToSVG(style)
  1299  				class := "light-code"
  1300  				if !isLight {
  1301  					class = "dark-code"
  1302  				}
  1303  				var fontSize string
  1304  				if targetShape.FontSize != d2fonts.FONT_SIZE_M {
  1305  					fontSize = fmt.Sprintf(` style="font-size:%v"`, targetShape.FontSize)
  1306  				}
  1307  				fmt.Fprintf(writer, `<g transform="translate(%f %f)" class="%s"%s>`,
  1308  					box.TopLeft.X, box.TopLeft.Y, class, fontSize,
  1309  				)
  1310  				rectEl := d2themes.NewThemableElement("rect")
  1311  				rectEl.Width = float64(targetShape.Width)
  1312  				rectEl.Height = float64(targetShape.Height)
  1313  				rectEl.Stroke = targetShape.Stroke
  1314  				rectEl.ClassName = "shape"
  1315  				rectEl.Style = fmt.Sprintf(`fill:%s;stroke-width:%d;`,
  1316  					style.Get(chroma.Background).Background.String(),
  1317  					targetShape.StrokeWidth,
  1318  				)
  1319  				fmt.Fprint(writer, rectEl.Render())
  1320  				// Padding = 0.5em
  1321  				padding := float64(targetShape.FontSize) / 2.
  1322  				fmt.Fprintf(writer, `<g transform="translate(%f %f)">`, padding, padding)
  1323  
  1324  				lineHeight := textmeasure.CODE_LINE_HEIGHT
  1325  				for index, tokens := range chroma.SplitTokensIntoLines(iterator.Tokens()) {
  1326  					fmt.Fprintf(writer, "<text class=\"text-mono\" x=\"0\" y=\"%fem\">", 1+float64(index)*lineHeight)
  1327  					for _, token := range tokens {
  1328  						text := svgEscaper.Replace(token.String())
  1329  						attr := styleAttr(svgStyles, token.Type)
  1330  						if attr != "" {
  1331  							text = fmt.Sprintf("<tspan %s>%s</tspan>", attr, text)
  1332  						}
  1333  						fmt.Fprint(writer, text)
  1334  					}
  1335  					fmt.Fprint(writer, "</text>")
  1336  				}
  1337  				fmt.Fprint(writer, "</g></g>")
  1338  			}
  1339  		} else if targetShape.Type == d2target.ShapeText && targetShape.Language == "latex" {
  1340  			render, err := d2latex.Render(targetShape.Label)
  1341  			if err != nil {
  1342  				return labelMask, err
  1343  			}
  1344  			gEl := d2themes.NewThemableElement("g")
  1345  			gEl.SetTranslate(float64(box.TopLeft.X), float64(box.TopLeft.Y))
  1346  			gEl.Color = targetShape.Stroke
  1347  			gEl.Content = render
  1348  			fmt.Fprint(writer, gEl.Render())
  1349  		} else if targetShape.Type == d2target.ShapeText && targetShape.Language != "" {
  1350  			render, err := textmeasure.RenderMarkdown(targetShape.Label)
  1351  			if err != nil {
  1352  				return labelMask, err
  1353  			}
  1354  			fmt.Fprintf(writer, `<g><foreignObject requiredFeatures="http://www.w3.org/TR/SVG11/feature#Extensibility" x="%f" y="%f" width="%d" height="%d">`,
  1355  				box.TopLeft.X, box.TopLeft.Y, targetShape.Width, targetShape.Height,
  1356  			)
  1357  			// we need the self closing form in this svg/xhtml context
  1358  			render = strings.ReplaceAll(render, "<hr>", "<hr />")
  1359  
  1360  			mdEl := d2themes.NewThemableElement("div")
  1361  			mdEl.ClassName = "md"
  1362  			mdEl.Content = render
  1363  
  1364  			// We have to set with styles since within foreignObject, we're in html
  1365  			// land and not SVG attributes
  1366  			var styles []string
  1367  			if targetShape.FontSize != textmeasure.MarkdownFontSize {
  1368  				styles = append(styles, fmt.Sprintf("font-size:%vpx", targetShape.FontSize))
  1369  			}
  1370  
  1371  			if !color.IsThemeColor(targetShape.Color) {
  1372  				styles = append(styles, fmt.Sprintf(`color:%s`, targetShape.Color))
  1373  			}
  1374  
  1375  			mdEl.Style = strings.Join(styles, ";")
  1376  
  1377  			fmt.Fprint(writer, mdEl.Render())
  1378  			fmt.Fprint(writer, `</foreignObject></g>`)
  1379  		} else {
  1380  			if targetShape.LabelFill != "" {
  1381  				rectEl := d2themes.NewThemableElement("rect")
  1382  				rectEl.X = labelTL.X
  1383  				rectEl.Y = labelTL.Y
  1384  				rectEl.Width = float64(targetShape.LabelWidth)
  1385  				rectEl.Height = float64(targetShape.LabelHeight)
  1386  				rectEl.Fill = targetShape.LabelFill
  1387  				fmt.Fprint(writer, rectEl.Render())
  1388  			}
  1389  			textEl := d2themes.NewThemableElement("text")
  1390  			textEl.X = labelTL.X + float64(targetShape.LabelWidth)/2
  1391  			// text is vertically positioned at its baseline which is at labelTL+FontSize
  1392  			textEl.Y = labelTL.Y + float64(targetShape.FontSize)
  1393  			textEl.Fill = targetShape.GetFontColor()
  1394  			textEl.ClassName = fontClass
  1395  			textEl.Style = fmt.Sprintf("text-anchor:%s;font-size:%vpx", "middle", targetShape.FontSize)
  1396  			textEl.Content = RenderText(targetShape.Label, textEl.X, float64(targetShape.LabelHeight))
  1397  			fmt.Fprint(writer, textEl.Render())
  1398  			if targetShape.Blend {
  1399  				labelMask = makeLabelMask(labelTL, targetShape.LabelWidth, targetShape.LabelHeight-d2graph.INNER_LABEL_PADDING, 1)
  1400  			}
  1401  		}
  1402  	}
  1403  	if targetShape.Tooltip != "" {
  1404  		fmt.Fprintf(writer, `<title>%s</title>`,
  1405  			svg.EscapeText(targetShape.Tooltip),
  1406  		)
  1407  	}
  1408  	addAppendixItems(appendixWriter, targetShape, s)
  1409  
  1410  	fmt.Fprint(writer, closingTag)
  1411  	return labelMask, nil
  1412  }
  1413  
  1414  func addAppendixItems(writer io.Writer, targetShape d2target.Shape, s shape.Shape) {
  1415  	var p1, p2 *geo.Point
  1416  	if targetShape.Tooltip != "" || targetShape.Link != "" {
  1417  		bothIcons := targetShape.Tooltip != "" && targetShape.Link != ""
  1418  		corner := geo.NewPoint(float64(targetShape.Pos.X+targetShape.Width), float64(targetShape.Pos.Y))
  1419  		center := geo.NewPoint(
  1420  			float64(targetShape.Pos.X)+float64(targetShape.Width)/2.,
  1421  			float64(targetShape.Pos.Y)+float64(targetShape.Height)/2.,
  1422  		)
  1423  		offset := geo.Vector{-2 * appendixIconRadius, 0}
  1424  		var leftOnShape bool
  1425  		switch s.GetType() {
  1426  		case shape.STEP_TYPE, shape.HEXAGON_TYPE, shape.QUEUE_TYPE, shape.PAGE_TYPE:
  1427  			// trace straight left for these
  1428  			center.Y = float64(targetShape.Pos.Y)
  1429  		case shape.PACKAGE_TYPE:
  1430  			// trace straight down
  1431  			center.X = float64(targetShape.Pos.X + targetShape.Width)
  1432  		case shape.CIRCLE_TYPE, shape.OVAL_TYPE, shape.DIAMOND_TYPE,
  1433  			shape.PERSON_TYPE, shape.CLOUD_TYPE, shape.CYLINDER_TYPE:
  1434  			if bothIcons {
  1435  				leftOnShape = true
  1436  				corner = corner.AddVector(offset)
  1437  			}
  1438  		}
  1439  		v1 := center.VectorTo(corner)
  1440  		p1 = shape.TraceToShapeBorder(s, corner, corner.AddVector(v1))
  1441  		if bothIcons {
  1442  			if leftOnShape {
  1443  				// these shapes should have p1 on shape border
  1444  				p2 = p1.AddVector(offset.Reverse())
  1445  				p1, p2 = p2, p1
  1446  			} else {
  1447  				p2 = p1.AddVector(offset)
  1448  			}
  1449  		}
  1450  	}
  1451  
  1452  	if targetShape.Tooltip != "" {
  1453  		x := int(math.Ceil(p1.X))
  1454  		y := int(math.Ceil(p1.Y))
  1455  
  1456  		fmt.Fprintf(writer, `<g transform="translate(%d %d)" class="appendix-icon"><title>%s</title>%s</g>`,
  1457  			x-appendixIconRadius,
  1458  			y-appendixIconRadius,
  1459  			svg.EscapeText(targetShape.Tooltip),
  1460  			TooltipIcon,
  1461  		)
  1462  	}
  1463  	if targetShape.Link != "" {
  1464  		if p2 == nil {
  1465  			p2 = p1
  1466  		}
  1467  		x := int(math.Ceil(p2.X))
  1468  		y := int(math.Ceil(p2.Y))
  1469  		fmt.Fprintf(writer, `<g transform="translate(%d %d)" class="appendix-icon">%s</g>`,
  1470  			x-appendixIconRadius,
  1471  			y-appendixIconRadius,
  1472  			LinkIcon,
  1473  		)
  1474  	}
  1475  }
  1476  
  1477  func RenderText(text string, x, height float64) string {
  1478  	if !strings.Contains(text, "\n") {
  1479  		return svg.EscapeText(text)
  1480  	}
  1481  	rendered := []string{}
  1482  	lines := strings.Split(text, "\n")
  1483  	for i, line := range lines {
  1484  		dy := height / float64(len(lines))
  1485  		if i == 0 {
  1486  			dy = 0
  1487  		}
  1488  		escaped := svg.EscapeText(line)
  1489  		if escaped == "" {
  1490  			// if there are multiple newlines in a row we still need text for the tspan to render
  1491  			escaped = " "
  1492  		}
  1493  		rendered = append(rendered, fmt.Sprintf(`<tspan x="%f" dy="%f">%s</tspan>`, x, dy, escaped))
  1494  	}
  1495  	return strings.Join(rendered, "")
  1496  }
  1497  
  1498  func EmbedFonts(buf *bytes.Buffer, diagramHash, source string, fontFamily *d2fonts.FontFamily, corpus string) {
  1499  	fmt.Fprint(buf, `<style type="text/css"><![CDATA[`)
  1500  
  1501  	appendOnTrigger(
  1502  		buf,
  1503  		source,
  1504  		[]string{
  1505  			`class="text"`,
  1506  			`class="text `,
  1507  			`class="md"`,
  1508  		},
  1509  		fmt.Sprintf(`
  1510  .%s .text {
  1511  	font-family: "%s-font-regular";
  1512  }
  1513  @font-face {
  1514  	font-family: %s-font-regular;
  1515  	src: url("%s");
  1516  }`,
  1517  			diagramHash,
  1518  			diagramHash,
  1519  			diagramHash,
  1520  			fontFamily.Font(0, d2fonts.FONT_STYLE_REGULAR).GetEncodedSubset(corpus),
  1521  		),
  1522  	)
  1523  
  1524  	appendOnTrigger(
  1525  		buf,
  1526  		source,
  1527  		[]string{`class="md"`},
  1528  		fmt.Sprintf(`
  1529  @font-face {
  1530  	font-family: %s-font-semibold;
  1531  	src: url("%s");
  1532  }`,
  1533  			diagramHash,
  1534  			fontFamily.Font(0, d2fonts.FONT_STYLE_SEMIBOLD).GetEncodedSubset(corpus),
  1535  		),
  1536  	)
  1537  
  1538  	appendOnTrigger(
  1539  		buf,
  1540  		source,
  1541  		[]string{
  1542  			`text-underline`,
  1543  		},
  1544  		`
  1545  .text-underline {
  1546  	text-decoration: underline;
  1547  }`,
  1548  	)
  1549  
  1550  	appendOnTrigger(
  1551  		buf,
  1552  		source,
  1553  		[]string{
  1554  			`animated-connection`,
  1555  		},
  1556  		`
  1557  @keyframes dashdraw {
  1558  	from {
  1559  		stroke-dashoffset: 0;
  1560  	}
  1561  }
  1562  `,
  1563  	)
  1564  
  1565  	appendOnTrigger(
  1566  		buf,
  1567  		source,
  1568  		[]string{
  1569  			`appendix-icon`,
  1570  		},
  1571  		`
  1572  .appendix-icon {
  1573  	filter: drop-shadow(0px 0px 32px rgba(31, 36, 58, 0.1));
  1574  }`,
  1575  	)
  1576  
  1577  	appendOnTrigger(
  1578  		buf,
  1579  		source,
  1580  		[]string{
  1581  			`class="text-bold`,
  1582  			`<b>`,
  1583  			`<strong>`,
  1584  		},
  1585  		fmt.Sprintf(`
  1586  .%s .text-bold {
  1587  	font-family: "%s-font-bold";
  1588  }
  1589  @font-face {
  1590  	font-family: %s-font-bold;
  1591  	src: url("%s");
  1592  }`,
  1593  			diagramHash,
  1594  			diagramHash,
  1595  			diagramHash,
  1596  			fontFamily.Font(0, d2fonts.FONT_STYLE_BOLD).GetEncodedSubset(corpus),
  1597  		),
  1598  	)
  1599  
  1600  	appendOnTrigger(
  1601  		buf,
  1602  		source,
  1603  		[]string{
  1604  			`class="text-italic`,
  1605  			`<em>`,
  1606  			`<dfn>`,
  1607  		},
  1608  		fmt.Sprintf(`
  1609  .%s .text-italic {
  1610  	font-family: "%s-font-italic";
  1611  }
  1612  @font-face {
  1613  	font-family: %s-font-italic;
  1614  	src: url("%s");
  1615  }`,
  1616  			diagramHash,
  1617  			diagramHash,
  1618  			diagramHash,
  1619  			fontFamily.Font(0, d2fonts.FONT_STYLE_ITALIC).GetEncodedSubset(corpus),
  1620  		),
  1621  	)
  1622  
  1623  	appendOnTrigger(
  1624  		buf,
  1625  		source,
  1626  		[]string{
  1627  			`class="text-mono`,
  1628  			`<pre>`,
  1629  			`<code>`,
  1630  			`<kbd>`,
  1631  			`<samp>`,
  1632  		},
  1633  		fmt.Sprintf(`
  1634  .%s .text-mono {
  1635  	font-family: "%s-font-mono";
  1636  }
  1637  @font-face {
  1638  	font-family: %s-font-mono;
  1639  	src: url("%s");
  1640  }`,
  1641  			diagramHash,
  1642  			diagramHash,
  1643  			diagramHash,
  1644  			d2fonts.SourceCodePro.Font(0, d2fonts.FONT_STYLE_REGULAR).GetEncodedSubset(corpus),
  1645  		),
  1646  	)
  1647  
  1648  	appendOnTrigger(
  1649  		buf,
  1650  		source,
  1651  		[]string{
  1652  			`class="text-mono-bold`,
  1653  		},
  1654  		fmt.Sprintf(`
  1655  .%s .text-mono-bold {
  1656  	font-family: "%s-font-mono-bold";
  1657  }
  1658  @font-face {
  1659  	font-family: %s-font-mono-bold;
  1660  	src: url("%s");
  1661  }`,
  1662  			diagramHash,
  1663  			diagramHash,
  1664  			diagramHash,
  1665  			d2fonts.SourceCodePro.Font(0, d2fonts.FONT_STYLE_BOLD).GetEncodedSubset(corpus),
  1666  		),
  1667  	)
  1668  
  1669  	appendOnTrigger(
  1670  		buf,
  1671  		source,
  1672  		[]string{
  1673  			`class="text-mono-italic`,
  1674  		},
  1675  		fmt.Sprintf(`
  1676  .%s .text-mono-italic {
  1677  	font-family: "%s-font-mono-italic";
  1678  }
  1679  @font-face {
  1680  	font-family: %s-font-mono-italic;
  1681  	src: url("%s");
  1682  }`,
  1683  			diagramHash,
  1684  			diagramHash,
  1685  			diagramHash,
  1686  			d2fonts.SourceCodePro.Font(0, d2fonts.FONT_STYLE_ITALIC).GetEncodedSubset(corpus),
  1687  		),
  1688  	)
  1689  
  1690  	appendOnTrigger(
  1691  		buf,
  1692  		source,
  1693  		[]string{
  1694  			`sketch-overlay-bright`,
  1695  		},
  1696  		`
  1697  .sketch-overlay-bright {
  1698  	fill: url(#streaks-bright);
  1699  	mix-blend-mode: darken;
  1700  }`,
  1701  	)
  1702  
  1703  	appendOnTrigger(
  1704  		buf,
  1705  		source,
  1706  		[]string{
  1707  			`sketch-overlay-normal`,
  1708  		},
  1709  		`
  1710  .sketch-overlay-normal {
  1711  	fill: url(#streaks-normal);
  1712  	mix-blend-mode: color-burn;
  1713  }`,
  1714  	)
  1715  
  1716  	appendOnTrigger(
  1717  		buf,
  1718  		source,
  1719  		[]string{
  1720  			`sketch-overlay-dark`,
  1721  		},
  1722  		`
  1723  .sketch-overlay-dark {
  1724  	fill: url(#streaks-dark);
  1725  	mix-blend-mode: overlay;
  1726  }`,
  1727  	)
  1728  
  1729  	appendOnTrigger(
  1730  		buf,
  1731  		source,
  1732  		[]string{
  1733  			`sketch-overlay-darker`,
  1734  		},
  1735  		`
  1736  .sketch-overlay-darker {
  1737  	fill: url(#streaks-darker);
  1738  	mix-blend-mode: lighten;
  1739  }`,
  1740  	)
  1741  
  1742  	fmt.Fprint(buf, `]]></style>`)
  1743  }
  1744  
  1745  func appendOnTrigger(buf *bytes.Buffer, source string, triggers []string, newContent string) {
  1746  	for _, trigger := range triggers {
  1747  		if strings.Contains(source, trigger) {
  1748  			fmt.Fprint(buf, newContent)
  1749  			break
  1750  		}
  1751  	}
  1752  }
  1753  
  1754  var DEFAULT_DARK_THEME *int64 = nil // no theme selected
  1755  
  1756  func Render(diagram *d2target.Diagram, opts *RenderOpts) ([]byte, error) {
  1757  	var sketchRunner *d2sketch.Runner
  1758  	pad := DEFAULT_PADDING
  1759  	themeID := d2themescatalog.NeutralDefault.ID
  1760  	darkThemeID := DEFAULT_DARK_THEME
  1761  	var scale *float64
  1762  	if opts != nil {
  1763  		if opts.Pad != nil {
  1764  			pad = int(*opts.Pad)
  1765  		}
  1766  		if opts.Sketch != nil && *opts.Sketch {
  1767  			var err error
  1768  			sketchRunner, err = d2sketch.InitSketchVM()
  1769  			if err != nil {
  1770  				return nil, err
  1771  			}
  1772  		}
  1773  		if opts.ThemeID != nil {
  1774  			themeID = *opts.ThemeID
  1775  		}
  1776  		darkThemeID = opts.DarkThemeID
  1777  		scale = opts.Scale
  1778  	}
  1779  
  1780  	buf := &bytes.Buffer{}
  1781  
  1782  	// only define shadow filter if a shape uses it
  1783  	for _, s := range diagram.Shapes {
  1784  		if s.Shadow {
  1785  			defineShadowFilter(buf)
  1786  			break
  1787  		}
  1788  	}
  1789  
  1790  	// Apply hash on IDs for targeting, to be specific for this diagram
  1791  	diagramHash, err := diagram.HashID()
  1792  	if err != nil {
  1793  		return nil, err
  1794  	}
  1795  	// Some targeting is still per-board, like masks for connections
  1796  	isolatedDiagramHash := diagramHash
  1797  	if opts != nil && opts.MasterID != "" {
  1798  		diagramHash = opts.MasterID
  1799  	}
  1800  
  1801  	// SVG has no notion of z-index. The z-index is effectively the order it's drawn.
  1802  	// So draw from the least nested to most nested
  1803  	idToShape := make(map[string]d2target.Shape)
  1804  	allObjects := make([]DiagramObject, 0, len(diagram.Shapes)+len(diagram.Connections))
  1805  	for _, s := range diagram.Shapes {
  1806  		idToShape[s.ID] = s
  1807  		allObjects = append(allObjects, s)
  1808  	}
  1809  	for _, c := range diagram.Connections {
  1810  		allObjects = append(allObjects, c)
  1811  	}
  1812  
  1813  	sortObjects(allObjects)
  1814  
  1815  	appendixItemBuf := &bytes.Buffer{}
  1816  
  1817  	var labelMasks []string
  1818  	markers := map[string]struct{}{}
  1819  	for _, obj := range allObjects {
  1820  		if c, is := obj.(d2target.Connection); is {
  1821  			labelMask, err := drawConnection(buf, isolatedDiagramHash, c, markers, idToShape, sketchRunner)
  1822  			if err != nil {
  1823  				return nil, err
  1824  			}
  1825  			if labelMask != "" {
  1826  				labelMasks = append(labelMasks, labelMask)
  1827  			}
  1828  		} else if s, is := obj.(d2target.Shape); is {
  1829  			labelMask, err := drawShape(buf, appendixItemBuf, diagramHash, s, sketchRunner)
  1830  			if err != nil {
  1831  				return nil, err
  1832  			} else if labelMask != "" {
  1833  				labelMasks = append(labelMasks, labelMask)
  1834  			}
  1835  		} else {
  1836  			return nil, fmt.Errorf("unknown object of type %T", obj)
  1837  		}
  1838  	}
  1839  	// add all appendix items afterwards so they are always on top
  1840  	fmt.Fprint(buf, appendixItemBuf)
  1841  
  1842  	// Note: we always want this since we reference it on connections even if there end up being no masked labels
  1843  	left, top, w, h := dimensions(diagram, pad)
  1844  	fmt.Fprint(buf, strings.Join([]string{
  1845  		fmt.Sprintf(`<mask id="%s" maskUnits="userSpaceOnUse" x="%d" y="%d" width="%d" height="%d">`,
  1846  			isolatedDiagramHash, left, top, w, h,
  1847  		),
  1848  		fmt.Sprintf(`<rect x="%d" y="%d" width="%d" height="%d" fill="white"></rect>`,
  1849  			left, top, w, h,
  1850  		),
  1851  		strings.Join(labelMasks, "\n"),
  1852  		`</mask>`,
  1853  	}, "\n"))
  1854  
  1855  	// generate style elements that will be appended to the SVG tag
  1856  	upperBuf := &bytes.Buffer{}
  1857  	if opts.MasterID == "" {
  1858  		EmbedFonts(upperBuf, diagramHash, buf.String(), diagram.FontFamily, diagram.GetCorpus()) // EmbedFonts *must* run before `d2sketch.DefineFillPatterns`, but after all elements are appended to `buf`
  1859  		themeStylesheet, err := ThemeCSS(diagramHash, &themeID, darkThemeID, opts.ThemeOverrides, opts.DarkThemeOverrides)
  1860  		if err != nil {
  1861  			return nil, err
  1862  		}
  1863  		fmt.Fprintf(upperBuf, `<style type="text/css"><![CDATA[%s%s]]></style>`, BaseStylesheet, themeStylesheet)
  1864  
  1865  		hasMarkdown := false
  1866  		for _, s := range diagram.Shapes {
  1867  			if s.Label != "" && s.Type == d2target.ShapeText {
  1868  				hasMarkdown = true
  1869  				break
  1870  			}
  1871  		}
  1872  		if hasMarkdown {
  1873  			css := MarkdownCSS
  1874  			css = strings.ReplaceAll(css, ".md", fmt.Sprintf(".%s .md", diagramHash))
  1875  			css = strings.ReplaceAll(css, "font-italic", fmt.Sprintf("%s-font-italic", diagramHash))
  1876  			css = strings.ReplaceAll(css, "font-bold", fmt.Sprintf("%s-font-bold", diagramHash))
  1877  			css = strings.ReplaceAll(css, "font-mono", fmt.Sprintf("%s-font-mono", diagramHash))
  1878  			css = strings.ReplaceAll(css, "font-regular", fmt.Sprintf("%s-font-regular", diagramHash))
  1879  			css = strings.ReplaceAll(css, "font-semibold", fmt.Sprintf("%s-font-semibold", diagramHash))
  1880  			fmt.Fprintf(upperBuf, `<style type="text/css">%s</style>`, css)
  1881  		}
  1882  
  1883  		if sketchRunner != nil {
  1884  			d2sketch.DefineFillPatterns(upperBuf)
  1885  		}
  1886  	}
  1887  
  1888  	// This shift is for background el to envelop the diagram
  1889  	left -= int(math.Ceil(float64(diagram.Root.StrokeWidth) / 2.))
  1890  	top -= int(math.Ceil(float64(diagram.Root.StrokeWidth) / 2.))
  1891  	w += int(math.Ceil(float64(diagram.Root.StrokeWidth)/2.) * 2.)
  1892  	h += int(math.Ceil(float64(diagram.Root.StrokeWidth)/2.) * 2.)
  1893  	backgroundEl := d2themes.NewThemableElement("rect")
  1894  	// We don't want to change the document viewbox, only the background el
  1895  	backgroundEl.X = float64(left)
  1896  	backgroundEl.Y = float64(top)
  1897  	backgroundEl.Width = float64(w)
  1898  	backgroundEl.Height = float64(h)
  1899  	backgroundEl.Fill = diagram.Root.Fill
  1900  	backgroundEl.Stroke = diagram.Root.Stroke
  1901  	backgroundEl.FillPattern = diagram.Root.FillPattern
  1902  	backgroundEl.Rx = float64(diagram.Root.BorderRadius)
  1903  	if diagram.Root.StrokeDash != 0 {
  1904  		dashSize, gapSize := svg.GetStrokeDashAttributes(float64(diagram.Root.StrokeWidth), diagram.Root.StrokeDash)
  1905  		backgroundEl.StrokeDashArray = fmt.Sprintf("%f, %f", dashSize, gapSize)
  1906  	}
  1907  	backgroundEl.Attributes = fmt.Sprintf(`stroke-width="%d"`, diagram.Root.StrokeWidth)
  1908  
  1909  	// This shift is for viewbox to envelop the background el
  1910  	left -= int(math.Ceil(float64(diagram.Root.StrokeWidth) / 2.))
  1911  	top -= int(math.Ceil(float64(diagram.Root.StrokeWidth) / 2.))
  1912  	w += int(math.Ceil(float64(diagram.Root.StrokeWidth)/2.) * 2.)
  1913  	h += int(math.Ceil(float64(diagram.Root.StrokeWidth)/2.) * 2.)
  1914  
  1915  	doubleBorderElStr := ""
  1916  	if diagram.Root.DoubleBorder {
  1917  		offset := d2target.INNER_BORDER_OFFSET
  1918  
  1919  		left -= int(math.Ceil(float64(diagram.Root.StrokeWidth)/2.)) + offset
  1920  		top -= int(math.Ceil(float64(diagram.Root.StrokeWidth)/2.)) + offset
  1921  		w += int(math.Ceil(float64(diagram.Root.StrokeWidth)/2.)*2.) + 2*offset
  1922  		h += int(math.Ceil(float64(diagram.Root.StrokeWidth)/2.)*2.) + 2*offset
  1923  
  1924  		backgroundEl2 := backgroundEl.Copy()
  1925  		// No need to double-paint
  1926  		backgroundEl.Fill = "transparent"
  1927  
  1928  		backgroundEl2.X = float64(left)
  1929  		backgroundEl2.Y = float64(top)
  1930  		backgroundEl2.Width = float64(w)
  1931  		backgroundEl2.Height = float64(h)
  1932  		doubleBorderElStr = backgroundEl2.Render()
  1933  
  1934  		left -= int(math.Ceil(float64(diagram.Root.StrokeWidth) / 2.))
  1935  		top -= int(math.Ceil(float64(diagram.Root.StrokeWidth) / 2.))
  1936  		w += int(math.Ceil(float64(diagram.Root.StrokeWidth)/2.) * 2.)
  1937  		h += int(math.Ceil(float64(diagram.Root.StrokeWidth)/2.) * 2.)
  1938  	}
  1939  
  1940  	bufStr := buf.String()
  1941  	patternDefs := ""
  1942  	for _, pattern := range d2graph.FillPatterns {
  1943  		if strings.Contains(bufStr, fmt.Sprintf("%s-overlay", pattern)) || diagram.Root.FillPattern == pattern {
  1944  			if patternDefs == "" {
  1945  				fmt.Fprint(upperBuf, `<style type="text/css"><![CDATA[`)
  1946  			}
  1947  			switch pattern {
  1948  			case "dots":
  1949  				patternDefs += dots
  1950  			case "lines":
  1951  				patternDefs += lines
  1952  			case "grain":
  1953  				patternDefs += grain
  1954  			case "paper":
  1955  				patternDefs += paper
  1956  			}
  1957  			fmt.Fprintf(upperBuf, `
  1958  .%s-overlay {
  1959  	fill: url(#%s);
  1960  	mix-blend-mode: multiply;
  1961  }`, pattern, pattern)
  1962  		}
  1963  	}
  1964  	if patternDefs != "" {
  1965  		fmt.Fprint(upperBuf, `]]></style>`)
  1966  		fmt.Fprint(upperBuf, "<defs>")
  1967  		fmt.Fprint(upperBuf, patternDefs)
  1968  		fmt.Fprint(upperBuf, "</defs>")
  1969  	}
  1970  
  1971  	var dimensions string
  1972  	if scale != nil {
  1973  		dimensions = fmt.Sprintf(` width="%d" height="%d"`,
  1974  			int(math.Ceil((*scale)*float64(w))),
  1975  			int(math.Ceil((*scale)*float64(h))),
  1976  		)
  1977  	}
  1978  
  1979  	alignment := "xMinYMin"
  1980  	if opts.Center != nil && *opts.Center {
  1981  		alignment = "xMidYMid"
  1982  	}
  1983  	fitToScreenWrapperOpening := ""
  1984  	xmlTag := ""
  1985  	fitToScreenWrapperClosing := ""
  1986  	idAttr := ""
  1987  	tag := "g"
  1988  	// Many things change when this is rendering for animation
  1989  	if opts.MasterID == "" {
  1990  		fitToScreenWrapperOpening = fmt.Sprintf(`<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" d2Version="%s" preserveAspectRatio="%s meet" viewBox="0 0 %d %d"%s>`,
  1991  			version.Version,
  1992  			alignment,
  1993  			w, h,
  1994  			dimensions,
  1995  		)
  1996  		xmlTag = `<?xml version="1.0" encoding="utf-8"?>`
  1997  		fitToScreenWrapperClosing = "</svg>"
  1998  		idAttr = `id="d2-svg"`
  1999  		tag = "svg"
  2000  	}
  2001  
  2002  	// TODO minify
  2003  	docRendered := fmt.Sprintf(`%s%s<%s %s class="%s" width="%d" height="%d" viewBox="%d %d %d %d">%s%s%s%s</%s>%s`,
  2004  		xmlTag,
  2005  		fitToScreenWrapperOpening,
  2006  		tag,
  2007  		idAttr,
  2008  		diagramHash,
  2009  		w, h, left, top, w, h,
  2010  		doubleBorderElStr,
  2011  		backgroundEl.Render(),
  2012  		upperBuf.String(),
  2013  		buf.String(),
  2014  		tag,
  2015  		fitToScreenWrapperClosing,
  2016  	)
  2017  	return []byte(docRendered), nil
  2018  }
  2019  
  2020  // TODO include only colors that are being used to reduce size
  2021  func ThemeCSS(diagramHash string, themeID *int64, darkThemeID *int64, overrides, darkOverrides *d2target.ThemeOverrides) (stylesheet string, err error) {
  2022  	if themeID == nil {
  2023  		themeID = &d2themescatalog.NeutralDefault.ID
  2024  	}
  2025  	out, err := singleThemeRulesets(diagramHash, *themeID, overrides)
  2026  	if err != nil {
  2027  		return "", err
  2028  	}
  2029  
  2030  	if darkThemeID != nil {
  2031  		darkOut, err := singleThemeRulesets(diagramHash, *darkThemeID, darkOverrides)
  2032  		if err != nil {
  2033  			return "", err
  2034  		}
  2035  		out += fmt.Sprintf("@media screen and (prefers-color-scheme:dark){%s}", darkOut)
  2036  	}
  2037  
  2038  	return out, nil
  2039  }
  2040  
  2041  func singleThemeRulesets(diagramHash string, themeID int64, overrides *d2target.ThemeOverrides) (rulesets string, err error) {
  2042  	out := ""
  2043  	theme := d2themescatalog.Find(themeID)
  2044  	theme.ApplyOverrides(overrides)
  2045  
  2046  	// Global theme colors
  2047  	for _, property := range []string{"fill", "stroke", "background-color", "color"} {
  2048  		out += fmt.Sprintf(`
  2049  		.%s .%s-N1{%s:%s;}
  2050  		.%s .%s-N2{%s:%s;}
  2051  		.%s .%s-N3{%s:%s;}
  2052  		.%s .%s-N4{%s:%s;}
  2053  		.%s .%s-N5{%s:%s;}
  2054  		.%s .%s-N6{%s:%s;}
  2055  		.%s .%s-N7{%s:%s;}
  2056  		.%s .%s-B1{%s:%s;}
  2057  		.%s .%s-B2{%s:%s;}
  2058  		.%s .%s-B3{%s:%s;}
  2059  		.%s .%s-B4{%s:%s;}
  2060  		.%s .%s-B5{%s:%s;}
  2061  		.%s .%s-B6{%s:%s;}
  2062  		.%s .%s-AA2{%s:%s;}
  2063  		.%s .%s-AA4{%s:%s;}
  2064  		.%s .%s-AA5{%s:%s;}
  2065  		.%s .%s-AB4{%s:%s;}
  2066  		.%s .%s-AB5{%s:%s;}`,
  2067  			diagramHash,
  2068  			property, property, theme.Colors.Neutrals.N1,
  2069  			diagramHash,
  2070  			property, property, theme.Colors.Neutrals.N2,
  2071  			diagramHash,
  2072  			property, property, theme.Colors.Neutrals.N3,
  2073  			diagramHash,
  2074  			property, property, theme.Colors.Neutrals.N4,
  2075  			diagramHash,
  2076  			property, property, theme.Colors.Neutrals.N5,
  2077  			diagramHash,
  2078  			property, property, theme.Colors.Neutrals.N6,
  2079  			diagramHash,
  2080  			property, property, theme.Colors.Neutrals.N7,
  2081  			diagramHash,
  2082  			property, property, theme.Colors.B1,
  2083  			diagramHash,
  2084  			property, property, theme.Colors.B2,
  2085  			diagramHash,
  2086  			property, property, theme.Colors.B3,
  2087  			diagramHash,
  2088  			property, property, theme.Colors.B4,
  2089  			diagramHash,
  2090  			property, property, theme.Colors.B5,
  2091  			diagramHash,
  2092  			property, property, theme.Colors.B6,
  2093  			diagramHash,
  2094  			property, property, theme.Colors.AA2,
  2095  			diagramHash,
  2096  			property, property, theme.Colors.AA4,
  2097  			diagramHash,
  2098  			property, property, theme.Colors.AA5,
  2099  			diagramHash,
  2100  			property, property, theme.Colors.AB4,
  2101  			diagramHash,
  2102  			property, property, theme.Colors.AB5,
  2103  		)
  2104  	}
  2105  
  2106  	// Appendix
  2107  	out += fmt.Sprintf(".appendix text.text{fill:%s}", theme.Colors.Neutrals.N1)
  2108  
  2109  	// Markdown specific rulesets
  2110  	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;}",
  2111  		theme.Colors.Neutrals.N1, theme.Colors.Neutrals.N2, theme.Colors.Neutrals.N3,
  2112  		theme.Colors.Neutrals.N7, theme.Colors.Neutrals.N6,
  2113  		theme.Colors.B1, theme.Colors.B2,
  2114  		theme.Colors.Neutrals.N6,
  2115  		theme.Colors.B2, theme.Colors.B2,
  2116  		theme.Colors.Neutrals.N2, // TODO or N3 --color-attention-subtle
  2117  		"red",
  2118  	)
  2119  
  2120  	// Sketch style specific rulesets
  2121  	// B
  2122  	lc, err := color.LuminanceCategory(theme.Colors.B1)
  2123  	if err != nil {
  2124  		return "", err
  2125  	}
  2126  	out += fmt.Sprintf(".sketch-overlay-%s{fill:url(#streaks-%s);mix-blend-mode:%s}", color.B1, lc, blendMode(lc))
  2127  	lc, err = color.LuminanceCategory(theme.Colors.B2)
  2128  	if err != nil {
  2129  		return "", err
  2130  	}
  2131  	out += fmt.Sprintf(".sketch-overlay-%s{fill:url(#streaks-%s);mix-blend-mode:%s}", color.B2, lc, blendMode(lc))
  2132  	lc, err = color.LuminanceCategory(theme.Colors.B3)
  2133  	if err != nil {
  2134  		return "", err
  2135  	}
  2136  	out += fmt.Sprintf(".sketch-overlay-%s{fill:url(#streaks-%s);mix-blend-mode:%s}", color.B3, lc, blendMode(lc))
  2137  	lc, err = color.LuminanceCategory(theme.Colors.B4)
  2138  	if err != nil {
  2139  		return "", err
  2140  	}
  2141  	out += fmt.Sprintf(".sketch-overlay-%s{fill:url(#streaks-%s);mix-blend-mode:%s}", color.B4, lc, blendMode(lc))
  2142  	lc, err = color.LuminanceCategory(theme.Colors.B5)
  2143  	if err != nil {
  2144  		return "", err
  2145  	}
  2146  	out += fmt.Sprintf(".sketch-overlay-%s{fill:url(#streaks-%s);mix-blend-mode:%s}", color.B5, lc, blendMode(lc))
  2147  	lc, err = color.LuminanceCategory(theme.Colors.B6)
  2148  	if err != nil {
  2149  		return "", err
  2150  	}
  2151  	out += fmt.Sprintf(".sketch-overlay-%s{fill:url(#streaks-%s);mix-blend-mode:%s}", color.B6, lc, blendMode(lc))
  2152  
  2153  	// AA
  2154  	lc, err = color.LuminanceCategory(theme.Colors.AA2)
  2155  	if err != nil {
  2156  		return "", err
  2157  	}
  2158  	out += fmt.Sprintf(".sketch-overlay-%s{fill:url(#streaks-%s);mix-blend-mode:%s}", color.AA2, lc, blendMode(lc))
  2159  	lc, err = color.LuminanceCategory(theme.Colors.AA4)
  2160  	if err != nil {
  2161  		return "", err
  2162  	}
  2163  	out += fmt.Sprintf(".sketch-overlay-%s{fill:url(#streaks-%s);mix-blend-mode:%s}", color.AA4, lc, blendMode(lc))
  2164  	lc, err = color.LuminanceCategory(theme.Colors.AA5)
  2165  	if err != nil {
  2166  		return "", err
  2167  	}
  2168  	out += fmt.Sprintf(".sketch-overlay-%s{fill:url(#streaks-%s);mix-blend-mode:%s}", color.AA5, lc, blendMode(lc))
  2169  
  2170  	// AB
  2171  	lc, err = color.LuminanceCategory(theme.Colors.AB4)
  2172  	if err != nil {
  2173  		return "", err
  2174  	}
  2175  	out += fmt.Sprintf(".sketch-overlay-%s{fill:url(#streaks-%s);mix-blend-mode:%s}", color.AB4, lc, blendMode(lc))
  2176  	lc, err = color.LuminanceCategory(theme.Colors.AB5)
  2177  	if err != nil {
  2178  		return "", err
  2179  	}
  2180  	out += fmt.Sprintf(".sketch-overlay-%s{fill:url(#streaks-%s);mix-blend-mode:%s}", color.AB5, lc, blendMode(lc))
  2181  
  2182  	// Neutrals
  2183  	lc, err = color.LuminanceCategory(theme.Colors.Neutrals.N1)
  2184  	if err != nil {
  2185  		return "", err
  2186  	}
  2187  	out += fmt.Sprintf(".sketch-overlay-%s{fill:url(#streaks-%s);mix-blend-mode:%s}", color.N1, lc, blendMode(lc))
  2188  	lc, err = color.LuminanceCategory(theme.Colors.Neutrals.N2)
  2189  	if err != nil {
  2190  		return "", err
  2191  	}
  2192  	out += fmt.Sprintf(".sketch-overlay-%s{fill:url(#streaks-%s);mix-blend-mode:%s}", color.N2, lc, blendMode(lc))
  2193  	lc, err = color.LuminanceCategory(theme.Colors.Neutrals.N3)
  2194  	if err != nil {
  2195  		return "", err
  2196  	}
  2197  	out += fmt.Sprintf(".sketch-overlay-%s{fill:url(#streaks-%s);mix-blend-mode:%s}", color.N3, lc, blendMode(lc))
  2198  	lc, err = color.LuminanceCategory(theme.Colors.Neutrals.N4)
  2199  	if err != nil {
  2200  		return "", err
  2201  	}
  2202  	out += fmt.Sprintf(".sketch-overlay-%s{fill:url(#streaks-%s);mix-blend-mode:%s}", color.N4, lc, blendMode(lc))
  2203  	lc, err = color.LuminanceCategory(theme.Colors.Neutrals.N5)
  2204  	if err != nil {
  2205  		return "", err
  2206  	}
  2207  	out += fmt.Sprintf(".sketch-overlay-%s{fill:url(#streaks-%s);mix-blend-mode:%s}", color.N5, lc, blendMode(lc))
  2208  	lc, err = color.LuminanceCategory(theme.Colors.Neutrals.N6)
  2209  	if err != nil {
  2210  		return "", err
  2211  	}
  2212  	out += fmt.Sprintf(".sketch-overlay-%s{fill:url(#streaks-%s);mix-blend-mode:%s}", color.N6, lc, blendMode(lc))
  2213  	lc, err = color.LuminanceCategory(theme.Colors.Neutrals.N7)
  2214  	if err != nil {
  2215  		return "", err
  2216  	}
  2217  	out += fmt.Sprintf(".sketch-overlay-%s{fill:url(#streaks-%s);mix-blend-mode:%s}", color.N7, lc, blendMode(lc))
  2218  
  2219  	if theme.IsDark() {
  2220  		out += ".light-code{display: none}"
  2221  		out += ".dark-code{display: block}"
  2222  	} else {
  2223  		out += ".light-code{display: block}"
  2224  		out += ".dark-code{display: none}"
  2225  	}
  2226  
  2227  	return out, nil
  2228  }
  2229  
  2230  func blendMode(lc string) string {
  2231  	switch lc {
  2232  	case "bright":
  2233  		return "darken"
  2234  	case "normal":
  2235  		return "color-burn"
  2236  	case "dark":
  2237  		return "overlay"
  2238  	case "darker":
  2239  		return "lighten"
  2240  	}
  2241  	panic("invalid luminance category")
  2242  }
  2243  
  2244  type DiagramObject interface {
  2245  	GetID() string
  2246  	GetZIndex() int
  2247  }
  2248  
  2249  // sortObjects sorts all diagrams objects (shapes and connections) in the desired drawing order
  2250  // the sorting criteria is:
  2251  // 1. zIndex, lower comes first
  2252  // 2. two shapes with the same zIndex are sorted by their level (container nesting), containers come first
  2253  // 3. two shapes with the same zIndex and same level, are sorted in the order they were exported
  2254  // 4. shape and edge, shapes come first
  2255  func sortObjects(allObjects []DiagramObject) {
  2256  	sort.SliceStable(allObjects, func(i, j int) bool {
  2257  		// first sort by zIndex
  2258  		iZIndex := allObjects[i].GetZIndex()
  2259  		jZIndex := allObjects[j].GetZIndex()
  2260  		if iZIndex != jZIndex {
  2261  			return iZIndex < jZIndex
  2262  		}
  2263  
  2264  		// then, if both are shapes, parents come before their children
  2265  		iShape, iIsShape := allObjects[i].(d2target.Shape)
  2266  		jShape, jIsShape := allObjects[j].(d2target.Shape)
  2267  		if iIsShape && jIsShape {
  2268  			return iShape.Level < jShape.Level
  2269  		}
  2270  
  2271  		// then, shapes come before connections
  2272  		_, jIsConnection := allObjects[j].(d2target.Connection)
  2273  		return iIsShape && jIsConnection
  2274  	})
  2275  }
  2276  
  2277  func hash(s string) string {
  2278  	const secret = "lalalas"
  2279  	h := fnv.New32a()
  2280  	h.Write([]byte(fmt.Sprintf("%s%s", s, secret)))
  2281  	return fmt.Sprint(h.Sum32())
  2282  }
  2283  
  2284  func RenderMultiboard(diagram *d2target.Diagram, opts *RenderOpts) ([][]byte, error) {
  2285  	var boards [][]byte
  2286  	for _, dl := range diagram.Layers {
  2287  		childrenBoards, err := RenderMultiboard(dl, opts)
  2288  		if err != nil {
  2289  			return nil, err
  2290  		}
  2291  		boards = append(boards, childrenBoards...)
  2292  	}
  2293  	for _, dl := range diagram.Scenarios {
  2294  		childrenBoards, err := RenderMultiboard(dl, opts)
  2295  		if err != nil {
  2296  			return nil, err
  2297  		}
  2298  		boards = append(boards, childrenBoards...)
  2299  	}
  2300  	for _, dl := range diagram.Steps {
  2301  		childrenBoards, err := RenderMultiboard(dl, opts)
  2302  		if err != nil {
  2303  			return nil, err
  2304  		}
  2305  		boards = append(boards, childrenBoards...)
  2306  	}
  2307  
  2308  	if !diagram.IsFolderOnly {
  2309  		out, err := Render(diagram, opts)
  2310  		if err != nil {
  2311  			return boards, err
  2312  		}
  2313  		boards = append([][]byte{out}, boards...)
  2314  		return boards, nil
  2315  	}
  2316  	return boards, nil
  2317  }
  2318  

View as plain text