...

Source file src/oss.terrastruct.com/d2/d2compiler/compile.go

Documentation: oss.terrastruct.com/d2/d2compiler

     1  package d2compiler
     2  
     3  import (
     4  	"encoding/xml"
     5  	"fmt"
     6  	"io"
     7  	"io/fs"
     8  	"net/url"
     9  	"strconv"
    10  	"strings"
    11  
    12  	"oss.terrastruct.com/util-go/go2"
    13  
    14  	"oss.terrastruct.com/d2/d2ast"
    15  	"oss.terrastruct.com/d2/d2format"
    16  	"oss.terrastruct.com/d2/d2graph"
    17  	"oss.terrastruct.com/d2/d2ir"
    18  	"oss.terrastruct.com/d2/d2parser"
    19  	"oss.terrastruct.com/d2/d2target"
    20  	"oss.terrastruct.com/d2/lib/color"
    21  	"oss.terrastruct.com/d2/lib/textmeasure"
    22  )
    23  
    24  type CompileOptions struct {
    25  	UTF16Pos bool
    26  	// FS is the file system used for resolving imports in the d2 text.
    27  	// It should correspond to the root path.
    28  	FS fs.FS
    29  }
    30  
    31  func Compile(p string, r io.Reader, opts *CompileOptions) (*d2graph.Graph, *d2target.Config, error) {
    32  	if opts == nil {
    33  		opts = &CompileOptions{}
    34  	}
    35  
    36  	ast, err := d2parser.Parse(p, r, &d2parser.ParseOptions{
    37  		UTF16Pos: opts.UTF16Pos,
    38  	})
    39  	if err != nil {
    40  		return nil, nil, err
    41  	}
    42  
    43  	ir, err := d2ir.Compile(ast, &d2ir.CompileOptions{
    44  		UTF16Pos: opts.UTF16Pos,
    45  		FS:       opts.FS,
    46  	})
    47  	if err != nil {
    48  		return nil, nil, err
    49  	}
    50  
    51  	g, err := compileIR(ast, ir)
    52  	if err != nil {
    53  		return nil, nil, err
    54  	}
    55  	g.FS = opts.FS
    56  	g.SortObjectsByAST()
    57  	g.SortEdgesByAST()
    58  	config, err := compileConfig(ir)
    59  	if err != nil {
    60  		return nil, nil, err
    61  	}
    62  	return g, config, nil
    63  }
    64  
    65  func compileIR(ast *d2ast.Map, m *d2ir.Map) (*d2graph.Graph, error) {
    66  	c := &compiler{
    67  		err: &d2parser.ParseError{},
    68  	}
    69  
    70  	g := d2graph.NewGraph()
    71  	g.AST = ast
    72  	c.compileBoard(g, m)
    73  	if len(c.err.Errors) > 0 {
    74  		return nil, c.err
    75  	}
    76  	c.validateBoardLinks(g)
    77  	if len(c.err.Errors) > 0 {
    78  		return nil, c.err
    79  	}
    80  	return g, nil
    81  }
    82  
    83  func (c *compiler) compileBoard(g *d2graph.Graph, ir *d2ir.Map) *d2graph.Graph {
    84  	ir = ir.Copy(nil).(*d2ir.Map)
    85  	// c.preprocessSeqDiagrams(ir)
    86  	c.compileMap(g.Root, ir)
    87  	if len(c.err.Errors) == 0 {
    88  		c.validateKeys(g.Root, ir)
    89  	}
    90  	c.validateLabels(g)
    91  	c.validateNear(g)
    92  	c.validateEdges(g)
    93  
    94  	c.compileBoardsField(g, ir, "layers")
    95  	c.compileBoardsField(g, ir, "scenarios")
    96  	c.compileBoardsField(g, ir, "steps")
    97  	if d2ir.ParentMap(ir).CopyBase(nil).Equal(ir.CopyBase(nil)) {
    98  		if len(g.Layers) > 0 || len(g.Scenarios) > 0 || len(g.Steps) > 0 {
    99  			g.IsFolderOnly = true
   100  		}
   101  	}
   102  	if len(g.Objects) == 0 {
   103  		g.IsFolderOnly = true
   104  	}
   105  	return g
   106  }
   107  
   108  func (c *compiler) compileBoardsField(g *d2graph.Graph, ir *d2ir.Map, fieldName string) {
   109  	layers := ir.GetField(fieldName)
   110  	if layers.Map() == nil {
   111  		return
   112  	}
   113  	for _, f := range layers.Map().Fields {
   114  		if f.Map() == nil {
   115  			continue
   116  		}
   117  		if g.GetBoard(f.Name) != nil {
   118  			c.errorf(f.References[0].AST(), "board name %v already used by another board", f.Name)
   119  			continue
   120  		}
   121  		g2 := d2graph.NewGraph()
   122  		g2.Parent = g
   123  		g2.AST = f.Map().AST().(*d2ast.Map)
   124  		g2.BaseAST = findFieldAST(g.AST, f)
   125  		c.compileBoard(g2, f.Map())
   126  		g2.Name = f.Name
   127  		switch fieldName {
   128  		case "layers":
   129  			g.Layers = append(g.Layers, g2)
   130  		case "scenarios":
   131  			g.Scenarios = append(g.Scenarios, g2)
   132  		case "steps":
   133  			g.Steps = append(g.Steps, g2)
   134  		}
   135  	}
   136  }
   137  
   138  func findFieldAST(ast *d2ast.Map, f *d2ir.Field) *d2ast.Map {
   139  	path := []string{}
   140  	var curr *d2ir.Field = f
   141  	for {
   142  		path = append([]string{curr.Name}, path...)
   143  		boardKind := d2ir.NodeBoardKind(curr)
   144  		if boardKind == "" {
   145  			break
   146  		}
   147  		curr = d2ir.ParentField(curr)
   148  	}
   149  
   150  	currAST := ast
   151  	for len(path) > 0 {
   152  		head := path[0]
   153  		found := false
   154  		for _, n := range currAST.Nodes {
   155  			if n.MapKey == nil {
   156  				continue
   157  			}
   158  			if n.MapKey.Key == nil {
   159  				continue
   160  			}
   161  			if len(n.MapKey.Key.Path) != 1 {
   162  				continue
   163  			}
   164  			head2 := n.MapKey.Key.Path[0].Unbox().ScalarString()
   165  			if head == head2 {
   166  				currAST = n.MapKey.Value.Map
   167  				found = true
   168  				break
   169  			}
   170  		}
   171  		if !found {
   172  			return nil
   173  		}
   174  		path = path[1:]
   175  	}
   176  
   177  	return currAST
   178  }
   179  
   180  type compiler struct {
   181  	err *d2parser.ParseError
   182  }
   183  
   184  func (c *compiler) errorf(n d2ast.Node, f string, v ...interface{}) {
   185  	err := d2parser.Errorf(n, f, v...).(d2ast.Error)
   186  	if c.err.ErrorsLookup == nil {
   187  		c.err.ErrorsLookup = make(map[d2ast.Error]struct{})
   188  	}
   189  	if _, ok := c.err.ErrorsLookup[err]; !ok {
   190  		c.err.Errors = append(c.err.Errors, err)
   191  		c.err.ErrorsLookup[err] = struct{}{}
   192  	}
   193  }
   194  
   195  func (c *compiler) compileMap(obj *d2graph.Object, m *d2ir.Map) {
   196  	class := m.GetField("class")
   197  	if class != nil {
   198  		var classNames []string
   199  		if class.Primary() != nil {
   200  			classNames = append(classNames, class.Primary().String())
   201  		} else if class.Composite != nil {
   202  			if arr, ok := class.Composite.(*d2ir.Array); ok {
   203  				for _, class := range arr.Values {
   204  					if scalar, ok := class.(*d2ir.Scalar); ok {
   205  						classNames = append(classNames, scalar.Value.ScalarString())
   206  					} else {
   207  						c.errorf(class.LastPrimaryKey(), "invalid value in array")
   208  					}
   209  				}
   210  			}
   211  		} else {
   212  			c.errorf(class.LastRef().AST(), "class missing value")
   213  		}
   214  
   215  		for _, className := range classNames {
   216  			classMap := m.GetClassMap(className)
   217  			if classMap != nil {
   218  				c.compileMap(obj, classMap)
   219  			} else {
   220  				if strings.Contains(className, ",") {
   221  					split := strings.Split(className, ",")
   222  					allFound := true
   223  					for _, maybeClassName := range split {
   224  						maybeClassName = strings.TrimSpace(maybeClassName)
   225  						if m.GetClassMap(maybeClassName) == nil {
   226  							allFound = false
   227  							break
   228  						}
   229  					}
   230  					if allFound {
   231  						c.errorf(class.LastRef().AST(), `class "%s" not found. Did you mean to use ";" to separate array items?`, className)
   232  					}
   233  				}
   234  			}
   235  		}
   236  	}
   237  	shape := m.GetField("shape")
   238  	if shape != nil {
   239  		if shape.Composite != nil {
   240  			c.errorf(shape.LastPrimaryKey(), "reserved field shape does not accept composite")
   241  		} else {
   242  			c.compileField(obj, shape)
   243  		}
   244  	}
   245  	for _, f := range m.Fields {
   246  		if f.Name == "shape" {
   247  			continue
   248  		}
   249  		if _, ok := d2graph.BoardKeywords[f.Name]; ok {
   250  			continue
   251  		}
   252  		c.compileField(obj, f)
   253  	}
   254  
   255  	if !m.IsClass() {
   256  		switch obj.Shape.Value {
   257  		case d2target.ShapeClass:
   258  			c.compileClass(obj)
   259  		case d2target.ShapeSQLTable:
   260  			c.compileSQLTable(obj)
   261  		}
   262  
   263  		for _, e := range m.Edges {
   264  			c.compileEdge(obj, e)
   265  		}
   266  	}
   267  }
   268  
   269  func (c *compiler) compileField(obj *d2graph.Object, f *d2ir.Field) {
   270  	keyword := strings.ToLower(f.Name)
   271  	_, isStyleReserved := d2graph.StyleKeywords[keyword]
   272  	if isStyleReserved {
   273  		c.errorf(f.LastRef().AST(), "%v must be style.%v", f.Name, f.Name)
   274  		return
   275  	}
   276  	_, isReserved := d2graph.SimpleReservedKeywords[keyword]
   277  	if f.Name == "classes" {
   278  		if f.Map() != nil {
   279  			if len(f.Map().Edges) > 0 {
   280  				c.errorf(f.Map().Edges[0].LastRef().AST(), "classes cannot contain an edge")
   281  			}
   282  			for _, classesField := range f.Map().Fields {
   283  				if classesField.Map() == nil {
   284  					continue
   285  				}
   286  				for _, cf := range classesField.Map().Fields {
   287  					if _, ok := d2graph.ReservedKeywords[cf.Name]; !ok {
   288  						c.errorf(cf.LastRef().AST(), "%s is an invalid class field, must be reserved keyword", cf.Name)
   289  					}
   290  					if cf.Name == "class" {
   291  						c.errorf(cf.LastRef().AST(), `"class" cannot appear within "classes"`)
   292  					}
   293  				}
   294  			}
   295  		}
   296  		return
   297  	} else if f.Name == "vars" {
   298  		return
   299  	} else if f.Name == "source-arrowhead" || f.Name == "target-arrowhead" {
   300  		c.errorf(f.LastRef().AST(), `%#v can only be used on connections`, f.Name)
   301  		return
   302  
   303  	} else if isReserved {
   304  		c.compileReserved(&obj.Attributes, f)
   305  		return
   306  	} else if f.Name == "style" {
   307  		if f.Map() == nil || len(f.Map().Fields) == 0 {
   308  			c.errorf(f.LastRef().AST(), `"style" expected to be set to a map of key-values, or contain an additional keyword like "style.opacity: 0.4"`)
   309  			return
   310  		}
   311  		c.compileStyle(&obj.Attributes, f.Map())
   312  		if obj.Style.Animated != nil {
   313  			c.errorf(obj.Style.Animated.MapKey, `key "animated" can only be applied to edges`)
   314  		}
   315  		return
   316  	}
   317  
   318  	if obj.Parent != nil {
   319  		if obj.Parent.Shape.Value == d2target.ShapeSQLTable {
   320  			c.errorf(f.LastRef().AST(), "sql_table columns cannot have children")
   321  			return
   322  		}
   323  		if obj.Parent.Shape.Value == d2target.ShapeClass {
   324  			c.errorf(f.LastRef().AST(), "class fields cannot have children")
   325  			return
   326  		}
   327  	}
   328  
   329  	obj = obj.EnsureChild(d2graphIDA([]string{f.Name}))
   330  	if f.Primary() != nil {
   331  		c.compileLabel(&obj.Attributes, f)
   332  	}
   333  	if f.Map() != nil {
   334  		c.compileMap(obj, f.Map())
   335  	}
   336  
   337  	if obj.Label.MapKey == nil {
   338  		obj.Label.MapKey = f.LastPrimaryKey()
   339  	}
   340  	for _, fr := range f.References {
   341  		if fr.Primary() {
   342  			if fr.Context_.Key.Value.Map != nil {
   343  				obj.Map = fr.Context_.Key.Value.Map
   344  			}
   345  		}
   346  		r := d2graph.Reference{
   347  			Key:          fr.KeyPath,
   348  			KeyPathIndex: fr.KeyPathIndex(),
   349  
   350  			MapKey:          fr.Context_.Key,
   351  			MapKeyEdgeIndex: fr.Context_.EdgeIndex(),
   352  			Scope:           fr.Context_.Scope,
   353  			ScopeAST:        fr.Context_.ScopeAST,
   354  		}
   355  		if fr.Context_.ScopeMap != nil && !d2ir.IsVar(fr.Context_.ScopeMap) {
   356  			scopeObjIDA := d2graphIDA(d2ir.BoardIDA(fr.Context_.ScopeMap))
   357  			r.ScopeObj = obj.Graph.Root.EnsureChild(scopeObjIDA)
   358  		}
   359  		obj.References = append(obj.References, r)
   360  	}
   361  }
   362  
   363  func (c *compiler) compileLabel(attrs *d2graph.Attributes, f d2ir.Node) {
   364  	scalar := f.Primary().Value
   365  	switch scalar := scalar.(type) {
   366  	case *d2ast.BlockString:
   367  		if strings.TrimSpace(scalar.ScalarString()) == "" {
   368  			c.errorf(f.LastPrimaryKey(), "block string cannot be empty")
   369  		}
   370  		attrs.Language = scalar.Tag
   371  		fullTag, ok := ShortToFullLanguageAliases[scalar.Tag]
   372  		if ok {
   373  			attrs.Language = fullTag
   374  		}
   375  		switch attrs.Language {
   376  		case "latex":
   377  			attrs.Shape.Value = d2target.ShapeText
   378  		case "markdown":
   379  			rendered, err := textmeasure.RenderMarkdown(scalar.ScalarString())
   380  			if err != nil {
   381  				c.errorf(f.LastPrimaryKey(), "malformed Markdown")
   382  			}
   383  			rendered = "<div>" + rendered + "</div>"
   384  			var xmlParsed interface{}
   385  			err = xml.Unmarshal([]byte(rendered), &xmlParsed)
   386  			if err != nil {
   387  				switch xmlErr := err.(type) {
   388  				case *xml.SyntaxError:
   389  					c.errorf(f.LastPrimaryKey(), "malformed Markdown: %s", xmlErr.Msg)
   390  				default:
   391  					c.errorf(f.LastPrimaryKey(), "malformed Markdown: %s", err.Error())
   392  				}
   393  			}
   394  			attrs.Shape.Value = d2target.ShapeText
   395  		default:
   396  			attrs.Shape.Value = d2target.ShapeCode
   397  		}
   398  		attrs.Label.Value = scalar.ScalarString()
   399  	default:
   400  		attrs.Label.Value = scalar.ScalarString()
   401  	}
   402  	attrs.Label.MapKey = f.LastPrimaryKey()
   403  }
   404  
   405  func (c *compiler) compilePosition(attrs *d2graph.Attributes, f *d2ir.Field) {
   406  	name := f.Name
   407  	if f.Map() != nil {
   408  		for _, f := range f.Map().Fields {
   409  			if f.Name == "near" {
   410  				if f.Primary() == nil {
   411  					c.errorf(f.LastPrimaryKey(), `invalid "near" field`)
   412  				} else {
   413  					scalar := f.Primary().Value
   414  					switch scalar := scalar.(type) {
   415  					case *d2ast.Null:
   416  						attrs.LabelPosition = nil
   417  					default:
   418  						if _, ok := d2graph.LabelPositions[scalar.ScalarString()]; !ok {
   419  							c.errorf(f.LastPrimaryKey(), `invalid "near" field`)
   420  						} else {
   421  							switch name {
   422  							case "label":
   423  								attrs.LabelPosition = &d2graph.Scalar{}
   424  								attrs.LabelPosition.Value = scalar.ScalarString()
   425  								attrs.LabelPosition.MapKey = f.LastPrimaryKey()
   426  							case "icon":
   427  								attrs.IconPosition = &d2graph.Scalar{}
   428  								attrs.IconPosition.Value = scalar.ScalarString()
   429  								attrs.IconPosition.MapKey = f.LastPrimaryKey()
   430  							}
   431  						}
   432  					}
   433  				}
   434  			} else {
   435  				if f.LastPrimaryKey() != nil {
   436  					c.errorf(f.LastPrimaryKey(), `unexpected field %s`, f.Name)
   437  				}
   438  			}
   439  		}
   440  		if len(f.Map().Edges) > 0 {
   441  			c.errorf(f.LastPrimaryKey(), "unexpected edges in map")
   442  		}
   443  	}
   444  }
   445  
   446  func (c *compiler) compileReserved(attrs *d2graph.Attributes, f *d2ir.Field) {
   447  	if f.Primary() == nil {
   448  		if f.Composite != nil {
   449  			switch f.Name {
   450  			case "class":
   451  				if arr, ok := f.Composite.(*d2ir.Array); ok {
   452  					for _, class := range arr.Values {
   453  						if scalar, ok := class.(*d2ir.Scalar); ok {
   454  							attrs.Classes = append(attrs.Classes, scalar.Value.ScalarString())
   455  						}
   456  					}
   457  				}
   458  			case "constraint":
   459  				if arr, ok := f.Composite.(*d2ir.Array); ok {
   460  					for _, constraint := range arr.Values {
   461  						if scalar, ok := constraint.(*d2ir.Scalar); ok {
   462  							switch scalar.Value.(type) {
   463  							case *d2ast.Null:
   464  								attrs.Constraint = append(attrs.Constraint, "null")
   465  							default:
   466  								attrs.Constraint = append(attrs.Constraint, scalar.Value.ScalarString())
   467  							}
   468  						}
   469  					}
   470  				}
   471  			case "label", "icon":
   472  				c.compilePosition(attrs, f)
   473  			default:
   474  				c.errorf(f.LastPrimaryKey(), "reserved field %v does not accept composite", f.Name)
   475  			}
   476  		}
   477  		return
   478  	}
   479  	scalar := f.Primary().Value
   480  	switch f.Name {
   481  	case "label":
   482  		c.compileLabel(attrs, f)
   483  		c.compilePosition(attrs, f)
   484  	case "shape":
   485  		in := d2target.IsShape(scalar.ScalarString())
   486  		_, isArrowhead := d2target.Arrowheads[scalar.ScalarString()]
   487  		if !in && !isArrowhead {
   488  			c.errorf(scalar, "unknown shape %q", scalar.ScalarString())
   489  			return
   490  		}
   491  		attrs.Shape.Value = scalar.ScalarString()
   492  		if attrs.Shape.Value == d2target.ShapeCode {
   493  			// Explicit code shape is plaintext.
   494  			attrs.Language = d2target.ShapeText
   495  		}
   496  		attrs.Shape.MapKey = f.LastPrimaryKey()
   497  	case "icon":
   498  		iconURL, err := url.Parse(scalar.ScalarString())
   499  		if err != nil {
   500  			c.errorf(scalar, "bad icon url %#v: %s", scalar.ScalarString(), err)
   501  			return
   502  		}
   503  		attrs.Icon = iconURL
   504  		c.compilePosition(attrs, f)
   505  	case "near":
   506  		nearKey, err := d2parser.ParseKey(scalar.ScalarString())
   507  		if err != nil {
   508  			c.errorf(scalar, "bad near key %#v: %s", scalar.ScalarString(), err)
   509  			return
   510  		}
   511  		nearKey.Range = scalar.GetRange()
   512  		attrs.NearKey = nearKey
   513  	case "tooltip":
   514  		attrs.Tooltip = &d2graph.Scalar{}
   515  		attrs.Tooltip.Value = scalar.ScalarString()
   516  		attrs.Tooltip.MapKey = f.LastPrimaryKey()
   517  	case "width":
   518  		_, err := strconv.Atoi(scalar.ScalarString())
   519  		if err != nil {
   520  			c.errorf(scalar, "non-integer width %#v: %s", scalar.ScalarString(), err)
   521  			return
   522  		}
   523  		attrs.WidthAttr = &d2graph.Scalar{}
   524  		attrs.WidthAttr.Value = scalar.ScalarString()
   525  		attrs.WidthAttr.MapKey = f.LastPrimaryKey()
   526  	case "height":
   527  		_, err := strconv.Atoi(scalar.ScalarString())
   528  		if err != nil {
   529  			c.errorf(scalar, "non-integer height %#v: %s", scalar.ScalarString(), err)
   530  			return
   531  		}
   532  		attrs.HeightAttr = &d2graph.Scalar{}
   533  		attrs.HeightAttr.Value = scalar.ScalarString()
   534  		attrs.HeightAttr.MapKey = f.LastPrimaryKey()
   535  	case "top":
   536  		v, err := strconv.Atoi(scalar.ScalarString())
   537  		if err != nil {
   538  			c.errorf(scalar, "non-integer top %#v: %s", scalar.ScalarString(), err)
   539  			return
   540  		}
   541  		if v < 0 {
   542  			c.errorf(scalar, "top must be a non-negative integer: %#v", scalar.ScalarString())
   543  			return
   544  		}
   545  		attrs.Top = &d2graph.Scalar{}
   546  		attrs.Top.Value = scalar.ScalarString()
   547  		attrs.Top.MapKey = f.LastPrimaryKey()
   548  	case "left":
   549  		v, err := strconv.Atoi(scalar.ScalarString())
   550  		if err != nil {
   551  			c.errorf(scalar, "non-integer left %#v: %s", scalar.ScalarString(), err)
   552  			return
   553  		}
   554  		if v < 0 {
   555  			c.errorf(scalar, "left must be a non-negative integer: %#v", scalar.ScalarString())
   556  			return
   557  		}
   558  		attrs.Left = &d2graph.Scalar{}
   559  		attrs.Left.Value = scalar.ScalarString()
   560  		attrs.Left.MapKey = f.LastPrimaryKey()
   561  	case "link":
   562  		attrs.Link = &d2graph.Scalar{}
   563  		attrs.Link.Value = scalar.ScalarString()
   564  		attrs.Link.MapKey = f.LastPrimaryKey()
   565  	case "direction":
   566  		dirs := []string{"up", "down", "right", "left"}
   567  		if !go2.Contains(dirs, scalar.ScalarString()) {
   568  			c.errorf(scalar, `direction must be one of %v, got %q`, strings.Join(dirs, ", "), scalar.ScalarString())
   569  			return
   570  		}
   571  		attrs.Direction.Value = scalar.ScalarString()
   572  		attrs.Direction.MapKey = f.LastPrimaryKey()
   573  	case "constraint":
   574  		if _, ok := scalar.(d2ast.String); !ok {
   575  			c.errorf(f.LastPrimaryKey(), "constraint value must be a string")
   576  			return
   577  		}
   578  		attrs.Constraint = append(attrs.Constraint, scalar.ScalarString())
   579  	case "grid-rows":
   580  		v, err := strconv.Atoi(scalar.ScalarString())
   581  		if err != nil {
   582  			c.errorf(scalar, "non-integer grid-rows %#v: %s", scalar.ScalarString(), err)
   583  			return
   584  		}
   585  		if v <= 0 {
   586  			c.errorf(scalar, "grid-rows must be a positive integer: %#v", scalar.ScalarString())
   587  			return
   588  		}
   589  		attrs.GridRows = &d2graph.Scalar{}
   590  		attrs.GridRows.Value = scalar.ScalarString()
   591  		attrs.GridRows.MapKey = f.LastPrimaryKey()
   592  	case "grid-columns":
   593  		v, err := strconv.Atoi(scalar.ScalarString())
   594  		if err != nil {
   595  			c.errorf(scalar, "non-integer grid-columns %#v: %s", scalar.ScalarString(), err)
   596  			return
   597  		}
   598  		if v <= 0 {
   599  			c.errorf(scalar, "grid-columns must be a positive integer: %#v", scalar.ScalarString())
   600  			return
   601  		}
   602  		attrs.GridColumns = &d2graph.Scalar{}
   603  		attrs.GridColumns.Value = scalar.ScalarString()
   604  		attrs.GridColumns.MapKey = f.LastPrimaryKey()
   605  	case "grid-gap":
   606  		v, err := strconv.Atoi(scalar.ScalarString())
   607  		if err != nil {
   608  			c.errorf(scalar, "non-integer grid-gap %#v: %s", scalar.ScalarString(), err)
   609  			return
   610  		}
   611  		if v < 0 {
   612  			c.errorf(scalar, "grid-gap must be a non-negative integer: %#v", scalar.ScalarString())
   613  			return
   614  		}
   615  		attrs.GridGap = &d2graph.Scalar{}
   616  		attrs.GridGap.Value = scalar.ScalarString()
   617  		attrs.GridGap.MapKey = f.LastPrimaryKey()
   618  	case "vertical-gap":
   619  		v, err := strconv.Atoi(scalar.ScalarString())
   620  		if err != nil {
   621  			c.errorf(scalar, "non-integer vertical-gap %#v: %s", scalar.ScalarString(), err)
   622  			return
   623  		}
   624  		if v < 0 {
   625  			c.errorf(scalar, "vertical-gap must be a non-negative integer: %#v", scalar.ScalarString())
   626  			return
   627  		}
   628  		attrs.VerticalGap = &d2graph.Scalar{}
   629  		attrs.VerticalGap.Value = scalar.ScalarString()
   630  		attrs.VerticalGap.MapKey = f.LastPrimaryKey()
   631  	case "horizontal-gap":
   632  		v, err := strconv.Atoi(scalar.ScalarString())
   633  		if err != nil {
   634  			c.errorf(scalar, "non-integer horizontal-gap %#v: %s", scalar.ScalarString(), err)
   635  			return
   636  		}
   637  		if v < 0 {
   638  			c.errorf(scalar, "horizontal-gap must be a non-negative integer: %#v", scalar.ScalarString())
   639  			return
   640  		}
   641  		attrs.HorizontalGap = &d2graph.Scalar{}
   642  		attrs.HorizontalGap.Value = scalar.ScalarString()
   643  		attrs.HorizontalGap.MapKey = f.LastPrimaryKey()
   644  	case "class":
   645  		attrs.Classes = append(attrs.Classes, scalar.ScalarString())
   646  	case "classes":
   647  	}
   648  
   649  	if attrs.Link != nil && attrs.Tooltip != nil {
   650  		u, err := url.ParseRequestURI(attrs.Tooltip.Value)
   651  		if err == nil && u.Host != "" {
   652  			c.errorf(scalar, "Tooltip cannot be set to URL when link is also set (for security)")
   653  		}
   654  	}
   655  }
   656  
   657  func (c *compiler) compileStyle(attrs *d2graph.Attributes, m *d2ir.Map) {
   658  	for _, f := range m.Fields {
   659  		c.compileStyleField(attrs, f)
   660  	}
   661  }
   662  
   663  func (c *compiler) compileStyleField(attrs *d2graph.Attributes, f *d2ir.Field) {
   664  	if _, ok := d2graph.StyleKeywords[strings.ToLower(f.Name)]; !ok {
   665  		c.errorf(f.LastRef().AST(), `invalid style keyword: "%s"`, f.Name)
   666  		return
   667  	}
   668  	if f.Primary() == nil {
   669  		return
   670  	}
   671  	compileStyleFieldInit(attrs, f)
   672  	scalar := f.Primary().Value
   673  	err := attrs.Style.Apply(f.Name, scalar.ScalarString())
   674  	if err != nil {
   675  		c.errorf(scalar, err.Error())
   676  		return
   677  	}
   678  }
   679  
   680  func compileStyleFieldInit(attrs *d2graph.Attributes, f *d2ir.Field) {
   681  	switch f.Name {
   682  	case "opacity":
   683  		attrs.Style.Opacity = &d2graph.Scalar{MapKey: f.LastPrimaryKey()}
   684  	case "stroke":
   685  		attrs.Style.Stroke = &d2graph.Scalar{MapKey: f.LastPrimaryKey()}
   686  	case "fill":
   687  		attrs.Style.Fill = &d2graph.Scalar{MapKey: f.LastPrimaryKey()}
   688  	case "fill-pattern":
   689  		attrs.Style.FillPattern = &d2graph.Scalar{MapKey: f.LastPrimaryKey()}
   690  	case "stroke-width":
   691  		attrs.Style.StrokeWidth = &d2graph.Scalar{MapKey: f.LastPrimaryKey()}
   692  	case "stroke-dash":
   693  		attrs.Style.StrokeDash = &d2graph.Scalar{MapKey: f.LastPrimaryKey()}
   694  	case "border-radius":
   695  		attrs.Style.BorderRadius = &d2graph.Scalar{MapKey: f.LastPrimaryKey()}
   696  	case "shadow":
   697  		attrs.Style.Shadow = &d2graph.Scalar{MapKey: f.LastPrimaryKey()}
   698  	case "3d":
   699  		attrs.Style.ThreeDee = &d2graph.Scalar{MapKey: f.LastPrimaryKey()}
   700  	case "multiple":
   701  		attrs.Style.Multiple = &d2graph.Scalar{MapKey: f.LastPrimaryKey()}
   702  	case "font":
   703  		attrs.Style.Font = &d2graph.Scalar{MapKey: f.LastPrimaryKey()}
   704  	case "font-size":
   705  		attrs.Style.FontSize = &d2graph.Scalar{MapKey: f.LastPrimaryKey()}
   706  	case "font-color":
   707  		attrs.Style.FontColor = &d2graph.Scalar{MapKey: f.LastPrimaryKey()}
   708  	case "animated":
   709  		attrs.Style.Animated = &d2graph.Scalar{MapKey: f.LastPrimaryKey()}
   710  	case "bold":
   711  		attrs.Style.Bold = &d2graph.Scalar{MapKey: f.LastPrimaryKey()}
   712  	case "italic":
   713  		attrs.Style.Italic = &d2graph.Scalar{MapKey: f.LastPrimaryKey()}
   714  	case "underline":
   715  		attrs.Style.Underline = &d2graph.Scalar{MapKey: f.LastPrimaryKey()}
   716  	case "filled":
   717  		attrs.Style.Filled = &d2graph.Scalar{MapKey: f.LastPrimaryKey()}
   718  	case "width":
   719  		attrs.WidthAttr = &d2graph.Scalar{MapKey: f.LastPrimaryKey()}
   720  	case "height":
   721  		attrs.HeightAttr = &d2graph.Scalar{MapKey: f.LastPrimaryKey()}
   722  	case "top":
   723  		attrs.Top = &d2graph.Scalar{MapKey: f.LastPrimaryKey()}
   724  	case "left":
   725  		attrs.Left = &d2graph.Scalar{MapKey: f.LastPrimaryKey()}
   726  	case "double-border":
   727  		attrs.Style.DoubleBorder = &d2graph.Scalar{MapKey: f.LastPrimaryKey()}
   728  	case "text-transform":
   729  		attrs.Style.TextTransform = &d2graph.Scalar{MapKey: f.LastPrimaryKey()}
   730  	}
   731  }
   732  
   733  func (c *compiler) compileEdge(obj *d2graph.Object, e *d2ir.Edge) {
   734  	edge, err := obj.Connect(d2graphIDA(e.ID.SrcPath), d2graphIDA(e.ID.DstPath), e.ID.SrcArrow, e.ID.DstArrow, "")
   735  	if err != nil {
   736  		c.errorf(e.References[0].AST(), err.Error())
   737  		return
   738  	}
   739  
   740  	if e.Primary() != nil {
   741  		c.compileLabel(&edge.Attributes, e)
   742  	}
   743  	if e.Map() != nil {
   744  		c.compileEdgeMap(edge, e.Map())
   745  	}
   746  
   747  	edge.Label.MapKey = e.LastPrimaryKey()
   748  	for _, er := range e.References {
   749  		r := d2graph.EdgeReference{
   750  			Edge:            er.Context_.Edge,
   751  			MapKey:          er.Context_.Key,
   752  			MapKeyEdgeIndex: er.Context_.EdgeIndex(),
   753  			Scope:           er.Context_.Scope,
   754  			ScopeAST:        er.Context_.ScopeAST,
   755  		}
   756  		if er.Context_.ScopeMap != nil && !d2ir.IsVar(er.Context_.ScopeMap) {
   757  			scopeObjIDA := d2graphIDA(d2ir.BoardIDA(er.Context_.ScopeMap))
   758  			r.ScopeObj = edge.Src.Graph.Root.EnsureChild(scopeObjIDA)
   759  		}
   760  		edge.References = append(edge.References, r)
   761  	}
   762  }
   763  
   764  func (c *compiler) compileEdgeMap(edge *d2graph.Edge, m *d2ir.Map) {
   765  	class := m.GetField("class")
   766  	if class != nil {
   767  		var classNames []string
   768  		if class.Primary() != nil {
   769  			classNames = append(classNames, class.Primary().String())
   770  		} else if class.Composite != nil {
   771  			if arr, ok := class.Composite.(*d2ir.Array); ok {
   772  				for _, class := range arr.Values {
   773  					if scalar, ok := class.(*d2ir.Scalar); ok {
   774  						classNames = append(classNames, scalar.Value.ScalarString())
   775  					} else {
   776  						c.errorf(class.LastPrimaryKey(), "invalid value in array")
   777  					}
   778  				}
   779  			}
   780  		} else {
   781  			c.errorf(class.LastRef().AST(), "class missing value")
   782  		}
   783  
   784  		for _, className := range classNames {
   785  			classMap := m.GetClassMap(className)
   786  			if classMap != nil {
   787  				c.compileEdgeMap(edge, classMap)
   788  			}
   789  		}
   790  	}
   791  	for _, f := range m.Fields {
   792  		_, ok := d2graph.ReservedKeywords[f.Name]
   793  		if !ok {
   794  			c.errorf(f.References[0].AST(), `edge map keys must be reserved keywords`)
   795  			continue
   796  		}
   797  		c.compileEdgeField(edge, f)
   798  	}
   799  }
   800  
   801  func (c *compiler) compileEdgeField(edge *d2graph.Edge, f *d2ir.Field) {
   802  	keyword := strings.ToLower(f.Name)
   803  	_, isStyleReserved := d2graph.StyleKeywords[keyword]
   804  	if isStyleReserved {
   805  		c.errorf(f.LastRef().AST(), "%v must be style.%v", f.Name, f.Name)
   806  		return
   807  	}
   808  	_, isReserved := d2graph.SimpleReservedKeywords[keyword]
   809  	if isReserved {
   810  		c.compileReserved(&edge.Attributes, f)
   811  		return
   812  	} else if f.Name == "style" {
   813  		if f.Map() == nil {
   814  			return
   815  		}
   816  		c.compileStyle(&edge.Attributes, f.Map())
   817  		return
   818  	}
   819  
   820  	if f.Name == "source-arrowhead" || f.Name == "target-arrowhead" {
   821  		c.compileArrowheads(edge, f)
   822  	}
   823  }
   824  
   825  func (c *compiler) compileArrowheads(edge *d2graph.Edge, f *d2ir.Field) {
   826  	var attrs *d2graph.Attributes
   827  	if f.Name == "source-arrowhead" {
   828  		if edge.SrcArrowhead == nil {
   829  			edge.SrcArrowhead = &d2graph.Attributes{}
   830  		}
   831  		attrs = edge.SrcArrowhead
   832  	} else {
   833  		if edge.DstArrowhead == nil {
   834  			edge.DstArrowhead = &d2graph.Attributes{}
   835  		}
   836  		attrs = edge.DstArrowhead
   837  	}
   838  
   839  	if f.Primary() != nil {
   840  		c.compileLabel(attrs, f)
   841  	}
   842  
   843  	if f.Map() != nil {
   844  		for _, f2 := range f.Map().Fields {
   845  			keyword := strings.ToLower(f2.Name)
   846  			_, isReserved := d2graph.SimpleReservedKeywords[keyword]
   847  			if isReserved {
   848  				c.compileReserved(attrs, f2)
   849  				continue
   850  			} else if f2.Name == "style" {
   851  				if f2.Map() == nil {
   852  					continue
   853  				}
   854  				c.compileStyle(attrs, f2.Map())
   855  				continue
   856  			} else {
   857  				c.errorf(f2.LastRef().AST(), `source-arrowhead/target-arrowhead map keys must be reserved keywords`)
   858  				continue
   859  			}
   860  		}
   861  	}
   862  }
   863  
   864  // TODO add more, e.g. C, bash
   865  var ShortToFullLanguageAliases = map[string]string{
   866  	"md":  "markdown",
   867  	"tex": "latex",
   868  	"js":  "javascript",
   869  	"go":  "golang",
   870  	"py":  "python",
   871  	"rb":  "ruby",
   872  	"ts":  "typescript",
   873  }
   874  var FullToShortLanguageAliases map[string]string
   875  
   876  func (c *compiler) compileClass(obj *d2graph.Object) {
   877  	obj.Class = &d2target.Class{}
   878  	for _, f := range obj.ChildrenArray {
   879  		visibility := "public"
   880  		name := f.IDVal
   881  		// See https://www.uml-diagrams.org/visibility.html
   882  		if name != "" {
   883  			switch name[0] {
   884  			case '+':
   885  				name = name[1:]
   886  			case '-':
   887  				visibility = "private"
   888  				name = name[1:]
   889  			case '#':
   890  				visibility = "protected"
   891  				name = name[1:]
   892  			}
   893  		}
   894  
   895  		if !strings.Contains(f.IDVal, "(") {
   896  			typ := f.Label.Value
   897  			if typ == f.IDVal {
   898  				typ = ""
   899  			}
   900  			obj.Class.Fields = append(obj.Class.Fields, d2target.ClassField{
   901  				Name:       name,
   902  				Type:       typ,
   903  				Visibility: visibility,
   904  			})
   905  		} else {
   906  			// TODO: Not great, AST should easily allow specifying alternate primary field
   907  			// as an explicit label should change the name.
   908  			returnType := f.Label.Value
   909  			if returnType == f.IDVal {
   910  				returnType = "void"
   911  			}
   912  			obj.Class.Methods = append(obj.Class.Methods, d2target.ClassMethod{
   913  				Name:       name,
   914  				Return:     returnType,
   915  				Visibility: visibility,
   916  			})
   917  		}
   918  	}
   919  
   920  	for _, ch := range obj.ChildrenArray {
   921  		for i := 0; i < len(obj.Graph.Objects); i++ {
   922  			if obj.Graph.Objects[i] == ch {
   923  				obj.Graph.Objects = append(obj.Graph.Objects[:i], obj.Graph.Objects[i+1:]...)
   924  				i--
   925  			}
   926  		}
   927  	}
   928  	obj.Children = nil
   929  	obj.ChildrenArray = nil
   930  }
   931  
   932  func (c *compiler) compileSQLTable(obj *d2graph.Object) {
   933  	obj.SQLTable = &d2target.SQLTable{}
   934  	for _, col := range obj.ChildrenArray {
   935  		typ := col.Label.Value
   936  		if typ == col.IDVal {
   937  			// Not great, AST should easily allow specifying alternate primary field
   938  			// as an explicit label should change the name.
   939  			typ = ""
   940  		}
   941  		d2Col := d2target.SQLColumn{
   942  			Name:       d2target.Text{Label: col.IDVal},
   943  			Type:       d2target.Text{Label: typ},
   944  			Constraint: col.Constraint,
   945  		}
   946  		obj.SQLTable.Columns = append(obj.SQLTable.Columns, d2Col)
   947  	}
   948  
   949  	for _, ch := range obj.ChildrenArray {
   950  		for i := 0; i < len(obj.Graph.Objects); i++ {
   951  			if obj.Graph.Objects[i] == ch {
   952  				obj.Graph.Objects = append(obj.Graph.Objects[:i], obj.Graph.Objects[i+1:]...)
   953  				i--
   954  			}
   955  		}
   956  	}
   957  	obj.Children = nil
   958  	obj.ChildrenArray = nil
   959  }
   960  
   961  func (c *compiler) validateKeys(obj *d2graph.Object, m *d2ir.Map) {
   962  	for _, f := range m.Fields {
   963  		if _, ok := d2graph.BoardKeywords[f.Name]; ok {
   964  			continue
   965  		}
   966  		c.validateKey(obj, f)
   967  	}
   968  }
   969  
   970  func (c *compiler) validateKey(obj *d2graph.Object, f *d2ir.Field) {
   971  	keyword := strings.ToLower(f.Name)
   972  	_, isReserved := d2graph.ReservedKeywords[keyword]
   973  	if isReserved {
   974  		switch obj.Shape.Value {
   975  		case d2target.ShapeCircle, d2target.ShapeSquare:
   976  			checkEqual := (keyword == "width" && obj.HeightAttr != nil) || (keyword == "height" && obj.WidthAttr != nil)
   977  			if checkEqual && obj.WidthAttr.Value != obj.HeightAttr.Value {
   978  				c.errorf(f.LastPrimaryKey(), "width and height must be equal for %s shapes", obj.Shape.Value)
   979  			}
   980  		}
   981  
   982  		switch f.Name {
   983  		case "style":
   984  			if obj.Style.ThreeDee != nil {
   985  				if !strings.EqualFold(obj.Shape.Value, d2target.ShapeSquare) && !strings.EqualFold(obj.Shape.Value, d2target.ShapeRectangle) && !strings.EqualFold(obj.Shape.Value, d2target.ShapeHexagon) {
   986  					c.errorf(obj.Style.ThreeDee.MapKey, `key "3d" can only be applied to squares, rectangles, and hexagons`)
   987  				}
   988  			}
   989  			if obj.Style.DoubleBorder != nil {
   990  				if obj.Shape.Value != "" && obj.Shape.Value != d2target.ShapeSquare && obj.Shape.Value != d2target.ShapeRectangle && obj.Shape.Value != d2target.ShapeCircle && obj.Shape.Value != d2target.ShapeOval {
   991  					c.errorf(obj.Style.DoubleBorder.MapKey, `key "double-border" can only be applied to squares, rectangles, circles, ovals`)
   992  				}
   993  			}
   994  		case "shape":
   995  			if obj.Shape.Value == d2target.ShapeImage && obj.Icon == nil {
   996  				c.errorf(f.LastPrimaryKey(), `image shape must include an "icon" field`)
   997  			}
   998  
   999  			in := d2target.IsShape(obj.Shape.Value)
  1000  			_, arrowheadIn := d2target.Arrowheads[obj.Shape.Value]
  1001  			if !in && arrowheadIn {
  1002  				c.errorf(f.LastPrimaryKey(), fmt.Sprintf(`invalid shape, can only set "%s" for arrowheads`, obj.Shape.Value))
  1003  			}
  1004  		case "constraint":
  1005  			if obj.Shape.Value != d2target.ShapeSQLTable {
  1006  				c.errorf(f.LastPrimaryKey(), `"constraint" keyword can only be used in "sql_table" shapes`)
  1007  			}
  1008  		}
  1009  		return
  1010  	}
  1011  
  1012  	if obj.Shape.Value == d2target.ShapeImage {
  1013  		c.errorf(f.LastRef().AST(), "image shapes cannot have children.")
  1014  		return
  1015  	}
  1016  
  1017  	obj, ok := obj.HasChild([]string{f.Name})
  1018  	if ok && f.Map() != nil {
  1019  		c.validateKeys(obj, f.Map())
  1020  	}
  1021  }
  1022  
  1023  func (c *compiler) validateLabels(g *d2graph.Graph) {
  1024  	for _, obj := range g.Objects {
  1025  		if obj.Shape.Value != d2target.ShapeText {
  1026  			continue
  1027  		}
  1028  		if obj.Attributes.Language != "" {
  1029  			// blockstrings have already been validated
  1030  			continue
  1031  		}
  1032  		if strings.TrimSpace(obj.Label.Value) == "" {
  1033  			c.errorf(obj.Label.MapKey, "shape text must have a non-empty label")
  1034  			continue
  1035  		}
  1036  	}
  1037  }
  1038  
  1039  func (c *compiler) validateNear(g *d2graph.Graph) {
  1040  	for _, obj := range g.Objects {
  1041  		if obj.NearKey != nil {
  1042  			nearObj, isKey := g.Root.HasChild(d2graph.Key(obj.NearKey))
  1043  			_, isConst := d2graph.NearConstants[d2graph.Key(obj.NearKey)[0]]
  1044  			if isKey {
  1045  				// Doesn't make sense to set near to an ancestor or descendant
  1046  				nearIsAncestor := false
  1047  				for curr := obj; curr != nil; curr = curr.Parent {
  1048  					if curr == nearObj {
  1049  						nearIsAncestor = true
  1050  						break
  1051  					}
  1052  				}
  1053  				if nearIsAncestor {
  1054  					c.errorf(obj.NearKey, "near keys cannot be set to an ancestor")
  1055  					continue
  1056  				}
  1057  				nearIsDescendant := false
  1058  				for curr := nearObj; curr != nil; curr = curr.Parent {
  1059  					if curr == obj {
  1060  						nearIsDescendant = true
  1061  						break
  1062  					}
  1063  				}
  1064  				if nearIsDescendant {
  1065  					c.errorf(obj.NearKey, "near keys cannot be set to an descendant")
  1066  					continue
  1067  				}
  1068  				if nearObj.OuterSequenceDiagram() != nil {
  1069  					c.errorf(obj.NearKey, "near keys cannot be set to an object within sequence diagrams")
  1070  					continue
  1071  				}
  1072  				if nearObj.NearKey != nil {
  1073  					_, nearObjNearIsConst := d2graph.NearConstants[d2graph.Key(nearObj.NearKey)[0]]
  1074  					if nearObjNearIsConst {
  1075  						c.errorf(obj.NearKey, "near keys cannot be set to an object with a constant near key")
  1076  						continue
  1077  					}
  1078  				}
  1079  			} else if isConst {
  1080  				if obj.Parent != g.Root {
  1081  					c.errorf(obj.NearKey, "constant near keys can only be set on root level shapes")
  1082  					continue
  1083  				}
  1084  			} else {
  1085  				c.errorf(obj.NearKey, "near key %#v must be the absolute path to a shape or one of the following constants: %s", d2format.Format(obj.NearKey), strings.Join(d2graph.NearConstantsArray, ", "))
  1086  				continue
  1087  			}
  1088  		}
  1089  	}
  1090  
  1091  	for _, edge := range g.Edges {
  1092  		if edge.Src.IsConstantNear() && edge.Dst.IsDescendantOf(edge.Src) {
  1093  			c.errorf(edge.GetAstEdge(), "edge from constant near %#v cannot enter itself", edge.Src.AbsID())
  1094  			continue
  1095  		}
  1096  		if edge.Dst.IsConstantNear() && edge.Src.IsDescendantOf(edge.Dst) {
  1097  			c.errorf(edge.GetAstEdge(), "edge from constant near %#v cannot enter itself", edge.Dst.AbsID())
  1098  			continue
  1099  		}
  1100  	}
  1101  
  1102  }
  1103  
  1104  func (c *compiler) validateEdges(g *d2graph.Graph) {
  1105  	for _, edge := range g.Edges {
  1106  		// edges from a grid to something outside is ok
  1107  		//   grid -> outside : ok
  1108  		//   grid -> grid.cell : not ok
  1109  		//   grid -> grid.cell.inner : not ok
  1110  		if edge.Src.IsGridDiagram() && edge.Dst.IsDescendantOf(edge.Src) {
  1111  			c.errorf(edge.GetAstEdge(), "edge from grid diagram %#v cannot enter itself", edge.Src.AbsID())
  1112  			continue
  1113  		}
  1114  		if edge.Dst.IsGridDiagram() && edge.Src.IsDescendantOf(edge.Dst) {
  1115  			c.errorf(edge.GetAstEdge(), "edge from grid diagram %#v cannot enter itself", edge.Dst.AbsID())
  1116  			continue
  1117  		}
  1118  		if edge.Src.Parent.IsGridDiagram() && edge.Dst.IsDescendantOf(edge.Src) {
  1119  			c.errorf(edge.GetAstEdge(), "edge from grid cell %#v cannot enter itself", edge.Src.AbsID())
  1120  			continue
  1121  		}
  1122  		if edge.Dst.Parent.IsGridDiagram() && edge.Src.IsDescendantOf(edge.Dst) {
  1123  			c.errorf(edge.GetAstEdge(), "edge from grid cell %#v cannot enter itself", edge.Dst.AbsID())
  1124  			continue
  1125  		}
  1126  		if edge.Src.IsSequenceDiagram() && edge.Dst.IsDescendantOf(edge.Src) {
  1127  			c.errorf(edge.GetAstEdge(), "edge from sequence diagram %#v cannot enter itself", edge.Src.AbsID())
  1128  			continue
  1129  		}
  1130  		if edge.Dst.IsSequenceDiagram() && edge.Src.IsDescendantOf(edge.Dst) {
  1131  			c.errorf(edge.GetAstEdge(), "edge from sequence diagram %#v cannot enter itself", edge.Dst.AbsID())
  1132  			continue
  1133  		}
  1134  	}
  1135  }
  1136  
  1137  func (c *compiler) validateBoardLinks(g *d2graph.Graph) {
  1138  	for _, obj := range g.Objects {
  1139  		if obj.Link == nil {
  1140  			continue
  1141  		}
  1142  
  1143  		linkKey, err := d2parser.ParseKey(obj.Link.Value)
  1144  		if err != nil {
  1145  			continue
  1146  		}
  1147  
  1148  		if linkKey.Path[0].Unbox().ScalarString() != "root" {
  1149  			continue
  1150  		}
  1151  
  1152  		if !hasBoard(g.RootBoard(), linkKey.IDA()) {
  1153  			c.errorf(obj.Link.MapKey, "linked board not found")
  1154  			continue
  1155  		}
  1156  	}
  1157  	for _, b := range g.Layers {
  1158  		c.validateBoardLinks(b)
  1159  	}
  1160  	for _, b := range g.Scenarios {
  1161  		c.validateBoardLinks(b)
  1162  	}
  1163  	for _, b := range g.Steps {
  1164  		c.validateBoardLinks(b)
  1165  	}
  1166  }
  1167  
  1168  func hasBoard(root *d2graph.Graph, ida []string) bool {
  1169  	if len(ida) == 0 {
  1170  		return true
  1171  	}
  1172  	if ida[0] == "root" {
  1173  		return hasBoard(root, ida[1:])
  1174  	}
  1175  	id := ida[0]
  1176  	if len(ida) == 1 {
  1177  		return root.Name == id
  1178  	}
  1179  	nextID := ida[1]
  1180  	switch id {
  1181  	case "layers":
  1182  		for _, b := range root.Layers {
  1183  			if b.Name == nextID {
  1184  				return hasBoard(b, ida[2:])
  1185  			}
  1186  		}
  1187  	case "scenarios":
  1188  		for _, b := range root.Scenarios {
  1189  			if b.Name == nextID {
  1190  				return hasBoard(b, ida[2:])
  1191  			}
  1192  		}
  1193  	case "steps":
  1194  		for _, b := range root.Steps {
  1195  			if b.Name == nextID {
  1196  				return hasBoard(b, ida[2:])
  1197  			}
  1198  		}
  1199  	}
  1200  	return false
  1201  }
  1202  
  1203  func init() {
  1204  	FullToShortLanguageAliases = make(map[string]string, len(ShortToFullLanguageAliases))
  1205  	for k, v := range ShortToFullLanguageAliases {
  1206  		FullToShortLanguageAliases[v] = k
  1207  	}
  1208  }
  1209  
  1210  func d2graphIDA(irIDA []string) (ida []string) {
  1211  	for _, el := range irIDA {
  1212  		n := &d2ast.KeyPath{
  1213  			Path: []*d2ast.StringBox{d2ast.MakeValueBox(d2ast.RawString(el, true)).StringBox()},
  1214  		}
  1215  		ida = append(ida, d2format.Format(n))
  1216  	}
  1217  	return ida
  1218  }
  1219  
  1220  // Unused for now until shape: edge_group
  1221  func (c *compiler) preprocessSeqDiagrams(m *d2ir.Map) {
  1222  	for _, f := range m.Fields {
  1223  		if f.Name == "shape" && f.Primary_.Value.ScalarString() == d2target.ShapeSequenceDiagram {
  1224  			c.preprocessEdgeGroup(m, m)
  1225  			return
  1226  		}
  1227  		if f.Map() != nil {
  1228  			c.preprocessSeqDiagrams(f.Map())
  1229  		}
  1230  	}
  1231  }
  1232  
  1233  func (c *compiler) preprocessEdgeGroup(seqDiagram, m *d2ir.Map) {
  1234  	// Any child of a sequence diagram can be either an actor, edge group or a span.
  1235  	// 1. Actors are shapes without edges inside them defined at the top level scope of a
  1236  	//    sequence diagram.
  1237  	// 2. Spans are the children of actors. For our purposes we can ignore them.
  1238  	// 3. Edge groups are defined as having at least one connection within them and also not
  1239  	//    being connected to anything. All direct children of an edge group are either edge
  1240  	//    groups or top level actors.
  1241  
  1242  	// Go through all the fields and hoist actors from edge groups while also processing
  1243  	// the edge groups recursively.
  1244  	for _, f := range m.Fields {
  1245  		if isEdgeGroup(f) {
  1246  			if f.Map() != nil {
  1247  				c.preprocessEdgeGroup(seqDiagram, f.Map())
  1248  			}
  1249  		} else {
  1250  			if m == seqDiagram {
  1251  				// Ignore for root.
  1252  				continue
  1253  			}
  1254  			hoistActor(seqDiagram, f)
  1255  		}
  1256  	}
  1257  
  1258  	// We need to adjust all edges recursively to point to actual actors instead.
  1259  	for _, e := range m.Edges {
  1260  		if isCrossEdgeGroupEdge(m, e) {
  1261  			c.errorf(e.References[0].AST(), "illegal edge between edge groups")
  1262  			continue
  1263  		}
  1264  
  1265  		if m == seqDiagram {
  1266  			// Root edges between actors directly do not require hoisting.
  1267  			continue
  1268  		}
  1269  
  1270  		srcParent := seqDiagram
  1271  		for i, el := range e.ID.SrcPath {
  1272  			f := srcParent.GetField(el)
  1273  			if !isEdgeGroup(f) {
  1274  				for j := 0; j < i+1; j++ {
  1275  					e.ID.SrcPath = append([]string{"_"}, e.ID.SrcPath...)
  1276  					e.ID.DstPath = append([]string{"_"}, e.ID.DstPath...)
  1277  				}
  1278  				break
  1279  			}
  1280  			srcParent = f.Map()
  1281  		}
  1282  	}
  1283  }
  1284  
  1285  func hoistActor(seqDiagram *d2ir.Map, f *d2ir.Field) {
  1286  	f2 := seqDiagram.GetField(f.Name)
  1287  	if f2 == nil {
  1288  		seqDiagram.Fields = append(seqDiagram.Fields, f.Copy(seqDiagram).(*d2ir.Field))
  1289  	} else {
  1290  		d2ir.OverlayField(f2, f)
  1291  		d2ir.ParentMap(f).DeleteField(f.Name)
  1292  	}
  1293  }
  1294  
  1295  func isCrossEdgeGroupEdge(m *d2ir.Map, e *d2ir.Edge) bool {
  1296  	srcParent := m
  1297  	for _, el := range e.ID.SrcPath {
  1298  		f := srcParent.GetField(el)
  1299  		if f == nil {
  1300  			// Hoisted already.
  1301  			break
  1302  		}
  1303  		if isEdgeGroup(f) {
  1304  			return true
  1305  		}
  1306  		srcParent = f.Map()
  1307  	}
  1308  
  1309  	dstParent := m
  1310  	for _, el := range e.ID.DstPath {
  1311  		f := dstParent.GetField(el)
  1312  		if f == nil {
  1313  			// Hoisted already.
  1314  			break
  1315  		}
  1316  		if isEdgeGroup(f) {
  1317  			return true
  1318  		}
  1319  		dstParent = f.Map()
  1320  	}
  1321  
  1322  	return false
  1323  }
  1324  
  1325  func isEdgeGroup(n d2ir.Node) bool {
  1326  	return n.Map().EdgeCountRecursive() > 0
  1327  }
  1328  
  1329  func parentSeqDiagram(n d2ir.Node) *d2ir.Map {
  1330  	for {
  1331  		m := d2ir.ParentMap(n)
  1332  		if m == nil {
  1333  			return nil
  1334  		}
  1335  		for _, f := range m.Fields {
  1336  			if f.Name == "shape" && f.Primary_.Value.ScalarString() == d2target.ShapeSequenceDiagram {
  1337  				return m
  1338  			}
  1339  		}
  1340  		n = m
  1341  	}
  1342  }
  1343  
  1344  func compileConfig(ir *d2ir.Map) (*d2target.Config, error) {
  1345  	f := ir.GetField("vars", "d2-config")
  1346  	if f == nil || f.Map() == nil {
  1347  		return nil, nil
  1348  	}
  1349  
  1350  	configMap := f.Map()
  1351  
  1352  	config := &d2target.Config{}
  1353  
  1354  	f = configMap.GetField("sketch")
  1355  	if f != nil {
  1356  		val, _ := strconv.ParseBool(f.Primary().Value.ScalarString())
  1357  		config.Sketch = &val
  1358  	}
  1359  
  1360  	f = configMap.GetField("theme-id")
  1361  	if f != nil {
  1362  		val, _ := strconv.Atoi(f.Primary().Value.ScalarString())
  1363  		config.ThemeID = go2.Pointer(int64(val))
  1364  	}
  1365  
  1366  	f = configMap.GetField("dark-theme-id")
  1367  	if f != nil {
  1368  		val, _ := strconv.Atoi(f.Primary().Value.ScalarString())
  1369  		config.DarkThemeID = go2.Pointer(int64(val))
  1370  	}
  1371  
  1372  	f = configMap.GetField("pad")
  1373  	if f != nil {
  1374  		val, _ := strconv.Atoi(f.Primary().Value.ScalarString())
  1375  		config.Pad = go2.Pointer(int64(val))
  1376  	}
  1377  
  1378  	f = configMap.GetField("layout-engine")
  1379  	if f != nil {
  1380  		config.LayoutEngine = go2.Pointer(f.Primary().Value.ScalarString())
  1381  	}
  1382  
  1383  	f = configMap.GetField("theme-overrides")
  1384  	if f != nil {
  1385  		overrides, err := compileThemeOverrides(f.Map())
  1386  		if err != nil {
  1387  			return nil, err
  1388  		}
  1389  		config.ThemeOverrides = overrides
  1390  	}
  1391  	f = configMap.GetField("dark-theme-overrides")
  1392  	if f != nil {
  1393  		overrides, err := compileThemeOverrides(f.Map())
  1394  		if err != nil {
  1395  			return nil, err
  1396  		}
  1397  		config.DarkThemeOverrides = overrides
  1398  	}
  1399  
  1400  	return config, nil
  1401  }
  1402  
  1403  func compileThemeOverrides(m *d2ir.Map) (*d2target.ThemeOverrides, error) {
  1404  	if m == nil {
  1405  		return nil, nil
  1406  	}
  1407  	themeOverrides := d2target.ThemeOverrides{}
  1408  
  1409  	err := &d2parser.ParseError{}
  1410  FOR:
  1411  	for _, f := range m.Fields {
  1412  		switch strings.ToUpper(f.Name) {
  1413  		case "N1":
  1414  			themeOverrides.N1 = go2.Pointer(f.Primary().Value.ScalarString())
  1415  		case "N2":
  1416  			themeOverrides.N2 = go2.Pointer(f.Primary().Value.ScalarString())
  1417  		case "N3":
  1418  			themeOverrides.N3 = go2.Pointer(f.Primary().Value.ScalarString())
  1419  		case "N4":
  1420  			themeOverrides.N4 = go2.Pointer(f.Primary().Value.ScalarString())
  1421  		case "N5":
  1422  			themeOverrides.N5 = go2.Pointer(f.Primary().Value.ScalarString())
  1423  		case "N6":
  1424  			themeOverrides.N6 = go2.Pointer(f.Primary().Value.ScalarString())
  1425  		case "N7":
  1426  			themeOverrides.N7 = go2.Pointer(f.Primary().Value.ScalarString())
  1427  		case "B1":
  1428  			themeOverrides.B1 = go2.Pointer(f.Primary().Value.ScalarString())
  1429  		case "B2":
  1430  			themeOverrides.B2 = go2.Pointer(f.Primary().Value.ScalarString())
  1431  		case "B3":
  1432  			themeOverrides.B3 = go2.Pointer(f.Primary().Value.ScalarString())
  1433  		case "B4":
  1434  			themeOverrides.B4 = go2.Pointer(f.Primary().Value.ScalarString())
  1435  		case "B5":
  1436  			themeOverrides.B5 = go2.Pointer(f.Primary().Value.ScalarString())
  1437  		case "B6":
  1438  			themeOverrides.B6 = go2.Pointer(f.Primary().Value.ScalarString())
  1439  		case "AA2":
  1440  			themeOverrides.AA2 = go2.Pointer(f.Primary().Value.ScalarString())
  1441  		case "AA4":
  1442  			themeOverrides.AA4 = go2.Pointer(f.Primary().Value.ScalarString())
  1443  		case "AA5":
  1444  			themeOverrides.AA5 = go2.Pointer(f.Primary().Value.ScalarString())
  1445  		case "AB4":
  1446  			themeOverrides.AB4 = go2.Pointer(f.Primary().Value.ScalarString())
  1447  		case "AB5":
  1448  			themeOverrides.AB5 = go2.Pointer(f.Primary().Value.ScalarString())
  1449  		default:
  1450  			err.Errors = append(err.Errors, d2parser.Errorf(f.LastPrimaryKey(), fmt.Sprintf(`"%s" is not a valid theme code`, f.Name)).(d2ast.Error))
  1451  			continue FOR
  1452  		}
  1453  		if !go2.Contains(color.NamedColors, strings.ToLower(f.Primary().Value.ScalarString())) && !color.ColorHexRegex.MatchString(f.Primary().Value.ScalarString()) {
  1454  			err.Errors = append(err.Errors, d2parser.Errorf(f.LastPrimaryKey(), fmt.Sprintf(`expected "%s" to be a valid named color ("orange") or a hex code ("#f0ff3a")`, f.Name)).(d2ast.Error))
  1455  		}
  1456  	}
  1457  
  1458  	if !err.Empty() {
  1459  		return nil, err
  1460  	}
  1461  
  1462  	if themeOverrides != (d2target.ThemeOverrides{}) {
  1463  		return &themeOverrides, nil
  1464  	}
  1465  	return nil, nil
  1466  }
  1467  

View as plain text