1
2
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
48 var TooltipIcon string
49
50
51 var LinkIcon string
52
53
54 var BaseStylesheet string
55
56
57 var MarkdownCSS string
58
59
60 var dots string
61
62
63 var lines string
64
65
66 var grain string
67
68
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
81 Scale *float64
82
83
84
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
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
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
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
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
452
453 path = append(path, fmt.Sprintf("C %f %f %f %f %f %f",
454
455 prevTarget.X+prevTranslations.X,
456 prevTarget.Y+prevTranslations.Y,
457
458 currTarget.X-nextTranslations.X,
459 currTarget.Y-nextTranslations.Y,
460
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
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
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
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
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
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
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
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
795 var borderSegments []string
796
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
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
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
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
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
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
915 opacityStyle := ""
916 if targetShape.Opacity != 1.0 {
917 opacityStyle = fmt.Sprintf(" style='opacity:%f'", targetShape.Opacity)
918 }
919
920
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
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
1201
1202
1203
1204
1205
1206
1207
1208
1209
1210
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
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
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
1358 render = strings.ReplaceAll(render, "<hr>", "<hr />")
1359
1360 mdEl := d2themes.NewThemableElement("div")
1361 mdEl.ClassName = "md"
1362 mdEl.Content = render
1363
1364
1365
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
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
1428 center.Y = float64(targetShape.Pos.Y)
1429 case shape.PACKAGE_TYPE:
1430
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
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
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
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
1783 for _, s := range diagram.Shapes {
1784 if s.Shadow {
1785 defineShadowFilter(buf)
1786 break
1787 }
1788 }
1789
1790
1791 diagramHash, err := diagram.HashID()
1792 if err != nil {
1793 return nil, err
1794 }
1795
1796 isolatedDiagramHash := diagramHash
1797 if opts != nil && opts.MasterID != "" {
1798 diagramHash = opts.MasterID
1799 }
1800
1801
1802
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
1840 fmt.Fprint(buf, appendixItemBuf)
1841
1842
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
1856 upperBuf := &bytes.Buffer{}
1857 if opts.MasterID == "" {
1858 EmbedFonts(upperBuf, diagramHash, buf.String(), diagram.FontFamily, diagram.GetCorpus())
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
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
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
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
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
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
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
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
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
2107 out += fmt.Sprintf(".appendix text.text{fill:%s}", theme.Colors.Neutrals.N1)
2108
2109
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,
2117 "red",
2118 )
2119
2120
2121
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
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
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
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
2250
2251
2252
2253
2254
2255 func sortObjects(allObjects []DiagramObject) {
2256 sort.SliceStable(allObjects, func(i, j int) bool {
2257
2258 iZIndex := allObjects[i].GetZIndex()
2259 jZIndex := allObjects[j].GetZIndex()
2260 if iZIndex != jZIndex {
2261 return iZIndex < jZIndex
2262 }
2263
2264
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
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