...

Source file src/oss.terrastruct.com/d2/d2graph/d2graph.go

Documentation: oss.terrastruct.com/d2/d2graph

     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  	// IsFolderOnly indicates a board or scenario itself makes no modifications from its
    44  	// base. Folder only boards do not have a render and are used purely for organizing
    45  	// the board tree.
    46  	IsFolderOnly bool       `json:"isFolderOnly"`
    47  	AST          *d2ast.Map `json:"ast"`
    48  	// BaseAST is the AST of the original graph without inherited fields and edges
    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  	// Object.Level uses the location of a nested graph
    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  // TODO consider having different Scalar types
    86  // Right now we'll hold any types in Value and just convert, e.g. floats
    87  type Scalar struct {
    88  	Value  string     `json:"value"`
    89  	MapKey *d2ast.Key `json:"-"`
    90  }
    91  
    92  // TODO maybe rename to Shape
    93  type Object struct {
    94  	Graph  *Graph  `json:"-"`
    95  	Parent *Object `json:"-"`
    96  
    97  	// IDVal is the actual value of the ID whereas ID is the value in d2 syntax.
    98  	// e.g. ID:    "yes'\""
    99  	//      IDVal: yes'"
   100  	//
   101  	// ID allows joining on . naively and construct a valid D2 key path
   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  	// TODO consider separate Attributes struct for shape-specific and edge-specific
   140  	// Shapes only
   141  	NearKey  *d2ast.KeyPath `json:"near_key"`
   142  	Language string         `json:"language,omitempty"`
   143  	// TODO: default to ShapeRectangle instead of empty string
   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  	// These names are attached to the rendered elements in SVG
   159  	// so that users can target them however they like outside of D2
   160  	Classes []string `json:"classes,omitempty"`
   161  }
   162  
   163  // ApplyTextTransform will alter the `Label.Value` of the current object based
   164  // on the specification of the `text-transform` styling option. This function
   165  // has side-effects!
   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  // NoneTextTransform will return a boolean if the text should not have any
   234  // transformation applied. This should overwrite theme specific transformations
   235  // like `CapsLock` from the `terminal` theme.
   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  	// Largest to smallest
   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  	// fill for spans
   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  		// Note: during grid layout when children are temporarily removed `IsContainer` is false
   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  	// Class and Table objects have Label set to header
   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  // TODO: remove once not used anywhere
   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  // TODO: remove edges []edge and scope each edge inside Object.
   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  			// This will only be called for connecting edges where we want to truncate to the
   789  			// container.
   790  			return obj
   791  		default:
   792  			obj = obj.EnsureChild(ida[i : i+1])
   793  		}
   794  	}
   795  	return obj
   796  }
   797  
   798  // EnsureChild grabs the child by ids or creates it if it does not exist including all
   799  // intermediate nodes.
   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  					// In cases of a.a where EnsureChild is called on the parent a, the second a should
   807  					// be created as a child of a and not as a child of the diagram. This is super
   808  					// unfortunate code but alas.
   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  		// measure with placeholder text to determine height
   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  		// 0.5em padding on each side
   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  		//    ┌─PrefixWidth ┌─CenterPadding
   973  		// ┌─┬─┬───────┬──────┬───┬──┐
   974  		// │ + getJobs()      Job[]  │
   975  		// └─┴─┴───────┴──────┴───┴──┘
   976  		//  └─PrefixPadding        └──TypePadding
   977  		//     ├───────┤   +  ├───┤  = maxWidth
   978  		dims.Width = d2target.PrefixPadding + d2target.PrefixWidth + maxWidth + d2target.CenterPadding + d2target.TypePadding
   979  
   980  		// All rows should be the same height
   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  			// Note: we want to set dimensions of actual column not the for loop copy of the struct
  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  		// The rows get padded a little due to header font being larger than row font
  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  // resizes the object to fit content of the given width and height in its inner box with the given padding.
  1049  // this accounts for the shape of the object, and if there is a desired width or height set for the object
  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  	// interesting if there is a shape with id=top-left, then top-left isn't treated a constant near
  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  	// TODO alixander (Mon Sep 12 2022): deprecate SrcArrow and DstArrow and just use SrcArrowhead and DstArrowhead
  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  			// Ignore edge to column inside table.
  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  // TODO: Treat undirectional/bidirectional edge here and in HasEdge flipped. Same with
  1291  // SrcArrow.
  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  			// count empty leading and trailing lines since ruler will not be able to measure it
  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  		// user-specified label/icon positions
  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  		// if there is a desired width or height, fit to content box without inner label padding for smallest minimum size
  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  			// images don't need further processing
  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  		// give shapes with icons extra padding to fit their label
  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  				// Evenly pad enough to fit label above icon
  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  // All reserved keywords. See init below.
  1666  var ReservedKeywords map[string]struct{}
  1667  
  1668  // Non Style/Holder keywords.
  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  // ReservedKeywordHolders are reserved keywords that are meaningless on its own and must hold composites
  1693  var ReservedKeywordHolders = map[string]struct{}{
  1694  	"style":            {},
  1695  	"source-arrowhead": {},
  1696  	"target-arrowhead": {},
  1697  }
  1698  
  1699  // CompositeReservedKeywords are reserved keywords that can hold composites
  1700  var CompositeReservedKeywords = map[string]struct{}{
  1701  	"classes":    {},
  1702  	"constraint": {},
  1703  	"label":      {},
  1704  	"icon":       {},
  1705  }
  1706  
  1707  // StyleKeywords are reserved keywords which cannot exist outside of the "style" keyword
  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  	// Only for text
  1718  	"font":           {},
  1719  	"font-size":      {},
  1720  	"font-color":     {},
  1721  	"bold":           {},
  1722  	"italic":         {},
  1723  	"underline":      {},
  1724  	"text-transform": {},
  1725  
  1726  	// Only for shapes
  1727  	"shadow":        {},
  1728  	"multiple":      {},
  1729  	"double-border": {},
  1730  
  1731  	// Only for squares
  1732  	"3d": {},
  1733  
  1734  	// Only for edges
  1735  	"animated": {},
  1736  	"filled":   {},
  1737  }
  1738  
  1739  // TODO maybe autofmt should allow other values, and transform them to conform
  1740  // e.g. left-center becomes center-left
  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  // LabelPositionsArray are the values that labels and icons can set `near` to
  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  // convert to label.Position
  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  // BoardKeywords contains the keywords that create new boards.
  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  // ApplyTheme applies themes on the graph level
  1921  // This is different than on the render level, which only changes colors
  1922  // A theme applied on the graph level applies special rules that change the graph
  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