1 package d2graph
2
3 import (
4 "bytes"
5 "context"
6 "errors"
7 "fmt"
8 "io/fs"
9 "math"
10 "net/url"
11 "sort"
12 "strconv"
13 "strings"
14
15 "golang.org/x/text/cases"
16 "golang.org/x/text/language"
17
18 "oss.terrastruct.com/util-go/go2"
19
20 "oss.terrastruct.com/d2/d2ast"
21 "oss.terrastruct.com/d2/d2format"
22 "oss.terrastruct.com/d2/d2parser"
23 "oss.terrastruct.com/d2/d2renderers/d2fonts"
24 "oss.terrastruct.com/d2/d2renderers/d2latex"
25 "oss.terrastruct.com/d2/d2target"
26 "oss.terrastruct.com/d2/d2themes"
27 "oss.terrastruct.com/d2/d2themes/d2themescatalog"
28 "oss.terrastruct.com/d2/lib/color"
29 "oss.terrastruct.com/d2/lib/geo"
30 "oss.terrastruct.com/d2/lib/label"
31 "oss.terrastruct.com/d2/lib/shape"
32 "oss.terrastruct.com/d2/lib/textmeasure"
33 )
34
35 const INNER_LABEL_PADDING int = 5
36 const DEFAULT_SHAPE_SIZE = 100.
37 const MIN_SHAPE_SIZE = 5
38
39 type Graph struct {
40 FS fs.FS `json:"-"`
41 Parent *Graph `json:"-"`
42 Name string `json:"name"`
43
44
45
46 IsFolderOnly bool `json:"isFolderOnly"`
47 AST *d2ast.Map `json:"ast"`
48
49 BaseAST *d2ast.Map `json:"-"`
50
51 Root *Object `json:"root"`
52 Edges []*Edge `json:"edges"`
53 Objects []*Object `json:"objects"`
54
55 Layers []*Graph `json:"layers,omitempty"`
56 Scenarios []*Graph `json:"scenarios,omitempty"`
57 Steps []*Graph `json:"steps,omitempty"`
58
59 Theme *d2themes.Theme `json:"theme,omitempty"`
60
61
62 RootLevel int `json:"rootLevel,omitempty"`
63 }
64
65 func NewGraph() *Graph {
66 d := &Graph{}
67 d.Root = &Object{
68 Graph: d,
69 Parent: nil,
70 Children: make(map[string]*Object),
71 }
72 return d
73 }
74
75 func (g *Graph) RootBoard() *Graph {
76 for g.Parent != nil {
77 g = g.Parent
78 }
79 return g
80 }
81
82 type LayoutGraph func(context.Context, *Graph) error
83 type RouteEdges func(context.Context, *Graph, []*Edge) error
84
85
86
87 type Scalar struct {
88 Value string `json:"value"`
89 MapKey *d2ast.Key `json:"-"`
90 }
91
92
93 type Object struct {
94 Graph *Graph `json:"-"`
95 Parent *Object `json:"-"`
96
97
98
99
100
101
102 ID string `json:"id"`
103 IDVal string `json:"id_val"`
104 Map *d2ast.Map `json:"-"`
105 References []Reference `json:"references,omitempty"`
106
107 *geo.Box `json:"box,omitempty"`
108 LabelPosition *string `json:"labelPosition,omitempty"`
109 IconPosition *string `json:"iconPosition,omitempty"`
110
111 ContentAspectRatio *float64 `json:"contentAspectRatio,omitempty"`
112
113 Class *d2target.Class `json:"class,omitempty"`
114 SQLTable *d2target.SQLTable `json:"sql_table,omitempty"`
115
116 Children map[string]*Object `json:"-"`
117 ChildrenArray []*Object `json:"-"`
118
119 Attributes `json:"attributes"`
120
121 ZIndex int `json:"zIndex"`
122 }
123
124 type Attributes struct {
125 Label Scalar `json:"label"`
126 LabelDimensions d2target.TextDimensions `json:"labelDimensions"`
127
128 Style Style `json:"style"`
129 Icon *url.URL `json:"icon,omitempty"`
130 Tooltip *Scalar `json:"tooltip,omitempty"`
131 Link *Scalar `json:"link,omitempty"`
132
133 WidthAttr *Scalar `json:"width,omitempty"`
134 HeightAttr *Scalar `json:"height,omitempty"`
135
136 Top *Scalar `json:"top,omitempty"`
137 Left *Scalar `json:"left,omitempty"`
138
139
140
141 NearKey *d2ast.KeyPath `json:"near_key"`
142 Language string `json:"language,omitempty"`
143
144 Shape Scalar `json:"shape"`
145
146 Direction Scalar `json:"direction"`
147 Constraint []string `json:"constraint"`
148
149 GridRows *Scalar `json:"gridRows,omitempty"`
150 GridColumns *Scalar `json:"gridColumns,omitempty"`
151 GridGap *Scalar `json:"gridGap,omitempty"`
152 VerticalGap *Scalar `json:"verticalGap,omitempty"`
153 HorizontalGap *Scalar `json:"horizontalGap,omitempty"`
154
155 LabelPosition *Scalar `json:"labelPosition,omitempty"`
156 IconPosition *Scalar `json:"iconPosition,omitempty"`
157
158
159
160 Classes []string `json:"classes,omitempty"`
161 }
162
163
164
165
166 func (a *Attributes) ApplyTextTransform() {
167 if a.Style.NoneTextTransform() {
168 return
169 }
170
171 if a.Style.TextTransform != nil && a.Style.TextTransform.Value == "uppercase" {
172 a.Label.Value = strings.ToUpper(a.Label.Value)
173 }
174 if a.Style.TextTransform != nil && a.Style.TextTransform.Value == "lowercase" {
175 a.Label.Value = strings.ToLower(a.Label.Value)
176 }
177 if a.Style.TextTransform != nil && a.Style.TextTransform.Value == "capitalize" {
178 a.Label.Value = cases.Title(language.Und).String(a.Label.Value)
179 }
180 }
181
182 func (a *Attributes) ToArrowhead() d2target.Arrowhead {
183 var filled *bool
184 if a.Style.Filled != nil {
185 v, _ := strconv.ParseBool(a.Style.Filled.Value)
186 filled = go2.Pointer(v)
187 }
188 return d2target.ToArrowhead(a.Shape.Value, filled)
189 }
190
191 type Reference struct {
192 Key *d2ast.KeyPath `json:"key"`
193 KeyPathIndex int `json:"key_path_index"`
194
195 MapKey *d2ast.Key `json:"-"`
196 MapKeyEdgeIndex int `json:"map_key_edge_index"`
197 Scope *d2ast.Map `json:"-"`
198 ScopeObj *Object `json:"-"`
199 ScopeAST *d2ast.Map `json:"-"`
200 }
201
202 func (r Reference) MapKeyEdgeDest() bool {
203 return r.Key == r.MapKey.Edges[r.MapKeyEdgeIndex].Dst
204 }
205
206 func (r Reference) InEdge() bool {
207 return r.Key != r.MapKey.Key
208 }
209
210 type Style struct {
211 Opacity *Scalar `json:"opacity,omitempty"`
212 Stroke *Scalar `json:"stroke,omitempty"`
213 Fill *Scalar `json:"fill,omitempty"`
214 FillPattern *Scalar `json:"fillPattern,omitempty"`
215 StrokeWidth *Scalar `json:"strokeWidth,omitempty"`
216 StrokeDash *Scalar `json:"strokeDash,omitempty"`
217 BorderRadius *Scalar `json:"borderRadius,omitempty"`
218 Shadow *Scalar `json:"shadow,omitempty"`
219 ThreeDee *Scalar `json:"3d,omitempty"`
220 Multiple *Scalar `json:"multiple,omitempty"`
221 Font *Scalar `json:"font,omitempty"`
222 FontSize *Scalar `json:"fontSize,omitempty"`
223 FontColor *Scalar `json:"fontColor,omitempty"`
224 Animated *Scalar `json:"animated,omitempty"`
225 Bold *Scalar `json:"bold,omitempty"`
226 Italic *Scalar `json:"italic,omitempty"`
227 Underline *Scalar `json:"underline,omitempty"`
228 Filled *Scalar `json:"filled,omitempty"`
229 DoubleBorder *Scalar `json:"doubleBorder,omitempty"`
230 TextTransform *Scalar `json:"textTransform,omitempty"`
231 }
232
233
234
235
236 func (s Style) NoneTextTransform() bool {
237 return s.TextTransform != nil && s.TextTransform.Value == "none"
238 }
239
240 func (s *Style) Apply(key, value string) error {
241 switch key {
242 case "opacity":
243 if s.Opacity == nil {
244 break
245 }
246 f, err := strconv.ParseFloat(value, 64)
247 if err != nil || (f < 0 || f > 1) {
248 return errors.New(`expected "opacity" to be a number between 0.0 and 1.0`)
249 }
250 s.Opacity.Value = value
251 case "stroke":
252 if s.Stroke == nil {
253 break
254 }
255 if !go2.Contains(color.NamedColors, strings.ToLower(value)) && !color.ColorHexRegex.MatchString(value) {
256 return errors.New(`expected "stroke" to be a valid named color ("orange") or a hex code ("#f0ff3a")`)
257 }
258 s.Stroke.Value = value
259 case "fill":
260 if s.Fill == nil {
261 break
262 }
263 if !go2.Contains(color.NamedColors, strings.ToLower(value)) && !color.ColorHexRegex.MatchString(value) {
264 return errors.New(`expected "fill" to be a valid named color ("orange") or a hex code ("#f0ff3a")`)
265 }
266 s.Fill.Value = value
267 case "fill-pattern":
268 if s.FillPattern == nil {
269 break
270 }
271 if !go2.Contains(FillPatterns, strings.ToLower(value)) {
272 return fmt.Errorf(`expected "fill-pattern" to be one of: %s`, strings.Join(FillPatterns, ", "))
273 }
274 s.FillPattern.Value = value
275 case "stroke-width":
276 if s.StrokeWidth == nil {
277 break
278 }
279 f, err := strconv.Atoi(value)
280 if err != nil || (f < 0 || f > 15) {
281 return errors.New(`expected "stroke-width" to be a number between 0 and 15`)
282 }
283 s.StrokeWidth.Value = value
284 case "stroke-dash":
285 if s.StrokeDash == nil {
286 break
287 }
288 f, err := strconv.Atoi(value)
289 if err != nil || (f < 0 || f > 10) {
290 return errors.New(`expected "stroke-dash" to be a number between 0 and 10`)
291 }
292 s.StrokeDash.Value = value
293 case "border-radius":
294 if s.BorderRadius == nil {
295 break
296 }
297 f, err := strconv.Atoi(value)
298 if err != nil || (f < 0) {
299 return errors.New(`expected "border-radius" to be a number greater or equal to 0`)
300 }
301 s.BorderRadius.Value = value
302 case "shadow":
303 if s.Shadow == nil {
304 break
305 }
306 _, err := strconv.ParseBool(value)
307 if err != nil {
308 return errors.New(`expected "shadow" to be true or false`)
309 }
310 s.Shadow.Value = value
311 case "3d":
312 if s.ThreeDee == nil {
313 break
314 }
315 _, err := strconv.ParseBool(value)
316 if err != nil {
317 return errors.New(`expected "3d" to be true or false`)
318 }
319 s.ThreeDee.Value = value
320 case "multiple":
321 if s.Multiple == nil {
322 break
323 }
324 _, err := strconv.ParseBool(value)
325 if err != nil {
326 return errors.New(`expected "multiple" to be true or false`)
327 }
328 s.Multiple.Value = value
329 case "font":
330 if s.Font == nil {
331 break
332 }
333 if _, ok := d2fonts.D2_FONT_TO_FAMILY[strings.ToLower(value)]; !ok {
334 return fmt.Errorf(`"%v" is not a valid font in our system`, value)
335 }
336 s.Font.Value = strings.ToLower(value)
337 case "font-size":
338 if s.FontSize == nil {
339 break
340 }
341 f, err := strconv.Atoi(value)
342 if err != nil || (f < 8 || f > 100) {
343 return errors.New(`expected "font-size" to be a number between 8 and 100`)
344 }
345 s.FontSize.Value = value
346 case "font-color":
347 if s.FontColor == nil {
348 break
349 }
350 if !go2.Contains(color.NamedColors, strings.ToLower(value)) && !color.ColorHexRegex.MatchString(value) {
351 return errors.New(`expected "font-color" to be a valid named color ("orange") or a hex code ("#f0ff3a")`)
352 }
353 s.FontColor.Value = value
354 case "animated":
355 if s.Animated == nil {
356 break
357 }
358 _, err := strconv.ParseBool(value)
359 if err != nil {
360 return errors.New(`expected "animated" to be true or false`)
361 }
362 s.Animated.Value = value
363 case "bold":
364 if s.Bold == nil {
365 break
366 }
367 _, err := strconv.ParseBool(value)
368 if err != nil {
369 return errors.New(`expected "bold" to be true or false`)
370 }
371 s.Bold.Value = value
372 case "italic":
373 if s.Italic == nil {
374 break
375 }
376 _, err := strconv.ParseBool(value)
377 if err != nil {
378 return errors.New(`expected "italic" to be true or false`)
379 }
380 s.Italic.Value = value
381 case "underline":
382 if s.Underline == nil {
383 break
384 }
385 _, err := strconv.ParseBool(value)
386 if err != nil {
387 return errors.New(`expected "underline" to be true or false`)
388 }
389 s.Underline.Value = value
390 case "filled":
391 if s.Filled == nil {
392 break
393 }
394 _, err := strconv.ParseBool(value)
395 if err != nil {
396 return errors.New(`expected "filled" to be true or false`)
397 }
398 s.Filled.Value = value
399 case "double-border":
400 if s.DoubleBorder == nil {
401 break
402 }
403 _, err := strconv.ParseBool(value)
404 if err != nil {
405 return errors.New(`expected "double-border" to be true or false`)
406 }
407 s.DoubleBorder.Value = value
408 case "text-transform":
409 if s.TextTransform == nil {
410 break
411 }
412 if !go2.Contains(textTransforms, strings.ToLower(value)) {
413 return fmt.Errorf(`expected "text-transform" to be one of (%s)`, strings.Join(textTransforms, ", "))
414 }
415 s.TextTransform.Value = value
416 default:
417 return fmt.Errorf("unknown style key: %s", key)
418 }
419
420 return nil
421 }
422
423 type ContainerLevel int
424
425 func (l ContainerLevel) LabelSize() int {
426
427 if l == 1 {
428 return d2fonts.FONT_SIZE_XXL
429 } else if l == 2 {
430 return d2fonts.FONT_SIZE_XL
431 } else if l == 3 {
432 return d2fonts.FONT_SIZE_L
433 }
434 return d2fonts.FONT_SIZE_M
435 }
436
437 func (obj *Object) GetFill() string {
438 level := int(obj.Level())
439 shape := obj.Shape.Value
440
441 if strings.EqualFold(shape, d2target.ShapeSQLTable) || strings.EqualFold(shape, d2target.ShapeClass) {
442 return color.N1
443 }
444
445 if obj.IsSequenceDiagramNote() {
446 return color.N7
447 } else if obj.IsSequenceDiagramGroup() {
448 return color.N5
449 } else if obj.Parent.IsSequenceDiagram() {
450 return color.B5
451 }
452
453
454 sd := obj.OuterSequenceDiagram()
455 if sd != nil {
456 level -= int(sd.Level())
457 if level == 1 {
458 return color.B3
459 } else if level == 2 {
460 return color.B4
461 } else if level == 3 {
462 return color.B5
463 } else if level == 4 {
464 return color.N6
465 }
466 return color.N7
467 }
468
469 if obj.IsSequenceDiagram() {
470 return color.N7
471 }
472
473 if shape == "" || strings.EqualFold(shape, d2target.ShapeSquare) || strings.EqualFold(shape, d2target.ShapeCircle) || strings.EqualFold(shape, d2target.ShapeOval) || strings.EqualFold(shape, d2target.ShapeRectangle) {
474 if level == 1 {
475 if !obj.IsContainer() {
476 return color.B6
477 }
478 return color.B4
479 } else if level == 2 {
480 return color.B5
481 } else if level == 3 {
482 return color.B6
483 }
484 return color.N7
485 }
486
487 if strings.EqualFold(shape, d2target.ShapeCylinder) || strings.EqualFold(shape, d2target.ShapeStoredData) || strings.EqualFold(shape, d2target.ShapePackage) {
488 if level == 1 {
489 return color.AA4
490 }
491 return color.AA5
492 }
493
494 if strings.EqualFold(shape, d2target.ShapeStep) || strings.EqualFold(shape, d2target.ShapePage) || strings.EqualFold(shape, d2target.ShapeDocument) {
495 if level == 1 {
496 return color.AB4
497 }
498 return color.AB5
499 }
500
501 if strings.EqualFold(shape, d2target.ShapePerson) {
502 return color.B3
503 }
504 if strings.EqualFold(shape, d2target.ShapeDiamond) {
505 return color.N4
506 }
507 if strings.EqualFold(shape, d2target.ShapeCloud) || strings.EqualFold(shape, d2target.ShapeCallout) {
508 return color.N7
509 }
510 if strings.EqualFold(shape, d2target.ShapeQueue) || strings.EqualFold(shape, d2target.ShapeParallelogram) || strings.EqualFold(shape, d2target.ShapeHexagon) {
511 return color.N5
512 }
513
514 return color.N7
515 }
516
517 func (obj *Object) GetStroke(dashGapSize interface{}) string {
518 shape := obj.Shape.Value
519 if strings.EqualFold(shape, d2target.ShapeCode) ||
520 strings.EqualFold(shape, d2target.ShapeText) {
521 return color.N1
522 }
523 if strings.EqualFold(shape, d2target.ShapeClass) ||
524 strings.EqualFold(shape, d2target.ShapeSQLTable) {
525 return color.N7
526 }
527 if dashGapSize != 0.0 {
528 return color.B2
529 }
530 return color.B1
531 }
532
533 func (obj *Object) Level() ContainerLevel {
534 if obj.Parent == nil {
535 return ContainerLevel(obj.Graph.RootLevel)
536 }
537 return 1 + obj.Parent.Level()
538 }
539
540 func (obj *Object) IsContainer() bool {
541 return len(obj.Children) > 0
542 }
543
544 func (obj *Object) HasOutsideBottomLabel() bool {
545 if obj == nil {
546 return false
547 }
548 switch obj.Shape.Value {
549 case d2target.ShapeImage, d2target.ShapePerson:
550 return true
551 default:
552 return false
553 }
554 }
555
556 func (obj *Object) HasLabel() bool {
557 if obj == nil {
558 return false
559 }
560 switch obj.Shape.Value {
561 case d2target.ShapeText, d2target.ShapeClass, d2target.ShapeSQLTable, d2target.ShapeCode:
562 return false
563 default:
564 return obj.Label.Value != ""
565 }
566 }
567
568 func (obj *Object) HasIcon() bool {
569 return obj.Icon != nil && obj.Shape.Value != d2target.ShapeImage
570 }
571
572 func (obj *Object) AbsID() string {
573 if obj.Parent != nil && obj.Parent.ID != "" {
574 return obj.Parent.AbsID() + "." + obj.ID
575 }
576 return obj.ID
577 }
578
579 func (obj *Object) AbsIDArray() []string {
580 if obj.Parent == nil {
581 return nil
582 }
583 return append(obj.Parent.AbsIDArray(), obj.ID)
584 }
585
586 func (obj *Object) Text() *d2target.MText {
587 isBold := !obj.IsContainer() && obj.Shape.Value != "text"
588 isItalic := false
589 if obj.Style.Bold != nil && obj.Style.Bold.Value == "true" {
590 isBold = true
591 }
592 if obj.Style.Italic != nil && obj.Style.Italic.Value == "true" {
593 isItalic = true
594 }
595 fontSize := d2fonts.FONT_SIZE_M
596
597 if obj.Class != nil || obj.SQLTable != nil {
598 fontSize = d2fonts.FONT_SIZE_L
599 }
600
601 if obj.OuterSequenceDiagram() == nil {
602
603 if (obj.IsContainer() || obj.IsGridDiagram()) && obj.Shape.Value != "text" {
604 fontSize = obj.Level().LabelSize()
605 }
606 } else {
607 isBold = false
608 }
609 if obj.Style.FontSize != nil {
610 fontSize, _ = strconv.Atoi(obj.Style.FontSize.Value)
611 }
612
613 if obj.Class != nil || obj.SQLTable != nil {
614 fontSize += d2target.HeaderFontAdd
615 }
616 if obj.Class != nil {
617 isBold = false
618 }
619 return &d2target.MText{
620 Text: obj.Label.Value,
621 FontSize: fontSize,
622 IsBold: isBold,
623 IsItalic: isItalic,
624 Language: obj.Language,
625 Shape: obj.Shape.Value,
626
627 Dimensions: obj.LabelDimensions,
628 }
629 }
630
631 func (obj *Object) newObject(id string) *Object {
632 idval := id
633 k, _ := d2parser.ParseKey(id)
634 if k != nil && len(k.Path) > 0 {
635 idval = k.Path[0].Unbox().ScalarString()
636 }
637 child := &Object{
638 ID: id,
639 IDVal: idval,
640 Attributes: Attributes{
641 Label: Scalar{
642 Value: idval,
643 },
644 Shape: Scalar{
645 Value: d2target.ShapeRectangle,
646 },
647 },
648
649 Graph: obj.Graph,
650 Parent: obj,
651
652 Children: make(map[string]*Object),
653 }
654
655 obj.Children[strings.ToLower(id)] = child
656 obj.ChildrenArray = append(obj.ChildrenArray, child)
657
658 if obj.Graph != nil {
659 obj.Graph.Objects = append(obj.Graph.Objects, child)
660 }
661
662 return child
663 }
664
665 func (obj *Object) HasChild(ids []string) (*Object, bool) {
666 if len(ids) == 0 {
667 return obj, true
668 }
669 if len(ids) == 1 && ids[0] != "style" {
670 _, ok := ReservedKeywords[ids[0]]
671 if ok {
672 return obj, true
673 }
674 }
675
676 id := ids[0]
677 ids = ids[1:]
678
679 child, ok := obj.Children[strings.ToLower(id)]
680 if !ok {
681 return nil, false
682 }
683
684 if len(ids) >= 1 {
685 return child.HasChild(ids)
686 }
687 return child, true
688 }
689
690 func (obj *Object) HasEdge(mk *d2ast.Key) (*Edge, bool) {
691 ea, ok := obj.FindEdges(mk)
692 if !ok {
693 return nil, false
694 }
695 for _, e := range ea {
696 if e.Index == *mk.EdgeIndex.Int {
697 return e, true
698 }
699 }
700 return nil, false
701 }
702
703
704 func ResolveUnderscoreKey(ida []string, obj *Object) (resolvedObj *Object, resolvedIDA []string, _ error) {
705 if len(ida) > 0 && !obj.IsSequenceDiagram() {
706 objSD := obj.OuterSequenceDiagram()
707 if objSD != nil {
708 referencesActor := false
709 for _, c := range objSD.ChildrenArray {
710 if c.ID == ida[0] {
711 referencesActor = true
712 break
713 }
714 }
715 if referencesActor {
716 obj = objSD
717 }
718 }
719 }
720
721 resolvedObj = obj
722 resolvedIDA = ida
723
724 for i, id := range ida {
725 if id != "_" {
726 continue
727 }
728 if i != 0 && ida[i-1] != "_" {
729 return nil, nil, errors.New(`parent "_" can only be used in the beginning of paths, e.g. "_.x"`)
730 }
731 if resolvedObj == obj.Graph.Root {
732 return nil, nil, errors.New(`parent "_" cannot be used in the root scope`)
733 }
734 if i == len(ida)-1 {
735 return nil, nil, errors.New(`invalid use of parent "_"`)
736 }
737 resolvedObj = resolvedObj.Parent
738 resolvedIDA = resolvedIDA[1:]
739 }
740
741 return resolvedObj, resolvedIDA, nil
742 }
743
744
745 func (obj *Object) FindEdges(mk *d2ast.Key) ([]*Edge, bool) {
746 if len(mk.Edges) != 1 {
747 return nil, false
748 }
749 if mk.EdgeIndex.Int == nil {
750 return nil, false
751 }
752 ae := mk.Edges[0]
753
754 srcObj, srcID, err := ResolveUnderscoreKey(Key(ae.Src), obj)
755 if err != nil {
756 return nil, false
757 }
758 dstObj, dstID, err := ResolveUnderscoreKey(Key(ae.Dst), obj)
759 if err != nil {
760 return nil, false
761 }
762
763 src := strings.Join(srcID, ".")
764 dst := strings.Join(dstID, ".")
765 if srcObj.Parent != nil {
766 src = srcObj.AbsID() + "." + src
767 }
768 if dstObj.Parent != nil {
769 dst = dstObj.AbsID() + "." + dst
770 }
771
772 var ea []*Edge
773 for _, e := range obj.Graph.Edges {
774 if strings.EqualFold(src, e.Src.AbsID()) &&
775 ((ae.SrcArrow == "<" && e.SrcArrow) || (ae.SrcArrow == "" && !e.SrcArrow)) &&
776 strings.EqualFold(dst, e.Dst.AbsID()) &&
777 ((ae.DstArrow == ">" && e.DstArrow) || (ae.DstArrow == "" && !e.DstArrow)) {
778 ea = append(ea, e)
779 }
780 }
781 return ea, true
782 }
783
784 func (obj *Object) ensureChildEdge(ida []string) *Object {
785 for i := range ida {
786 switch obj.Shape.Value {
787 case d2target.ShapeClass, d2target.ShapeSQLTable:
788
789
790 return obj
791 default:
792 obj = obj.EnsureChild(ida[i : i+1])
793 }
794 }
795 return obj
796 }
797
798
799
800 func (obj *Object) EnsureChild(ida []string) *Object {
801 seq := obj.OuterSequenceDiagram()
802 if seq != nil {
803 for _, c := range seq.ChildrenArray {
804 if c.ID == ida[0] {
805 if obj.ID == ida[0] {
806
807
808
809 break
810 }
811 obj = seq
812 break
813 }
814 }
815 }
816
817 if len(ida) == 0 {
818 return obj
819 }
820
821 _, is := ReservedKeywordHolders[ida[0]]
822 if len(ida) == 1 && !is {
823 _, ok := ReservedKeywords[ida[0]]
824 if ok {
825 return obj
826 }
827 }
828
829 id := ida[0]
830 ida = ida[1:]
831
832 if id == "_" {
833 return obj.Parent.EnsureChild(ida)
834 }
835
836 child, ok := obj.Children[strings.ToLower(id)]
837 if !ok {
838 child = obj.newObject(id)
839 }
840
841 if len(ida) >= 1 {
842 return child.EnsureChild(ida)
843 }
844 return child
845 }
846
847 func (obj *Object) AppendReferences(ida []string, ref Reference, unresolvedObj *Object) {
848 ref.ScopeObj = unresolvedObj
849 numUnderscores := 0
850 for i := range ida {
851 if ida[i] == "_" {
852 numUnderscores++
853 continue
854 }
855 child, ok := obj.HasChild(ida[numUnderscores : i+1])
856 if !ok {
857 return
858 }
859 ref.KeyPathIndex = i
860 child.References = append(child.References, ref)
861 }
862 }
863
864 func (obj *Object) GetLabelSize(mtexts []*d2target.MText, ruler *textmeasure.Ruler, fontFamily *d2fonts.FontFamily) (*d2target.TextDimensions, error) {
865 shapeType := strings.ToLower(obj.Shape.Value)
866
867 if obj.Style.Font != nil {
868 f := d2fonts.D2_FONT_TO_FAMILY[obj.Style.Font.Value]
869 fontFamily = &f
870 }
871
872 var dims *d2target.TextDimensions
873 switch shapeType {
874 case d2target.ShapeText:
875 if obj.Language == "latex" {
876 width, height, err := d2latex.Measure(obj.Text().Text)
877 if err != nil {
878 return nil, err
879 }
880 dims = d2target.NewTextDimensions(width, height)
881 } else if obj.Language != "" {
882 var err error
883 dims, err = getMarkdownDimensions(mtexts, ruler, obj.Text(), fontFamily)
884 if err != nil {
885 return nil, err
886 }
887 } else {
888 dims = GetTextDimensions(mtexts, ruler, obj.Text(), fontFamily)
889 }
890
891 case d2target.ShapeClass:
892 dims = GetTextDimensions(mtexts, ruler, obj.Text(), go2.Pointer(d2fonts.SourceCodePro))
893
894 default:
895 dims = GetTextDimensions(mtexts, ruler, obj.Text(), fontFamily)
896 }
897
898 if shapeType == d2target.ShapeSQLTable && obj.Label.Value == "" {
899
900 placeholder := *obj.Text()
901 placeholder.Text = "Table"
902 dims = GetTextDimensions(mtexts, ruler, &placeholder, fontFamily)
903 }
904
905 if dims == nil {
906 if obj.Text().Text == "" {
907 return d2target.NewTextDimensions(0, 0), nil
908 }
909 if shapeType == d2target.ShapeImage {
910 dims = d2target.NewTextDimensions(0, 0)
911 } else {
912 return nil, fmt.Errorf("dimensions for object label %#v not found", obj.Text())
913 }
914 }
915
916 return dims, nil
917 }
918
919 func (obj *Object) GetDefaultSize(mtexts []*d2target.MText, ruler *textmeasure.Ruler, fontFamily *d2fonts.FontFamily, labelDims d2target.TextDimensions, withLabelPadding bool) (*d2target.TextDimensions, error) {
920 dims := d2target.TextDimensions{}
921 dslShape := strings.ToLower(obj.Shape.Value)
922
923 if dslShape == d2target.ShapeCode {
924 fontSize := obj.Text().FontSize
925
926 labelDims.Width += fontSize
927 labelDims.Height += fontSize
928 } else if withLabelPadding {
929 labelDims.Width += INNER_LABEL_PADDING
930 labelDims.Height += INNER_LABEL_PADDING
931 }
932
933 switch dslShape {
934 default:
935 return d2target.NewTextDimensions(labelDims.Width, labelDims.Height), nil
936 case d2target.ShapeText:
937 w := labelDims.Width
938 if w < MIN_SHAPE_SIZE {
939 w = MIN_SHAPE_SIZE
940 }
941 h := labelDims.Height
942 if h < MIN_SHAPE_SIZE {
943 h = MIN_SHAPE_SIZE
944 }
945 return d2target.NewTextDimensions(w, h), nil
946
947 case d2target.ShapeImage:
948 return d2target.NewTextDimensions(128, 128), nil
949
950 case d2target.ShapeClass:
951 maxWidth := go2.Max(12, labelDims.Width)
952
953 fontSize := d2fonts.FONT_SIZE_L
954 if obj.Style.FontSize != nil {
955 fontSize, _ = strconv.Atoi(obj.Style.FontSize.Value)
956 }
957
958 for _, f := range obj.Class.Fields {
959 fdims := GetTextDimensions(mtexts, ruler, f.Text(fontSize), go2.Pointer(d2fonts.SourceCodePro))
960 if fdims == nil {
961 return nil, fmt.Errorf("dimensions for class field %#v not found", f.Text(fontSize))
962 }
963 maxWidth = go2.Max(maxWidth, fdims.Width)
964 }
965 for _, m := range obj.Class.Methods {
966 mdims := GetTextDimensions(mtexts, ruler, m.Text(fontSize), go2.Pointer(d2fonts.SourceCodePro))
967 if mdims == nil {
968 return nil, fmt.Errorf("dimensions for class method %#v not found", m.Text(fontSize))
969 }
970 maxWidth = go2.Max(maxWidth, mdims.Width)
971 }
972
973
974
975
976
977
978 dims.Width = d2target.PrefixPadding + d2target.PrefixWidth + maxWidth + d2target.CenterPadding + d2target.TypePadding
979
980
981 var anyRowText *d2target.MText
982 if len(obj.Class.Fields) > 0 {
983 anyRowText = obj.Class.Fields[0].Text(fontSize)
984 } else if len(obj.Class.Methods) > 0 {
985 anyRowText = obj.Class.Methods[0].Text(fontSize)
986 }
987 if anyRowText != nil {
988 rowHeight := GetTextDimensions(mtexts, ruler, anyRowText, go2.Pointer(d2fonts.SourceCodePro)).Height + d2target.VerticalPadding
989 dims.Height = rowHeight * (len(obj.Class.Fields) + len(obj.Class.Methods) + 2)
990 } else {
991 dims.Height = 2*go2.Max(12, labelDims.Height) + d2target.VerticalPadding
992 }
993
994 case d2target.ShapeSQLTable:
995 maxNameWidth := 0
996 maxTypeWidth := 0
997 maxConstraintWidth := 0
998
999 colFontSize := d2fonts.FONT_SIZE_L
1000 if obj.Style.FontSize != nil {
1001 colFontSize, _ = strconv.Atoi(obj.Style.FontSize.Value)
1002 }
1003
1004 for i := range obj.SQLTable.Columns {
1005
1006 c := &obj.SQLTable.Columns[i]
1007
1008 ctexts := c.Texts(colFontSize)
1009
1010 nameDims := GetTextDimensions(mtexts, ruler, ctexts[0], fontFamily)
1011 if nameDims == nil {
1012 return nil, fmt.Errorf("dimensions for sql_table name %#v not found", ctexts[0].Text)
1013 }
1014 c.Name.LabelWidth = nameDims.Width
1015 c.Name.LabelHeight = nameDims.Height
1016 maxNameWidth = go2.Max(maxNameWidth, nameDims.Width)
1017
1018 typeDims := GetTextDimensions(mtexts, ruler, ctexts[1], fontFamily)
1019 if typeDims == nil {
1020 return nil, fmt.Errorf("dimensions for sql_table type %#v not found", ctexts[1].Text)
1021 }
1022 c.Type.LabelWidth = typeDims.Width
1023 c.Type.LabelHeight = typeDims.Height
1024 maxTypeWidth = go2.Max(maxTypeWidth, typeDims.Width)
1025
1026 if l := len(c.Constraint); l > 0 {
1027 constraintDims := GetTextDimensions(mtexts, ruler, ctexts[2], fontFamily)
1028 if constraintDims == nil {
1029 return nil, fmt.Errorf("dimensions for sql_table constraint %#v not found", ctexts[2].Text)
1030 }
1031 maxConstraintWidth = go2.Max(maxConstraintWidth, constraintDims.Width)
1032 }
1033 }
1034
1035
1036 dims.Height = go2.Max(12, labelDims.Height*(len(obj.SQLTable.Columns)+1))
1037 headerWidth := d2target.HeaderPadding + labelDims.Width + d2target.HeaderPadding
1038 rowsWidth := d2target.NamePadding + maxNameWidth + d2target.TypePadding + maxTypeWidth + d2target.TypePadding + maxConstraintWidth
1039 if maxConstraintWidth != 0 {
1040 rowsWidth += d2target.ConstraintPadding
1041 }
1042 dims.Width = go2.Max(12, go2.Max(headerWidth, rowsWidth))
1043 }
1044
1045 return &dims, nil
1046 }
1047
1048
1049
1050 func (obj *Object) SizeToContent(contentWidth, contentHeight, paddingX, paddingY float64) {
1051 var desiredWidth int
1052 var desiredHeight int
1053 if obj.WidthAttr != nil {
1054 desiredWidth, _ = strconv.Atoi(obj.WidthAttr.Value)
1055 }
1056 if obj.HeightAttr != nil {
1057 desiredHeight, _ = strconv.Atoi(obj.HeightAttr.Value)
1058 }
1059
1060 dslShape := strings.ToLower(obj.Shape.Value)
1061 shapeType := d2target.DSL_SHAPE_TO_SHAPE_TYPE[dslShape]
1062 s := shape.NewShape(shapeType, geo.NewBox(geo.NewPoint(0, 0), contentWidth, contentHeight))
1063
1064 var fitWidth, fitHeight float64
1065 if shapeType == shape.PERSON_TYPE {
1066 fitWidth = contentWidth + paddingX
1067 fitHeight = contentHeight + paddingY
1068 } else {
1069 fitWidth, fitHeight = s.GetDimensionsToFit(contentWidth, contentHeight, paddingX, paddingY)
1070 }
1071 obj.Width = math.Max(float64(desiredWidth), fitWidth)
1072 obj.Height = math.Max(float64(desiredHeight), fitHeight)
1073 if s.AspectRatio1() {
1074 sideLength := math.Max(obj.Width, obj.Height)
1075 obj.Width = sideLength
1076 obj.Height = sideLength
1077 } else if desiredHeight == 0 || desiredWidth == 0 {
1078 switch shapeType {
1079 case shape.PERSON_TYPE:
1080 obj.Width, obj.Height = shape.LimitAR(obj.Width, obj.Height, shape.PERSON_AR_LIMIT)
1081 case shape.OVAL_TYPE:
1082 obj.Width, obj.Height = shape.LimitAR(obj.Width, obj.Height, shape.OVAL_AR_LIMIT)
1083 }
1084 }
1085 if shapeType == shape.CLOUD_TYPE {
1086 innerBox := s.GetInnerBoxForContent(contentWidth, contentHeight)
1087 obj.ContentAspectRatio = go2.Pointer(innerBox.Width / innerBox.Height)
1088 }
1089 }
1090
1091 func (obj *Object) OuterNearContainer() *Object {
1092 for obj != nil {
1093 if obj.NearKey != nil {
1094 return obj
1095 }
1096 obj = obj.Parent
1097 }
1098 return nil
1099 }
1100
1101 func (obj *Object) IsConstantNear() bool {
1102 if obj.NearKey == nil {
1103 return false
1104 }
1105 keyPath := Key(obj.NearKey)
1106
1107
1108 _, isKey := obj.Graph.Root.HasChild(keyPath)
1109 if isKey {
1110 return false
1111 }
1112 _, isConst := NearConstants[keyPath[0]]
1113 return isConst
1114 }
1115
1116 type Edge struct {
1117 Index int `json:"index"`
1118
1119 SrcTableColumnIndex *int `json:"srcTableColumnIndex,omitempty"`
1120 DstTableColumnIndex *int `json:"dstTableColumnIndex,omitempty"`
1121
1122 LabelPosition *string `json:"labelPosition,omitempty"`
1123 LabelPercentage *float64 `json:"labelPercentage,omitempty"`
1124
1125 IsCurve bool `json:"isCurve"`
1126 Route []*geo.Point `json:"route,omitempty"`
1127
1128 Src *Object `json:"-"`
1129 SrcArrow bool `json:"src_arrow"`
1130 SrcArrowhead *Attributes `json:"srcArrowhead,omitempty"`
1131 Dst *Object `json:"-"`
1132
1133 DstArrow bool `json:"dst_arrow"`
1134 DstArrowhead *Attributes `json:"dstArrowhead,omitempty"`
1135
1136 References []EdgeReference `json:"references,omitempty"`
1137 Attributes `json:"attributes,omitempty"`
1138
1139 ZIndex int `json:"zIndex"`
1140 }
1141
1142 type EdgeReference struct {
1143 Edge *d2ast.Edge `json:"-"`
1144
1145 MapKey *d2ast.Key `json:"-"`
1146 MapKeyEdgeIndex int `json:"map_key_edge_index"`
1147 Scope *d2ast.Map `json:"-"`
1148 ScopeObj *Object `json:"-"`
1149 ScopeAST *d2ast.Map `json:"-"`
1150 }
1151
1152 func (e *Edge) GetAstEdge() *d2ast.Edge {
1153 return e.References[0].Edge
1154 }
1155
1156 func (e *Edge) GetStroke(dashGapSize interface{}) string {
1157 if dashGapSize != 0.0 {
1158 return color.B2
1159 }
1160 return color.B1
1161 }
1162
1163 func (e *Edge) ArrowString() string {
1164 if e.SrcArrow && e.DstArrow {
1165 return "<->"
1166 }
1167 if e.SrcArrow {
1168 return "<-"
1169 }
1170 if e.DstArrow {
1171 return "->"
1172 }
1173 return "--"
1174 }
1175
1176 func (e *Edge) Text() *d2target.MText {
1177 fontSize := d2fonts.FONT_SIZE_M
1178 if e.Style.FontSize != nil {
1179 fontSize, _ = strconv.Atoi(e.Style.FontSize.Value)
1180 }
1181 isBold := false
1182 if e.Style.Bold != nil {
1183 isBold, _ = strconv.ParseBool(e.Style.Bold.Value)
1184 }
1185 return &d2target.MText{
1186 Text: e.Label.Value,
1187 FontSize: fontSize,
1188 IsBold: isBold,
1189 IsItalic: true,
1190
1191 Dimensions: e.LabelDimensions,
1192 }
1193 }
1194
1195 func (e *Edge) Move(dx, dy float64) {
1196 for _, p := range e.Route {
1197 p.X += dx
1198 p.Y += dy
1199 }
1200 }
1201
1202 func (e *Edge) AbsID() string {
1203 srcIDA := e.Src.AbsIDArray()
1204 dstIDA := e.Dst.AbsIDArray()
1205
1206 var commonIDA []string
1207 for len(srcIDA) > 1 && len(dstIDA) > 1 {
1208 if !strings.EqualFold(srcIDA[0], dstIDA[0]) {
1209 break
1210 }
1211 commonIDA = append(commonIDA, srcIDA[0])
1212 srcIDA = srcIDA[1:]
1213 dstIDA = dstIDA[1:]
1214 }
1215
1216 commonKey := ""
1217 if len(commonIDA) > 0 {
1218 commonKey = strings.Join(commonIDA, ".") + "."
1219 }
1220
1221 return fmt.Sprintf("%s(%s %s %s)[%d]", commonKey, strings.Join(srcIDA, "."), e.ArrowString(), strings.Join(dstIDA, "."), e.Index)
1222 }
1223
1224 func (obj *Object) Connect(srcID, dstID []string, srcArrow, dstArrow bool, label string) (*Edge, error) {
1225 for _, id := range [][]string{srcID, dstID} {
1226 for _, p := range id {
1227 if _, ok := ReservedKeywords[p]; ok {
1228 return nil, errors.New("cannot connect to reserved keyword")
1229 }
1230 }
1231 }
1232
1233 src := obj.ensureChildEdge(srcID)
1234 dst := obj.ensureChildEdge(dstID)
1235
1236 e := &Edge{
1237 Attributes: Attributes{
1238 Label: Scalar{
1239 Value: label,
1240 },
1241 },
1242 Src: src,
1243 SrcArrow: srcArrow,
1244 Dst: dst,
1245 DstArrow: dstArrow,
1246 }
1247 e.initIndex()
1248
1249 addSQLTableColumnIndices(e, srcID, dstID, obj, src, dst)
1250
1251 obj.Graph.Edges = append(obj.Graph.Edges, e)
1252 return e, nil
1253 }
1254
1255 func addSQLTableColumnIndices(e *Edge, srcID, dstID []string, obj, src, dst *Object) {
1256 if src.Shape.Value == d2target.ShapeSQLTable {
1257 if src == dst {
1258
1259 return
1260 }
1261 objAbsID := obj.AbsIDArray()
1262 srcAbsID := src.AbsIDArray()
1263 if len(objAbsID)+len(srcID) > len(srcAbsID) {
1264 for i, d2col := range src.SQLTable.Columns {
1265 if d2col.Name.Label == srcID[len(srcID)-1] {
1266 d2col.Reference = dst.AbsID()
1267 e.SrcTableColumnIndex = new(int)
1268 *e.SrcTableColumnIndex = i
1269 break
1270 }
1271 }
1272 }
1273 }
1274 if dst.Shape.Value == d2target.ShapeSQLTable {
1275 objAbsID := obj.AbsIDArray()
1276 dstAbsID := dst.AbsIDArray()
1277 if len(objAbsID)+len(dstID) > len(dstAbsID) {
1278 for i, d2col := range dst.SQLTable.Columns {
1279 if d2col.Name.Label == dstID[len(dstID)-1] {
1280 d2col.Reference = dst.AbsID()
1281 e.DstTableColumnIndex = new(int)
1282 *e.DstTableColumnIndex = i
1283 break
1284 }
1285 }
1286 }
1287 }
1288 }
1289
1290
1291
1292 func (e *Edge) initIndex() {
1293 for _, e2 := range e.Src.Graph.Edges {
1294 if e.Src == e2.Src &&
1295 e.SrcArrow == e2.SrcArrow &&
1296 e.Dst == e2.Dst &&
1297 e.DstArrow == e2.DstArrow {
1298 e.Index++
1299 }
1300 }
1301 }
1302
1303 func findMeasured(mtexts []*d2target.MText, t1 *d2target.MText) *d2target.TextDimensions {
1304 for i, t2 := range mtexts {
1305 if t1.Text != t2.Text {
1306 continue
1307 }
1308 if t1.FontSize != t2.FontSize {
1309 continue
1310 }
1311 if t1.IsBold != t2.IsBold {
1312 continue
1313 }
1314 if t1.IsItalic != t2.IsItalic {
1315 continue
1316 }
1317 if t1.Language != t2.Language {
1318 continue
1319 }
1320 return &mtexts[i].Dimensions
1321 }
1322 return nil
1323 }
1324
1325 func getMarkdownDimensions(mtexts []*d2target.MText, ruler *textmeasure.Ruler, t *d2target.MText, fontFamily *d2fonts.FontFamily) (*d2target.TextDimensions, error) {
1326 if dims := findMeasured(mtexts, t); dims != nil {
1327 return dims, nil
1328 }
1329
1330 if ruler != nil {
1331 width, height, err := textmeasure.MeasureMarkdown(t.Text, ruler, fontFamily, t.FontSize)
1332 if err != nil {
1333 return nil, err
1334 }
1335 return d2target.NewTextDimensions(width, height), nil
1336 }
1337
1338 if strings.TrimSpace(t.Text) == "" {
1339 return d2target.NewTextDimensions(1, 1), nil
1340 }
1341
1342 return nil, fmt.Errorf("text not pre-measured and no ruler provided")
1343 }
1344
1345 func GetTextDimensions(mtexts []*d2target.MText, ruler *textmeasure.Ruler, t *d2target.MText, fontFamily *d2fonts.FontFamily) *d2target.TextDimensions {
1346 if dims := findMeasured(mtexts, t); dims != nil {
1347 return dims
1348 }
1349
1350 if ruler != nil {
1351 var w int
1352 var h int
1353 if t.Language != "" {
1354 originalLineHeight := ruler.LineHeightFactor
1355 ruler.LineHeightFactor = textmeasure.CODE_LINE_HEIGHT
1356 w, h = ruler.MeasureMono(d2fonts.SourceCodePro.Font(t.FontSize, d2fonts.FONT_STYLE_REGULAR), t.Text)
1357 ruler.LineHeightFactor = originalLineHeight
1358
1359
1360 lines := strings.Split(t.Text, "\n")
1361 hasLeading := false
1362 if len(lines) > 0 && strings.TrimSpace(lines[0]) == "" {
1363 hasLeading = true
1364 }
1365 numTrailing := 0
1366 for i := len(lines) - 1; i >= 0; i-- {
1367 if strings.TrimSpace(lines[i]) == "" {
1368 numTrailing++
1369 } else {
1370 break
1371 }
1372 }
1373 if hasLeading && numTrailing < len(lines) {
1374 h += t.FontSize
1375 }
1376 h += int(math.Ceil(textmeasure.CODE_LINE_HEIGHT * float64(t.FontSize*numTrailing)))
1377 } else {
1378 style := d2fonts.FONT_STYLE_REGULAR
1379 if t.IsBold {
1380 style = d2fonts.FONT_STYLE_BOLD
1381 } else if t.IsItalic {
1382 style = d2fonts.FONT_STYLE_ITALIC
1383 }
1384 if fontFamily == nil {
1385 fontFamily = go2.Pointer(d2fonts.SourceSansPro)
1386 }
1387 w, h = ruler.Measure(fontFamily.Font(t.FontSize, style), t.Text)
1388 }
1389 return d2target.NewTextDimensions(w, h)
1390 }
1391
1392 return nil
1393 }
1394
1395 func appendTextDedup(texts []*d2target.MText, t *d2target.MText) []*d2target.MText {
1396 if GetTextDimensions(texts, nil, t, nil) == nil {
1397 return append(texts, t)
1398 }
1399 return texts
1400 }
1401
1402 func (g *Graph) SetDimensions(mtexts []*d2target.MText, ruler *textmeasure.Ruler, fontFamily *d2fonts.FontFamily) error {
1403 if ruler != nil && fontFamily != nil {
1404 if ok := ruler.HasFontFamilyLoaded(fontFamily); !ok {
1405 return fmt.Errorf("ruler does not have entire font family %s loaded, is a style missing?", *fontFamily)
1406 }
1407 }
1408
1409 if g.Theme != nil && g.Theme.SpecialRules.Mono {
1410 tmp := d2fonts.SourceCodePro
1411 fontFamily = &tmp
1412 }
1413
1414 for _, obj := range g.Objects {
1415 obj.Box = &geo.Box{}
1416
1417
1418 if obj.HasLabel() && obj.Attributes.LabelPosition != nil {
1419 scalar := *obj.Attributes.LabelPosition
1420 position := LabelPositionsMapping[scalar.Value]
1421 obj.LabelPosition = go2.Pointer(position.String())
1422 }
1423 if obj.Icon != nil && obj.Attributes.IconPosition != nil {
1424 scalar := *obj.Attributes.IconPosition
1425 position := LabelPositionsMapping[scalar.Value]
1426 obj.IconPosition = go2.Pointer(position.String())
1427 }
1428
1429 var desiredWidth int
1430 var desiredHeight int
1431 if obj.WidthAttr != nil {
1432 desiredWidth, _ = strconv.Atoi(obj.WidthAttr.Value)
1433 }
1434 if obj.HeightAttr != nil {
1435 desiredHeight, _ = strconv.Atoi(obj.HeightAttr.Value)
1436 }
1437
1438 dslShape := strings.ToLower(obj.Shape.Value)
1439
1440 if obj.Label.Value == "" &&
1441 dslShape != d2target.ShapeImage &&
1442 dslShape != d2target.ShapeSQLTable &&
1443 dslShape != d2target.ShapeClass {
1444
1445 if dslShape == d2target.ShapeCircle || dslShape == d2target.ShapeSquare {
1446 sideLength := DEFAULT_SHAPE_SIZE
1447 if desiredWidth != 0 || desiredHeight != 0 {
1448 sideLength = float64(go2.Max(desiredWidth, desiredHeight))
1449 }
1450 obj.Width = sideLength
1451 obj.Height = sideLength
1452 } else {
1453 obj.Width = DEFAULT_SHAPE_SIZE
1454 obj.Height = DEFAULT_SHAPE_SIZE
1455 if desiredWidth != 0 {
1456 obj.Width = float64(desiredWidth)
1457 }
1458 if desiredHeight != 0 {
1459 obj.Height = float64(desiredHeight)
1460 }
1461 }
1462
1463 continue
1464 }
1465
1466 if g.Theme != nil && g.Theme.SpecialRules.CapsLock && !strings.EqualFold(obj.Shape.Value, d2target.ShapeCode) {
1467 if obj.Language != "latex" && !obj.Style.NoneTextTransform() {
1468 obj.Label.Value = strings.ToUpper(obj.Label.Value)
1469 }
1470 }
1471 obj.ApplyTextTransform()
1472
1473 labelDims, err := obj.GetLabelSize(mtexts, ruler, fontFamily)
1474 if err != nil {
1475 return err
1476 }
1477 obj.LabelDimensions = *labelDims
1478
1479
1480 withInnerLabelPadding := desiredWidth == 0 && desiredHeight == 0 &&
1481 dslShape != d2target.ShapeText && obj.Label.Value != ""
1482 defaultDims, err := obj.GetDefaultSize(mtexts, ruler, fontFamily, *labelDims, withInnerLabelPadding)
1483 if err != nil {
1484 return err
1485 }
1486
1487 if dslShape == d2target.ShapeImage {
1488 if desiredWidth == 0 {
1489 desiredWidth = defaultDims.Width
1490 }
1491 if desiredHeight == 0 {
1492 desiredHeight = defaultDims.Height
1493 }
1494 obj.Width = float64(go2.Max(MIN_SHAPE_SIZE, desiredWidth))
1495 obj.Height = float64(go2.Max(MIN_SHAPE_SIZE, desiredHeight))
1496
1497 continue
1498 }
1499
1500 contentBox := geo.NewBox(geo.NewPoint(0, 0), float64(defaultDims.Width), float64(defaultDims.Height))
1501 shapeType := d2target.DSL_SHAPE_TO_SHAPE_TYPE[dslShape]
1502 s := shape.NewShape(shapeType, contentBox)
1503 paddingX, paddingY := s.GetDefaultPadding()
1504 if desiredWidth != 0 {
1505 paddingX = 0.
1506 }
1507 if desiredHeight != 0 {
1508 paddingY = 0.
1509 }
1510
1511
1512 if obj.Icon != nil {
1513 switch shapeType {
1514 case shape.TABLE_TYPE, shape.CLASS_TYPE, shape.CODE_TYPE, shape.TEXT_TYPE:
1515 default:
1516 labelHeight := float64(labelDims.Height + INNER_LABEL_PADDING)
1517
1518 if desiredWidth == 0 {
1519 paddingX += labelHeight
1520 }
1521 if desiredHeight == 0 {
1522 paddingY += labelHeight
1523 }
1524 }
1525 }
1526 if desiredWidth == 0 {
1527 switch shapeType {
1528 case shape.TABLE_TYPE, shape.CLASS_TYPE, shape.CODE_TYPE:
1529 default:
1530 if obj.Link != nil {
1531 paddingX += 32
1532 }
1533 if obj.Tooltip != nil {
1534 paddingX += 32
1535 }
1536 }
1537 }
1538
1539 obj.SizeToContent(contentBox.Width, contentBox.Height, paddingX, paddingY)
1540 }
1541 for _, edge := range g.Edges {
1542 usedFont := fontFamily
1543 if edge.Style.Font != nil {
1544 f := d2fonts.D2_FONT_TO_FAMILY[edge.Style.Font.Value]
1545 usedFont = &f
1546 }
1547
1548 if edge.SrcArrowhead != nil && edge.SrcArrowhead.Label.Value != "" {
1549 t := edge.Text()
1550 t.Text = edge.SrcArrowhead.Label.Value
1551 dims := GetTextDimensions(mtexts, ruler, t, usedFont)
1552 edge.SrcArrowhead.LabelDimensions = *dims
1553 }
1554 if edge.DstArrowhead != nil && edge.DstArrowhead.Label.Value != "" {
1555 t := edge.Text()
1556 t.Text = edge.DstArrowhead.Label.Value
1557 dims := GetTextDimensions(mtexts, ruler, t, usedFont)
1558 edge.DstArrowhead.LabelDimensions = *dims
1559 }
1560
1561 if edge.Label.Value == "" {
1562 continue
1563 }
1564
1565 if g.Theme != nil && g.Theme.SpecialRules.CapsLock && !edge.Style.NoneTextTransform() {
1566 edge.Label.Value = strings.ToUpper(edge.Label.Value)
1567 }
1568 edge.ApplyTextTransform()
1569
1570 dims := GetTextDimensions(mtexts, ruler, edge.Text(), usedFont)
1571 if dims == nil {
1572 return fmt.Errorf("dimensions for edge label %#v not found", edge.Text())
1573 }
1574
1575 edge.LabelDimensions = *dims
1576 }
1577 return nil
1578 }
1579
1580 func (g *Graph) Texts() []*d2target.MText {
1581 var texts []*d2target.MText
1582
1583 capsLock := g.Theme != nil && g.Theme.SpecialRules.CapsLock
1584
1585 for _, obj := range g.Objects {
1586 if obj.Label.Value != "" {
1587 obj.ApplyTextTransform()
1588 text := obj.Text()
1589 if capsLock && !strings.EqualFold(obj.Shape.Value, d2target.ShapeCode) {
1590 if obj.Language != "latex" && !obj.Style.NoneTextTransform() {
1591 text.Text = strings.ToUpper(text.Text)
1592 }
1593 }
1594 texts = appendTextDedup(texts, text)
1595 }
1596 if obj.Class != nil {
1597 fontSize := d2fonts.FONT_SIZE_L
1598 if obj.Style.FontSize != nil {
1599 fontSize, _ = strconv.Atoi(obj.Style.FontSize.Value)
1600 }
1601 for _, field := range obj.Class.Fields {
1602 texts = appendTextDedup(texts, field.Text(fontSize))
1603 }
1604 for _, method := range obj.Class.Methods {
1605 texts = appendTextDedup(texts, method.Text(fontSize))
1606 }
1607 } else if obj.SQLTable != nil {
1608 colFontSize := d2fonts.FONT_SIZE_L
1609 if obj.Style.FontSize != nil {
1610 colFontSize, _ = strconv.Atoi(obj.Style.FontSize.Value)
1611 }
1612 for _, column := range obj.SQLTable.Columns {
1613 for _, t := range column.Texts(colFontSize) {
1614 texts = appendTextDedup(texts, t)
1615 }
1616 }
1617 }
1618 }
1619 for _, edge := range g.Edges {
1620 if edge.Label.Value != "" {
1621 edge.ApplyTextTransform()
1622 text := edge.Text()
1623 if capsLock && !edge.Style.NoneTextTransform() {
1624 text.Text = strings.ToUpper(text.Text)
1625 }
1626 texts = appendTextDedup(texts, text)
1627 }
1628 if edge.SrcArrowhead != nil && edge.SrcArrowhead.Label.Value != "" {
1629 t := edge.Text()
1630 t.Text = edge.SrcArrowhead.Label.Value
1631 texts = appendTextDedup(texts, t)
1632 }
1633 if edge.DstArrowhead != nil && edge.DstArrowhead.Label.Value != "" {
1634 t := edge.Text()
1635 t.Text = edge.DstArrowhead.Label.Value
1636 texts = appendTextDedup(texts, t)
1637 }
1638 }
1639
1640 for _, board := range g.Layers {
1641 for _, t := range board.Texts() {
1642 texts = appendTextDedup(texts, t)
1643 }
1644 }
1645
1646 for _, board := range g.Scenarios {
1647 for _, t := range board.Texts() {
1648 texts = appendTextDedup(texts, t)
1649 }
1650 }
1651
1652 for _, board := range g.Steps {
1653 for _, t := range board.Texts() {
1654 texts = appendTextDedup(texts, t)
1655 }
1656 }
1657
1658 return texts
1659 }
1660
1661 func Key(k *d2ast.KeyPath) []string {
1662 return d2format.KeyPath(k)
1663 }
1664
1665
1666 var ReservedKeywords map[string]struct{}
1667
1668
1669 var SimpleReservedKeywords = map[string]struct{}{
1670 "label": {},
1671 "desc": {},
1672 "shape": {},
1673 "icon": {},
1674 "constraint": {},
1675 "tooltip": {},
1676 "link": {},
1677 "near": {},
1678 "width": {},
1679 "height": {},
1680 "direction": {},
1681 "top": {},
1682 "left": {},
1683 "grid-rows": {},
1684 "grid-columns": {},
1685 "grid-gap": {},
1686 "vertical-gap": {},
1687 "horizontal-gap": {},
1688 "class": {},
1689 "vars": {},
1690 }
1691
1692
1693 var ReservedKeywordHolders = map[string]struct{}{
1694 "style": {},
1695 "source-arrowhead": {},
1696 "target-arrowhead": {},
1697 }
1698
1699
1700 var CompositeReservedKeywords = map[string]struct{}{
1701 "classes": {},
1702 "constraint": {},
1703 "label": {},
1704 "icon": {},
1705 }
1706
1707
1708 var StyleKeywords = map[string]struct{}{
1709 "opacity": {},
1710 "stroke": {},
1711 "fill": {},
1712 "fill-pattern": {},
1713 "stroke-width": {},
1714 "stroke-dash": {},
1715 "border-radius": {},
1716
1717
1718 "font": {},
1719 "font-size": {},
1720 "font-color": {},
1721 "bold": {},
1722 "italic": {},
1723 "underline": {},
1724 "text-transform": {},
1725
1726
1727 "shadow": {},
1728 "multiple": {},
1729 "double-border": {},
1730
1731
1732 "3d": {},
1733
1734
1735 "animated": {},
1736 "filled": {},
1737 }
1738
1739
1740
1741 var NearConstantsArray = []string{
1742 "top-left",
1743 "top-center",
1744 "top-right",
1745
1746 "center-left",
1747 "center-right",
1748
1749 "bottom-left",
1750 "bottom-center",
1751 "bottom-right",
1752 }
1753 var NearConstants map[string]struct{}
1754
1755
1756 var LabelPositionsArray = []string{
1757 "top-left",
1758 "top-center",
1759 "top-right",
1760
1761 "center-left",
1762 "center-center",
1763 "center-right",
1764
1765 "bottom-left",
1766 "bottom-center",
1767 "bottom-right",
1768
1769 "outside-top-left",
1770 "outside-top-center",
1771 "outside-top-right",
1772
1773 "outside-left-top",
1774 "outside-left-center",
1775 "outside-left-bottom",
1776
1777 "outside-right-top",
1778 "outside-right-center",
1779 "outside-right-bottom",
1780
1781 "outside-bottom-left",
1782 "outside-bottom-center",
1783 "outside-bottom-right",
1784 }
1785 var LabelPositions map[string]struct{}
1786
1787
1788 var LabelPositionsMapping = map[string]label.Position{
1789 "top-left": label.InsideTopLeft,
1790 "top-center": label.InsideTopCenter,
1791 "top-right": label.InsideTopRight,
1792
1793 "center-left": label.InsideMiddleLeft,
1794 "center-center": label.InsideMiddleCenter,
1795 "center-right": label.InsideMiddleRight,
1796
1797 "bottom-left": label.InsideBottomLeft,
1798 "bottom-center": label.InsideBottomCenter,
1799 "bottom-right": label.InsideBottomRight,
1800
1801 "outside-top-left": label.OutsideTopLeft,
1802 "outside-top-center": label.OutsideTopCenter,
1803 "outside-top-right": label.OutsideTopRight,
1804
1805 "outside-left-top": label.OutsideLeftTop,
1806 "outside-left-center": label.OutsideLeftMiddle,
1807 "outside-left-bottom": label.OutsideLeftBottom,
1808
1809 "outside-right-top": label.OutsideRightTop,
1810 "outside-right-center": label.OutsideRightMiddle,
1811 "outside-right-bottom": label.OutsideRightBottom,
1812
1813 "outside-bottom-left": label.OutsideBottomLeft,
1814 "outside-bottom-center": label.OutsideBottomCenter,
1815 "outside-bottom-right": label.OutsideBottomRight,
1816 }
1817
1818 var FillPatterns = []string{
1819 "dots",
1820 "lines",
1821 "grain",
1822 "paper",
1823 }
1824
1825 var textTransforms = []string{"none", "uppercase", "lowercase", "capitalize"}
1826
1827
1828 var BoardKeywords = map[string]struct{}{
1829 "layers": {},
1830 "scenarios": {},
1831 "steps": {},
1832 }
1833
1834 func init() {
1835 ReservedKeywords = make(map[string]struct{})
1836 for k, v := range SimpleReservedKeywords {
1837 ReservedKeywords[k] = v
1838 }
1839 for k, v := range StyleKeywords {
1840 ReservedKeywords[k] = v
1841 }
1842 for k, v := range ReservedKeywordHolders {
1843 CompositeReservedKeywords[k] = v
1844 }
1845 for k, v := range BoardKeywords {
1846 CompositeReservedKeywords[k] = v
1847 }
1848 for k, v := range CompositeReservedKeywords {
1849 ReservedKeywords[k] = v
1850 }
1851
1852 NearConstants = make(map[string]struct{}, len(NearConstantsArray))
1853 for _, k := range NearConstantsArray {
1854 NearConstants[k] = struct{}{}
1855 }
1856
1857 LabelPositions = make(map[string]struct{}, len(LabelPositionsArray))
1858 for _, k := range LabelPositionsArray {
1859 LabelPositions[k] = struct{}{}
1860 }
1861 }
1862
1863 func (g *Graph) GetBoard(name string) *Graph {
1864 for _, l := range g.Layers {
1865 if l.Name == name {
1866 return l
1867 }
1868 }
1869 for _, l := range g.Scenarios {
1870 if l.Name == name {
1871 return l
1872 }
1873 }
1874 for _, l := range g.Steps {
1875 if l.Name == name {
1876 return l
1877 }
1878 }
1879 return nil
1880 }
1881
1882 func (g *Graph) SortObjectsByAST() {
1883 objects := append([]*Object(nil), g.Objects...)
1884 sort.Slice(objects, func(i, j int) bool {
1885 o1 := objects[i]
1886 o2 := objects[j]
1887 if len(o1.References) == 0 || len(o2.References) == 0 {
1888 return i < j
1889 }
1890 r1 := o1.References[0]
1891 r2 := o2.References[0]
1892 return r1.Key.Path[r1.KeyPathIndex].Unbox().GetRange().Before(r2.Key.Path[r2.KeyPathIndex].Unbox().GetRange())
1893 })
1894 g.Objects = objects
1895 }
1896
1897 func (g *Graph) SortEdgesByAST() {
1898 edges := append([]*Edge(nil), g.Edges...)
1899 sort.Slice(edges, func(i, j int) bool {
1900 e1 := edges[i]
1901 e2 := edges[j]
1902 if len(e1.References) == 0 || len(e2.References) == 0 {
1903 return i < j
1904 }
1905 return e1.References[0].Edge.Range.Before(e2.References[0].Edge.Range)
1906 })
1907 g.Edges = edges
1908 }
1909
1910 func (obj *Object) IsDescendantOf(ancestor *Object) bool {
1911 if obj == ancestor {
1912 return true
1913 }
1914 if obj.Parent == nil {
1915 return false
1916 }
1917 return obj.Parent.IsDescendantOf(ancestor)
1918 }
1919
1920
1921
1922
1923 func (g *Graph) ApplyTheme(themeID int64) error {
1924 theme := d2themescatalog.Find(themeID)
1925 if theme == (d2themes.Theme{}) {
1926 return fmt.Errorf("theme %d not found", themeID)
1927 }
1928 g.Theme = &theme
1929 return nil
1930 }
1931
1932 func (g *Graph) PrintString() string {
1933 buf := &bytes.Buffer{}
1934 fmt.Fprint(buf, "Objects: [")
1935 for _, obj := range g.Objects {
1936 fmt.Fprintf(buf, "%v, ", obj.AbsID())
1937 }
1938 fmt.Fprint(buf, "]")
1939 return buf.String()
1940 }
1941
1942 func (obj *Object) IterDescendants(apply func(parent, child *Object)) {
1943 for _, c := range obj.ChildrenArray {
1944 apply(obj, c)
1945 c.IterDescendants(apply)
1946 }
1947 }
1948
1949 func (obj *Object) IsMultiple() bool {
1950 return obj.Style.Multiple != nil && obj.Style.Multiple.Value == "true"
1951 }
1952
1953 func (obj *Object) Is3D() bool {
1954 return obj.Style.ThreeDee != nil && obj.Style.ThreeDee.Value == "true"
1955 }
1956
1957 func (obj *Object) Spacing() (margin, padding geo.Spacing) {
1958 return obj.SpacingOpt(2*label.PADDING, 2*label.PADDING, true)
1959 }
1960
1961 func (obj *Object) SpacingOpt(labelPadding, iconPadding float64, maxIconSize bool) (margin, padding geo.Spacing) {
1962 if obj.HasLabel() {
1963 var position label.Position
1964 if obj.LabelPosition != nil {
1965 position = label.FromString(*obj.LabelPosition)
1966 }
1967
1968 var labelWidth, labelHeight float64
1969 if obj.LabelDimensions.Width > 0 {
1970 labelWidth = float64(obj.LabelDimensions.Width) + labelPadding
1971 }
1972 if obj.LabelDimensions.Height > 0 {
1973 labelHeight = float64(obj.LabelDimensions.Height) + labelPadding
1974 }
1975
1976 switch position {
1977 case label.OutsideTopLeft, label.OutsideTopCenter, label.OutsideTopRight:
1978 margin.Top = labelHeight
1979 case label.OutsideBottomLeft, label.OutsideBottomCenter, label.OutsideBottomRight:
1980 margin.Bottom = labelHeight
1981 case label.OutsideLeftTop, label.OutsideLeftMiddle, label.OutsideLeftBottom:
1982 margin.Left = labelWidth
1983 case label.OutsideRightTop, label.OutsideRightMiddle, label.OutsideRightBottom:
1984 margin.Right = labelWidth
1985 case label.InsideTopLeft, label.InsideTopCenter, label.InsideTopRight:
1986 padding.Top = labelHeight
1987 case label.InsideBottomLeft, label.InsideBottomCenter, label.InsideBottomRight:
1988 padding.Bottom = labelHeight
1989 case label.InsideMiddleLeft:
1990 padding.Left = labelWidth
1991 case label.InsideMiddleRight:
1992 padding.Right = labelWidth
1993 }
1994 }
1995
1996 if obj.HasIcon() {
1997 var position label.Position
1998 if obj.IconPosition != nil {
1999 position = label.FromString(*obj.IconPosition)
2000 }
2001
2002 iconSize := float64(d2target.MAX_ICON_SIZE + iconPadding)
2003 if !maxIconSize {
2004 iconSize = float64(d2target.GetIconSize(obj.Box, position.String())) + iconPadding
2005 }
2006 switch position {
2007 case label.OutsideTopLeft, label.OutsideTopCenter, label.OutsideTopRight:
2008 margin.Top = math.Max(margin.Top, iconSize)
2009 case label.OutsideBottomLeft, label.OutsideBottomCenter, label.OutsideBottomRight:
2010 margin.Bottom = math.Max(margin.Bottom, iconSize)
2011 case label.OutsideLeftTop, label.OutsideLeftMiddle, label.OutsideLeftBottom:
2012 margin.Left = math.Max(margin.Left, iconSize)
2013 case label.OutsideRightTop, label.OutsideRightMiddle, label.OutsideRightBottom:
2014 margin.Right = math.Max(margin.Right, iconSize)
2015 case label.InsideTopLeft, label.InsideTopCenter, label.InsideTopRight:
2016 padding.Top = math.Max(padding.Top, iconSize)
2017 case label.InsideBottomLeft, label.InsideBottomCenter, label.InsideBottomRight:
2018 padding.Bottom = math.Max(padding.Bottom, iconSize)
2019 case label.InsideMiddleLeft:
2020 padding.Left = math.Max(padding.Left, iconSize)
2021 case label.InsideMiddleRight:
2022 padding.Right = math.Max(padding.Right, iconSize)
2023 }
2024 }
2025
2026 dx, dy := obj.GetModifierElementAdjustments()
2027 margin.Right += dx
2028 margin.Top += dy
2029
2030 return
2031 }
2032
View as plain text