// d2svg implements an SVG renderer for d2 diagrams. // The input is d2exporter's output
package d2svg import ( _ ) const ( DEFAULT_PADDING = 100 appendixIconRadius = 16 // Legend constants LEGEND_PADDING = 20 LEGEND_ITEM_SPACING = 15 LEGEND_ICON_SIZE = 24 LEGEND_FONT_SIZE = 14 LEGEND_CORNER_PADDING = 10 ) var multipleOffset = geo.NewVector(d2target.MULTIPLE_OFFSET, -d2target.MULTIPLE_OFFSET) //go:embed tooltip.svg var TooltipIcon string //go:embed link.svg var LinkIcon string //go:embed style.css var BaseStylesheet string // MarkdownCSS is retained for compatibility with callers that used the // browser-era Markdown stylesheet directly. // // Deprecated: native Markdown SVG does not use or inject this HTML stylesheet. // //go:embed github-markdown.css var MarkdownCSS string //go:embed dots.txt var dots string //go:embed lines.txt var lines string type RenderOpts struct { Pad *int64 Sketch *bool // Isometric uses the native vector renderer with the compiled layout preserved. Isometric *bool Center *bool ThemeID *int64 DarkThemeID *int64 ThemeOverrides *d2target.ThemeOverrides DarkThemeOverrides *d2target.ThemeOverrides Font string // the svg will be scaled by this factor, if unset the svg will fit to screen Scale *float64 // 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 MasterID string NoXMLTag *bool Salt *string OmitVersion *bool } func invalidPaddingError( int64) error { return fmt.Errorf("padding %d produces invalid SVG dimensions", ) } func checkedIntAdd(, , , int64) (int64, bool) { if > 0 && > - || < 0 && < - { return 0, false } return + , true } func checkedIntSub(, , , int64) (int64, bool) { if > 0 && < + || < 0 && > + { return 0, false } return - , true } func validatePadding(, d2target.Point, int64) (int, error) { := int64(^uint(0) >> 1) := - - 1 if < || > { return 0, invalidPaddingError() } , := checkedIntAdd(, , , ) if ! { return 0, invalidPaddingError() } for , := range [][2]int64{ {int64(.X), int64(.X)}, {int64(.Y), int64(.Y)}, } { if , := checkedIntSub([0], , , ); ! { return 0, invalidPaddingError() } , := checkedIntSub([1], [0], , ) if ! { return 0, invalidPaddingError() } if , := checkedIntAdd(, , , ); ! { return 0, invalidPaddingError() } } return int(), nil } func expandDimensions(, , , , int) (int, int, int, int, bool) { := int64(^uint(0) >> 1) := - - 1 := int64() , := checkedIntAdd(, , , ) if ! { return 0, 0, 0, 0, false } , := checkedIntSub(int64(), , , ) if ! { return 0, 0, 0, 0, false } , := checkedIntSub(int64(), , , ) if ! { return 0, 0, 0, 0, false } , := checkedIntAdd(int64(), , , ) if ! { return 0, 0, 0, 0, false } , := checkedIntAdd(int64(), , , ) if ! { return 0, 0, 0, 0, false } return int(), int(), int(), int(), true } func dimensions( *d2target.Diagram, int, , d2target.Point) (, , , int) { = .X - = .Y - = .X - .X + *2 = .Y - .Y + *2 // Account for legend dimensions if present if .Legend != nil && (len(.Legend.Shapes) > 0 || len(.Legend.Connections) > 0) { := LEGEND_PADDING + LEGEND_FONT_SIZE + LEGEND_ITEM_SPACING := 0 := 0 , := textmeasure.NewRuler() if == nil && != nil { for , := range .Legend.Shapes { if .Label == "" { continue } := &d2target.MText{ Text: .Label, FontSize: LEGEND_FONT_SIZE, } := d2graph.GetTextDimensions(nil, , , fontToFamily(.FontFamily)) = go2.IntMax(, .Width) += go2.IntMax(.Height, LEGEND_ICON_SIZE) + LEGEND_ITEM_SPACING ++ } for , := range .Legend.Connections { if .Label == "" { continue } := &d2target.MText{ Text: .Label, FontSize: LEGEND_FONT_SIZE, } := d2graph.GetTextDimensions(nil, , , fontToFamily(.FontFamily)) = go2.IntMax(, .Width) += go2.IntMax(.Height, LEGEND_ICON_SIZE) + LEGEND_ITEM_SPACING ++ } if > 0 { -= LEGEND_ITEM_SPACING / 2 } if > 0 && len(.Legend.Connections) > 0 { += LEGEND_PADDING * 1.5 } else { += LEGEND_PADDING * 1.2 } if > 0 && > 0 { := LEGEND_PADDING*2 + LEGEND_ICON_SIZE + LEGEND_PADDING + := .Y - if < .Y { = .Y } := .X + LEGEND_CORNER_PADDING + if + < { = - + /2 } if < { := - -= += } := + if + < { = - + /2 } } } } return , , , } func ( *bytes.Buffer, *d2target.Diagram, string, *d2themes.Theme) error { if .Legend == nil || (len(.Legend.Shapes) == 0 && len(.Legend.Connections) == 0) { return nil } for , := range .Legend.Shapes { if textmeasure.IsDangerousLink(.Link) { return fmt.Errorf("legend shape %q uses an unsafe link URL scheme", .ID) } } , := .BoundingBox() , := textmeasure.NewRuler() if != nil { return } := LEGEND_PADDING + LEGEND_FONT_SIZE + LEGEND_ITEM_SPACING := 0 := 0 for , := range .Legend.Shapes { if .Label == "" { continue } := &d2target.MText{ Text: .Label, FontSize: LEGEND_FONT_SIZE, } := d2graph.GetTextDimensions(nil, , , fontToFamily(.FontFamily)) = go2.IntMax(, .Width) += go2.IntMax(.Height, LEGEND_ICON_SIZE) + LEGEND_ITEM_SPACING ++ } for , := range .Legend.Connections { if .Label == "" { continue } := &d2target.MText{ Text: .Label, FontSize: LEGEND_FONT_SIZE, } := d2graph.GetTextDimensions(nil, , , fontToFamily(.FontFamily)) = go2.IntMax(, .Width) += go2.IntMax(.Height, LEGEND_ICON_SIZE) + LEGEND_ITEM_SPACING ++ } if > 0 { -= LEGEND_ITEM_SPACING / 2 } if > 0 && len(.Legend.Connections) > 0 { += LEGEND_PADDING * 1.5 } else { += LEGEND_PADDING * 1.2 } := LEGEND_PADDING*2 + LEGEND_ICON_SIZE + LEGEND_PADDING + := .X + LEGEND_CORNER_PADDING , := .BoundingBox() := .Y - if < .Y { = .Y } := d2themes.NewThemableElement("rect", ) .Fill = "#F7F7FA" .Stroke = "#DEE1EB" .Style = "stroke-width: 1px; filter: drop-shadow(0px 2px 3px rgba(0, 0, 0, 0.1))" .X = float64() .Y = float64() .Width = float64() .Height = float64() .Rx = 4 fmt.Fprint(, .Render()) := d2themes.NewThemableElement("rect", ) .Fill = "#ffffff" .Stroke = "#DEE1EB" .Style = "stroke-width: 1px" .X = float64() .Y = float64() .Width = float64() .Height = float64() .Rx = 4 fmt.Fprint(, .Render()) := "Legend" if .Legend.Label != "" { = .Legend.Label } fmt.Fprintf(, `<text class="text-bold" x="%d" y="%d" style="font-size: %dpx;">%s</text>`, +LEGEND_PADDING, +LEGEND_PADDING+LEGEND_FONT_SIZE, LEGEND_FONT_SIZE+2, html.EscapeString()) := + LEGEND_PADDING*2 + LEGEND_FONT_SIZE := 0 for , := range .Legend.Shapes { if .Label == "" { continue } := + LEGEND_PADDING := , := renderLegendShapeIcon(, , , , ) if != nil { return } fmt.Fprint(, ) := &d2target.MText{ Text: .Label, FontSize: LEGEND_FONT_SIZE, } := d2graph.GetTextDimensions(nil, , , fontToFamily(.FontFamily)) := go2.IntMax(.Height, LEGEND_ICON_SIZE) := + /2 + int(float64(.Height)*0.3) fmt.Fprintf(, `<text class="text" x="%d" y="%d" style="font-size: %dpx;">%s</text>`, +LEGEND_ICON_SIZE+LEGEND_PADDING, , LEGEND_FONT_SIZE, html.EscapeString(.Label)) += + LEGEND_ITEM_SPACING ++ } if > 0 && len(.Legend.Connections) > 0 { += LEGEND_ITEM_SPACING / 2 := d2themes.NewThemableElement("line", ) .X1 = float64( + LEGEND_PADDING) .Y1 = float64() .X2 = float64( + - LEGEND_PADDING) .Y2 = float64() .Stroke = "#DEE1EB" .StrokeDashArray = "2,2" fmt.Fprint(, .Render()) += LEGEND_ITEM_SPACING } for , := range .Legend.Connections { if .Label == "" { continue } := + LEGEND_PADDING := + LEGEND_ICON_SIZE/2 , := renderLegendConnectionIcon(, , , ) if != nil { return } fmt.Fprint(, ) := &d2target.MText{ Text: .Label, FontSize: LEGEND_FONT_SIZE, } := d2graph.GetTextDimensions(nil, , , fontToFamily(.FontFamily)) := go2.IntMax(.Height, LEGEND_ICON_SIZE) := + /2 + int(float64(.Height)*0.2) fmt.Fprintf(, `<text class="text" x="%d" y="%d" style="font-size: %dpx;">%s</text>`, +LEGEND_ICON_SIZE+LEGEND_PADDING, , LEGEND_FONT_SIZE, html.EscapeString(.Label)) += + LEGEND_ITEM_SPACING } return nil } func renderLegendShapeIcon( d2target.Shape, , int, string, *d2themes.Theme) (string, error) { := const = 5 .Pos.X = 0 .Pos.Y = 0 .Width = LEGEND_ICON_SIZE * .Height = LEGEND_ICON_SIZE * .Label = "" := &bytes.Buffer{} := &bytes.Buffer{} := &bytes.Buffer{} fmt.Fprintf(, `<g transform="translate(%d, %d) scale(%s)">`, , , svg.FormatFloat(1.0/)) , := drawShape(, , , , false, , newMarkdownRenderer(nil, nil, )) if != nil { return "", } fmt.Fprint(, .String()) fmt.Fprint(, `</g>`) return .String(), nil } func renderLegendConnectionIcon( d2target.Connection, , int, *d2themes.Theme) (string, error) { := &bytes.Buffer{} := &bytes.Buffer{} const = 2 := *d2target.BaseConnection() .ID = .ID .SrcArrow = .SrcArrow .DstArrow = .DstArrow .StrokeDash = .StrokeDash .StrokeWidth = .StrokeWidth .Stroke = .Stroke .Fill = .Fill .BorderRadius = .BorderRadius .Opacity = .Opacity .Animated = .Animated := 0.0 := 0.0 := float64(LEGEND_ICON_SIZE * ) .Route = []*geo.Point{ {X: , Y: }, {X: + , Y: }, } := fmt.Sprintf("legend-%s", hash(fmt.Sprintf("%s-%d-%d", .ID, , ))) := make(map[string]struct{}) := make(map[string]d2target.Shape) fmt.Fprintf(, `<g transform="translate(%d, %d) scale(%s)">`, , , svg.FormatFloat(1.0/)) , := drawConnection(, , , , , false, , newMarkdownRenderer(nil, nil, )) if != nil { return "", } fmt.Fprint(, .String()) fmt.Fprint(, `</g>`) return .String(), nil } func arrowheadMarkerID( string, bool, d2target.Connection) string { var d2target.Arrowhead if { = .DstArrow } else { = .SrcArrow } return fmt.Sprintf("mk-%s-%s", , hash(fmt.Sprintf("%s,%t,%d,%s", , , .StrokeWidth, .Stroke, ))) } func arrowheadMarker( bool, string, d2target.Connection, *d2themes.Theme) string { := .DstArrow if ! { = .SrcArrow } := float64(.StrokeWidth) , := .Dimensions() var string switch { case d2target.ArrowArrowhead: := d2themes.NewThemableElement("polygon", ) .Fill = .Stroke .ClassName = "connection" .AddAttributes(svg.IntAttr("stroke-width", .StrokeWidth)) if { .Points = fmt.Sprintf("%s,%s %s,%s %s,%s %s,%s", svg.FormatFloat(0.), svg.FormatFloat(0.), svg.FormatFloat(), svg.FormatFloat(/2), svg.FormatFloat(0.), svg.FormatFloat(), svg.FormatFloat(/4), svg.FormatFloat(/2), ) } else { .Points = fmt.Sprintf("%s,%s %s,%s %s,%s %s,%s", svg.FormatFloat(0.), svg.FormatFloat(/2), svg.FormatFloat(), svg.FormatFloat(0.), svg.FormatFloat(*3/4), svg.FormatFloat(/2), svg.FormatFloat(), svg.FormatFloat(), ) } = .Render() case d2target.UnfilledTriangleArrowhead: := d2themes.NewThemableElement("polygon", ) .Fill = d2target.BG_COLOR .Stroke = .Stroke .ClassName = "connection" .AddAttributes(svg.IntAttr("stroke-width", .StrokeWidth)) := / 2 if { .Points = fmt.Sprintf("%s,%s %s,%s %s,%s", svg.FormatFloat(), svg.FormatFloat(), svg.FormatFloat(-), svg.FormatFloat(/2.0), svg.FormatFloat(), svg.FormatFloat(-), ) } else { .Points = fmt.Sprintf("%s,%s %s,%s %s,%s", svg.FormatFloat(-), svg.FormatFloat(), svg.FormatFloat(), svg.FormatFloat(/2.0), svg.FormatFloat(-), svg.FormatFloat(-), ) } = .Render() case d2target.TriangleArrowhead: := d2themes.NewThemableElement("polygon", ) .Fill = .Stroke .ClassName = "connection" .AddAttributes(svg.IntAttr("stroke-width", .StrokeWidth)) if { .Points = fmt.Sprintf("%s,%s %s,%s %s,%s", svg.FormatFloat(0.), svg.FormatFloat(0.), svg.FormatFloat(), svg.FormatFloat(/2.0), svg.FormatFloat(0.), svg.FormatFloat(), ) } else { .Points = fmt.Sprintf("%s,%s %s,%s %s,%s", svg.FormatFloat(), svg.FormatFloat(0.), svg.FormatFloat(0.), svg.FormatFloat(/2.0), svg.FormatFloat(), svg.FormatFloat(), ) } = .Render() case d2target.LineArrowhead: := d2themes.NewThemableElement("polyline", ) .Fill = color.None .ClassName = "connection" .Stroke = .Stroke .AddAttributes(svg.IntAttr("stroke-width", .StrokeWidth)) if { .Points = fmt.Sprintf("%s,%s %s,%s %s,%s", svg.FormatFloat(/2), svg.FormatFloat(/2), svg.FormatFloat(-/2), svg.FormatFloat(/2), svg.FormatFloat(/2), svg.FormatFloat(-/2), ) } else { .Points = fmt.Sprintf("%s,%s %s,%s %s,%s", svg.FormatFloat(-/2), svg.FormatFloat(/2), svg.FormatFloat(/2), svg.FormatFloat(/2), svg.FormatFloat(-/2), svg.FormatFloat(-/2), ) } = .Render() case d2target.FilledDiamondArrowhead: := d2themes.NewThemableElement("polygon", ) .ClassName = "connection" .Fill = .Stroke .AddAttributes(svg.IntAttr("stroke-width", .StrokeWidth)) if { .Points = fmt.Sprintf("%s,%s %s,%s %s,%s %s,%s", svg.FormatFloat(0.), svg.FormatFloat(/2.0), svg.FormatFloat(/2.0), svg.FormatFloat(0.), svg.FormatFloat(), svg.FormatFloat(/2.0), svg.FormatFloat(/2.0), svg.FormatFloat(), ) } else { .Points = fmt.Sprintf("%s,%s %s,%s %s,%s %s,%s", svg.FormatFloat(0.), svg.FormatFloat(/2.0), svg.FormatFloat(/2.0), svg.FormatFloat(0.), svg.FormatFloat(), svg.FormatFloat(/2.0), svg.FormatFloat(/2.0), svg.FormatFloat(), ) } = .Render() case d2target.DiamondArrowhead: := d2themes.NewThemableElement("polygon", ) .ClassName = "connection" .Fill = d2target.BG_COLOR .Stroke = .Stroke .AddAttributes(svg.IntAttr("stroke-width", .StrokeWidth)) if { .Points = fmt.Sprintf("%s,%s %s,%s %s,%s %s,%s", svg.FormatFloat(0.), svg.FormatFloat(/2.0), svg.FormatFloat(/2), svg.FormatFloat(/8), svg.FormatFloat(), svg.FormatFloat(/2.0), svg.FormatFloat(/2.0), svg.FormatFloat(*0.9), ) } else { .Points = fmt.Sprintf("%s,%s %s,%s %s,%s %s,%s", svg.FormatFloat(/8), svg.FormatFloat(/2.0), svg.FormatFloat(*0.6), svg.FormatFloat(/8), svg.FormatFloat(*1.1), svg.FormatFloat(/2.0), svg.FormatFloat(*0.6), svg.FormatFloat(*7/8), ) } = .Render() case d2target.FilledCircleArrowhead: := / 2 := d2themes.NewThemableElement("circle", ) .Cy = .R = - /2 .Fill = .Stroke .ClassName = "connection" .AddAttributes(svg.IntAttr("stroke-width", .StrokeWidth)) if { .Cx = + /2 } else { .Cx = - /2 } = .Render() case d2target.CircleArrowhead: := / 2 := d2themes.NewThemableElement("circle", ) .Cy = .R = - .Fill = d2target.BG_COLOR .Stroke = .Stroke .AddAttributes(svg.IntAttr("stroke-width", .StrokeWidth)) if { .Cx = + /2 } else { .Cx = - /2 } = .Render() case d2target.CrossArrowhead: := / 8 := math.Pi / 4 := geo.NewPoint(/2, /2) := geo.NewPoint(math.Cos()*.X-math.Sin()*.Y, math.Sin()*.X+math.Cos()*.Y) := d2themes.NewThemableElement("polygon", ) .Points = fmt.Sprintf("%s,%s %s,%s %s,%s %s,%s, %s,%s %s,%s %s,%s %s,%s %s,%s %s,%s %s,%s %s,%s", svg.FormatFloat(0.), svg.FormatFloat(/2+), svg.FormatFloat(/2-), svg.FormatFloat(/2+), svg.FormatFloat(/2-), svg.FormatFloat(), svg.FormatFloat(/2+), svg.FormatFloat(), svg.FormatFloat(/2+), svg.FormatFloat(/2+), svg.FormatFloat(), svg.FormatFloat(/2+), svg.FormatFloat(), svg.FormatFloat(/2-), svg.FormatFloat(/2+), svg.FormatFloat(/2-), svg.FormatFloat(/2+), svg.FormatFloat(0.), svg.FormatFloat(/2-), svg.FormatFloat(0.), svg.FormatFloat(/2-), svg.FormatFloat(/2-), svg.FormatFloat(0.), svg.FormatFloat(/2-), ) .Transform = fmt.Sprintf("translate(%s, %s) rotate(45)", svg.FormatFloat(-.X+/2), svg.FormatFloat(-.Y+/2)) := d2themes.NewThemableElement("path", ) if { .D = fmt.Sprintf("M%s,%s %s,%s", svg.FormatFloat(/2), svg.FormatFloat(/2), svg.FormatFloat(), svg.FormatFloat(/2), ) } else { .D = fmt.Sprintf("M%s,%s %s,%s", svg.FormatFloat(/2), svg.FormatFloat(/2), svg.FormatFloat(0.), svg.FormatFloat(/2), ) } := d2themes.NewThemableElement("g", ) .Fill = d2target.BG_COLOR .Stroke = .Stroke .ClassName = "connection" .AddAttributes(svg.IntAttr("stroke-width", .StrokeWidth)) .SetInnerSVG(svg.Join(svg.TrustedFragment(.Render()), svg.TrustedFragment(.Render()))) = .Render() case d2target.FilledBoxArrowhead: := d2themes.NewThemableElement("polygon", ) .ClassName = "connection" .Fill = .Stroke .AddAttributes(svg.IntAttr("stroke-width", .StrokeWidth)) if { .Points = fmt.Sprintf("%s,%s %s,%s %s,%s %s,%s", svg.FormatFloat(0.), svg.FormatFloat(0.), svg.FormatFloat(0.), svg.FormatFloat(), svg.FormatFloat(), svg.FormatFloat(), svg.FormatFloat(), svg.FormatFloat(0.), ) } else { .Points = fmt.Sprintf("%s,%s %s,%s %s,%s %s,%s", svg.FormatFloat(0.), svg.FormatFloat(0.), svg.FormatFloat(0.), svg.FormatFloat(), svg.FormatFloat(), svg.FormatFloat(), svg.FormatFloat(), svg.FormatFloat(0.), ) } = .Render() case d2target.BoxArrowhead: := d2themes.NewThemableElement("polygon", ) .ClassName = "connection" .Fill = d2target.BG_COLOR .Stroke = .Stroke .AddAttributes(svg.IntAttr("stroke-width", .StrokeWidth)) .Style = fmt.Sprintf("%sstroke-linejoin:miter;", .Style) := / 2 if { .Points = fmt.Sprintf("%s,%s %s,%s %s,%s %s,%s", svg.FormatFloat(), svg.FormatFloat(), svg.FormatFloat(), svg.FormatFloat(-), svg.FormatFloat(-), svg.FormatFloat(-), svg.FormatFloat(-), svg.FormatFloat(), ) } else { .Points = fmt.Sprintf("%s,%s %s,%s %s,%s %s,%s", svg.FormatFloat(), svg.FormatFloat(), svg.FormatFloat(), svg.FormatFloat(-), svg.FormatFloat(-), svg.FormatFloat(-), svg.FormatFloat(-), svg.FormatFloat(), ) } = .Render() case d2target.CfOne, d2target.CfMany, d2target.CfOneRequired, d2target.CfManyRequired: := 3.0 + float64(.StrokeWidth)*1.8 var *d2themes.ThemableElement if == d2target.CfOneRequired || == d2target.CfManyRequired { = d2themes.NewThemableElement("path", ) .D = fmt.Sprintf("M%s,%s %s,%s", svg.FormatFloat(), svg.FormatFloat(0.), svg.FormatFloat(), svg.FormatFloat(), ) .Fill = d2target.BG_COLOR .Stroke = .Stroke .ClassName = "connection" .AddAttributes(svg.IntAttr("stroke-width", .StrokeWidth)) } else { = d2themes.NewThemableElement("circle", ) .Cx = /2.0 + 2.0 .Cy = / 2.0 .R = / 2.0 .Fill = d2target.BG_COLOR .Stroke = .Stroke .ClassName = "connection" .AddAttributes(svg.IntAttr("stroke-width", .StrokeWidth)) } := d2themes.NewThemableElement("path", ) if == d2target.CfMany || == d2target.CfManyRequired { .D = fmt.Sprintf("M%s,%s %s,%s M%s,%s %s,%s M%s,%s %s,%s", svg.FormatFloat(-3.0), svg.FormatFloat(/2.0), svg.FormatFloat(+), svg.FormatFloat(/2.0), svg.FormatFloat(+3.0), svg.FormatFloat(/2.0), svg.FormatFloat(+), svg.FormatFloat(0.), svg.FormatFloat(+3.0), svg.FormatFloat(/2.0), svg.FormatFloat(+), svg.FormatFloat(), ) } else { .D = fmt.Sprintf("M%s,%s %s,%s M%s,%s %s,%s", svg.FormatFloat(-3.0), svg.FormatFloat(/2.0), svg.FormatFloat(+), svg.FormatFloat(/2.0), svg.FormatFloat(*2.0), svg.FormatFloat(0.), svg.FormatFloat(*2.0), svg.FormatFloat(), ) } := d2themes.NewThemableElement("g", ) if ! { .Transform = fmt.Sprintf("scale(-1) translate(-%s, -%s)", svg.FormatFloat(), svg.FormatFloat()) } .Fill = d2target.BG_COLOR .Stroke = .Stroke .ClassName = "connection" .AddAttributes(svg.IntAttr("stroke-width", .StrokeWidth)) .SetInnerSVG(svg.Join(svg.TrustedFragment(.Render()), svg.TrustedFragment(.Render()))) = .Render() default: return "" } var float64 := / 2 switch { case d2target.DiamondArrowhead: if { = - 0.6* } else { = /8 + 0.6* } *= 1.1 default: if { = - 1.5* } else { = 1.5 * } } return strings.Join([]string{ fmt.Sprintf(`<marker id="%s" markerWidth="%s" markerHeight="%s" refX="%s" refY="%s"`, , svg.FormatFloat(), svg.FormatFloat(), svg.FormatFloat(), svg.FormatFloat(), ), fmt.Sprintf(`viewBox="%s %s %s %s"`, svg.FormatFloat(0.), svg.FormatFloat(0.), svg.FormatFloat(), svg.FormatFloat()), `orient="auto" markerUnits="userSpaceOnUse">`, , "</marker>", }, " ") } // compute the (dx, dy) adjustment to apply to get the arrowhead-adjusted end point func arrowheadAdjustment(, *geo.Point, d2target.Arrowhead, , int) *geo.Point { := (float64() + float64()) / 2.0 if != d2target.NoArrowhead { += float64() } := geo.NewVector(.X-.X, .Y-.Y) return .Unit().Multiply(-).ToPoint() } func getArrowheadAdjustments( d2target.Connection, map[string]d2target.Shape) (, *geo.Point) { := .Route := [.Src] := [.Dst] := arrowheadAdjustment([1], [0], .SrcArrow, .StrokeWidth, .StrokeWidth) := arrowheadAdjustment([len()-2], [len()-1], .DstArrow, .StrokeWidth, .StrokeWidth) return , } // returns the path's d attribute for the given connection func pathData( d2target.Connection, , *geo.Point) string { var []string := .Route = append(, fmt.Sprintf("M %s %s", svg.FormatFloat([0].X+.X), svg.FormatFloat([0].Y+.Y), )) if .IsCurve { := 1 for ; < len()-3; += 3 { = append(, fmt.Sprintf("C %s %s %s %s %s %s", svg.FormatFloat([].X), svg.FormatFloat([].Y), svg.FormatFloat([+1].X), svg.FormatFloat([+1].Y), svg.FormatFloat([+2].X), svg.FormatFloat([+2].Y), )) } // final curve target adjustment = append(, fmt.Sprintf("C %s %s %s %s %s %s", svg.FormatFloat([].X), svg.FormatFloat([].Y), svg.FormatFloat([+1].X), svg.FormatFloat([+1].Y), svg.FormatFloat([+2].X+.X), svg.FormatFloat([+2].Y+.Y), )) } else { for := 1; < len()-1; ++ { := [-1] := [] := [+1] := .VectorTo() := .VectorTo() := geo.EuclideanDistance(.X, .Y, .X, .Y) := .BorderRadius := math.Min(, /2) := .Unit().Multiply().ToPoint() := .Unit().Multiply().ToPoint() = append(, fmt.Sprintf("L %s %s", svg.FormatFloat(.X-.X), svg.FormatFloat(.Y-.Y), )) // If the segment length is too small, instead of drawing 2 arcs, just skip this segment and bezier curve to the next one if < && < len()-2 { := [+2] := geo.NewVector(.X-.X, .Y-.Y) ++ := .Unit().Multiply().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 = append(, fmt.Sprintf("C %s %s %s %s %s %s", // Control point svg.FormatFloat(.X+.X), svg.FormatFloat(.Y+.Y), // Control point svg.FormatFloat(.X-.X), svg.FormatFloat(.Y-.Y), // Where curve ends svg.FormatFloat(.X+.X), svg.FormatFloat(.Y+.Y), )) } else { = append(, fmt.Sprintf("S %s %s %s %s", svg.FormatFloat(.X), svg.FormatFloat(.Y), svg.FormatFloat(.X+.X), svg.FormatFloat(.Y+.Y), )) } } := [len()-1] = append(, fmt.Sprintf("L %s %s", svg.FormatFloat(.X+.X), svg.FormatFloat(.Y+.Y), )) } return strings.Join(, " ") } func makeLabelMask( *geo.Point, , int, float64) string { return makeLabelMaskWithTransform(, , , , nil) } func makeLabelMaskWithTransform( *geo.Point, , int, float64, *geo.Point) string { := "black" if != 1 { = fmt.Sprintf("rgba(0,0,0,%.2f)", ) } := .X - 2 := .Y // In sketch mode, shapes use transform="translate(shapePos.X, shapePos.Y)" // so we need to adjust mask coordinates to account for this transform if != nil { -= .X -= .Y } return fmt.Sprintf(`<rect x="%s" y="%s" width="%d" height="%d" fill="%s"></rect>`, svg.FormatFloat(), svg.FormatFloat(), +4, , , ) } // border label mask is minimal, intending to only cover the portion of the border that passes through the label // otherwise, when the inside background and outside background colors don't match, there's a bigger discrepancy func makeBorderLabelMask( label.Position, *geo.Point, , int, *geo.Box, int, float64, *geo.Point) string { := "black" if != 1 { = fmt.Sprintf("rgba(0,0,0,%.2f)", ) } // In sketch mode, multiply stroke width by 3 for mask calculations to account for // the hand-drawn style that can be wider and more irregular := float64() if != nil { *= 3 } var , , , float64 switch { case label.BorderTopLeft, label.BorderTopCenter, label.BorderTopRight: = .X - 2 = .TopLeft.Y - /2 = float64( + 4) = case label.BorderBottomLeft, label.BorderBottomCenter, label.BorderBottomRight: = .X - 2 = .TopLeft.Y + .Height - /2 = float64( + 4) = case label.BorderLeftTop, label.BorderLeftMiddle, label.BorderLeftBottom: = .TopLeft.X - /2 = .Y - 2 = = float64( + 4) case label.BorderRightTop, label.BorderRightMiddle, label.BorderRightBottom: = .TopLeft.X + .Width - /2 = .Y - 2 = = float64( + 4) default: // never gets here return "" } // In sketch mode, adjust coordinates to account for shape transform if != nil { -= .X -= .Y } return fmt.Sprintf(`<rect x="%s" y="%s" width="%s" height="%s" fill="%s"></rect>`, svg.FormatFloat(), svg.FormatFloat(), svg.FormatFloat(), svg.FormatFloat(), , ) } func drawConnection( io.Writer, string, d2target.Connection, map[string]struct{}, map[string]d2target.Shape, bool, *d2themes.Theme, *markdownRenderer) ( string, error) { := []string{base64.URLEncoding.EncodeToString([]byte(svg.EscapeText(.ID)))} = append(, .Classes...) := []svg.Attribute{svg.Attr("class", strings.Join(, " "))} := "" if .Opacity != 1.0 { = fmt.Sprintf(" style='opacity:%s'", svg.FormatFloat(.Opacity)) } fmt.Fprint(, strings.TrimSuffix(svg.OpenElement("g", ...), ">")++">") var []svg.Attribute if .SrcArrow != d2target.NoArrowhead { := arrowheadMarkerID(, false, ) if , := []; ! { := arrowheadMarker(false, , , ) if == "" { panic(fmt.Sprintf("received empty arrow head marker for: %#v", )) } fmt.Fprint(, ) [] = struct{}{} } = []svg.Attribute{svg.Attr("marker-start", svg.LocalIRI(svg.LiteralID()))} } var []svg.Attribute if .DstArrow != d2target.NoArrowhead { := arrowheadMarkerID(, true, ) if , := []; ! { := arrowheadMarker(true, , , ) if == "" { panic(fmt.Sprintf("received empty arrow head marker for: %#v", )) } fmt.Fprint(, ) [] = struct{}{} } = []svg.Attribute{svg.Attr("marker-end", svg.LocalIRI(svg.LiteralID()))} } if .Icon != nil { := .GetIconPosition() if != nil { := []svg.Attribute{ svg.Attr("href", .Icon.String()), svg.FloatAttr("x", .X), svg.FloatAttr("y", .Y), svg.IntAttr("width", d2target.DEFAULT_ICON_SIZE), svg.IntAttr("height", d2target.DEFAULT_ICON_SIZE), } if .IconBorderRadius != 0 { = append(, svg.Attr("clip-path", "inset(0 round "+svg.FormatFloat(.IconBorderRadius)+"px)")) } fmt.Fprint(, svg.EmptyElement("image", ...)) } } var *geo.Point if .Label != "" { = .GetLabelTopLeft() .X = math.Round(.X) .Y = math.Round(.Y) := .Copy() := .LabelWidth := .LabelHeight if .Icon != nil { += d2target.CONNECTION_ICON_LABEL_GAP + d2target.DEFAULT_ICON_SIZE .X -= float64(d2target.CONNECTION_ICON_LABEL_GAP + d2target.DEFAULT_ICON_SIZE) } if label.FromString(.LabelPosition).IsOnEdge() { = makeLabelMask(, , , 1) } else { = makeLabelMask(, , , 0.75) } } else if .Icon != nil { := .GetIconPosition() if != nil { := &geo.Point{ X: .X, Y: .Y, } if label.FromString(.IconPosition).IsOnEdge() { = makeLabelMask(, d2target.DEFAULT_ICON_SIZE, d2target.DEFAULT_ICON_SIZE, 1) } else { = makeLabelMask(, d2target.DEFAULT_ICON_SIZE, d2target.DEFAULT_ICON_SIZE, 0.75) } } } , := getArrowheadAdjustments(, ) := pathData(, , ) := svg.Attr("mask", svg.LocalIRI(svg.LiteralID())) if { , := d2sketch.ConnectionWithAttributes(, , []svg.Attribute{}) if != nil { return "", } fmt.Fprint(, ) // render sketch arrowheads separately , := d2sketch.Arrowheads(, , ) if != nil { return "", } fmt.Fprint(, ) } else { := "" if .Animated { = " animated-connection" } // If connection is animated and bidirectional if .Animated && ((.DstArrow == d2target.NoArrowhead && .SrcArrow == d2target.NoArrowhead) || (.DstArrow != d2target.NoArrowhead && .SrcArrow != d2target.NoArrowhead)) { // There is no pure CSS way to animate bidirectional connections in two directions, so we split it up , , := svg.SplitPath(, 0.5) if != nil { return "", } := d2themes.NewThemableElement("path", ) .D = .Fill = color.None .Stroke = .Stroke .ClassName = fmt.Sprintf("connection%s", ) .Style = .CSSStyle() .Style += "animation-direction: reverse;" .AddAttributes(append(, )...) fmt.Fprint(, .Render()) := d2themes.NewThemableElement("path", ) .D = .Fill = color.None .Stroke = .Stroke .ClassName = fmt.Sprintf("connection%s", ) .Style = .CSSStyle() .AddAttributes(append(, )...) fmt.Fprint(, .Render()) } else { := d2themes.NewThemableElement("path", ) .D = .Fill = color.None .Stroke = .Stroke .ClassName = fmt.Sprintf("connection%s", ) .Style = .CSSStyle() := append(append([]svg.Attribute{}, ...), ...) .AddAttributes(append(, )...) fmt.Fprint(, .Render()) } } if .Label != "" { := .Link != "" && .Language != "" if { fmt.Fprint(, svg.OpenElement("a", svg.HrefAttributes(.Link)...)) } if .Language == "latex" { , := d2latex.Render(.Label) if != nil { return , } // Remove XML declaration and DOCTYPE from LaTeX SVG output := `<?xml version="1.0" encoding="UTF-8"?>` := `<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.0//EN" "http://www.w3.org/TR/2001/REC-SVG-20010904/DTD/svg10.dtd">` = strings.ReplaceAll(, , "") = strings.ReplaceAll(, , "") := d2themes.NewThemableElement("g", ) .SetTranslate(.X, .Y) .Color = .Color .SetInnerSVG(svg.TrustedFragment()) fmt.Fprint(, .Render()) } else if .Language == "markdown" { , := .layout(.Label, .FontFamily, .FontSize) if != nil { return , } fmt.Fprint(, .render( , .X, .Y, .LabelWidth, .LabelHeight, .GetFontColor(), .Fill, .Link != "", .Underline, )) } else if .Language != "" { := lexers.Get(.Language) if == nil { = lexers.Fallback } for , := range []bool{true, false} { := "github" if ! { = "catppuccin-mocha" } := styles.Get() if == nil { return , errors.New(`code snippet style "github" not found`) } , := .Tokenise(nil, .Label) if != nil { return , } := styleToSVG() := "light-code" if ! { = "dark-code" } var string if .FontSize != d2fonts.FONT_SIZE_M { = fmt.Sprintf(` style="font-size:%v"`, .FontSize) } fmt.Fprintf(, `<g transform="translate(%s %s)" class="%s"%s>`, svg.FormatFloat(.X), svg.FormatFloat(.Y), , , ) := textmeasure.CODE_LINE_HEIGHT for , := range chroma.SplitTokensIntoLines(.Tokens()) { fmt.Fprintf(, "<text class=\"text-mono\" x=\"0\" y=\"%sem\">", svg.FormatFloat(1+float64()*)) for , := range { := svgEscaper.Replace(.String()) := styleAttr(, .Type) if != "" { = fmt.Sprintf("<tspan %s>%s</tspan>", , ) } fmt.Fprint(, ) } fmt.Fprint(, "</text>") } fmt.Fprint(, "</g>") } } else { := "text" if .FontFamily == "mono" { = "text-mono" } if .Bold { += "-bold" } else if .Italic { += "-italic" } if .Underline { += " text-underline" } if .Fill != color.Empty { := d2themes.NewThemableElement("rect", ) .Rx = 10 .X, .Y = .X-4, .Y-3 .Width, .Height = float64(.LabelWidth)+8, float64(.LabelHeight)+6 .Fill = .Fill fmt.Fprint(, .Render()) } := d2themes.NewThemableElement("text", ) .X = .X + float64(.LabelWidth)/2 .Y = .Y + float64(.FontSize) .ClassName = .Style = fmt.Sprintf("text-anchor:%s;font-size:%vpx", "middle", .FontSize) .SetInnerSVG(svg.TrustedFragment(RenderText(.Label, .X, float64(.LabelHeight)))) if .Link != "" { .ClassName += " text-underline text-link" fmt.Fprint(, svg.OpenElement("a", svg.HrefAttributes(.Link)...)) } else { .Fill = .GetFontColor() } fmt.Fprint(, .Render()) if .Link != "" { fmt.Fprintf(, "</a>") } } if { fmt.Fprint(, "</a>") } } if .SrcLabel != nil && .SrcLabel.Label != "" { fmt.Fprint(, renderArrowheadLabel(, .SrcLabel.Label, false, )) } if .DstLabel != nil && .DstLabel.Label != "" { fmt.Fprint(, renderArrowheadLabel(, .DstLabel.Label, true, )) } fmt.Fprintf(, `</g>`) return } func renderArrowheadLabel( d2target.Connection, string, bool, *d2themes.Theme) string { var , float64 if { = float64(.DstLabel.LabelWidth) = float64(.DstLabel.LabelHeight) } else { = float64(.SrcLabel.LabelWidth) = float64(.SrcLabel.LabelHeight) } := .GetArrowheadLabelPosition() // svg text is positioned with the center of its baseline := geo.Point{ X: .X + /2., Y: .Y + float64(.FontSize), } := d2themes.NewThemableElement("text", ) .X = .X .Y = .Y .Fill = d2target.FG_COLOR if { if .DstLabel.Color != "" { .Fill = .DstLabel.Color } } else { if .SrcLabel.Color != "" { .Fill = .SrcLabel.Color } } .ClassName = "text-italic" .Style = fmt.Sprintf("text-anchor:middle;font-size:%vpx", .FontSize) .SetInnerSVG(svg.TrustedFragment(RenderText(, .X, ))) return .Render() } func renderOval( *geo.Point, , float64, , , , string, *d2themes.Theme) string { := d2themes.NewThemableElement("ellipse", ) .Rx = / 2 .Ry = / 2 .Cx = .X + .Rx .Cy = .Y + .Ry .Fill, .Stroke = , .FillPattern = .ClassName = "shape" .Style = return .Render() } func renderDoubleOval( *geo.Point, , float64, , , , string, *d2themes.Theme) string { var *geo.Point = .AddVector(geo.NewVector(d2target.INNER_BORDER_OFFSET, d2target.INNER_BORDER_OFFSET)) return renderOval(, , , , , , , ) + renderOval(, -10, -10, , "", , , ) } func defineGradients( io.Writer, string) { , := color.ParseGradient() fmt.Fprint(, fmt.Sprintf(`<defs>%s</defs>`, color.GradientToSVG())) } func defineShadowFilter( io.Writer) { fmt.Fprint(, `<defs> <filter id="shadow-filter" width="200%" height="200%" x="-50%" y="-50%"> <feGaussianBlur stdDeviation="1.7 " in="SourceGraphic"></feGaussianBlur> <feFlood flood-color="#3d4574" flood-opacity="0.4" result="ShadowFeFlood" in="SourceGraphic"></feFlood> <feComposite in="ShadowFeFlood" in2="SourceAlpha" operator="in" result="ShadowFeComposite"></feComposite> <feOffset dx="3" dy="5" result="ShadowFeOffset" in="ShadowFeComposite"></feOffset> <feBlend in="SourceGraphic" in2="ShadowFeOffset" mode="normal" result="ShadowFeBlend"></feBlend> </filter> </defs>`) } func render3DRect( string, d2target.Shape, *d2themes.Theme) string { := func( d2target.Point) string { return fmt.Sprintf("M%d,%d", .X+.Pos.X, .Y+.Pos.Y) } := func( d2target.Point) string { return fmt.Sprintf("L%d,%d", .X+.Pos.X, .Y+.Pos.Y) } // draw border all in one path to prevent overlapping sections var []string = append(, (d2target.Point{X: 0, Y: 0}), ) for , := range []d2target.Point{ {X: d2target.THREE_DEE_OFFSET, Y: -d2target.THREE_DEE_OFFSET}, {X: .Width + d2target.THREE_DEE_OFFSET, Y: -d2target.THREE_DEE_OFFSET}, {X: .Width + d2target.THREE_DEE_OFFSET, Y: .Height - d2target.THREE_DEE_OFFSET}, {X: .Width, Y: .Height}, {X: 0, Y: .Height}, {X: 0, Y: 0}, {X: .Width, Y: 0}, {X: .Width, Y: .Height}, } { = append(, ()) } // move to top right to draw last segment without overlapping = append(, (d2target.Point{X: .Width, Y: 0}), ) = append(, (d2target.Point{X: .Width + d2target.THREE_DEE_OFFSET, Y: -d2target.THREE_DEE_OFFSET}), ) := d2themes.NewThemableElement("path", ) .D = strings.Join(, " ") .Fill = color.None , := d2themes.ShapeTheme() .Stroke = := .CSSStyle() .Style = := .Render() // create mask from border stroke, to cut away from the shape fills := svg.ScopedID("border-mask-"++"-", .ID, "") := strings.Join([]string{ svg.OpenElement("defs") + svg.OpenElement("mask", svg.Attr("id", .String()), svg.Attr("maskUnits", "userSpaceOnUse"), svg.IntAttr("x", .Pos.X), svg.IntAttr("y", .Pos.Y-d2target.THREE_DEE_OFFSET), svg.IntAttr("width", .Width+d2target.THREE_DEE_OFFSET), svg.IntAttr("height", .Height+d2target.THREE_DEE_OFFSET), ), svg.OpenElement("rect", svg.IntAttr("x", .Pos.X), svg.IntAttr("y", .Pos.Y-d2target.THREE_DEE_OFFSET), svg.IntAttr("width", .Width+d2target.THREE_DEE_OFFSET), svg.IntAttr("height", .Height+d2target.THREE_DEE_OFFSET), svg.Attr("fill", "white"), ) + svg.CloseElement("rect"), svg.CompactEmptyElement("path", svg.Attr("d", strings.Join(, "")), svg.Attr("style", +";stroke:#000;fill:none;opacity:1;"), ) + svg.CloseElement("mask") + svg.CloseElement("defs"), }, "\n") // render the main rectangle without stroke and the border mask := d2themes.NewThemableElement("rect", ) .X = float64(.Pos.X) .Y = float64(.Pos.Y) .Width = float64(.Width) .Height = float64(.Height) .SetMaskID() , := d2themes.ShapeTheme() .Fill = .FillPattern = .FillPattern .Stroke = color.None .Style = .CSSStyle() := .Render() // render the side shapes in the darkened color without stroke and the border mask var []string for , := range []d2target.Point{ {X: 0, Y: 0}, {X: d2target.THREE_DEE_OFFSET, Y: -d2target.THREE_DEE_OFFSET}, {X: .Width + d2target.THREE_DEE_OFFSET, Y: -d2target.THREE_DEE_OFFSET}, {X: .Width + d2target.THREE_DEE_OFFSET, Y: .Height - d2target.THREE_DEE_OFFSET}, {X: .Width, Y: .Height}, {X: .Width, Y: 0}, } { = append(, fmt.Sprintf("%d,%d", .X+.Pos.X, .Y+.Pos.Y), ) } , := color.Darken(.Fill) if != nil { = .Fill } := d2themes.NewThemableElement("polygon", ) .Fill = .Points = strings.Join(, " ") .SetMaskID() .Style = .CSSStyle() := .Render() return + + + } func render3DHexagon( string, d2target.Shape, *d2themes.Theme) string { := func( d2target.Point) string { return fmt.Sprintf("M%d,%d", .X+.Pos.X, .Y+.Pos.Y) } := func( d2target.Point) string { return fmt.Sprintf("L%d,%d", .X+.Pos.X, .Y+.Pos.Y) } := func( int, float64) int { return int(float64() * ) } := 43.6 / 87.3 // draw border all in one path to prevent overlapping sections var []string // start from the top-left = append(, (d2target.Point{X: (.Width, 0.25), Y: 0}), ) := 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 , := range []d2target.Point{ {X: (.Width, 0.25) + d2target.THREE_DEE_OFFSET, Y: -}, {X: (.Width, 0.75) + d2target.THREE_DEE_OFFSET, Y: -}, {X: .Width + d2target.THREE_DEE_OFFSET, Y: (.Height, ) - }, {X: (.Width, 0.75) + d2target.THREE_DEE_OFFSET, Y: .Height - }, {X: (.Width, 0.75), Y: .Height}, {X: (.Width, 0.25), Y: .Height}, {X: 0, Y: (.Height, )}, {X: (.Width, 0.25), Y: 0}, {X: (.Width, 0.75), Y: 0}, {X: .Width, Y: (.Height, )}, {X: (.Width, 0.75), Y: .Height}, } { = append(, ()) } for , := range []d2target.Point{ {X: (.Width, 0.75), Y: 0}, {X: .Width, Y: (.Height, )}, {X: (.Width, 0.75), Y: .Height}, } { = append(, ()) = append(, ( d2target.Point{X: .X + d2target.THREE_DEE_OFFSET, Y: .Y - }, )) } := d2themes.NewThemableElement("path", ) .D = strings.Join(, " ") .Fill = color.None , := d2themes.ShapeTheme() .Stroke = := .CSSStyle() .Style = := .Render() var []string for , := range []d2target.Point{ {X: (.Width, 0.25), Y: 0}, {X: (.Width, 0.75), Y: 0}, {X: .Width, Y: (.Height, )}, {X: (.Width, 0.75), Y: .Height}, {X: (.Width, 0.25), Y: .Height}, {X: 0, Y: (.Height, )}, } { = append(, fmt.Sprintf("%d,%d", .X+.Pos.X, .Y+.Pos.Y), ) } := strings.Join(, " ") // create mask from border stroke, to cut away from the shape fills := svg.ScopedID("border-mask-"++"-", .ID, "") := strings.Join([]string{ svg.OpenElement("defs") + svg.OpenElement("mask", svg.Attr("id", .String()), svg.Attr("maskUnits", "userSpaceOnUse"), svg.IntAttr("x", .Pos.X), svg.IntAttr("y", .Pos.Y-d2target.THREE_DEE_OFFSET), svg.IntAttr("width", .Width+d2target.THREE_DEE_OFFSET), svg.IntAttr("height", .Height+d2target.THREE_DEE_OFFSET), ), svg.OpenElement("rect", svg.IntAttr("x", .Pos.X), svg.IntAttr("y", .Pos.Y-d2target.THREE_DEE_OFFSET), svg.IntAttr("width", .Width+d2target.THREE_DEE_OFFSET), svg.IntAttr("height", .Height+d2target.THREE_DEE_OFFSET), svg.Attr("fill", "white"), ) + svg.CloseElement("rect"), svg.CompactEmptyElement("path", svg.Attr("d", strings.Join(, "")), svg.Attr("style", +";stroke:#000;fill:none;opacity:1;"), ) + svg.CloseElement("mask") + svg.CloseElement("defs"), }, "\n") // render the main hexagon without stroke and the border mask := d2themes.NewThemableElement("polygon", ) .X = float64(.Pos.X) .Y = float64(.Pos.Y) .Points = .SetMaskID() , := d2themes.ShapeTheme() .FillPattern = .FillPattern .Fill = .Stroke = color.None .Style = .CSSStyle() := .Render() // render the side shapes in the darkened color without stroke and the border mask var []string for , := range []d2target.Point{ {X: (.Width, 0.25) + d2target.THREE_DEE_OFFSET, Y: -}, {X: (.Width, 0.75) + d2target.THREE_DEE_OFFSET, Y: -}, {X: .Width + d2target.THREE_DEE_OFFSET, Y: (.Height, ) - }, {X: (.Width, 0.75) + d2target.THREE_DEE_OFFSET, Y: .Height - }, {X: (.Width, 0.75), Y: .Height}, {X: .Width, Y: (.Height, )}, {X: (.Width, 0.75), Y: 0}, {X: (.Width, 0.25), Y: 0}, } { = append(, fmt.Sprintf("%d,%d", .X+.Pos.X, .Y+.Pos.Y), ) } // TODO make darker color part of the theme? or just keep this bypass , := color.Darken(.Fill) if != nil { = .Fill } := d2themes.NewThemableElement("polygon", ) .Fill = .Points = strings.Join(, " ") .SetMaskID() .Style = .CSSStyle() := .Render() return + + + } func drawShape(, io.Writer, string, d2target.Shape, bool, *d2themes.Theme, *markdownRenderer) ( string, error) { := "</g>" if .Link != "" { fmt.Fprint(, svg.OpenElement("a", svg.HrefAttributes(.Link)...)) += "</a>" } // this clipPath must be defined outside `g` element if .BorderRadius != 0 && (.Type == d2target.ShapeClass || .Type == d2target.ShapeSQLTable) { fmt.Fprint(, clipPathForBorderRadius(, )) } var svg.ID if .IconBorderRadius != 0 && (.Type == d2target.ShapeImage) { // Set the icon's border-radius to half of it's smaller dimension in case it exceeds that // https://www.w3.org/Style/CSS/Tracker/issues/29?changelog .IconBorderRadius = min(.IconBorderRadius, min(.Width, .Height)/2) = svg.LiteralID(fmt.Sprintf("%v-%v-icon", , svg.SVGID(.ID))) fmt.Fprint(, applyIconBorderRadius(, )) } := []string{base64.URLEncoding.EncodeToString([]byte(svg.EscapeText(.ID)))} if .Animated { = append(, "animated-shape") } = append(, .Classes...) := []svg.Attribute{svg.Attr("class", strings.Join(, " "))} := "" if .Opacity != 1.0 { = fmt.Sprintf(" style='opacity:%s'", svg.FormatFloat(.Opacity)) } fmt.Fprint(, strings.TrimSuffix(svg.OpenElement("g", ...), ">")++">") := geo.NewPoint(float64(.Pos.X), float64(.Pos.Y)) := float64(.Width) := float64(.Height) , := d2themes.ShapeTheme() := .CSSStyle() := d2target.DSL_SHAPE_TO_SHAPE_TYPE[.Type] := shape.NewShape(, geo.NewBox(, , )) if == shape.CLOUD_TYPE && .ContentAspectRatio != nil { .SetInnerBoxAspectRatio(*.ContentAspectRatio) } var string if .Shadow { switch .Type { case d2target.ShapeText, d2target.ShapeCode, d2target.ShapeClass, d2target.ShapeSQLTable: default: = `filter="url(#shadow-filter)" ` } } var string if .Blend { = " blend" } fmt.Fprintf(, `<g class="shape%s" %s>`, , ) var *geo.Point if .Multiple { = .AddVector(multipleOffset) } switch .Type { case d2target.ShapeClass: if { , := d2sketch.Class() if != nil { return "", } fmt.Fprint(, ) } else { drawClass(, , , ) } := addAppendixItems(, , , , ) if != nil { return "", } fmt.Fprint(, `</g>`) fmt.Fprint(, ) return , nil case d2target.ShapeSQLTable: if { , := d2sketch.Table() if != nil { return "", } fmt.Fprint(, ) } else { drawTable(, , , ) } := addAppendixItems(, , , , ) if != nil { return "", } fmt.Fprint(, `</g>`) fmt.Fprint(, ) return , nil case d2target.ShapeOval: if .DoubleBorder { if .Multiple { fmt.Fprint(, renderDoubleOval(, , , , "", , , )) } if { , := d2sketch.DoubleOval(, ) if != nil { return "", } fmt.Fprint(, ) } else { fmt.Fprint(, renderDoubleOval(, , , , .FillPattern, , , )) } } else { if .Multiple { fmt.Fprint(, renderOval(, , , , "", , , )) } if { , := d2sketch.Oval(, ) if != nil { return "", } fmt.Fprint(, ) } else { fmt.Fprint(, renderOval(, , , , .FillPattern, , , )) } } case d2target.ShapeImage: := d2themes.NewThemableElement("image", ) .X = float64(.Pos.X) .Y = float64(.Pos.Y) .Width = float64(.Width) .Height = float64(.Height) .Href = .Icon.String() .Fill = .Stroke = .Style = if .IconBorderRadius != 0 { .SetClipPathID() } fmt.Fprint(, .Render()) // TODO should standardize "" to rectangle case d2target.ShapeRectangle, d2target.ShapeSequenceDiagram, d2target.ShapeHierarchy, "": := math.MaxFloat64 if .BorderRadius != 0 { = float64(.BorderRadius) } if .ThreeDee { fmt.Fprint(, render3DRect(, , )) } else { if !.DoubleBorder { if .Multiple { := d2themes.NewThemableElement("rect", ) .X = float64(.Pos.X + 10) .Y = float64(.Pos.Y - 10) .Width = float64(.Width) .Height = float64(.Height) .Fill = .Stroke = .Style = .Rx = fmt.Fprint(, .Render()) } if { , := d2sketch.Rect(, ) if != nil { return "", } fmt.Fprint(, ) } else { := d2themes.NewThemableElement("rect", ) .X = float64(.Pos.X) .Y = float64(.Pos.Y) .Width = float64(.Width) .Height = float64(.Height) .Fill = .FillPattern = .FillPattern .Stroke = .Style = .Rx = if .Label != "" && label.FromString(.LabelPosition).IsBorder() { .SetMaskID(svg.LiteralID()) } fmt.Fprint(, .Render()) } } else { if .Multiple { := d2themes.NewThemableElement("rect", ) .X = float64(.Pos.X + 10) .Y = float64(.Pos.Y - 10) .Width = float64(.Width) .Height = float64(.Height) .Fill = .FillPattern = .FillPattern .Stroke = .Style = .Rx = fmt.Fprint(, .Render()) = d2themes.NewThemableElement("rect", ) .X = float64(.Pos.X + 10 + d2target.INNER_BORDER_OFFSET) .Y = float64(.Pos.Y - 10 + d2target.INNER_BORDER_OFFSET) .Width = float64(.Width - 2*d2target.INNER_BORDER_OFFSET) .Height = float64(.Height - 2*d2target.INNER_BORDER_OFFSET) .Fill = .Stroke = .Style = .Rx = fmt.Fprint(, .Render()) } if { , := d2sketch.DoubleRect(, ) if != nil { return "", } fmt.Fprint(, ) } else { := d2themes.NewThemableElement("rect", ) .X = float64(.Pos.X) .Y = float64(.Pos.Y) .Width = float64(.Width) .Height = float64(.Height) .Fill = .FillPattern = .FillPattern .Stroke = .Style = .Rx = fmt.Fprint(, .Render()) = d2themes.NewThemableElement("rect", ) .X = float64(.Pos.X + d2target.INNER_BORDER_OFFSET) .Y = float64(.Pos.Y + d2target.INNER_BORDER_OFFSET) .Width = float64(.Width - 2*d2target.INNER_BORDER_OFFSET) .Height = float64(.Height - 2*d2target.INNER_BORDER_OFFSET) .Fill = "transparent" .Stroke = .Style = .Rx = fmt.Fprint(, .Render()) } } } case d2target.ShapeHexagon: if .ThreeDee { fmt.Fprint(, render3DHexagon(, , )) } else { if .Multiple { := shape.NewShape(, geo.NewBox(, , )).GetSVGPathData() := d2themes.NewThemableElement("path", ) .Fill = .Stroke = .Style = for , := range { .D = fmt.Fprint(, .Render()) } } if { , := d2sketch.Paths(, , .GetSVGPathData()) if != nil { return "", } fmt.Fprint(, ) } else { := d2themes.NewThemableElement("path", ) .Fill = .FillPattern = .FillPattern .Stroke = .Style = for , := range .GetSVGPathData() { .D = fmt.Fprint(, .Render()) } } } case d2target.ShapeText, d2target.ShapeCode: default: if .Multiple { := shape.NewShape(, geo.NewBox(, , )).GetSVGPathData() := d2themes.NewThemableElement("path", ) .Fill = .Stroke = .Style = for , := range { .D = fmt.Fprint(, .Render()) } } if { , := d2sketch.Paths(, , .GetSVGPathData()) if != nil { return "", } fmt.Fprint(, ) } else { := d2themes.NewThemableElement("path", ) .Fill = .FillPattern = .FillPattern .Stroke = .Style = if .Label != "" && label.FromString(.LabelPosition).IsBorder() { .SetMaskID(svg.LiteralID()) } for , := range .GetSVGPathData() { .D = fmt.Fprint(, .Render()) } } } // // to examine shape's innerBox // innerBox := s.GetInnerBox() // el := d2themes.NewThemableElement("rect", inlineTheme) // 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()) // Closes the class=shape fmt.Fprint(, `</g>`) if .Icon != nil && .Type != d2target.ShapeImage && .Opacity != 0 { := label.FromString(.IconPosition) var *geo.Box if .IsOutside() { = .GetBox() } else { = .GetInnerBox() } := d2target.GetIconSize(, .IconPosition) := .GetPointOnBox(, label.PADDING, float64(), float64()) := []svg.Attribute{ svg.Attr("href", .Icon.String()), svg.FloatAttr("x", .X), svg.FloatAttr("y", .Y), svg.IntAttr("width", ), svg.IntAttr("height", ), } if .IconBorderRadius != 0 { = append(, svg.Attr("clip-path", fmt.Sprintf("inset(0 round %dpx)", .IconBorderRadius))) } fmt.Fprint(, svg.EmptyElement("image", ...)) } if .Label != "" && .Opacity != 0 { := label.FromString(.LabelPosition) // The foreignObject Markdown renderer historically defaulted an unset // label position to the middle of the shape. Keep that behavior when the // native label viewport is smaller than an explicitly sized shape. if .Language == "markdown" && == label.Unset { = label.InsideMiddleCenter } var *geo.Box if .IsOutside() || .IsBorder() { = .GetBox().Copy() // if it is 3d/multiple, place label using box around those if .ThreeDee { := d2target.THREE_DEE_OFFSET if .Type == d2target.ShapeHexagon { /= 2 } .TopLeft.Y -= float64() .Height += float64() .Width += d2target.THREE_DEE_OFFSET } else if .Multiple { .TopLeft.Y -= d2target.MULTIPLE_OFFSET .Height += d2target.MULTIPLE_OFFSET .Width += d2target.MULTIPLE_OFFSET } } else { = .GetInnerBox() } := .GetPointOnBox(, label.PADDING, float64(.LabelWidth), float64(.LabelHeight), ) if .IsBorder() { if { = makeBorderLabelMask(, , .LabelWidth, .LabelHeight, , .StrokeWidth, 1.0, ) } else { = makeBorderLabelMask(, , .LabelWidth, .LabelHeight, , .StrokeWidth, 1.0, nil) } } := "text" if .FontFamily == "mono" { = "text-mono" } if .Bold { += "-bold" } else if .Italic { += "-italic" } if .Underline { += " text-underline" } if .Language == "latex" { , := d2latex.Render(.Label) if != nil { return , } // Remove XML declaration and DOCTYPE from LaTeX SVG output := `<?xml version="1.0" encoding="UTF-8"?>` := `<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.0//EN" "http://www.w3.org/TR/2001/REC-SVG-20010904/DTD/svg10.dtd">` = strings.ReplaceAll(, , "") = strings.ReplaceAll(, , "") := d2themes.NewThemableElement("g", ) := label.FromString(.LabelPosition) if == label.Unset { = label.InsideMiddleCenter } var *geo.Box if .IsOutside() { = .GetBox() } else { = .GetInnerBox() } := .GetPointOnBox(, label.PADDING, float64(.LabelWidth), float64(.LabelHeight), ) .SetTranslate(.X, .Y) .Color = .Stroke .SetInnerSVG(svg.TrustedFragment()) fmt.Fprint(, .Render()) } else if .Language == "markdown" { , := .layout(.Label, .FontFamily, .FontSize) if != nil { return , } fmt.Fprint(, .render( , .X, .Y, .LabelWidth, .LabelHeight, .GetFontColor(), .Fill, .Link != "", .Underline, )) } else if .Language != "" { := lexers.Get(.Language) if == nil { = lexers.Fallback } for , := range []bool{true, false} { := "github" if ! { = "catppuccin-mocha" } := styles.Get() if == nil { return , errors.New(`code snippet style "github" not found`) } := formatters.Get("svg") if == nil { return , errors.New(`code snippet formatter "svg" not found`) } , := .Tokenise(nil, .Label) if != nil { return , } := styleToSVG() := "light-code" if ! { = "dark-code" } var string if .FontSize != d2fonts.FONT_SIZE_M { = fmt.Sprintf(` style="font-size:%v"`, .FontSize) } fmt.Fprintf(, `<g transform="translate(%s %s)" class="%s"%s>`, svg.FormatFloat(.TopLeft.X), svg.FormatFloat(.TopLeft.Y), , , ) := d2themes.NewThemableElement("rect", ) .Width = float64(.Width) .Height = float64(.Height) .Stroke = .Stroke .ClassName = "shape" .Style = fmt.Sprintf(`fill:%s;stroke-width:%d;`, .Get(chroma.Background).Background.String(), .StrokeWidth, ) fmt.Fprint(, .Render()) // Padding = 0.5em := float64(.FontSize) / 2. fmt.Fprintf(, `<g transform="translate(%s %s)">`, svg.FormatFloat(), svg.FormatFloat()) := textmeasure.CODE_LINE_HEIGHT for , := range chroma.SplitTokensIntoLines(.Tokens()) { fmt.Fprintf(, "<text class=\"text-mono\" x=\"0\" y=\"%sem\">", svg.FormatFloat(1+float64()*)) for , := range { := svgEscaper.Replace(.String()) := styleAttr(, .Type) if != "" { = fmt.Sprintf("<tspan %s>%s</tspan>", , ) } fmt.Fprint(, ) } fmt.Fprint(, "</text>") } fmt.Fprint(, "</g></g>") } } else { if .LabelFill != "" { := d2themes.NewThemableElement("rect", ) .X = .X .Y = .Y .Width = float64(.LabelWidth) .Height = float64(.LabelHeight) .Fill = .LabelFill fmt.Fprint(, .Render()) } := d2themes.NewThemableElement("text", ) .X = .X + float64(.LabelWidth)/2 // text is vertically positioned at its baseline which is at labelTL+FontSize .Y = .Y + float64(.FontSize) .Fill = .GetFontColor() .ClassName = .Style = fmt.Sprintf("text-anchor:%s;font-size:%vpx", "middle", .FontSize) .SetInnerSVG(svg.TrustedFragment(RenderText(.Label, .X, float64(.LabelHeight)))) fmt.Fprint(, .Render()) } } if .Tooltip != "" && .TooltipPosition == "" { fmt.Fprintf(, `<title>%s</title>`, svg.EscapeText(.Tooltip), ) } = addAppendixItems(, , , , ) if != nil { return "", } fmt.Fprint(, ) return , nil } func applyIconBorderRadius( svg.ID, d2target.Shape) string { := geo.NewBox( geo.NewPoint(float64(.Pos.X), float64(.Pos.Y)), float64(.Width), float64(.Height), ) , := .TopLeft.X+.Width, .TopLeft.Y := svg.OpenElement("clipPath", svg.Attr("id", .String())) += fmt.Sprintf(`<path d="M %s %s L %s %s S %s %s %s %s `, svg.FormatFloat(.TopLeft.X), svg.FormatFloat(.TopLeft.Y+float64(.IconBorderRadius)), svg.FormatFloat(.TopLeft.X), svg.FormatFloat(.TopLeft.Y+float64(.IconBorderRadius)), svg.FormatFloat(.TopLeft.X), svg.FormatFloat(.TopLeft.Y), svg.FormatFloat(.TopLeft.X+float64(.IconBorderRadius)), svg.FormatFloat(.TopLeft.Y)) += fmt.Sprintf(`L %s %s L %s %s `, svg.FormatFloat(.TopLeft.X+.Width-float64(.IconBorderRadius)), svg.FormatFloat(.TopLeft.Y), svg.FormatFloat(-float64(.IconBorderRadius)), svg.FormatFloat()) += fmt.Sprintf(`S %s %s %s %s `, svg.FormatFloat(), svg.FormatFloat(), svg.FormatFloat(), svg.FormatFloat(+float64(.IconBorderRadius))) += fmt.Sprintf(`L %s %s `, svg.FormatFloat(), svg.FormatFloat(+.Height-float64(.IconBorderRadius))) += fmt.Sprintf(`S %s %s %s %s `, svg.FormatFloat(), svg.FormatFloat(+.Height), svg.FormatFloat(-float64(.IconBorderRadius)), svg.FormatFloat(+.Height)) += fmt.Sprintf(`L %s %s `, svg.FormatFloat(.TopLeft.X+float64(.IconBorderRadius)), svg.FormatFloat(.TopLeft.Y+.Height)) += fmt.Sprintf(`S %s %s %s %s`, svg.FormatFloat(.TopLeft.X), svg.FormatFloat(.TopLeft.Y+.Height), svg.FormatFloat(.TopLeft.X), svg.FormatFloat(.TopLeft.Y+.Height-float64(.IconBorderRadius))) += fmt.Sprintf(`L %s %s`, svg.FormatFloat(.TopLeft.X), svg.FormatFloat(.TopLeft.Y+float64(.IconBorderRadius))) += fmt.Sprintf(`Z %s %s" `, svg.FormatFloat(.TopLeft.X), svg.FormatFloat(.TopLeft.Y)) return + `fill="none" /> </clipPath>` } func addAppendixItems( io.Writer, string, d2target.Shape, shape.Shape, *markdownRenderer) error { var , *geo.Point if .Tooltip != "" || .Link != "" { := .Tooltip != "" && .Link != "" := geo.NewPoint(float64(.Pos.X+.Width), float64(.Pos.Y)) := geo.NewPoint( float64(.Pos.X)+float64(.Width)/2., float64(.Pos.Y)+float64(.Height)/2., ) := geo.Vector{-2 * appendixIconRadius, 0} var bool switch .GetType() { case shape.STEP_TYPE, shape.HEXAGON_TYPE, shape.QUEUE_TYPE, shape.PAGE_TYPE: // trace straight left for these .Y = float64(.Pos.Y) case shape.PACKAGE_TYPE: // trace straight down .X = float64(.Pos.X + .Width) case shape.CIRCLE_TYPE, shape.OVAL_TYPE, shape.DIAMOND_TYPE, shape.PERSON_TYPE, shape.CLOUD_TYPE, shape.CYLINDER_TYPE: if { = true = .AddVector() } } := .VectorTo() = shape.TraceToShapeBorder(, , .AddVector()) if { if { // these shapes should have p1 on shape border = .AddVector(.Reverse()) , = , } else { = .AddVector() } } } if .Tooltip != "" { if .TooltipPosition != "" { , := renderPositionedTooltip(, .TooltipPosition, ) if != nil { return } fmt.Fprint(, ) } else { := int(math.Ceil(.X)) := int(math.Ceil(.Y)) fmt.Fprintf(, `<g transform="translate(%d %d)" class="appendix-icon"><title>%s</title>%s</g>`, -appendixIconRadius, -appendixIconRadius, svg.EscapeText(.Tooltip), fmt.Sprintf(TooltipIcon, , svg.SVGID(.ID)), ) } } if .Link != "" { if == nil { = } := int(math.Ceil(.X)) := int(math.Ceil(.Y)) fmt.Fprintf(, `<g transform="translate(%d %d)" class="appendix-icon">%s</g>`, -appendixIconRadius, -appendixIconRadius, fmt.Sprintf(LinkIcon, , svg.SVGID(.ID)), ) } return nil } func calculateTooltipPosition( d2target.Shape, string, , int) (, float64, string, , float64) { := float64(.Pos.X) := float64(.Pos.Y) := float64(.Width) := float64(.Height) , = d2target.CalculateTooltipPosition(, , , , , , ) switch { case "top-left": = "bottom" = 20 = float64() case "top-center": = "bottom" = float64() / 2 = float64() case "top-right": = "bottom" = float64() - 20 = float64() case "center-left": = "right" = float64() = float64() / 2 case "center-right": = "left" = 0 = float64() / 2 case "bottom-left": = "top" = 20 = 0 case "bottom-center": = "top" = float64() / 2 = 0 case "bottom-right": = "top" = float64() - 20 = 0 default: = "bottom" = float64() / 2 = float64() } return , , , , } func renderTooltipTail( string, , float64, *d2themes.Theme) string { := 8.0 var string switch { case "top": = fmt.Sprintf(`M %s %s L %s %s L %s %s Z`, svg.FormatFloat(-/2), svg.FormatFloat(), svg.FormatFloat(+/2), svg.FormatFloat(), svg.FormatFloat(), svg.FormatFloat(-)) case "bottom": = fmt.Sprintf(`M %s %s L %s %s L %s %s Z`, svg.FormatFloat(-/2), svg.FormatFloat(), svg.FormatFloat(+/2), svg.FormatFloat(), svg.FormatFloat(), svg.FormatFloat(+)) case "left": = fmt.Sprintf(`M %s %s L %s %s L %s %s Z`, svg.FormatFloat(), svg.FormatFloat(-/2), svg.FormatFloat(), svg.FormatFloat(+/2), svg.FormatFloat(-), svg.FormatFloat()) case "right": = fmt.Sprintf(`M %s %s L %s %s L %s %s Z`, svg.FormatFloat(), svg.FormatFloat(-/2), svg.FormatFloat(), svg.FormatFloat(+/2), svg.FormatFloat(+), svg.FormatFloat()) default: return "" } := d2themes.NewThemableElement("path", ) .D = .Fill = color.N7 .Stroke = color.N5 .AddAttributes(svg.IntAttr("stroke-width", 1)) return .Render() } func renderPositionedTooltip( d2target.Shape, string, *markdownRenderer) (string, error) { if .Tooltip == "" || == "" { return "", nil } , := .layout(.Tooltip, .FontFamily, d2fonts.FONT_SIZE_M) if != nil { return "", } := .Width + 20 := .Height + 20 , , , , := calculateTooltipPosition(, , , ) := .render( , +10, +10, .Width, .Height, color.N1, "", false, false, ) := d2themes.NewThemableElement("rect", .inlineTheme) .X = .Y = .Width = float64() .Height = float64() .Rx = 4 .Ry = 4 .Fill = color.N7 .Stroke = color.N5 .AddAttributes(svg.IntAttr("stroke-width", 1)) := renderTooltipTail(, +, +, .inlineTheme) return fmt.Sprintf(`<g class="positioned-tooltip">%s%s%s</g>`, .Render(), , ), nil } func ( string, , float64) string { if !strings.Contains(, "\n") { return svg.EscapeText() } := []string{} := strings.Split(, "\n") for , := range { := / float64(len()) if == 0 { = 0 } := svg.EscapeText() if == "" { // if there are multiple newlines in a row we still need text for the tspan to render = " " } = append(, fmt.Sprintf(`<tspan x="%s" dy="%s">%s</tspan>`, svg.FormatFloat(), svg.FormatFloat(), )) } return strings.Join(, "") } func ( *bytes.Buffer, , string, *d2fonts.FontFamily, *d2fonts.FontFamily, string) { embedFonts(, , , , , , nil) } func embedFonts( *bytes.Buffer, , string, *d2fonts.FontFamily, *d2fonts.FontFamily, string, fontCorpora) { if == nil || * == "" { := d2fonts.SourceSansPro = & } if == nil || * == "" { := d2fonts.SourceCodePro = & } // Markdown generates text that may not exist literally in the D2 source, // such as list markers and decoded entities. Include those rendered runs in // every font subset, including multi-board animations where render-local // Markdown layout state is not available here. for , := range nativeMarkdownTextPattern.FindAllStringSubmatch(, -1) { += html.UnescapeString([1]) } = restrictFontCorpora(, ) fmt.Fprint(, `<style type="text/css"><![CDATA[`) appendFontOnTrigger( , , []string{ `class="text"`, `class="text `, `class="md"`, `class="md `, }, , "text", , func( string) string { return fmt.Sprintf(` .%s .text { font-family: "%s-font-regular"; } @font-face { font-family: %s-font-regular; src: url("%s"); }`, , , , .Font(0, d2fonts.FONT_STYLE_REGULAR).GetEncodedSubset(), ) }, ) appendFontOnTrigger( , , []string{ `text-semibold`, }, , "text-semibold", , func( string) string { return fmt.Sprintf(` .%s .text-semibold { font-family: "%s-font-semibold"; } @font-face { font-family: %s-font-semibold; src: url("%s"); }`, , , , .Font(0, d2fonts.FONT_STYLE_SEMIBOLD).GetEncodedSubset(), ) }, ) appendOnTrigger( , , []string{ `text-underline`, }, ` .text-underline { text-decoration: underline; }`, ) appendOnTrigger( , , []string{ `text-link`, }, ` .text-link { fill: blue; } .text-link:visited { fill: purple; }`, ) appendOnTrigger( , , []string{ `animated-connection`, }, ` @keyframes dashdraw { from { stroke-dashoffset: 0; } } `, ) appendOnTrigger( , , []string{ `animated-shape`, }, ` @keyframes shapeappear { 0%, 100% { transform: translateY(0); filter: drop-shadow(0px 0px 0px rgba(0,0,0,0)); } 50% { transform: translateY(-4px); filter: drop-shadow(0px 12.6px 25.2px rgba(50,50,93,0.25)) drop-shadow(0px 7.56px 15.12px rgba(0,0,0,0.1)); } } .animated-shape { animation: shapeappear 1s linear infinite; } `, ) appendOnTrigger( , , []string{ `appendix-icon`, }, ` .appendix-icon { filter: drop-shadow(0px 0px 32px rgba(31, 36, 58, 0.1)); }`, ) appendFontOnTrigger( , , []string{ `text-bold`, `<b>`, `<strong>`, }, , "text-bold", , func( string) string { return fmt.Sprintf(` .%s .text-bold { font-family: "%s-font-bold"; } @font-face { font-family: %s-font-bold; src: url("%s"); }`, , , , .Font(0, d2fonts.FONT_STYLE_BOLD).GetEncodedSubset(), ) }, ) appendFontOnTrigger( , , []string{ `text-italic`, `<em>`, `<dfn>`, }, , "text-italic", , func( string) string { return fmt.Sprintf(` .%s .text-italic { font-family: "%s-font-italic"; } @font-face { font-family: %s-font-italic; src: url("%s"); }`, , , , .Font(0, d2fonts.FONT_STYLE_ITALIC).GetEncodedSubset(), ) }, ) appendFontOnTrigger( , , []string{ `text-mono`, `<pre>`, `<code>`, `<kbd>`, `<samp>`, }, , "text-mono", , func( string) string { return fmt.Sprintf(` .%s .text-mono { font-family: "%s-font-mono"; } @font-face { font-family: %s-font-mono; src: url("%s"); }`, , , , .Font(0, d2fonts.FONT_STYLE_REGULAR).GetEncodedSubset(), ) }, ) appendFontOnTrigger( , , []string{ `text-mono-semibold`, }, , "text-mono-semibold", , func( string) string { return fmt.Sprintf(` .%s .text-mono-semibold { font-family: "%s-font-mono-semibold"; } @font-face { font-family: %s-font-mono-semibold; src: url("%s"); }`, , , , .Font(0, d2fonts.FONT_STYLE_SEMIBOLD).GetEncodedSubset(), ) }, ) appendFontOnTrigger( , , []string{ `text-mono-bold`, }, , "text-mono-bold", , func( string) string { return fmt.Sprintf(` .%s .text-mono-bold { font-family: "%s-font-mono-bold"; } @font-face { font-family: %s-font-mono-bold; src: url("%s"); }`, , , , .Font(0, d2fonts.FONT_STYLE_BOLD).GetEncodedSubset(), ) }, ) appendFontOnTrigger( , , []string{ `text-mono-italic`, }, , "text-mono-italic", , func( string) string { return fmt.Sprintf(` .%s .text-mono-italic { font-family: "%s-font-mono-italic"; } @font-face { font-family: %s-font-mono-italic; src: url("%s"); }`, , , , .Font(0, d2fonts.FONT_STYLE_ITALIC).GetEncodedSubset(), ) }, ) appendOnTrigger( , , []string{ `sketch-overlay-bright`, }, fmt.Sprintf(` .sketch-overlay-bright { fill: url(#streaks-bright-%s); mix-blend-mode: darken; }`, ), ) appendOnTrigger( , , []string{ `sketch-overlay-normal`, }, fmt.Sprintf(` .sketch-overlay-normal { fill: url(#streaks-normal-%s); mix-blend-mode: color-burn; }`, ), ) appendOnTrigger( , , []string{ `sketch-overlay-dark`, }, fmt.Sprintf(` .sketch-overlay-dark { fill: url(#streaks-dark-%s); mix-blend-mode: overlay; }`, ), ) appendOnTrigger( , , []string{ `sketch-overlay-darker`, }, fmt.Sprintf(` .sketch-overlay-darker { fill: url(#streaks-darker-%s); mix-blend-mode: lighten; }`, ), ) fmt.Fprint(, `]]></style>`) } var nativeMarkdownTextPattern = regexp.MustCompile(`<text\b[^>]*\bclass="[^"]*\bmd-text\b[^"]*"[^>]*>([^<]*)</text>`) func appendOnTrigger( *bytes.Buffer, string, []string, string) { for , := range { if strings.Contains(, ) { fmt.Fprint(, ) break } } } // appendOnTriggerLazy avoids constructing a font subset unless its CSS is used. // Match the source and trigger order used by appendOnTrigger exactly. func appendOnTriggerLazy( *bytes.Buffer, string, []string, func() string) { for , := range { if strings.Contains(, ) { fmt.Fprint(, ()) break } } } var DEFAULT_DARK_THEME *int64 = nil // no theme selected func ( *d2target.Diagram, *RenderOpts) ([]byte, error) { if == nil { return nil, fmt.Errorf("render target is nil") } if := validateRenderLinks(); != nil { return nil, } if := d2target.ValidateRenderTarget(); != nil { return nil, } return renderValidated(, ) } func validateRenderLinks( *d2target.Diagram) error { for , := range .Shapes { if textmeasure.IsDangerousLink(.Link) { return fmt.Errorf("shape %q uses an unsafe link URL scheme", .ID) } } for , := range .Connections { if textmeasure.IsDangerousLink(.Link) { return fmt.Errorf("connection %q uses an unsafe link URL scheme", .ID) } } return nil } // renderValidated renders one board after its complete target tree has been // validated. Keeping this internal lets RenderMultiboard avoid repeatedly // walking every descendant subtree. func renderValidated( *d2target.Diagram, *RenderOpts) ([]byte, error) { if := validateRenderPaints(, ); != nil { return nil, } if != nil && .Isometric != nil && *.Isometric { return renderIsometric(, ) } := false := DEFAULT_PADDING , := .BoundingBox() := d2themescatalog.NeutralDefault.ID := DEFAULT_DARK_THEME var *float64 if != nil { if .Pad != nil { var error , = validatePadding(, , *.Pad) if != nil { return nil, } } if .Sketch != nil && *.Sketch { = true } if .ThemeID != nil { = *.ThemeID } = .DarkThemeID = .Scale } else { = &RenderOpts{} } := &bytes.Buffer{} // only define shadow filter if a shape uses it for , := range .Shapes { if .Shadow { defineShadowFilter() break } } if color.IsGradient(.Root.Fill) { defineGradients(, .Root.Fill) } if color.IsGradient(.Root.Stroke) { defineGradients(, .Root.Stroke) } for , := range .Shapes { if color.IsGradient(.Fill) { defineGradients(, .Fill) } if color.IsGradient(.Stroke) { defineGradients(, .Stroke) } if color.IsGradient(.Color) { defineGradients(, .Color) } } for , := range .Connections { if color.IsGradient(.Stroke) { defineGradients(, .Stroke) } if color.IsGradient(.Fill) { defineGradients(, .Fill) } } // Apply hash on IDs for targeting, to be specific for this diagram , := .HashID(.Salt) if != nil { return nil, } // Some targeting is still per-board, like masks for connections := if != nil && .MasterID != "" { = .MasterID } // 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 := make(map[string]d2target.Shape) := make([]DiagramObject, 0, len(.Shapes)+len(.Connections)) for , := range .Shapes { [.ID] = = append(, ) } for , := range .Connections { = append(, ) } sortObjects() := &bytes.Buffer{} var []string := map[string]struct{}{} var *d2themes.Theme // We only want to inline when no dark theme is specified, otherwise the inline style will override the CSS if == nil { = go2.Pointer(d2themescatalog.Find()) .ApplyOverrides(.ThemeOverrides) } := newMarkdownRenderer(.FontFamily, .MonoFontFamily, ) for , := range { if , := .(d2target.Connection); { , := drawConnection(, , , , , , , ) if != nil { return nil, } if != "" { = append(, ) } } else if , := .(d2target.Shape); { , := drawShape(, , , , , , ) if != nil { return nil, } else if != "" { = append(, ) } } else { return nil, fmt.Errorf("unknown object of type %T", ) } } // add all appendix items afterwards so they are always on top fmt.Fprint(, ) if .Legend != nil && (len(.Legend.Shapes) > 0 || len(.Legend.Connections) > 0) { := &bytes.Buffer{} := RenderLegend(, , , ) if != nil { return nil, } fmt.Fprint(, ) } // Note: we always want this since we reference it on connections even if there end up being no masked labels , , , := dimensions(, , , ) if .Legend != nil && (len(.Legend.Shapes) > 0 || len(.Legend.Connections) > 0) { := LEGEND_PADDING + LEGEND_FONT_SIZE + LEGEND_ITEM_SPACING := 0 := 0 , := textmeasure.NewRuler() if != nil { for , := range .Legend.Shapes { if .Label == "" { continue } := &d2target.MText{ Text: .Label, FontSize: LEGEND_FONT_SIZE, } := d2graph.GetTextDimensions(nil, , , fontToFamily(.FontFamily)) = go2.IntMax(, .Width) += go2.IntMax(.Height, LEGEND_ICON_SIZE) + LEGEND_ITEM_SPACING ++ } for , := range .Legend.Connections { if .Label == "" { continue } := &d2target.MText{ Text: .Label, FontSize: LEGEND_FONT_SIZE, } := d2graph.GetTextDimensions(nil, , , fontToFamily(.FontFamily)) = go2.IntMax(, .Width) += go2.IntMax(.Height, LEGEND_ICON_SIZE) + LEGEND_ITEM_SPACING ++ } if > 0 { -= LEGEND_ITEM_SPACING / 2 } += LEGEND_PADDING if > 0 && > 0 { := LEGEND_PADDING*2 + LEGEND_ICON_SIZE + LEGEND_PADDING + := .Y - if < .Y { = .Y } := .X + LEGEND_CORNER_PADDING + if + < { = - + /2 } if < { := - -= += } := + if + < { = - + /2 } } } } if < 0 || < 0 { return nil, invalidPaddingError(int64()) } fmt.Fprint(, strings.Join([]string{ fmt.Sprintf(`<mask id="%s" maskUnits="userSpaceOnUse" x="%d" y="%d" width="%d" height="%d">`, , , , , , ), fmt.Sprintf(`<rect x="%d" y="%d" width="%d" height="%d" fill="white"></rect>`, , , , , ), strings.Join(, "\n"), `</mask>`, }, "\n")) // Validate the theme before expanding the background, preserving error order. var string if .MasterID == "" { var error , = ThemeCSS(, &, , .ThemeOverrides, .DarkThemeOverrides) if != nil { return nil, } } // This shift is for background el to envelop the diagram := int(math.Ceil(float64(.Root.StrokeWidth) / 2.)) , , , , := expandDimensions(, , , , ) if ! { return nil, invalidPaddingError(int64()) } := d2themes.NewThemableElement("rect", ) // We don't want to change the document viewbox, only the background el .X = float64() .Y = float64() .Width = float64() .Height = float64() .Fill = .Root.Fill .Stroke = .Root.Stroke .FillPattern = .Root.FillPattern .Rx = float64(.Root.BorderRadius) if .Root.StrokeDash != 0 { , := svg.GetStrokeDashAttributes(float64(.Root.StrokeWidth), .Root.StrokeDash) .StrokeDashArray = fmt.Sprintf("%s, %s", svg.FormatFloat(), svg.FormatFloat()) } .AddAttributes(svg.IntAttr("stroke-width", .Root.StrokeWidth)) // This shift is for viewbox to envelop the background el , , , , = expandDimensions(, , , , ) if ! { return nil, invalidPaddingError(int64()) } := "" if .Root.DoubleBorder { := d2target.INNER_BORDER_OFFSET , , , , = expandDimensions(, , , , +) if ! { return nil, invalidPaddingError(int64()) } := .Copy() // No need to double-paint .Fill = "transparent" .X = float64() .Y = float64() .Width = float64() .Height = float64() = .Render() , , , , = expandDimensions(, , , , ) if ! { return nil, invalidPaddingError(int64()) } } := .Render() // generate style elements that will be appended to the SVG tag := &bytes.Buffer{} if .MasterID == "" { , , := collectFontCorporaAndClasses(.String()) if ! { = nil } // Appendix postprocessing can add footnotes and numbered icons after // Render. Preserve both the old face selection and its whole corpus // whenever the diagram can receive that additional text. for , := range .Shapes { if .Tooltip != "" || .Link != "" || .PrettyLink != "" { = nil break } } embedFonts(, , .String(), .FontFamily, .MonoFontFamily, .GetCorpus()+.generatedCorpus(), ) // Must precede DefineFillPatterns, after all text is rendered. if == nil { = pruneThemeCSS(, .String(), , ) } else { = pruneThemeCSSWithClasses(, , , ) } fmt.Fprintf(, `<style type="text/css"><![CDATA[%s%s]]></style>`, BaseStylesheet, ) if { d2sketch.DefineFillPatterns(, ) } } := .String() := "" for , := range d2ast.FillPatterns { if strings.Contains(, fmt.Sprintf("%s-overlay", )) || strings.EqualFold(.Root.FillPattern, ) { if == "" { fmt.Fprint(, `<style type="text/css"><![CDATA[`) } switch { case "dots": += fmt.Sprintf(dots, ) case "lines": += fmt.Sprintf(lines, ) case "grain": += fmt.Sprintf(patternassets.GrainSVG(), ) case "paper": += fmt.Sprintf(patternassets.PaperSVG(), ) } fmt.Fprintf(, ` .%s-overlay { fill: url(#%s-%s); mix-blend-mode: multiply; }`, , , ) } } if != "" { fmt.Fprint(, `]]></style>`) fmt.Fprint(, "<defs>") fmt.Fprint(, ) fmt.Fprint(, "</defs>") } var string if != nil { = fmt.Sprintf(` width="%d" height="%d"`, int(math.Ceil((*)*float64())), int(math.Ceil((*)*float64())), ) } := "xMinYMin" if .Center != nil && *.Center { = "xMidYMid" } := "" := "" := "" := "" := "g" // Many things change when this is rendering for animation if .MasterID == "" { := "" if .OmitVersion == nil || !*.OmitVersion { = fmt.Sprintf(`data-d2-version="%s"`, version.Version) } = 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>`, , , , , , ) if .NoXMLTag == nil || !*.NoXMLTag { = `<?xml version="1.0" encoding="utf-8"?>` } = "</svg>" = `d2-svg` = "svg" } := fmt.Sprintf(`%s%s<%s class="%s" width="%d" height="%d" viewBox="%d %d %d %d">%s%s%s%s</%s>%s`, , , , strings.Join([]string{, }, " "), , , , , , , , , .String(), .String(), , , ) return []byte(), nil } func ( string, *int64, *int64, , *d2target.ThemeOverrides) ( string, error) { if := validateThemeOverrides(, "theme override"); != nil { return "", } if := validateThemeOverrides(, "dark theme override"); != nil { return "", } if == nil { = &d2themescatalog.NeutralDefault.ID } , := singleThemeRulesets(, *, ) if != nil { return "", } if != nil { , := singleThemeRulesets(, *, ) if != nil { return "", } += fmt.Sprintf("@media screen and (prefers-color-scheme:dark){%s}", ) } return , nil } func singleThemeRulesets( string, int64, *d2target.ThemeOverrides) ( string, error) { := "" := d2themescatalog.Find() .ApplyOverrides() // Global theme colors for , := range []string{"fill", "stroke", "background-color", "color"} { += 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;}`, , , , .Colors.Neutrals.N1, , , , .Colors.Neutrals.N2, , , , .Colors.Neutrals.N3, , , , .Colors.Neutrals.N4, , , , .Colors.Neutrals.N5, , , , .Colors.Neutrals.N6, , , , .Colors.Neutrals.N7, , , , .Colors.B1, , , , .Colors.B2, , , , .Colors.B3, , , , .Colors.B4, , , , .Colors.B5, , , , .Colors.B6, , , , .Colors.AA2, , , , .Colors.AA4, , , , .Colors.AA5, , , , .Colors.AB4, , , , .Colors.AB5, ) } // Appendix += fmt.Sprintf(".appendix text.text{fill:%s}", .Colors.Neutrals.N1) // Retain the Markdown custom properties for output compatibility and for // consumers that style the native .md group. Native D2 primitives use the // corresponding fill/stroke theme classes directly. += 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;}", .Colors.Neutrals.N1, .Colors.Neutrals.N2, .Colors.Neutrals.N3, .Colors.Neutrals.N7, .Colors.Neutrals.N6, .Colors.B1, .Colors.B2, .Colors.Neutrals.N6, .Colors.B2, .Colors.B2, .Colors.Neutrals.N2, "red", ) // Sketch style specific rulesets // B , := color.LuminanceCategory(.Colors.B1) if != nil { return "", } += fmt.Sprintf(".sketch-overlay-%s{fill:url(#streaks-%s-%s);mix-blend-mode:%s}", color.B1, , , blendMode()) , = color.LuminanceCategory(.Colors.B2) if != nil { return "", } += fmt.Sprintf(".sketch-overlay-%s{fill:url(#streaks-%s-%s);mix-blend-mode:%s}", color.B2, , , blendMode()) , = color.LuminanceCategory(.Colors.B3) if != nil { return "", } += fmt.Sprintf(".sketch-overlay-%s{fill:url(#streaks-%s-%s);mix-blend-mode:%s}", color.B3, , , blendMode()) , = color.LuminanceCategory(.Colors.B4) if != nil { return "", } += fmt.Sprintf(".sketch-overlay-%s{fill:url(#streaks-%s-%s);mix-blend-mode:%s}", color.B4, , , blendMode()) , = color.LuminanceCategory(.Colors.B5) if != nil { return "", } += fmt.Sprintf(".sketch-overlay-%s{fill:url(#streaks-%s-%s);mix-blend-mode:%s}", color.B5, , , blendMode()) , = color.LuminanceCategory(.Colors.B6) if != nil { return "", } += fmt.Sprintf(".sketch-overlay-%s{fill:url(#streaks-%s-%s);mix-blend-mode:%s}", color.B6, , , blendMode()) // AA , = color.LuminanceCategory(.Colors.AA2) if != nil { return "", } += fmt.Sprintf(".sketch-overlay-%s{fill:url(#streaks-%s-%s);mix-blend-mode:%s}", color.AA2, , , blendMode()) , = color.LuminanceCategory(.Colors.AA4) if != nil { return "", } += fmt.Sprintf(".sketch-overlay-%s{fill:url(#streaks-%s-%s);mix-blend-mode:%s}", color.AA4, , , blendMode()) , = color.LuminanceCategory(.Colors.AA5) if != nil { return "", } += fmt.Sprintf(".sketch-overlay-%s{fill:url(#streaks-%s-%s);mix-blend-mode:%s}", color.AA5, , , blendMode()) // AB , = color.LuminanceCategory(.Colors.AB4) if != nil { return "", } += fmt.Sprintf(".sketch-overlay-%s{fill:url(#streaks-%s-%s);mix-blend-mode:%s}", color.AB4, , , blendMode()) , = color.LuminanceCategory(.Colors.AB5) if != nil { return "", } += fmt.Sprintf(".sketch-overlay-%s{fill:url(#streaks-%s-%s);mix-blend-mode:%s}", color.AB5, , , blendMode()) // Neutrals , = color.LuminanceCategory(.Colors.Neutrals.N1) if != nil { return "", } += fmt.Sprintf(".sketch-overlay-%s{fill:url(#streaks-%s-%s);mix-blend-mode:%s}", color.N1, , , blendMode()) , = color.LuminanceCategory(.Colors.Neutrals.N2) if != nil { return "", } += fmt.Sprintf(".sketch-overlay-%s{fill:url(#streaks-%s-%s);mix-blend-mode:%s}", color.N2, , , blendMode()) , = color.LuminanceCategory(.Colors.Neutrals.N3) if != nil { return "", } += fmt.Sprintf(".sketch-overlay-%s{fill:url(#streaks-%s-%s);mix-blend-mode:%s}", color.N3, , , blendMode()) , = color.LuminanceCategory(.Colors.Neutrals.N4) if != nil { return "", } += fmt.Sprintf(".sketch-overlay-%s{fill:url(#streaks-%s-%s);mix-blend-mode:%s}", color.N4, , , blendMode()) , = color.LuminanceCategory(.Colors.Neutrals.N5) if != nil { return "", } += fmt.Sprintf(".sketch-overlay-%s{fill:url(#streaks-%s-%s);mix-blend-mode:%s}", color.N5, , , blendMode()) , = color.LuminanceCategory(.Colors.Neutrals.N6) if != nil { return "", } += fmt.Sprintf(".sketch-overlay-%s{fill:url(#streaks-%s-%s);mix-blend-mode:%s}", color.N6, , , blendMode()) , = color.LuminanceCategory(.Colors.Neutrals.N7) if != nil { return "", } += fmt.Sprintf(".sketch-overlay-%s{fill:url(#streaks-%s-%s);mix-blend-mode:%s}", color.N7, , , blendMode()) if .IsDark() { += ".light-code{display: none}" += ".dark-code{display: block}" } else { += ".light-code{display: block}" += ".dark-code{display: none}" } return , nil } func blendMode( string) string { switch { case "bright": return "darken" case "normal": return "color-burn" case "dark": return "overlay" case "darker": return "lighten" } panic("invalid luminance category") } type DiagramObject interface { GetID() string GetZIndex() int } // 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 func sortObjects( []DiagramObject) { sort.SliceStable(, func(, int) bool { // first sort by zIndex := [].GetZIndex() := [].GetZIndex() if != { return < } // then, if both are shapes, parents come before their children , := [].(d2target.Shape) , := [].(d2target.Shape) if && { return .Level < .Level } // then, shapes come before connections , := [].(d2target.Connection) return && }) } func fontToFamily( string) *d2fonts.FontFamily { , := d2fonts.D2_FONT_TO_FAMILY[] if ! { return nil } return & } func hash( string) string { const = "lalalas" := fnv.New32a() .Write([]byte(fmt.Sprintf("%s%s", , ))) return fmt.Sprint(.Sum32()) } func ( *d2target.Diagram, *RenderOpts) ([][]byte, error) { if := d2target.ValidateRenderTarget(); != nil { return nil, } return renderMultiboard(, ) } func renderMultiboard( *d2target.Diagram, *RenderOpts) ([][]byte, error) { var [][]byte for , := range .Layers { , := (, ) if != nil { return nil, } = append(, ...) } for , := range .Scenarios { , := (, ) if != nil { return nil, } = append(, ...) } for , := range .Steps { , := (, ) if != nil { return nil, } = append(, ...) } if !.IsFolderOnly { if := validateRenderLinks(); != nil { return , } , := renderValidated(, ) if != nil { return , } = append([][]byte{}, ...) return , nil } return , nil }