...

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

Documentation: oss.terrastruct.com/d2/d2compiler

     1  package d2compiler_test
     2  
     3  import (
     4  	"fmt"
     5  	"path/filepath"
     6  	"strings"
     7  	"testing"
     8  
     9  	tassert "github.com/stretchr/testify/assert"
    10  
    11  	"oss.terrastruct.com/util-go/assert"
    12  	"oss.terrastruct.com/util-go/diff"
    13  
    14  	"oss.terrastruct.com/d2/d2compiler"
    15  	"oss.terrastruct.com/d2/d2format"
    16  	"oss.terrastruct.com/d2/d2graph"
    17  	"oss.terrastruct.com/d2/d2target"
    18  )
    19  
    20  func TestCompile(t *testing.T) {
    21  	t.Parallel()
    22  
    23  	testCases := []struct {
    24  		name string
    25  		text string
    26  
    27  		expErr     string
    28  		assertions func(t *testing.T, g *d2graph.Graph)
    29  	}{
    30  		{
    31  			name: "basic_shape",
    32  
    33  			text: `
    34  x: {
    35    shape: circle
    36  }
    37  `,
    38  			assertions: func(t *testing.T, g *d2graph.Graph) {
    39  				if len(g.Objects) != 1 {
    40  					t.Fatalf("expected 1 objects: %#v", g.Objects)
    41  				}
    42  				if g.Objects[0].ID != "x" {
    43  					t.Fatalf("expected g.Objects[0].ID to be x: %#v", g.Objects[0])
    44  				}
    45  
    46  				if g.Objects[0].Shape.Value != d2target.ShapeCircle {
    47  					t.Fatalf("expected g.Objects[0].Shape.Value to be circle: %#v", g.Objects[0].Shape.Value)
    48  				}
    49  
    50  			},
    51  		},
    52  		{
    53  			name: "basic_style",
    54  
    55  			text: `
    56  x: {
    57  	style.opacity: 0.4
    58  }
    59  `,
    60  			assertions: func(t *testing.T, g *d2graph.Graph) {
    61  				if len(g.Objects) != 1 {
    62  					t.Fatalf("expected 1 objects: %#v", g.Objects)
    63  				}
    64  				if g.Objects[0].ID != "x" {
    65  					t.Fatalf("expected g.Objects[0].ID to be x: %#v", g.Objects[0])
    66  				}
    67  
    68  				if g.Objects[0].Style.Opacity.Value != "0.4" {
    69  					t.Fatalf("expected g.Objects[0].Style.Opacity.Value to be 0.4: %#v", g.Objects[0].Style.Opacity.Value)
    70  				}
    71  
    72  			},
    73  		},
    74  		{
    75  			name: "image_style",
    76  
    77  			text: `hey: "" {
    78    icon: https://icons.terrastruct.com/essentials/004-picture.svg
    79    shape: image
    80    style.stroke: "#0D32B2"
    81  }
    82  `,
    83  			assertions: func(t *testing.T, g *d2graph.Graph) {
    84  				if len(g.Objects) != 1 {
    85  					t.Fatalf("expected 1 objects: %#v", g.Objects)
    86  				}
    87  			},
    88  		},
    89  		{
    90  			name: "dimensions_on_nonimage",
    91  
    92  			text: `hey: "" {
    93    shape: hexagon
    94  	width: 200
    95  	height: 230
    96  }
    97  `,
    98  			assertions: func(t *testing.T, g *d2graph.Graph) {
    99  				if len(g.Objects) != 1 {
   100  					t.Fatalf("expected 1 objects: %#v", g.Objects)
   101  				}
   102  				if g.Objects[0].ID != "hey" {
   103  					t.Fatalf("expected g.Objects[0].ID to be 'hey': %#v", g.Objects[0])
   104  				}
   105  				if g.Objects[0].Shape.Value != d2target.ShapeHexagon {
   106  					t.Fatalf("expected g.Objects[0].Shape.Value to be hexagon: %#v", g.Objects[0].Shape.Value)
   107  				}
   108  				if g.Objects[0].WidthAttr.Value != "200" {
   109  					t.Fatalf("expected g.Objects[0].Width.Value to be 200: %#v", g.Objects[0].WidthAttr.Value)
   110  				}
   111  				if g.Objects[0].HeightAttr.Value != "230" {
   112  					t.Fatalf("expected g.Objects[0].Height.Value to be 230: %#v", g.Objects[0].HeightAttr.Value)
   113  				}
   114  			},
   115  		},
   116  		{
   117  			name: "positions",
   118  			text: `hey: {
   119  	top: 200
   120  	left: 230
   121  }
   122  `,
   123  			assertions: func(t *testing.T, g *d2graph.Graph) {
   124  				tassert.Equal(t, "200", g.Objects[0].Top.Value)
   125  			},
   126  		},
   127  		{
   128  			name: "positions_negative",
   129  			text: `hey: {
   130  	top: 200
   131  	left: -200
   132  }
   133  `,
   134  			expErr: `d2/testdata/d2compiler/TestCompile/positions_negative.d2:3:8: left must be a non-negative integer: "-200"`,
   135  		},
   136  		{
   137  			name: "equal_dimensions_on_circle",
   138  
   139  			text: `hey: "" {
   140  	shape: circle
   141  	width: 200
   142  	height: 230
   143  }
   144  `,
   145  			expErr: `d2/testdata/d2compiler/TestCompile/equal_dimensions_on_circle.d2:3:2: width and height must be equal for circle shapes
   146  d2/testdata/d2compiler/TestCompile/equal_dimensions_on_circle.d2:4:2: width and height must be equal for circle shapes`,
   147  		},
   148  		{
   149  			name: "single_dimension_on_circle",
   150  
   151  			text: `hey: "" {
   152  	shape: circle
   153  	height: 230
   154  }
   155  `,
   156  			assertions: func(t *testing.T, g *d2graph.Graph) {
   157  				if len(g.Objects) != 1 {
   158  					t.Fatalf("expected 1 objects: %#v", g.Objects)
   159  				}
   160  				if g.Objects[0].ID != "hey" {
   161  					t.Fatalf("expected ID to be 'hey': %#v", g.Objects[0])
   162  				}
   163  				if g.Objects[0].Shape.Value != d2target.ShapeCircle {
   164  					t.Fatalf("expected Attributes.Shape.Value to be circle: %#v", g.Objects[0].Shape.Value)
   165  				}
   166  				if g.Objects[0].WidthAttr != nil {
   167  					t.Fatalf("expected Attributes.Width to be nil: %#v", g.Objects[0].WidthAttr)
   168  				}
   169  				if g.Objects[0].HeightAttr == nil {
   170  					t.Fatalf("Attributes.Height is nil")
   171  				}
   172  			},
   173  		},
   174  		{
   175  			name: "dimensions_on_containers",
   176  			text: `
   177  containers: {
   178  	circle container: {
   179  		shape: circle
   180  		width: 512
   181  
   182  		diamond: {
   183  			shape: diamond
   184  			width: 128
   185  			height: 64
   186  		}
   187  	}
   188  	diamond container: {
   189  		shape: diamond
   190  		width: 512
   191  		height: 256
   192  
   193  		circle: {
   194  			shape: circle
   195  			width: 128
   196  		}
   197  	}
   198  	oval container: {
   199  		shape: oval
   200  		width: 512
   201  		height: 256
   202  
   203  		hexagon: {
   204  			shape: hexagon
   205  			width: 128
   206  			height: 64
   207  		}
   208  	}
   209  	hexagon container: {
   210  		shape: hexagon
   211  		width: 512
   212  		height: 256
   213  
   214  		oval: {
   215  			shape: oval
   216  			width: 128
   217  			height: 64
   218  		}
   219  	}
   220  }
   221  `,
   222  		},
   223  		{
   224  			name: "dimension_with_style",
   225  
   226  			text: `x: {
   227    width: 200
   228    style.multiple: true
   229  }
   230  `,
   231  		},
   232  		{
   233  			name: "basic_icon",
   234  
   235  			text: `hey: "" {
   236    icon: https://icons.terrastruct.com/essentials/004-picture.svg
   237  }
   238  `,
   239  			assertions: func(t *testing.T, g *d2graph.Graph) {
   240  				if g.Objects[0].Icon == nil {
   241  					t.Fatal("Attribute icon is nil")
   242  				}
   243  			},
   244  		},
   245  		{
   246  			name: "fill-pattern",
   247  			text: `x: {
   248  	style: {
   249      fill-pattern: dots
   250    }
   251  }
   252  `,
   253  		},
   254  		{
   255  			name: "invalid-fill-pattern",
   256  			text: `x: {
   257  	style: {
   258      fill-pattern: ddots
   259    }
   260  }
   261  `,
   262  			expErr: `d2/testdata/d2compiler/TestCompile/invalid-fill-pattern.d2:3:19: expected "fill-pattern" to be one of: dots, lines, grain, paper`,
   263  		},
   264  		{
   265  			name: "shape_unquoted_hex",
   266  
   267  			text: `x: {
   268  	style: {
   269      fill: #ffffff
   270    }
   271  }
   272  `,
   273  			expErr: `d2/testdata/d2compiler/TestCompile/shape_unquoted_hex.d2:3:10: missing value after colon`,
   274  		},
   275  		{
   276  			name: "edge_unquoted_hex",
   277  
   278  			text: `x -> y: {
   279  	style: {
   280      fill: #ffffff
   281    }
   282  }
   283  `,
   284  			expErr: `d2/testdata/d2compiler/TestCompile/edge_unquoted_hex.d2:3:10: missing value after colon`,
   285  		},
   286  		{
   287  			name: "blank_underscore",
   288  
   289  			text: `x: {
   290    y
   291    _
   292  }
   293  `,
   294  			expErr: `d2/testdata/d2compiler/TestCompile/blank_underscore.d2:3:3: field key must contain more than underscores`,
   295  		},
   296  		{
   297  			name: "image_non_style",
   298  
   299  			text: `x: {
   300    shape: image
   301    icon: https://icons.terrastruct.com/aws/_Group%20Icons/EC2-instance-container_light-bg.svg
   302    name: y
   303  }
   304  `,
   305  			expErr: `d2/testdata/d2compiler/TestCompile/image_non_style.d2:4:3: image shapes cannot have children.`,
   306  		},
   307  		{
   308  			name: "image_children_Steps",
   309  
   310  			text: `x: {
   311    icon: https://icons.terrastruct.com/aws/_Group%20Icons/EC2-instance-container_light-bg.svg
   312    shape: image
   313    Steps
   314  }
   315  `,
   316  			expErr: `d2/testdata/d2compiler/TestCompile/image_children_Steps.d2:4:3: steps is only allowed at a board root`,
   317  		},
   318  		{
   319  			name: "name-with-dot-underscore",
   320  			text: `A: {
   321    _.C
   322  }
   323  
   324  "D.E": {
   325    _.C
   326  }
   327  `,
   328  			assertions: func(t *testing.T, g *d2graph.Graph) {
   329  				tassert.Equal(t, 3, len(g.Objects))
   330  			},
   331  		},
   332  		{
   333  			name: "stroke-width",
   334  
   335  			text: `hey {
   336    style.stroke-width: 0
   337  }
   338  `,
   339  			assertions: func(t *testing.T, g *d2graph.Graph) {
   340  				if len(g.Objects) != 1 {
   341  					t.Fatalf("expected 1 objects: %#v", g.Objects)
   342  				}
   343  				if g.Objects[0].Style.StrokeWidth.Value != "0" {
   344  					t.Fatalf("unexpected")
   345  				}
   346  			},
   347  		},
   348  		{
   349  			name: "illegal-stroke-width",
   350  
   351  			text: `hey {
   352    style.stroke-width: -1
   353  }
   354  `,
   355  			expErr: `d2/testdata/d2compiler/TestCompile/illegal-stroke-width.d2:2:23: expected "stroke-width" to be a number between 0 and 15`,
   356  		},
   357  		{
   358  			name: "underscore_parent_create",
   359  
   360  			text: `
   361  x: {
   362  	_.y
   363  }
   364  `,
   365  			assertions: func(t *testing.T, g *d2graph.Graph) {
   366  				if len(g.Objects) != 2 {
   367  					t.Fatalf("expected 2 objects: %#v", g.Objects)
   368  				}
   369  				if g.Objects[0].ID != "x" {
   370  					t.Fatalf("expected g.Objects[0].ID to be x: %#v", g.Objects[0])
   371  				}
   372  				if g.Objects[1].ID != "y" {
   373  					t.Fatalf("expected g.Objects[1].ID to be y: %#v", g.Objects[1])
   374  				}
   375  
   376  				if len(g.Root.ChildrenArray) != 2 {
   377  					t.Fatalf("expected 2 objects at the root: %#v", len(g.Root.ChildrenArray))
   378  				}
   379  
   380  			},
   381  		},
   382  		{
   383  			name: "underscore_unresolved_obj",
   384  
   385  			text: `
   386  x: {
   387  	_.y
   388  }
   389  `,
   390  			assertions: func(t *testing.T, g *d2graph.Graph) {
   391  				tassert.Equal(t, "y", g.Objects[1].ID)
   392  				tassert.Equal(t, g.Objects[0].AbsID(), g.Objects[1].References[0].ScopeObj.AbsID())
   393  			},
   394  		},
   395  		{
   396  			name: "underscore_connection",
   397  			text: `a: {
   398    _.c.d -> _.c.b
   399  }
   400  `,
   401  			assertions: func(t *testing.T, g *d2graph.Graph) {
   402  				tassert.Equal(t, 4, len(g.Objects))
   403  				tassert.Equal(t, 1, len(g.Edges))
   404  			},
   405  		},
   406  		{
   407  			name: "underscore_parent_not_root",
   408  
   409  			text: `
   410  x: {
   411    y: {
   412      _.z
   413    }
   414  }
   415  `,
   416  			assertions: func(t *testing.T, g *d2graph.Graph) {
   417  				if len(g.Objects) != 3 {
   418  					t.Fatalf("expected 3 objects: %#v", g.Objects)
   419  				}
   420  				if g.Objects[0].ID != "x" {
   421  					t.Fatalf("expected g.Objects[0].ID to be x: %#v", g.Objects[0])
   422  				}
   423  				if g.Objects[1].ID != "y" {
   424  					t.Fatalf("expected g.Objects[1].ID to be y: %#v", g.Objects[1])
   425  				}
   426  
   427  				if len(g.Root.ChildrenArray) != 1 {
   428  					t.Fatalf("expected 1 object at the root: %#v", len(g.Root.ChildrenArray))
   429  				}
   430  				if len(g.Objects[0].ChildrenArray) != 2 {
   431  					t.Fatalf("expected 2 objects within x: %v", len(g.Objects[0].ChildrenArray))
   432  				}
   433  
   434  			},
   435  		},
   436  		{
   437  			name: "underscore_parent_preference_1",
   438  
   439  			text: `
   440  x: {
   441  	_.y: "All we are given is possibilities -- to make ourselves one thing or another."
   442  }
   443  y: "But it's real.  And if it's real it can be affected ...  we may not be able"
   444  `,
   445  			assertions: func(t *testing.T, g *d2graph.Graph) {
   446  				if len(g.Objects) != 2 {
   447  					t.Fatalf("expected 2 objects: %#v", g.Objects)
   448  				}
   449  				if g.Objects[0].ID != "x" {
   450  					t.Fatalf("expected g.Objects[0].ID to be x: %#v", g.Objects[0])
   451  				}
   452  				if g.Objects[1].ID != "y" {
   453  					t.Fatalf("expected g.Objects[1].ID to be y: %#v", g.Objects[1])
   454  				}
   455  
   456  				if len(g.Root.ChildrenArray) != 2 {
   457  					t.Fatalf("expected 2 objects at the root: %#v", len(g.Root.ChildrenArray))
   458  				}
   459  				if g.Objects[1].Label.Value != "But it's real.  And if it's real it can be affected ...  we may not be able" {
   460  					t.Fatalf("expected g.Objects[1].Label.Value to be last value: %#v", g.Objects[1].Label.Value)
   461  				}
   462  			},
   463  		},
   464  		{
   465  			name: "underscore_parent_preference_2",
   466  
   467  			text: `
   468  y: "But it's real.  And if it's real it can be affected ...  we may not be able"
   469  x: {
   470  	_.y: "All we are given is possibilities -- to make ourselves one thing or another."
   471  }
   472  `,
   473  			assertions: func(t *testing.T, g *d2graph.Graph) {
   474  				if len(g.Objects) != 2 {
   475  					t.Fatalf("expected 2 objects: %#v", g.Objects)
   476  				}
   477  				if g.Objects[0].ID != "y" {
   478  					t.Fatalf("expected g.Objects[0].ID to be y: %#v", g.Objects[0])
   479  				}
   480  				if g.Objects[1].ID != "x" {
   481  					t.Fatalf("expected g.Objects[1].ID to be x: %#v", g.Objects[1])
   482  				}
   483  
   484  				if len(g.Root.ChildrenArray) != 2 {
   485  					t.Fatalf("expected 2 objects at the root: %#v", len(g.Root.ChildrenArray))
   486  				}
   487  				if g.Objects[0].Label.Value != "All we are given is possibilities -- to make ourselves one thing or another." {
   488  					t.Fatalf("expected g.Objects[0].Label.Value to be last value: %#v", g.Objects[0].Label.Value)
   489  				}
   490  			},
   491  		},
   492  		{
   493  			name: "underscore_parent_squared",
   494  
   495  			text: `
   496  x: {
   497    y: {
   498      _._.z
   499    }
   500  }
   501  `,
   502  			assertions: func(t *testing.T, g *d2graph.Graph) {
   503  				if len(g.Objects) != 3 {
   504  					t.Fatalf("expected 3 objects: %#v", len(g.Objects))
   505  				}
   506  
   507  				if len(g.Root.ChildrenArray) != 2 {
   508  					t.Fatalf("expected 2 objects at the root: %#v", len(g.Root.ChildrenArray))
   509  				}
   510  			},
   511  		},
   512  		{
   513  			name: "underscore_parent_root",
   514  
   515  			text: `
   516  _.x
   517  `,
   518  			expErr: `d2/testdata/d2compiler/TestCompile/underscore_parent_root.d2:2:1: invalid underscore: no parent`,
   519  		},
   520  		{
   521  			name: "underscore_parent_middle_path",
   522  
   523  			text: `
   524  x: {
   525    y._.z
   526  }
   527  `,
   528  			expErr: `d2/testdata/d2compiler/TestCompile/underscore_parent_middle_path.d2:3:5: parent "_" can only be used in the beginning of paths, e.g. "_.x"`,
   529  		},
   530  		{
   531  			name: "underscore_parent_sandwich_path",
   532  
   533  			text: `
   534  x: {
   535    _.z._
   536  }
   537  `,
   538  			expErr: `d2/testdata/d2compiler/TestCompile/underscore_parent_sandwich_path.d2:3:7: parent "_" can only be used in the beginning of paths, e.g. "_.x"`,
   539  		},
   540  		{
   541  			name: "underscore_edge",
   542  
   543  			text: `
   544  x: {
   545    _.y -> _.x
   546  }
   547  `,
   548  			assertions: func(t *testing.T, g *d2graph.Graph) {
   549  				if len(g.Objects) != 2 {
   550  					t.Fatalf("expected 2 objects: %#v", g.Objects)
   551  				}
   552  				if g.Objects[0].ID != "x" {
   553  					t.Fatalf("expected g.Objects[0].ID to be x: %#v", g.Objects[0])
   554  				}
   555  				if g.Objects[1].ID != "y" {
   556  					t.Fatalf("expected g.Objects[1].ID to be y: %#v", g.Objects[1])
   557  				}
   558  
   559  				if len(g.Edges) != 1 {
   560  					t.Fatalf("expected 1 edge: %#v", g.Edges)
   561  				}
   562  				if g.Edges[0].Src.ID != "y" {
   563  					t.Fatalf("expected g.Edges[0].Src.ID to be y: %#v", g.Edges[0])
   564  				}
   565  				if g.Edges[0].Dst.ID != "x" {
   566  					t.Fatalf("expected g.Edges[0].Dst.ID to be x: %#v", g.Edges[0])
   567  				}
   568  				if g.Edges[0].SrcArrow {
   569  					t.Fatalf("expected g.Edges[0].SrcArrow to be false: %#v", g.Edges[0])
   570  				}
   571  				if !g.Edges[0].DstArrow {
   572  					t.Fatalf("expected g.Edges[0].DstArrow to be true: %#v", g.Edges[0])
   573  				}
   574  			},
   575  		},
   576  		{
   577  			name: "underscore_edge_chain",
   578  
   579  			text: `
   580  x: {
   581    _.y -> _.x -> _.z
   582  }
   583  `,
   584  			assertions: func(t *testing.T, g *d2graph.Graph) {
   585  				if len(g.Objects) != 3 {
   586  					t.Fatalf("expected 3 objects: %#v", g.Objects)
   587  				}
   588  				if g.Objects[0].ID != "x" {
   589  					t.Fatalf("expected g.Objects[0].ID to be x: %#v", g.Objects[0])
   590  				}
   591  				if g.Objects[1].ID != "y" {
   592  					t.Fatalf("expected g.Objects[1].ID to be y: %#v", g.Objects[1])
   593  				}
   594  				if g.Objects[2].ID != "z" {
   595  					t.Fatalf("expected g.Objects[2].ID to be z: %#v", g.Objects[2])
   596  				}
   597  
   598  				if len(g.Edges) != 2 {
   599  					t.Fatalf("expected 2 edge: %#v", g.Edges)
   600  				}
   601  				if g.Edges[0].Src.ID != "y" {
   602  					t.Fatalf("expected g.Edges[0].Src.ID to be y: %#v", g.Edges[0])
   603  				}
   604  				if g.Edges[0].Dst.ID != "x" {
   605  					t.Fatalf("expected g.Edges[0].Dst.ID to be x: %#v", g.Edges[0])
   606  				}
   607  				if g.Edges[1].Src.ID != "x" {
   608  					t.Fatalf("expected g.Edges[1].Src.ID to be x: %#v", g.Edges[1])
   609  				}
   610  				if g.Edges[1].Dst.ID != "z" {
   611  					t.Fatalf("expected g.Edges[1].Dst.ID to be z: %#v", g.Edges[1])
   612  				}
   613  			},
   614  		},
   615  		{
   616  			name: "md_block_string_err",
   617  
   618  			text: `test: |md
   619    # What about pipes
   620  
   621    Will escaping \| work?
   622  |
   623  `,
   624  			expErr: `d2/testdata/d2compiler/TestCompile/md_block_string_err.d2:4:19: unexpected text after md block string. See https://d2lang.com/tour/text#advanced-block-strings.
   625  d2/testdata/d2compiler/TestCompile/md_block_string_err.d2:5:1: block string must be terminated with |`,
   626  		},
   627  		{
   628  			name:   "no_empty_block_string",
   629  			text:   `Text: |md |`,
   630  			expErr: `d2/testdata/d2compiler/TestCompile/no_empty_block_string.d2:1:1: block string cannot be empty`,
   631  		},
   632  		{
   633  			name:   "no_white_spaces_only_block_string",
   634  			text:   `Text: |md      |`,
   635  			expErr: `d2/testdata/d2compiler/TestCompile/no_white_spaces_only_block_string.d2:1:1: block string cannot be empty`,
   636  		},
   637  		{
   638  			name: "no_new_lines_only_block_string",
   639  			text: `Text: |md
   640  
   641  
   642  |`,
   643  			expErr: `d2/testdata/d2compiler/TestCompile/no_new_lines_only_block_string.d2:1:1: block string cannot be empty`,
   644  		},
   645  		{
   646  			name: "underscore_edge_existing",
   647  
   648  			text: `
   649  a -> b: "Can you imagine how life could be improved if we could do away with"
   650  x: {
   651  	_.a -> _.b: "Well, it's garish, ugly, and derelicts have used it for a toilet."
   652  }
   653  `,
   654  			assertions: func(t *testing.T, g *d2graph.Graph) {
   655  				if len(g.Objects) != 3 {
   656  					t.Fatalf("expected 3 objects: %#v", g.Objects)
   657  				}
   658  				if len(g.Edges) != 2 {
   659  					t.Fatalf("expected 2 edge: %#v", g.Edges)
   660  				}
   661  				if g.Edges[0].Src.ID != "a" {
   662  					t.Fatalf("expected g.Edges[0].Src.ID to be a: %#v", g.Edges[0])
   663  				}
   664  				if g.Edges[0].Dst.ID != "b" {
   665  					t.Fatalf("expected g.Edges[0].Dst.ID to be b: %#v", g.Edges[0])
   666  				}
   667  				if g.Edges[1].Src.ID != "a" {
   668  					t.Fatalf("expected g.Edges[1].Src.ID to be a: %#v", g.Edges[1])
   669  				}
   670  				if g.Edges[1].Dst.ID != "b" {
   671  					t.Fatalf("expected g.Edges[1].Dst.ID to be b: %#v", g.Edges[1])
   672  				}
   673  				if g.Edges[0].Label.Value != "Can you imagine how life could be improved if we could do away with" {
   674  					t.Fatalf("unexpected g.Edges[0].Label: %#v", g.Edges[0].Label)
   675  				}
   676  				if g.Edges[1].Label.Value != "Well, it's garish, ugly, and derelicts have used it for a toilet." {
   677  					t.Fatalf("unexpected g.Edges[1].Label: %#v", g.Edges[1].Label)
   678  				}
   679  			},
   680  		},
   681  		{
   682  			name: "underscore_edge_index",
   683  
   684  			text: `
   685  a -> b: "Can you imagine how life could be improved if we could do away with"
   686  x: {
   687  	(_.a -> _.b)[0]: "Well, it's garish, ugly, and derelicts have used it for a toilet."
   688  }
   689  `,
   690  			assertions: func(t *testing.T, g *d2graph.Graph) {
   691  				if len(g.Objects) != 3 {
   692  					t.Fatalf("expected 3 objects: %#v", g.Objects)
   693  				}
   694  				if len(g.Edges) != 1 {
   695  					t.Fatalf("expected 1 edge: %#v", g.Edges)
   696  				}
   697  				if g.Edges[0].Src.ID != "a" {
   698  					t.Fatalf("expected g.Edges[0].Src.ID to be a: %#v", g.Edges[0])
   699  				}
   700  				if g.Edges[0].Dst.ID != "b" {
   701  					t.Fatalf("expected g.Edges[0].Dst.ID to be b: %#v", g.Edges[0])
   702  				}
   703  				if g.Edges[0].Label.Value != "Well, it's garish, ugly, and derelicts have used it for a toilet." {
   704  					t.Fatalf("unexpected g.Edges[0].Label: %#v", g.Edges[0].Label)
   705  				}
   706  			},
   707  		},
   708  		{
   709  			name: "underscore_edge_nested",
   710  
   711  			text: `
   712  x: {
   713  	y: {
   714  		_._.z -> _.y
   715  	}
   716  }
   717  `,
   718  			assertions: func(t *testing.T, g *d2graph.Graph) {
   719  				if len(g.Objects) != 3 {
   720  					t.Fatalf("expected 3 objects: %#v", g.Objects)
   721  				}
   722  				if len(g.Edges) != 1 {
   723  					t.Fatalf("expected 1 edge: %#v", g.Edges)
   724  				}
   725  				if g.Edges[0].Src.AbsID() != "z" {
   726  					t.Fatalf("expected g.Edges[0].Src.AbsID() to be z: %#v", g.Edges[0].Src.AbsID())
   727  				}
   728  				if g.Edges[0].Dst.AbsID() != "x.y" {
   729  					t.Fatalf("expected g.Edges[0].Dst.AbsID() to be x.y: %#v", g.Edges[0].Dst.AbsID())
   730  				}
   731  			},
   732  		},
   733  		{
   734  			name: "edge",
   735  
   736  			text: `
   737  x -> y
   738  `,
   739  			assertions: func(t *testing.T, g *d2graph.Graph) {
   740  				if len(g.Objects) != 2 {
   741  					t.Fatalf("expected 2 objects: %#v", g.Objects)
   742  				}
   743  				if g.Objects[0].ID != "x" {
   744  					t.Fatalf("expected g.Objects[0].ID to be x: %#v", g.Objects[0])
   745  				}
   746  				if g.Objects[1].ID != "y" {
   747  					t.Fatalf("expected g.Objects[1].ID to be y: %#v", g.Objects[1])
   748  				}
   749  
   750  				if len(g.Edges) != 1 {
   751  					t.Fatalf("expected 1 edge: %#v", g.Edges)
   752  				}
   753  				if g.Edges[0].Src.ID != "x" {
   754  					t.Fatalf("expected g.Edges[0].Src.ID to be x: %#v", g.Edges[0])
   755  				}
   756  				if g.Edges[0].Dst.ID != "y" {
   757  					t.Fatalf("expected g.Edges[0].Dst.ID to be y: %#v", g.Edges[0])
   758  				}
   759  				if g.Edges[0].SrcArrow {
   760  					t.Fatalf("expected g.Edges[0].SrcArrow to be false: %#v", g.Edges[0])
   761  				}
   762  				if !g.Edges[0].DstArrow {
   763  					t.Fatalf("expected g.Edges[0].DstArrow to be true: %#v", g.Edges[0])
   764  				}
   765  			},
   766  		},
   767  		{
   768  			name: "edge_chain",
   769  
   770  			text: `
   771  x -> y -> z: "The kids will love our inflatable slides"
   772  `,
   773  			assertions: func(t *testing.T, g *d2graph.Graph) {
   774  				if len(g.Objects) != 3 {
   775  					t.Fatalf("expected 2 objects: %#v", g.Objects)
   776  				}
   777  				if g.Objects[0].ID != "x" {
   778  					t.Fatalf("expected g.Objects[0].ID to be x: %#v", g.Objects[0])
   779  				}
   780  				if g.Objects[1].ID != "y" {
   781  					t.Fatalf("expected g.Objects[1].ID to be y: %#v", g.Objects[1])
   782  				}
   783  				if g.Objects[2].ID != "z" {
   784  					t.Fatalf("expected g.Objects[2].ID to be z: %#v", g.Objects[2])
   785  				}
   786  
   787  				if len(g.Edges) != 2 {
   788  					t.Fatalf("expected 2 edge: %#v", g.Edges)
   789  				}
   790  				if g.Edges[0].Src.ID != "x" {
   791  					t.Fatalf("expected g.Edges[0].Src.ID to be x: %#v", g.Edges[0])
   792  				}
   793  				if g.Edges[0].Dst.ID != "y" {
   794  					t.Fatalf("expected g.Edges[0].Dst.ID to be y: %#v", g.Edges[0])
   795  				}
   796  				if g.Edges[1].Src.ID != "y" {
   797  					t.Fatalf("expected g.Edges[1].Src.ID to be x: %#v", g.Edges[1])
   798  				}
   799  				if g.Edges[1].Dst.ID != "z" {
   800  					t.Fatalf("expected g.Edges[1].Dst.ID to be y: %#v", g.Edges[1])
   801  				}
   802  
   803  				if g.Edges[0].Label.Value != "The kids will love our inflatable slides" {
   804  					t.Fatalf("unexpected g.Edges[0].Label: %#v", g.Edges[0].Label.Value)
   805  				}
   806  				if g.Edges[1].Label.Value != "The kids will love our inflatable slides" {
   807  					t.Fatalf("unexpected g.Edges[1].Label: %#v", g.Edges[1].Label.Value)
   808  				}
   809  			},
   810  		},
   811  		{
   812  			name: "edge_index",
   813  
   814  			text: `
   815  x -> y: one
   816  (x -> y)[0]: two
   817  `,
   818  			assertions: func(t *testing.T, g *d2graph.Graph) {
   819  				if len(g.Objects) != 2 {
   820  					t.Fatalf("expected 2 objects: %#v", g.Objects)
   821  				}
   822  				if g.Objects[0].ID != "x" {
   823  					t.Fatalf("expected g.Objects[0].ID to be x: %#v", g.Objects[0])
   824  				}
   825  				if g.Objects[1].ID != "y" {
   826  					t.Fatalf("expected g.Objects[1].ID to be y: %#v", g.Objects[1])
   827  				}
   828  
   829  				if len(g.Edges) != 1 {
   830  					t.Fatalf("expected 1 edge: %#v", g.Edges)
   831  				}
   832  				if g.Edges[0].Src.ID != "x" {
   833  					t.Fatalf("expected g.Edges[0].Src.ID to be x: %#v", g.Edges[0].Src)
   834  				}
   835  				if g.Edges[0].Dst.ID != "y" {
   836  					t.Fatalf("expected g.Edges[0].Dst.ID to be y: %#v", g.Edges[0].Dst)
   837  				}
   838  				if g.Edges[0].SrcArrow {
   839  					t.Fatalf("expected g.Edges[0].SrcArrow to be false: %#v", g.Edges[0].SrcArrow)
   840  				}
   841  				if !g.Edges[0].DstArrow {
   842  					t.Fatalf("expected g.Edges[0].DstArrow to be true: %#v", g.Edges[0].DstArrow)
   843  				}
   844  				if g.Edges[0].Label.Value != "two" {
   845  					t.Fatalf("expected g.Edges[0].Label to be two: %#v", g.Edges[0].Label)
   846  				}
   847  			},
   848  		},
   849  		{
   850  			name: "edge_index_nested",
   851  
   852  			text: `
   853  b: {
   854  	x -> y: one
   855  	(x -> y)[0]: two
   856  }
   857  `,
   858  			assertions: func(t *testing.T, g *d2graph.Graph) {
   859  				if len(g.Objects) != 3 {
   860  					t.Fatalf("expected 3 objects: %#v", g.Objects)
   861  				}
   862  				if g.Objects[0].ID != "b" {
   863  					t.Fatalf("expected g.Objects[0].ID to be b: %#v", g.Objects[0])
   864  				}
   865  				if g.Objects[1].ID != "x" {
   866  					t.Fatalf("expected g.Objects[1].ID to be x: %#v", g.Objects[0])
   867  				}
   868  				if g.Objects[2].ID != "y" {
   869  					t.Fatalf("expected g.Objects[2].ID to be y: %#v", g.Objects[1])
   870  				}
   871  
   872  				if len(g.Edges) != 1 {
   873  					t.Fatalf("expected 1 edge: %#v", g.Edges)
   874  				}
   875  				if g.Edges[0].Src.AbsID() != "b.x" {
   876  					t.Fatalf("expected g.Edges[0].Src.AbsoluteID() to be x: %#v", g.Edges[0].Src)
   877  				}
   878  				if g.Edges[0].Dst.AbsID() != "b.y" {
   879  					t.Fatalf("expected g.Edges[0].Dst.AbsoluteID() to be y: %#v", g.Edges[0].Dst)
   880  				}
   881  				if g.Edges[0].SrcArrow {
   882  					t.Fatalf("expected g.Edges[0].SrcArrow to be false: %#v", g.Edges[0].SrcArrow)
   883  				}
   884  				if !g.Edges[0].DstArrow {
   885  					t.Fatalf("expected g.Edges[0].DstArrow to be true: %#v", g.Edges[0].DstArrow)
   886  				}
   887  				if g.Edges[0].Label.Value != "two" {
   888  					t.Fatalf("expected g.Edges[0].Label to be two: %#v", g.Edges[0].Label)
   889  				}
   890  			},
   891  		},
   892  		{
   893  			name: "edge_index_nested_cross_scope",
   894  
   895  			text: `
   896  b: {
   897  	x -> y: one
   898  }
   899  b.(x -> y)[0]: two
   900  `,
   901  			assertions: func(t *testing.T, g *d2graph.Graph) {
   902  				if len(g.Objects) != 3 {
   903  					t.Fatalf("expected 3 objects: %#v", g.Objects)
   904  				}
   905  				if g.Objects[0].ID != "b" {
   906  					t.Fatalf("expected g.Objects[0].ID to be b: %#v", g.Objects[0])
   907  				}
   908  				if g.Objects[1].ID != "x" {
   909  					t.Fatalf("expected g.Objects[1].ID to be x: %#v", g.Objects[0])
   910  				}
   911  				if g.Objects[2].ID != "y" {
   912  					t.Fatalf("expected g.Objects[2].ID to be y: %#v", g.Objects[1])
   913  				}
   914  
   915  				if len(g.Edges) != 1 {
   916  					t.Fatalf("expected 1 edge: %#v", g.Edges)
   917  				}
   918  				if g.Edges[0].Src.AbsID() != "b.x" {
   919  					t.Fatalf("expected g.Edges[0].Src.AbsoluteID() to be x: %#v", g.Edges[0].Src)
   920  				}
   921  				if g.Edges[0].Dst.AbsID() != "b.y" {
   922  					t.Fatalf("expected g.Edges[0].Dst.AbsoluteID() to be y: %#v", g.Edges[0].Dst)
   923  				}
   924  				if g.Edges[0].SrcArrow {
   925  					t.Fatalf("expected g.Edges[0].SrcArrow to be false: %#v", g.Edges[0].SrcArrow)
   926  				}
   927  				if !g.Edges[0].DstArrow {
   928  					t.Fatalf("expected g.Edges[0].DstArrow to be true: %#v", g.Edges[0].DstArrow)
   929  				}
   930  				if g.Edges[0].Label.Value != "two" {
   931  					t.Fatalf("expected g.Edges[0].Label to be two: %#v", g.Edges[0].Label)
   932  				}
   933  			},
   934  		},
   935  		{
   936  			name: "unsemantic_markdown",
   937  
   938  			text: `test:|
   939  foobar
   940  <p>
   941  |
   942  `,
   943  			expErr: `d2/testdata/d2compiler/TestCompile/unsemantic_markdown.d2:1:1: malformed Markdown: element <p> closed by </div>`,
   944  		},
   945  		{
   946  			name: "unsemantic_markdown_2",
   947  
   948  			text: `test:|
   949  foo<br>
   950  bar
   951  |
   952  `,
   953  			expErr: `d2/testdata/d2compiler/TestCompile/unsemantic_markdown_2.d2:1:1: malformed Markdown: element <br> closed by </p>`,
   954  		},
   955  		{
   956  			name: "edge_map",
   957  
   958  			text: `
   959  x -> y: {
   960    label: "Space: the final frontier.  These are the voyages of the starship Enterprise."
   961  }
   962  `,
   963  			assertions: func(t *testing.T, g *d2graph.Graph) {
   964  				if len(g.Objects) != 2 {
   965  					t.Fatalf("expected 2 objects: %#v", g.Objects)
   966  				}
   967  				if g.Objects[0].ID != "x" {
   968  					t.Fatalf("expected g.Objects[0].ID to be x: %#v", g.Objects[0])
   969  				}
   970  				if g.Objects[1].ID != "y" {
   971  					t.Fatalf("expected g.Objects[1].ID to be y: %#v", g.Objects[1])
   972  				}
   973  
   974  				if len(g.Edges) != 1 {
   975  					t.Fatalf("expected 1 edge: %#v", g.Edges)
   976  				}
   977  				if g.Edges[0].Src.ID != "x" {
   978  					t.Fatalf("expected g.Edges[0].Src.ID to be x: %#v", g.Edges[0])
   979  				}
   980  				if g.Edges[0].Dst.ID != "y" {
   981  					t.Fatalf("expected g.Edges[0].Dst.ID to be y: %#v", g.Edges[0])
   982  				}
   983  				if g.Edges[0].Label.Value != "Space: the final frontier.  These are the voyages of the starship Enterprise." {
   984  					t.Fatalf("unexpected g.Edges[0].Label.Value : %#v", g.Edges[0].Label.Value)
   985  				}
   986  			},
   987  		},
   988  		{
   989  			name: "edge_label_map",
   990  
   991  			text: `hey y9 -> qwer: asdf {style.opacity: 0.5}
   992  `,
   993  			assertions: func(t *testing.T, g *d2graph.Graph) {
   994  				if len(g.Edges) != 1 {
   995  					t.Fatalf("expected 1 edge: %#v", g.Edges)
   996  				}
   997  				if g.Edges[0].Label.Value != "asdf" {
   998  					t.Fatalf("unexpected g.Edges[0].Label.Value : %#v", g.Edges[0].Label.Value)
   999  				}
  1000  			},
  1001  		},
  1002  		{
  1003  			name: "edge_map_arrowhead",
  1004  
  1005  			text: `x -> y: {
  1006    source-arrowhead: {
  1007      shape: diamond
  1008    }
  1009  }
  1010  `,
  1011  			assertions: func(t *testing.T, g *d2graph.Graph) {
  1012  				if len(g.Edges) != 1 {
  1013  					t.Fatalf("expected 1 edge: %#v", g.Edges)
  1014  				}
  1015  				if len(g.Objects) != 2 {
  1016  					t.Fatalf("expected 2 objects: %#v", g.Objects)
  1017  				}
  1018  				assert.String(t, "diamond", g.Edges[0].SrcArrowhead.Shape.Value)
  1019  				assert.String(t, "", g.Edges[0].Shape.Value)
  1020  				// Make sure the DSL didn't change. this is a regression test where it did
  1021  				exp := `x -> y: {
  1022    source-arrowhead: {
  1023      shape: diamond
  1024    }
  1025  }
  1026  `
  1027  				newText := d2format.Format(g.AST)
  1028  				ds, err := diff.Strings(exp, newText)
  1029  				if err != nil {
  1030  					t.Fatal(err)
  1031  				}
  1032  				if ds != "" {
  1033  					t.Fatalf("exp != newText:\n%s", ds)
  1034  				}
  1035  			},
  1036  		},
  1037  		{
  1038  			name: "edge_arrowhead_primary",
  1039  
  1040  			text: `x -> y: {
  1041    source-arrowhead: Reisner's Rule of Conceptual Inertia
  1042  }
  1043  `,
  1044  			assertions: func(t *testing.T, g *d2graph.Graph) {
  1045  				assert.String(t, "Reisner's Rule of Conceptual Inertia", g.Edges[0].SrcArrowhead.Label.Value)
  1046  			},
  1047  		},
  1048  		{
  1049  			name: "edge_arrowhead_fields",
  1050  
  1051  			text: `x -> y: {
  1052    source-arrowhead: Reisner's Rule of Conceptual Inertia {
  1053      shape: diamond
  1054    }
  1055    target-arrowhead: QOTD
  1056    target-arrowhead: {
  1057      style.filled: true
  1058    }
  1059  }
  1060  `,
  1061  			assertions: func(t *testing.T, g *d2graph.Graph) {
  1062  				if len(g.Edges) != 1 {
  1063  					t.Fatalf("expected 1 edge: %#v", g.Edges)
  1064  				}
  1065  				if len(g.Objects) != 2 {
  1066  					t.Fatalf("expected 2 objects: %#v", g.Objects)
  1067  				}
  1068  				assert.String(t, "diamond", g.Edges[0].SrcArrowhead.Shape.Value)
  1069  				assert.String(t, "Reisner's Rule of Conceptual Inertia", g.Edges[0].SrcArrowhead.Label.Value)
  1070  				assert.String(t, "QOTD", g.Edges[0].DstArrowhead.Label.Value)
  1071  				assert.String(t, "true", g.Edges[0].DstArrowhead.Style.Filled.Value)
  1072  				assert.String(t, "", g.Edges[0].Shape.Value)
  1073  				assert.String(t, "", g.Edges[0].Label.Value)
  1074  				assert.JSON(t, nil, g.Edges[0].Style.Filled)
  1075  			},
  1076  		},
  1077  		{
  1078  			name: "edge_flat_arrowhead",
  1079  
  1080  			text: `x -> y
  1081  (x -> y)[0].source-arrowhead.shape: diamond
  1082  `,
  1083  			assertions: func(t *testing.T, g *d2graph.Graph) {
  1084  				if len(g.Edges) != 1 {
  1085  					t.Fatalf("expected 1 edge: %#v", g.Edges)
  1086  				}
  1087  				if len(g.Objects) != 2 {
  1088  					t.Fatalf("expected 2 objects: %#v", g.Objects)
  1089  				}
  1090  				assert.String(t, "diamond", g.Edges[0].SrcArrowhead.Shape.Value)
  1091  				assert.String(t, "", g.Edges[0].Shape.Value)
  1092  			},
  1093  		},
  1094  		{
  1095  			// tests setting to an arrowhead-only shape
  1096  			name: "edge_non_shape_arrowhead",
  1097  
  1098  			text: `x -> y: { source-arrowhead.shape: triangle }
  1099  `,
  1100  			assertions: func(t *testing.T, g *d2graph.Graph) {
  1101  				if len(g.Edges) != 1 {
  1102  					t.Fatalf("expected 1 edge: %#v", g.Edges)
  1103  				}
  1104  				if len(g.Objects) != 2 {
  1105  					t.Fatalf("expected 2 objects: %#v", g.Objects)
  1106  				}
  1107  				assert.String(t, "triangle", g.Edges[0].SrcArrowhead.Shape.Value)
  1108  				assert.String(t, "", g.Edges[0].Shape.Value)
  1109  			},
  1110  		},
  1111  		{
  1112  			name: "object_arrowhead_shape",
  1113  
  1114  			text: `x: {shape: triangle}
  1115  `,
  1116  			expErr: `d2/testdata/d2compiler/TestCompile/object_arrowhead_shape.d2:1:5: invalid shape, can only set "triangle" for arrowheads`,
  1117  		},
  1118  		{
  1119  			name: "edge_flat_label_arrowhead",
  1120  
  1121  			text: `x -> y: {
  1122    # comment
  1123    source-arrowhead.label: yo
  1124  }
  1125  `,
  1126  			assertions: func(t *testing.T, g *d2graph.Graph) {
  1127  				if len(g.Edges) != 1 {
  1128  					t.Fatalf("expected 1 edge: %#v", g.Edges)
  1129  				}
  1130  				if len(g.Objects) != 2 {
  1131  					t.Fatalf("expected 2 objects: %#v", g.Objects)
  1132  				}
  1133  				assert.String(t, "yo", g.Edges[0].SrcArrowhead.Label.Value)
  1134  				assert.String(t, "", g.Edges[0].Label.Value)
  1135  			},
  1136  		},
  1137  		{
  1138  			name: "edge_semiflat_arrowhead",
  1139  
  1140  			text: `x -> y
  1141  (x -> y)[0].source-arrowhead: {
  1142    shape: diamond
  1143  }
  1144  `,
  1145  			assertions: func(t *testing.T, g *d2graph.Graph) {
  1146  				if len(g.Edges) != 1 {
  1147  					t.Fatalf("expected 1 edge: %#v", g.Edges)
  1148  				}
  1149  				if len(g.Objects) != 2 {
  1150  					t.Fatalf("expected 2 objects: %#v", g.Objects)
  1151  				}
  1152  				assert.String(t, "diamond", g.Edges[0].SrcArrowhead.Shape.Value)
  1153  				assert.String(t, "", g.Edges[0].Shape.Value)
  1154  			},
  1155  		},
  1156  		{
  1157  			name: "edge_mixed_arrowhead",
  1158  
  1159  			text: `x -> y: {
  1160    target-arrowhead.shape: diamond
  1161  }
  1162  (x -> y)[0].source-arrowhead: {
  1163    shape: diamond
  1164  }
  1165  `,
  1166  			assertions: func(t *testing.T, g *d2graph.Graph) {
  1167  				if len(g.Edges) != 1 {
  1168  					t.Fatalf("expected 1 edge: %#v", g.Edges)
  1169  				}
  1170  				if len(g.Objects) != 2 {
  1171  					t.Fatalf("expected 2 objects: %#v", g.Objects)
  1172  				}
  1173  				assert.String(t, "diamond", g.Edges[0].SrcArrowhead.Shape.Value)
  1174  				assert.String(t, "diamond", g.Edges[0].DstArrowhead.Shape.Value)
  1175  				assert.String(t, "", g.Edges[0].Shape.Value)
  1176  			},
  1177  		},
  1178  		{
  1179  			name: "edge_exclusive_style",
  1180  
  1181  			text: `
  1182  x -> y: {
  1183  	style.animated: true
  1184  }
  1185  `,
  1186  			assertions: func(t *testing.T, g *d2graph.Graph) {
  1187  				if len(g.Edges) != 1 {
  1188  					t.Fatalf("expected 1 edge: %#v", g.Edges)
  1189  				}
  1190  				if g.Edges[0].Style.Animated.Value != "true" {
  1191  					t.Fatalf("Edges[0].Style.Animated.Value: %#v", g.Edges[0].Style.Animated.Value)
  1192  				}
  1193  			},
  1194  		},
  1195  		{
  1196  			name: "nested_edge",
  1197  
  1198  			text: `sequence -> quest: {
  1199    space -> stars
  1200  }
  1201  `,
  1202  			expErr: `d2/testdata/d2compiler/TestCompile/nested_edge.d2:2:3: cannot create edge inside edge`,
  1203  		},
  1204  		{
  1205  			name: "shape_edge_style",
  1206  
  1207  			text: `
  1208  x: {
  1209  	style.animated: true
  1210  }
  1211  `,
  1212  			expErr: `d2/testdata/d2compiler/TestCompile/shape_edge_style.d2:3:2: key "animated" can only be applied to edges`,
  1213  		},
  1214  		{
  1215  			name: "edge_invalid_style",
  1216  
  1217  			text: `x -> y: {
  1218    opacity: 0.5
  1219  }
  1220  `,
  1221  			expErr: `d2/testdata/d2compiler/TestCompile/edge_invalid_style.d2:2:3: opacity must be style.opacity`,
  1222  		},
  1223  		{
  1224  			name: "obj_invalid_style",
  1225  
  1226  			text: `x: {
  1227    opacity: 0.5
  1228  }
  1229  `,
  1230  			expErr: `d2/testdata/d2compiler/TestCompile/obj_invalid_style.d2:2:3: opacity must be style.opacity`,
  1231  		},
  1232  		{
  1233  			name: "edge_chain_map",
  1234  
  1235  			text: `
  1236  x -> y -> z: {
  1237    label: "Space: the final frontier.  These are the voyages of the starship Enterprise."
  1238  }
  1239  `,
  1240  			assertions: func(t *testing.T, g *d2graph.Graph) {
  1241  				if len(g.Objects) != 3 {
  1242  					t.Fatalf("expected 3 objects: %#v", g.Objects)
  1243  				}
  1244  
  1245  				if len(g.Edges) != 2 {
  1246  					t.Fatalf("expected 2 edge: %#v", g.Edges)
  1247  				}
  1248  				if g.Edges[0].Label.Value != "Space: the final frontier.  These are the voyages of the starship Enterprise." {
  1249  					t.Fatalf("unexpected g.Edges[0].Label.Value : %#v", g.Edges[0].Label.Value)
  1250  				}
  1251  				if g.Edges[1].Label.Value != "Space: the final frontier.  These are the voyages of the starship Enterprise." {
  1252  					t.Fatalf("unexpected g.Edges[0].Label.Value : %#v", g.Edges[1].Label.Value)
  1253  				}
  1254  			},
  1255  		},
  1256  		{
  1257  			name: "edge_index_map",
  1258  
  1259  			text: `
  1260  x -> y
  1261  (x -> y)[0]: {
  1262    label: "Space: the final frontier.  These are the voyages of the starship Enterprise."
  1263  }
  1264  `,
  1265  			assertions: func(t *testing.T, g *d2graph.Graph) {
  1266  				if len(g.Objects) != 2 {
  1267  					t.Fatalf("expected 2 objects: %#v", g.Objects)
  1268  				}
  1269  
  1270  				if len(g.Edges) != 1 {
  1271  					t.Fatalf("expected 1 edge: %#v", g.Edges)
  1272  				}
  1273  				if g.Edges[0].Label.Value != "Space: the final frontier.  These are the voyages of the starship Enterprise." {
  1274  					t.Fatalf("unexpected g.Edges[0].Label.Value : %#v", g.Edges[0].Label.Value)
  1275  				}
  1276  			},
  1277  		},
  1278  		{
  1279  			name: "edge_map_nested",
  1280  
  1281  			text: `
  1282  x -> y: {
  1283    style: {
  1284      opacity: 0.4
  1285    }
  1286  }
  1287  `,
  1288  			assertions: func(t *testing.T, g *d2graph.Graph) {
  1289  				if len(g.Objects) != 2 {
  1290  					t.Fatalf("expected 2 objects: %#v", g.Objects)
  1291  				}
  1292  
  1293  				if len(g.Edges) != 1 {
  1294  					t.Fatalf("expected 1 edge: %#v", g.Edges)
  1295  				}
  1296  				if g.Edges[0].Style.Opacity.Value != "0.4" {
  1297  					t.Fatalf("unexpected g.Edges[0].Style.Opacity.Value: %#v", g.Edges[0].Style.Opacity.Value)
  1298  				}
  1299  			},
  1300  		},
  1301  		{
  1302  			name: "edge_map_nested_flat",
  1303  
  1304  			text: `
  1305  x -> y: {
  1306  	style.opacity: 0.4
  1307  }
  1308  `,
  1309  			assertions: func(t *testing.T, g *d2graph.Graph) {
  1310  				if len(g.Objects) != 2 {
  1311  					t.Fatalf("expected 2 objects: %#v", g.Objects)
  1312  				}
  1313  
  1314  				if len(g.Edges) != 1 {
  1315  					t.Fatalf("expected 1 edge: %#v", g.Edges)
  1316  				}
  1317  				if g.Edges[0].Style.Opacity.Value != "0.4" {
  1318  					t.Fatalf("unexpected g.Edges[0].Style.Opacity.Value: %#v", g.Edges[0].Style.Opacity.Value)
  1319  				}
  1320  				if g.Edges[0].Label.Value != "" {
  1321  					t.Fatalf("unexpected g.Edges[0].Label.Value : %#v", g.Edges[0].Label.Value)
  1322  				}
  1323  			},
  1324  		},
  1325  		{
  1326  			name: "edge_map_group_flat",
  1327  
  1328  			text: `
  1329  x -> y
  1330  (x -> y)[0].style.opacity: 0.4
  1331  `,
  1332  			assertions: func(t *testing.T, g *d2graph.Graph) {
  1333  				if len(g.Objects) != 2 {
  1334  					t.Fatalf("expected 2 objects: %#v", g.Objects)
  1335  				}
  1336  
  1337  				if len(g.Edges) != 1 {
  1338  					t.Fatalf("expected 1 edge: %#v", g.Edges)
  1339  				}
  1340  				if g.Edges[0].Style.Opacity.Value != "0.4" {
  1341  					t.Fatalf("unexpected g.Edges[0].Style.Opacity.Value: %#v", g.Edges[0].Style.Opacity.Value)
  1342  				}
  1343  				if g.Edges[0].Label.Value != "" {
  1344  					t.Fatalf("unexpected g.Edges[0].Label.Value : %#v", g.Edges[0].Label.Value)
  1345  				}
  1346  			},
  1347  		},
  1348  		{
  1349  			name: "edge_map_group_semiflat",
  1350  
  1351  			text: `x -> y
  1352  (x -> y)[0].style: {
  1353    opacity: 0.4
  1354  }
  1355  `,
  1356  			assertions: func(t *testing.T, g *d2graph.Graph) {
  1357  				if len(g.Objects) != 2 {
  1358  					t.Fatalf("expected 2 objects: %#v", g.Objects)
  1359  				}
  1360  
  1361  				if len(g.Edges) != 1 {
  1362  					t.Fatalf("expected 1 edge: %#v", g.Edges)
  1363  				}
  1364  				if g.Edges[0].Style.Opacity.Value != "0.4" {
  1365  					t.Fatalf("unexpected g.Edges[0].Style.Opacity.Value: %#v", g.Edges[0].Style.Opacity.Value)
  1366  				}
  1367  				if g.Edges[0].Label.Value != "" {
  1368  					t.Fatalf("unexpected g.Edges[0].Label.Value : %#v", g.Edges[0].Label.Value)
  1369  				}
  1370  			},
  1371  		},
  1372  		{
  1373  			name: "edge_key_group_flat_nested",
  1374  
  1375  			text: `
  1376  x: {
  1377    a -> b
  1378  }
  1379  x.(a -> b)[0].style.opacity: 0.4
  1380  `,
  1381  			assertions: func(t *testing.T, g *d2graph.Graph) {
  1382  				if len(g.Objects) != 3 {
  1383  					t.Fatalf("expected 3 objects: %#v", g.Objects)
  1384  				}
  1385  
  1386  				if len(g.Edges) != 1 {
  1387  					t.Fatalf("expected 1 edge: %#v", g.Edges)
  1388  				}
  1389  				if g.Edges[0].Style.Opacity.Value != "0.4" {
  1390  					t.Fatalf("unexpected g.Edges[0].Style.Opacity.Value: %#v", g.Edges[0].Style.Opacity.Value)
  1391  				}
  1392  				if g.Edges[0].Label.Value != "" {
  1393  					t.Fatalf("unexpected g.Edges[0].Label.Value : %#v", g.Edges[0].Label.Value)
  1394  				}
  1395  			},
  1396  		},
  1397  		{
  1398  			name: "edge_key_group_flat_nested_underscore",
  1399  
  1400  			text: `
  1401  a -> b
  1402  x: {
  1403  	(_.a -> _.b)[0].style.opacity: 0.4
  1404  }
  1405  `,
  1406  			assertions: func(t *testing.T, g *d2graph.Graph) {
  1407  				if len(g.Objects) != 3 {
  1408  					t.Fatalf("expected 3 objects: %#v", g.Objects)
  1409  				}
  1410  
  1411  				if len(g.Edges) != 1 {
  1412  					t.Fatalf("expected 1 edge: %#v", g.Edges)
  1413  				}
  1414  				if g.Edges[0].Style.Opacity.Value != "0.4" {
  1415  					t.Fatalf("unexpected g.Edges[0].Style.Opacity.Value: %#v", g.Edges[0].Style.Opacity.Value)
  1416  				}
  1417  				if g.Edges[0].Label.Value != "" {
  1418  					t.Fatalf("unexpected g.Edges[0].Label.Value : %#v", g.Edges[0].Label.Value)
  1419  				}
  1420  			},
  1421  		},
  1422  		{
  1423  			name: "edge_key_group_map_nested_underscore",
  1424  
  1425  			text: `
  1426  a -> b
  1427  x: {
  1428  	(_.a -> _.b)[0]: {
  1429  		style: {
  1430  			opacity: 0.4
  1431  		}
  1432  	}
  1433  }
  1434  `,
  1435  			assertions: func(t *testing.T, g *d2graph.Graph) {
  1436  				if len(g.Objects) != 3 {
  1437  					t.Fatalf("expected 3 objects: %#v", g.Objects)
  1438  				}
  1439  
  1440  				if len(g.Edges) != 1 {
  1441  					t.Fatalf("expected 1 edge: %#v", g.Edges)
  1442  				}
  1443  				if g.Edges[0].Style.Opacity.Value != "0.4" {
  1444  					t.Fatalf("unexpected g.Edges[0].Style.Opacity.Value: %#v", g.Edges[0].Style.Opacity.Value)
  1445  				}
  1446  				if g.Edges[0].Label.Value != "" {
  1447  					t.Fatalf("unexpected g.Edges[0].Label.Value : %#v", g.Edges[0].Label.Value)
  1448  				}
  1449  			},
  1450  		},
  1451  		{
  1452  			name: "edge_key_group_map_flat_nested_underscore",
  1453  
  1454  			text: `
  1455  a -> b
  1456  x: {
  1457  	(_.a -> _.b)[0]: {
  1458  		style.opacity: 0.4
  1459  	}
  1460  }
  1461  `,
  1462  			assertions: func(t *testing.T, g *d2graph.Graph) {
  1463  				if len(g.Objects) != 3 {
  1464  					t.Fatalf("expected 3 objects: %#v", g.Objects)
  1465  				}
  1466  
  1467  				if len(g.Edges) != 1 {
  1468  					t.Fatalf("expected 1 edge: %#v", g.Edges)
  1469  				}
  1470  				if g.Edges[0].Style.Opacity.Value != "0.4" {
  1471  					t.Fatalf("unexpected g.Edges[0].Style.Opacity.Value: %#v", g.Edges[0].Style.Opacity.Value)
  1472  				}
  1473  				if g.Edges[0].Label.Value != "" {
  1474  					t.Fatalf("unexpected g.Edges[0].Label.Value : %#v", g.Edges[0].Label.Value)
  1475  				}
  1476  			},
  1477  		},
  1478  		{
  1479  			name: "edge_map_non_reserved",
  1480  
  1481  			text: `
  1482  x -> y: {
  1483    z
  1484  }
  1485  `,
  1486  			expErr: `d2/testdata/d2compiler/TestCompile/edge_map_non_reserved.d2:3:3: edge map keys must be reserved keywords`,
  1487  		},
  1488  		{
  1489  			name: "url_link",
  1490  
  1491  			text: `x: {
  1492    link: https://google.com
  1493  }
  1494  `,
  1495  			assertions: func(t *testing.T, g *d2graph.Graph) {
  1496  				if len(g.Objects) != 1 {
  1497  					t.Fatal(g.Objects)
  1498  				}
  1499  				if g.Objects[0].Link.Value != "https://google.com" {
  1500  					t.Fatal(g.Objects[0].Link.Value)
  1501  				}
  1502  			},
  1503  		},
  1504  		{
  1505  			name: "url_tooltip",
  1506  			text: `x: {tooltip: https://google.com}`,
  1507  			assertions: func(t *testing.T, g *d2graph.Graph) {
  1508  				if len(g.Objects) != 1 {
  1509  					t.Fatal(g.Objects)
  1510  				}
  1511  
  1512  				if g.Objects[0].Tooltip.Value != "https://google.com" {
  1513  					t.Fatal(g.Objects[0].Tooltip.Value)
  1514  				}
  1515  			},
  1516  		},
  1517  		{
  1518  			name:   "no_url_link_and_url_tooltip_concurrently",
  1519  			text:   `x: {link: https://not-google.com; tooltip: https://google.com}`,
  1520  			expErr: `d2/testdata/d2compiler/TestCompile/no_url_link_and_url_tooltip_concurrently.d2:1:44: Tooltip cannot be set to URL when link is also set (for security)`,
  1521  		},
  1522  		{
  1523  			name:   "url_link_non_url_tooltip_ok",
  1524  			text:   `x: {link: https://not-google.com; tooltip: note: url.ParseRequestURI might see this as a URL}`,
  1525  			expErr: ``,
  1526  		},
  1527  		{
  1528  			name: "url_link_and_not_url_tooltip_concurrently",
  1529  			text: `x: {link: https://google.com; tooltip: hello world}`,
  1530  			assertions: func(t *testing.T, g *d2graph.Graph) {
  1531  				if len(g.Objects) != 1 {
  1532  					t.Fatal(g.Objects)
  1533  				}
  1534  				if g.Objects[0].Link.Value != "https://google.com" {
  1535  					t.Fatal(g.Objects[0].Link.Value)
  1536  				}
  1537  
  1538  				if g.Objects[0].Tooltip.Value != "hello world" {
  1539  					t.Fatal(g.Objects[0].Tooltip.Value)
  1540  				}
  1541  			},
  1542  		},
  1543  		{
  1544  			name: "nil_scope_obj_regression",
  1545  
  1546  			text: `a
  1547  b: {
  1548    _.a
  1549  }
  1550  `,
  1551  			assertions: func(t *testing.T, g *d2graph.Graph) {
  1552  				tassert.Equal(t, "a", g.Objects[0].ID)
  1553  				for _, ref := range g.Objects[0].References {
  1554  					tassert.NotNil(t, ref.ScopeObj)
  1555  				}
  1556  			},
  1557  		},
  1558  		{
  1559  			name: "path_link",
  1560  
  1561  			text: `x: {
  1562    link: Overview.Untitled board 7.zzzzz
  1563  }
  1564  `,
  1565  			assertions: func(t *testing.T, g *d2graph.Graph) {
  1566  				if len(g.Objects) != 1 {
  1567  					t.Fatal(g.Objects)
  1568  				}
  1569  				if g.Objects[0].Link.Value != "Overview.Untitled board 7.zzzzz" {
  1570  					t.Fatal(g.Objects[0].Link.Value)
  1571  				}
  1572  			},
  1573  		},
  1574  		{
  1575  			name: "near_constant",
  1576  
  1577  			text: `x.near: top-center
  1578  `,
  1579  		},
  1580  		{
  1581  			name: "near-invalid",
  1582  
  1583  			text: `mongodb: MongoDB {
  1584    perspective: perspective (View) {
  1585      password
  1586    }
  1587  
  1588    explanation: |md
  1589      perspective.model.js
  1590    | {
  1591      near: mongodb
  1592    }
  1593  }
  1594  
  1595  a: {
  1596    near: a.b
  1597    b
  1598  }
  1599  `,
  1600  			expErr: `d2/testdata/d2compiler/TestCompile/near-invalid.d2:9:11: near keys cannot be set to an ancestor
  1601  d2/testdata/d2compiler/TestCompile/near-invalid.d2:14:9: near keys cannot be set to an descendant`,
  1602  		},
  1603  		{
  1604  			name: "near_bad_constant",
  1605  
  1606  			text: `x.near: txop-center
  1607  `,
  1608  			expErr: `d2/testdata/d2compiler/TestCompile/near_bad_constant.d2:1:9: near key "txop-center" must be the absolute path to a shape or one of the following constants: top-left, top-center, top-right, center-left, center-right, bottom-left, bottom-center, bottom-right`,
  1609  		},
  1610  		{
  1611  			name: "near_bad_connected",
  1612  
  1613  			text: `
  1614  				x: {
  1615  					near: top-center
  1616  				}
  1617  				x -> y
  1618  			`,
  1619  			expErr: ``,
  1620  		},
  1621  		{
  1622  			name: "near_descendant_connect_to_outside",
  1623  			text: `
  1624  				x: {
  1625  					near: top-left
  1626  					y
  1627  				}
  1628  				x.y -> z
  1629  			`,
  1630  			expErr: "",
  1631  		},
  1632  		{
  1633  			name: "nested_near_constant",
  1634  
  1635  			text: `x.y.near: top-center
  1636  `,
  1637  			expErr: `d2/testdata/d2compiler/TestCompile/nested_near_constant.d2:1:11: constant near keys can only be set on root level shapes`,
  1638  		},
  1639  		{
  1640  			name: "reserved_icon_near_style",
  1641  
  1642  			text: `x: {
  1643    icon: orange
  1644    style.opacity: 0.5
  1645    style.stroke: red
  1646  	style.fill: green
  1647  }
  1648  x.near: y
  1649  y
  1650  `,
  1651  			assertions: func(t *testing.T, g *d2graph.Graph) {
  1652  				if len(g.Objects) != 2 {
  1653  					t.Fatal(g.Objects)
  1654  				}
  1655  				if g.Objects[0].NearKey == nil {
  1656  					t.Fatal("missing near key")
  1657  				}
  1658  				if g.Objects[0].Icon.Path != "orange" {
  1659  					t.Fatal(g.Objects[0].Icon)
  1660  				}
  1661  				if g.Objects[0].Style.Opacity.Value != "0.5" {
  1662  					t.Fatal(g.Objects[0].Style.Opacity)
  1663  				}
  1664  				if g.Objects[0].Style.Stroke.Value != "red" {
  1665  					t.Fatal(g.Objects[0].Style.Stroke)
  1666  				}
  1667  				if g.Objects[0].Style.Fill.Value != "green" {
  1668  					t.Fatal(g.Objects[0].Style.Fill)
  1669  				}
  1670  			},
  1671  		},
  1672  		{
  1673  			name: "errors/reserved_icon_style",
  1674  
  1675  			text: `x: {
  1676    near: y
  1677    icon: "::????:::%%orange"
  1678    style.opacity: -1
  1679    style.opacity: 232
  1680  }
  1681  `,
  1682  			expErr: `d2/testdata/d2compiler/TestCompile/errors/reserved_icon_style.d2:3:9: bad icon url "::????:::%%orange": parse "::????:::%%orange": missing protocol scheme
  1683  d2/testdata/d2compiler/TestCompile/errors/reserved_icon_style.d2:5:18: expected "opacity" to be a number between 0.0 and 1.0
  1684  d2/testdata/d2compiler/TestCompile/errors/reserved_icon_style.d2:2:9: near key "y" must be the absolute path to a shape or one of the following constants: top-left, top-center, top-right, center-left, center-right, bottom-left, bottom-center, bottom-right`,
  1685  		},
  1686  		{
  1687  			name: "errors/missing_shape_icon",
  1688  
  1689  			text:   `x.shape: image`,
  1690  			expErr: `d2/testdata/d2compiler/TestCompile/errors/missing_shape_icon.d2:1:1: image shape must include an "icon" field`,
  1691  		},
  1692  		{
  1693  			name: "edge_in_column",
  1694  
  1695  			text: `x: {
  1696    shape: sql_table
  1697    x: {p -> q}
  1698  }`,
  1699  			expErr: `d2/testdata/d2compiler/TestCompile/edge_in_column.d2:3:7: sql_table columns cannot have children
  1700  d2/testdata/d2compiler/TestCompile/edge_in_column.d2:3:12: sql_table columns cannot have children`,
  1701  		},
  1702  		{
  1703  			name: "no-nested-columns-sql",
  1704  
  1705  			text: `x: {
  1706    shape: sql_table
  1707    a -- b.b
  1708  }`,
  1709  			expErr: `d2/testdata/d2compiler/TestCompile/no-nested-columns-sql.d2:3:10: sql_table columns cannot have children`,
  1710  		},
  1711  		{
  1712  			name: "no-nested-columns-sql-2",
  1713  
  1714  			text: `x: {
  1715    shape: sql_table
  1716    a
  1717  }
  1718  x.a.b`,
  1719  			expErr: `d2/testdata/d2compiler/TestCompile/no-nested-columns-sql-2.d2:5:5: sql_table columns cannot have children`,
  1720  		},
  1721  		{
  1722  			name: "no-nested-columns-class",
  1723  
  1724  			text: `x: {
  1725    shape: class
  1726    a.a
  1727  }`,
  1728  			expErr: `d2/testdata/d2compiler/TestCompile/no-nested-columns-class.d2:3:5: class fields cannot have children`,
  1729  		},
  1730  		{
  1731  			name: "improper-class-ref",
  1732  
  1733  			text:   `myobj.class.style.stroke-dash: 3`,
  1734  			expErr: `d2/testdata/d2compiler/TestCompile/improper-class-ref.d2:1:7: "class" must be the last part of the key`,
  1735  		},
  1736  		{
  1737  			name: "tail-style",
  1738  
  1739  			text:   `myobj.style: 3`,
  1740  			expErr: `d2/testdata/d2compiler/TestCompile/tail-style.d2:1:7: "style" expected to be set to a map of key-values, or contain an additional keyword like "style.opacity: 0.4"`,
  1741  		},
  1742  		{
  1743  			name: "tail-style-map",
  1744  
  1745  			text:   `myobj.style: {}`,
  1746  			expErr: `d2/testdata/d2compiler/TestCompile/tail-style-map.d2:1:7: "style" expected to be set to a map of key-values, or contain an additional keyword like "style.opacity: 0.4"`,
  1747  		},
  1748  		{
  1749  			name: "bad-style-nesting",
  1750  
  1751  			text:   `myobj.style.style.stroke-dash: 3`,
  1752  			expErr: `d2/testdata/d2compiler/TestCompile/bad-style-nesting.d2:1:13: invalid style keyword: "style"`,
  1753  		},
  1754  		{
  1755  			name: "edge_to_style",
  1756  
  1757  			text: `x: {style.opacity: 0.4}
  1758  y -> x.style
  1759  `,
  1760  			expErr: `d2/testdata/d2compiler/TestCompile/edge_to_style.d2:2:8: reserved keywords are prohibited in edges`,
  1761  		},
  1762  		{
  1763  			name: "keyword-container",
  1764  
  1765  			text: `a.near.b
  1766  `,
  1767  			expErr: `d2/testdata/d2compiler/TestCompile/keyword-container.d2:1:3: "near" must be the last part of the key`,
  1768  		},
  1769  		{
  1770  			name: "escaped_id",
  1771  
  1772  			text: `b\nb`,
  1773  			assertions: func(t *testing.T, g *d2graph.Graph) {
  1774  				if len(g.Objects) != 1 {
  1775  					t.Fatal(g.Objects)
  1776  				}
  1777  				assert.String(t, `"b\nb"`, g.Objects[0].ID)
  1778  				assert.String(t, `b
  1779  b`, g.Objects[0].Label.Value)
  1780  			},
  1781  		},
  1782  		{
  1783  			name: "unescaped_id_cr",
  1784  
  1785  			text: `b\rb`,
  1786  			assertions: func(t *testing.T, g *d2graph.Graph) {
  1787  				if len(g.Objects) != 1 {
  1788  					t.Fatal(g.Objects)
  1789  				}
  1790  				assert.String(t, "b\rb", g.Objects[0].ID)
  1791  				assert.String(t, "b\rb", g.Objects[0].Label.Value)
  1792  			},
  1793  		},
  1794  		{
  1795  			name: "class_style",
  1796  
  1797  			text: `IUserProperties: {
  1798    shape: "class"
  1799    firstName?: "string"
  1800    style.opacity: 0.4
  1801  }
  1802  `,
  1803  			assertions: func(t *testing.T, g *d2graph.Graph) {
  1804  				if len(g.Objects) != 1 {
  1805  					t.Fatal(g.Objects)
  1806  				}
  1807  				if len(g.Objects[0].Class.Fields) != 1 {
  1808  					t.Fatal(len(g.Objects[0].Class.Fields))
  1809  				}
  1810  				if len(g.Objects[0].Class.Methods) != 0 {
  1811  					t.Fatal(len(g.Objects[0].Class.Methods))
  1812  				}
  1813  				if g.Objects[0].Style.Opacity.Value != "0.4" {
  1814  					t.Fatal(g.Objects[0].Style.Opacity.Value)
  1815  				}
  1816  			},
  1817  		},
  1818  		{
  1819  			name: "table_style",
  1820  
  1821  			text: `IUserProperties: {
  1822    shape: sql_table
  1823    GetType(): string
  1824    style.opacity: 0.4
  1825  }
  1826  `,
  1827  			assertions: func(t *testing.T, g *d2graph.Graph) {
  1828  				if len(g.Objects) != 1 {
  1829  					t.Fatal(g.Objects)
  1830  				}
  1831  				if len(g.Objects[0].SQLTable.Columns) != 1 {
  1832  					t.Fatal(len(g.Objects[0].SQLTable.Columns))
  1833  				}
  1834  				if g.Objects[0].Style.Opacity.Value != "0.4" {
  1835  					t.Fatal(g.Objects[0].Style.Opacity.Value)
  1836  				}
  1837  			},
  1838  		},
  1839  		{
  1840  			name: "table_style_map",
  1841  
  1842  			text: `IUserProperties: {
  1843    shape: sql_table
  1844    GetType(): string
  1845    style: {
  1846      opacity: 0.4
  1847      font-color: blue
  1848    }
  1849  }
  1850  `,
  1851  			assertions: func(t *testing.T, g *d2graph.Graph) {
  1852  				if len(g.Objects) != 1 {
  1853  					t.Fatal(g.Objects)
  1854  				}
  1855  				if len(g.Objects[0].SQLTable.Columns) != 1 {
  1856  					t.Fatal(len(g.Objects[0].SQLTable.Columns))
  1857  				}
  1858  				if g.Objects[0].Style.Opacity.Value != "0.4" {
  1859  					t.Fatal(g.Objects[0].Style.Opacity.Value)
  1860  				}
  1861  			},
  1862  		},
  1863  		{
  1864  			name: "table_connection_attr",
  1865  
  1866  			text: `x: {
  1867    shape: sql_table
  1868    y
  1869  }
  1870  a: {
  1871    shape: sql_table
  1872    b
  1873  }
  1874  x.y -> a.b: {
  1875    style.animated: true
  1876  }
  1877  `,
  1878  			assertions: func(t *testing.T, g *d2graph.Graph) {
  1879  				tassert.Equal(t, "true", g.Edges[0].Style.Animated.Value)
  1880  			},
  1881  		},
  1882  		{
  1883  			name: "class_paren",
  1884  
  1885  			text: `_shape_: "shape" {
  1886    shape: class
  1887  
  1888  	field here
  1889    GetType(): string
  1890    Is(): bool
  1891  }`,
  1892  			assertions: func(t *testing.T, g *d2graph.Graph) {
  1893  				if len(g.Objects) != 1 {
  1894  					t.Fatal(g.Objects)
  1895  				}
  1896  				assert.String(t, `field here`, g.Objects[0].Class.Fields[0].Name)
  1897  				assert.String(t, `GetType()`, g.Objects[0].Class.Methods[0].Name)
  1898  				assert.String(t, `Is()`, g.Objects[0].Class.Methods[1].Name)
  1899  			},
  1900  		},
  1901  		{
  1902  			name: "sql_paren",
  1903  
  1904  			text: `_shape_: "shape" {
  1905    shape: sql_table
  1906  
  1907    GetType(): string
  1908    Is(): bool
  1909  }`,
  1910  			assertions: func(t *testing.T, g *d2graph.Graph) {
  1911  				if len(g.Objects) != 1 {
  1912  					t.Fatal(g.Objects)
  1913  				}
  1914  				assert.String(t, `GetType()`, g.Objects[0].SQLTable.Columns[0].Name.Label)
  1915  				assert.String(t, `Is()`, g.Objects[0].SQLTable.Columns[1].Name.Label)
  1916  			},
  1917  		},
  1918  		{
  1919  			name: "nested_sql",
  1920  
  1921  			text: `outer: {
  1922    table: {
  1923      shape: sql_table
  1924  
  1925      GetType(): string
  1926      Is(): bool
  1927    }
  1928  }`,
  1929  			assertions: func(t *testing.T, g *d2graph.Graph) {
  1930  				if len(g.Objects) != 2 {
  1931  					t.Fatal(g.Objects)
  1932  				}
  1933  				if _, has := g.Objects[0].HasChild([]string{"table"}); !has {
  1934  					t.Fatal(g.Objects)
  1935  				}
  1936  				if len(g.Objects[0].ChildrenArray) != 1 {
  1937  					t.Fatal(g.Objects)
  1938  				}
  1939  				assert.String(t, `GetType()`, g.Objects[1].SQLTable.Columns[0].Name.Label)
  1940  				assert.String(t, `Is()`, g.Objects[1].SQLTable.Columns[1].Name.Label)
  1941  			},
  1942  		},
  1943  		{
  1944  			name: "3d_oval",
  1945  
  1946  			text: `SVP1.shape: oval
  1947  SVP1.style.3d: true`,
  1948  			expErr: `d2/testdata/d2compiler/TestCompile/3d_oval.d2:2:1: key "3d" can only be applied to squares, rectangles, and hexagons`,
  1949  		}, {
  1950  			name: "edge_column_index",
  1951  			text: `src: {
  1952  	shape: sql_table
  1953  	id: int
  1954  	dst_id: int
  1955  }
  1956  
  1957  dst: {
  1958  	shape: sql_table
  1959  	id: int
  1960  	name: string
  1961  }
  1962  
  1963  dst.id <-> src.dst_id
  1964  `,
  1965  			assertions: func(t *testing.T, g *d2graph.Graph) {
  1966  				srcIndex := g.Edges[0].SrcTableColumnIndex
  1967  				if srcIndex == nil || *srcIndex != 0 {
  1968  					t.Fatalf("expected SrcTableColumnIndex to be 0, got %v", srcIndex)
  1969  				}
  1970  				dstIndex := g.Edges[0].DstTableColumnIndex
  1971  				if dstIndex == nil || *dstIndex != 1 {
  1972  					t.Fatalf("expected DstTableColumnIndex to be 1, got %v", dstIndex)
  1973  				}
  1974  			},
  1975  		},
  1976  		{
  1977  			name: "basic_sequence",
  1978  
  1979  			text: `x: {
  1980    shape: sequence_diagram
  1981  }
  1982  `,
  1983  			assertions: func(t *testing.T, g *d2graph.Graph) {
  1984  				assert.String(t, "sequence_diagram", g.Objects[0].Shape.Value)
  1985  			},
  1986  		},
  1987  		{
  1988  			name: "near_sequence",
  1989  
  1990  			text: `x: {
  1991    shape: sequence_diagram
  1992    a
  1993  }
  1994  b.near: x.a
  1995  `,
  1996  			expErr: `d2/testdata/d2compiler/TestCompile/near_sequence.d2:5:9: near keys cannot be set to an object within sequence diagrams`,
  1997  		},
  1998  		{
  1999  			name: "sequence-timestamp",
  2000  
  2001  			text: `shape: sequence_diagram
  2002  a
  2003  b
  2004  
  2005  "04:20,11:20": {
  2006    "loop through each table": {
  2007      a."start_time = datetime.datetime.now"
  2008      a -> b
  2009    }
  2010  }
  2011  `,
  2012  			assertions: func(t *testing.T, g *d2graph.Graph) {
  2013  				tassert.Equal(t, 1, len(g.Edges))
  2014  				tassert.Equal(t, 5, len(g.Objects))
  2015  				tassert.Equal(t, "a", g.Objects[0].ID)
  2016  				tassert.Equal(t, "b", g.Objects[1].ID)
  2017  				tassert.Equal(t, `"04:20,11:20"`, g.Objects[2].ID)
  2018  				tassert.Equal(t, `loop through each table`, g.Objects[3].ID)
  2019  				tassert.Equal(t, 1, len(g.Objects[0].ChildrenArray))
  2020  				tassert.Equal(t, 0, len(g.Objects[1].ChildrenArray))
  2021  				tassert.Equal(t, 1, len(g.Objects[2].ChildrenArray))
  2022  				tassert.True(t, g.Edges[0].ContainedBy(g.Objects[3]))
  2023  			},
  2024  		},
  2025  		{
  2026  			name: "root_sequence",
  2027  
  2028  			text: `shape: sequence_diagram
  2029  `,
  2030  			assertions: func(t *testing.T, g *d2graph.Graph) {
  2031  				assert.String(t, "sequence_diagram", g.Root.Shape.Value)
  2032  			},
  2033  		},
  2034  		{
  2035  			name: "leaky_sequence",
  2036  
  2037  			text: `x: {
  2038    shape: sequence_diagram
  2039    a
  2040  }
  2041  b -> x.a
  2042  `,
  2043  			expErr: ``,
  2044  		},
  2045  		{
  2046  			name: "sequence_scoping",
  2047  
  2048  			text: `x: {
  2049    shape: sequence_diagram
  2050  	a;b
  2051    group: {
  2052      a -> b
  2053      a.t1 -> b.t1
  2054      b.t1.t2 -> b.t1
  2055    }
  2056  }
  2057  `,
  2058  			assertions: func(t *testing.T, g *d2graph.Graph) {
  2059  				tassert.Equal(t, 7, len(g.Objects))
  2060  				tassert.Equal(t, 3, len(g.Objects[0].ChildrenArray))
  2061  			},
  2062  		},
  2063  		{
  2064  			name: "sequence_grouped_note",
  2065  
  2066  			text: `shape: sequence_diagram
  2067  a;d
  2068  choo: {
  2069    d."this note"
  2070  }
  2071  `,
  2072  			assertions: func(t *testing.T, g *d2graph.Graph) {
  2073  				tassert.Equal(t, 4, len(g.Objects))
  2074  				tassert.Equal(t, 3, len(g.Root.ChildrenArray))
  2075  			},
  2076  		},
  2077  		{
  2078  			name: "sequence_container",
  2079  
  2080  			text: `shape: sequence_diagram
  2081  x.y.q -> j.y.p
  2082  ok: {
  2083  	x.y.q -> j.y.p
  2084  }
  2085  `,
  2086  			assertions: func(t *testing.T, g *d2graph.Graph) {
  2087  				tassert.Equal(t, 7, len(g.Objects))
  2088  				tassert.Equal(t, 3, len(g.Root.ChildrenArray))
  2089  			},
  2090  		},
  2091  		{
  2092  			name: "sequence_container_2",
  2093  
  2094  			text: `shape: sequence_diagram
  2095  x.y.q
  2096  ok: {
  2097  	x.y.q -> j.y.p
  2098  	meow
  2099  }
  2100  `,
  2101  			assertions: func(t *testing.T, g *d2graph.Graph) {
  2102  				tassert.Equal(t, 8, len(g.Objects))
  2103  				tassert.Equal(t, 2, len(g.Root.ChildrenArray))
  2104  			},
  2105  		},
  2106  		{
  2107  			name: "root_direction",
  2108  
  2109  			text: `direction: right`,
  2110  			assertions: func(t *testing.T, g *d2graph.Graph) {
  2111  				assert.String(t, "right", g.Root.Direction.Value)
  2112  			},
  2113  		},
  2114  		{
  2115  			name: "default_direction",
  2116  
  2117  			text: `x`,
  2118  			assertions: func(t *testing.T, g *d2graph.Graph) {
  2119  				assert.String(t, "", g.Objects[0].Direction.Value)
  2120  			},
  2121  		},
  2122  		{
  2123  			name: "set_direction",
  2124  
  2125  			text: `x: {
  2126    direction: left
  2127  }`,
  2128  			assertions: func(t *testing.T, g *d2graph.Graph) {
  2129  				assert.String(t, "left", g.Objects[0].Direction.Value)
  2130  			},
  2131  		},
  2132  		{
  2133  			name: "constraint_label",
  2134  
  2135  			text: `foo {
  2136    label: bar
  2137    constraint: BIZ
  2138  }`,
  2139  			expErr: `d2/testdata/d2compiler/TestCompile/constraint_label.d2:3:3: "constraint" keyword can only be used in "sql_table" shapes`,
  2140  		},
  2141  		{
  2142  			name: "invalid_direction",
  2143  
  2144  			text: `x: {
  2145    direction: diagonal
  2146  }`,
  2147  			expErr: `d2/testdata/d2compiler/TestCompile/invalid_direction.d2:2:14: direction must be one of up, down, right, left, got "diagonal"`,
  2148  		},
  2149  		{
  2150  			name: "self-referencing",
  2151  
  2152  			text: `x -> x
  2153  `,
  2154  			assertions: func(t *testing.T, g *d2graph.Graph) {
  2155  				_, err := diff.Strings(g.Edges[0].Dst.ID, g.Edges[0].Src.ID)
  2156  				if err != nil {
  2157  					t.Fatal(err)
  2158  				}
  2159  			},
  2160  		},
  2161  		{
  2162  			name: "null",
  2163  
  2164  			text: `null
  2165  `,
  2166  			assertions: func(t *testing.T, g *d2graph.Graph) {
  2167  				tassert.Equal(t, "'null'", g.Objects[0].ID)
  2168  				tassert.Equal(t, "null", g.Objects[0].IDVal)
  2169  			},
  2170  		},
  2171  		{
  2172  			name: "sql-regression",
  2173  
  2174  			text: `a: {
  2175    style: {
  2176      fill: lemonchiffon
  2177    }
  2178    b: {
  2179      shape: sql_table
  2180      c
  2181    }
  2182    d
  2183  }
  2184  `,
  2185  			assertions: func(t *testing.T, g *d2graph.Graph) {
  2186  				tassert.Equal(t, 3, len(g.Objects))
  2187  			},
  2188  		},
  2189  		{
  2190  			name: "sql-constraints",
  2191  			text: `x: {
  2192    shape: sql_table
  2193    a: int {constraint: primary_key}
  2194    b: int {constraint: [primary_key; foreign_key]}
  2195  }`,
  2196  			assertions: func(t *testing.T, g *d2graph.Graph) {
  2197  				table := g.Objects[0].SQLTable
  2198  				tassert.Equal(t, []string{"primary_key"}, table.Columns[0].Constraint)
  2199  				tassert.Equal(t, []string{"primary_key", "foreign_key"}, table.Columns[1].Constraint)
  2200  			},
  2201  		},
  2202  		{
  2203  			name: "sql-null-constraint",
  2204  			text: `x: {
  2205    shape: sql_table
  2206    a: int {constraint: null}
  2207    b: int {constraint: [null]}
  2208  }`,
  2209  			assertions: func(t *testing.T, g *d2graph.Graph) {
  2210  				table := g.Objects[0].SQLTable
  2211  				tassert.Nil(t, table.Columns[0].Constraint)
  2212  				tassert.Equal(t, []string{"null"}, table.Columns[1].Constraint)
  2213  			},
  2214  		},
  2215  		{
  2216  			name: "wrong_column_index",
  2217  			text: `Chinchillas: {
  2218    shape: sql_table
  2219    id: int {constraint: primary_key}
  2220    whisker_len: int
  2221    fur_color: string
  2222    age: int
  2223    server: int {constraint: foreign_key}
  2224    caretaker: int {constraint: foreign_key}
  2225  }
  2226  
  2227  Chinchillas_Collectibles: {
  2228    shape: sql_table
  2229    id: int
  2230    collectible: id {constraint: foreign_key}
  2231    chinchilla: id {constraint: foreign_key}
  2232  }
  2233  
  2234  Chinchillas_Collectibles.chinchilla -> Chinchillas.id`,
  2235  			assertions: func(t *testing.T, g *d2graph.Graph) {
  2236  				tassert.Equal(t, 0, *g.Edges[0].DstTableColumnIndex)
  2237  				tassert.Equal(t, 2, *g.Edges[0].SrcTableColumnIndex)
  2238  			},
  2239  		},
  2240  		{
  2241  			name: "link-board-ok",
  2242  			text: `x.link: layers.x
  2243  layers: {
  2244  	x: {
  2245  	  y
  2246  	}
  2247  }`,
  2248  			assertions: func(t *testing.T, g *d2graph.Graph) {
  2249  				tassert.Equal(t, "root.layers.x", g.Objects[0].Link.Value)
  2250  			},
  2251  		},
  2252  		{
  2253  			name: "link-board-mixed",
  2254  			text: `question: How does the cat go?
  2255  question.link: layers.cat
  2256  
  2257  layers: {
  2258    cat: {
  2259      the cat -> meeeowwww: goes
  2260    }
  2261  }
  2262  
  2263  scenarios: {
  2264    green: {
  2265      question.style.fill: green
  2266    }
  2267  }`,
  2268  			assertions: func(t *testing.T, g *d2graph.Graph) {
  2269  				tassert.Equal(t, "root.layers.cat", g.Objects[0].Link.Value)
  2270  				tassert.Equal(t, "root.layers.cat", g.Scenarios[0].Objects[0].Link.Value)
  2271  			},
  2272  		},
  2273  		{
  2274  			name: "link-board-not-found",
  2275  			text: `x.link: layers.x
  2276  `,
  2277  			expErr: `d2/testdata/d2compiler/TestCompile/link-board-not-found.d2:1:1: linked board not found`,
  2278  		},
  2279  		{
  2280  			name: "link-board-not-board",
  2281  			text: `zzz
  2282  x.link: layers.x.y
  2283  layers: {
  2284    x: {
  2285      y
  2286    }
  2287  }`,
  2288  			expErr: `d2/testdata/d2compiler/TestCompile/link-board-not-board.d2:2:1: linked board not found`,
  2289  		},
  2290  		{
  2291  			name: "link-board-nested",
  2292  			text: `x.link: layers.x.layers.x
  2293  layers: {
  2294    x: {
  2295      layers: {
  2296        x: {
  2297          hello
  2298        }
  2299      }
  2300    }
  2301  }`,
  2302  			assertions: func(t *testing.T, g *d2graph.Graph) {
  2303  				tassert.Equal(t, "root.layers.x.layers.x", g.Objects[0].Link.Value)
  2304  			},
  2305  		},
  2306  		{
  2307  			name: "link-board-key-nested",
  2308  			text: `x: {
  2309    y.link: layers.x
  2310  }
  2311  layers: {
  2312    x: {
  2313      yo
  2314    }
  2315  }`,
  2316  			assertions: func(t *testing.T, g *d2graph.Graph) {
  2317  				tassert.Equal(t, "root.layers.x", g.Objects[1].Link.Value)
  2318  			},
  2319  		},
  2320  		{
  2321  			name: "link-board-underscore",
  2322  			text: `x
  2323  layers: {
  2324  	x: {
  2325  	  yo
  2326      layers: {
  2327        x: {
  2328          hello.link: _._.layers.x
  2329          hey.link: _
  2330        }
  2331      }
  2332    }
  2333  }`,
  2334  			assertions: func(t *testing.T, g *d2graph.Graph) {
  2335  				tassert.NotNil(t, g.Layers[0].Layers[0].Objects[0].Link.Value)
  2336  				tassert.Equal(t, "root.layers.x", g.Layers[0].Layers[0].Objects[0].Link.Value)
  2337  				tassert.Equal(t, "root.layers.x", g.Layers[0].Layers[0].Objects[1].Link.Value)
  2338  			},
  2339  		},
  2340  		{
  2341  			name: "link-board-underscore-not-found",
  2342  			text: `x
  2343  layers: {
  2344    x: {
  2345      yo
  2346      layers: {
  2347        x: {
  2348          hello.link: _._._
  2349        }
  2350      }
  2351    }
  2352  }`,
  2353  			expErr: `d2/testdata/d2compiler/TestCompile/link-board-underscore-not-found.d2:7:9: invalid underscore usage`,
  2354  		},
  2355  		{
  2356  			name: "border-radius-negative",
  2357  			text: `x
  2358  x: {
  2359    style.border-radius: -1
  2360  }`,
  2361  			expErr: `d2/testdata/d2compiler/TestCompile/border-radius-negative.d2:3:24: expected "border-radius" to be a number greater or equal to 0`,
  2362  		},
  2363  		{
  2364  			name: "text-transform",
  2365  			text: `direction: right
  2366  x -> y: hi {
  2367    style: {
  2368      text-transform: capitalize
  2369    }
  2370  }
  2371  x.style.text-transform: uppercase
  2372  y.style.text-transform: lowercase`,
  2373  		},
  2374  		{
  2375  			name: "near_near_const",
  2376  			text: `
  2377  title: Title {
  2378  	near: top-center
  2379  }
  2380  
  2381  obj {
  2382  	near: title
  2383  }
  2384  `,
  2385  			expErr: `d2/testdata/d2compiler/TestCompile/near_near_const.d2:7:8: near keys cannot be set to an object with a constant near key`,
  2386  		},
  2387  		{
  2388  			name: "label-near-parent",
  2389  			text: `hey: sushi {
  2390  	label.near: outside-top-left
  2391  }
  2392  `,
  2393  			assertions: func(t *testing.T, g *d2graph.Graph) {
  2394  				tassert.Equal(t, "sushi", g.Objects[0].Attributes.Label.Value)
  2395  				tassert.Equal(t, "outside-top-left", g.Objects[0].Attributes.LabelPosition.Value)
  2396  			},
  2397  		},
  2398  		{
  2399  			name: "label-near-composite-separate",
  2400  			text: `hey: {
  2401  	label: sushi
  2402  	label.near: outside-top-left
  2403  }
  2404  `,
  2405  			assertions: func(t *testing.T, g *d2graph.Graph) {
  2406  				tassert.Equal(t, "sushi", g.Objects[0].Attributes.Label.Value)
  2407  				tassert.Equal(t, "outside-top-left", g.Objects[0].Attributes.LabelPosition.Value)
  2408  			},
  2409  		},
  2410  		{
  2411  			name: "label-near-composite-together",
  2412  			text: `hey: {
  2413    label: sushi {
  2414  		near: outside-top-left
  2415    }
  2416  }
  2417  `,
  2418  			assertions: func(t *testing.T, g *d2graph.Graph) {
  2419  				tassert.Equal(t, "sushi", g.Objects[0].Attributes.Label.Value)
  2420  				tassert.Equal(t, "outside-top-left", g.Objects[0].Attributes.LabelPosition.Value)
  2421  			},
  2422  		},
  2423  		{
  2424  			name: "icon-near-composite-together",
  2425  			text: `hey: {
  2426  	icon: https://asdf.com {
  2427  		near: outside-top-left
  2428    }
  2429  }
  2430  `,
  2431  			assertions: func(t *testing.T, g *d2graph.Graph) {
  2432  				tassert.Equal(t, "asdf.com", g.Objects[0].Attributes.Icon.Host)
  2433  				tassert.Equal(t, "outside-top-left", g.Objects[0].Attributes.IconPosition.Value)
  2434  			},
  2435  		},
  2436  		{
  2437  			name: "label-near-invalid-edge",
  2438  			text: `hey: {
  2439    label: sushi {
  2440  		near: outside-top-left
  2441  		a -> b
  2442    }
  2443  }
  2444  `,
  2445  			expErr: `d2/testdata/d2compiler/TestCompile/label-near-invalid-edge.d2:2:3: unexpected edges in map`,
  2446  		},
  2447  		{
  2448  			name: "label-near-invalid-field",
  2449  			text: `hey: {
  2450    label: sushi {
  2451  		near: outside-top-left
  2452  		a
  2453    }
  2454  }
  2455  `,
  2456  			expErr: `d2/testdata/d2compiler/TestCompile/label-near-invalid-field.d2:4:3: unexpected field a`,
  2457  		},
  2458  		{
  2459  			name: "grid",
  2460  			text: `hey: {
  2461  	grid-rows: 200
  2462  	grid-columns: 230
  2463  }
  2464  `,
  2465  			assertions: func(t *testing.T, g *d2graph.Graph) {
  2466  				tassert.Equal(t, "200", g.Objects[0].GridRows.Value)
  2467  			},
  2468  		},
  2469  		{
  2470  			name: "grid_negative",
  2471  			text: `hey: {
  2472  	grid-rows: 200
  2473  	grid-columns: -200
  2474  }
  2475  `,
  2476  			expErr: `d2/testdata/d2compiler/TestCompile/grid_negative.d2:3:16: grid-columns must be a positive integer: "-200"`,
  2477  		},
  2478  		{
  2479  			name: "grid_gap_negative",
  2480  			text: `hey: {
  2481  	horizontal-gap: -200
  2482  	vertical-gap: -30
  2483  }
  2484  `,
  2485  			expErr: `d2/testdata/d2compiler/TestCompile/grid_gap_negative.d2:2:18: horizontal-gap must be a non-negative integer: "-200"
  2486  d2/testdata/d2compiler/TestCompile/grid_gap_negative.d2:3:16: vertical-gap must be a non-negative integer: "-30"`,
  2487  		},
  2488  		{
  2489  			name: "grid_edge",
  2490  			text: `hey: {
  2491  	grid-rows: 1
  2492  	a -> b: ok
  2493  }
  2494  c -> hey.b
  2495  hey.a -> c
  2496  hey -> hey.a
  2497  
  2498  hey -> c: ok
  2499  `,
  2500  			expErr: `d2/testdata/d2compiler/TestCompile/grid_edge.d2:7:1: edge from grid diagram "hey" cannot enter itself`,
  2501  		},
  2502  		{
  2503  			name: "grid_deeper_edge",
  2504  			text: `hey: {
  2505  	grid-rows: 1
  2506  	a -> b: ok
  2507  	b: {
  2508  		c -> d: ok now
  2509  		c.e -> c.f.g: ok
  2510  		c.e -> d.h: ok
  2511  		c -> d.h: ok
  2512  	}
  2513  	a: {
  2514  		grid-columns: 1
  2515  		e -> f: also ok now
  2516  		e: {
  2517  			g -> h: ok
  2518  			g -> h.h: ok
  2519  		}
  2520  		e -> f.i: ok now
  2521  		e.g -> f.i: ok now
  2522  	}
  2523  	a -> b.c: ok now
  2524  	a.e -> b.c: ok now
  2525  	a -> a.e: not ok
  2526  }
  2527  `,
  2528  			expErr: `d2/testdata/d2compiler/TestCompile/grid_deeper_edge.d2:22:2: edge from grid diagram "hey.a" cannot enter itself`,
  2529  		},
  2530  		{
  2531  			name: "parent_graph_edge_to_descendant",
  2532  			text: `tl: {
  2533  	near: top-left
  2534  	a.b
  2535  }
  2536  grid: {
  2537  	grid-rows: 1
  2538  	cell.c.d
  2539  }
  2540  seq: {
  2541  	shape: sequence_diagram
  2542  	e.f
  2543  }
  2544  tl -> tl.a: no
  2545  tl -> tl.a.b: no
  2546  grid-> grid.cell: no
  2547  grid-> grid.cell.c: no
  2548  grid.cell -> grid.cell.c: no
  2549  grid.cell -> grid.cell.c.d: no
  2550  seq -> seq.e: no
  2551  seq -> seq.e.f: no
  2552  `,
  2553  			expErr: `d2/testdata/d2compiler/TestCompile/parent_graph_edge_to_descendant.d2:13:1: edge from constant near "tl" cannot enter itself
  2554  d2/testdata/d2compiler/TestCompile/parent_graph_edge_to_descendant.d2:14:1: edge from constant near "tl" cannot enter itself
  2555  d2/testdata/d2compiler/TestCompile/parent_graph_edge_to_descendant.d2:17:1: edge from grid cell "grid.cell" cannot enter itself
  2556  d2/testdata/d2compiler/TestCompile/parent_graph_edge_to_descendant.d2:18:1: edge from grid cell "grid.cell" cannot enter itself
  2557  d2/testdata/d2compiler/TestCompile/parent_graph_edge_to_descendant.d2:15:1: edge from grid diagram "grid" cannot enter itself
  2558  d2/testdata/d2compiler/TestCompile/parent_graph_edge_to_descendant.d2:16:1: edge from grid diagram "grid" cannot enter itself
  2559  d2/testdata/d2compiler/TestCompile/parent_graph_edge_to_descendant.d2:19:1: edge from sequence diagram "seq" cannot enter itself
  2560  d2/testdata/d2compiler/TestCompile/parent_graph_edge_to_descendant.d2:20:1: edge from sequence diagram "seq" cannot enter itself`,
  2561  		},
  2562  		{
  2563  			name: "grid_nested",
  2564  			text: `hey: {
  2565  	grid-rows: 200
  2566  	grid-columns: 200
  2567  
  2568  	a
  2569  	b
  2570  	c
  2571  	d.valid descendant
  2572  	e: {
  2573  		grid-rows: 1
  2574  		grid-columns: 2
  2575  
  2576  		a
  2577  		b
  2578  	}
  2579  }
  2580  `,
  2581  			expErr: ``,
  2582  		},
  2583  		{
  2584  			name: "classes",
  2585  			text: `classes: {
  2586    dragon_ball: {
  2587      label: ""
  2588      shape: circle
  2589      style.fill: orange
  2590    }
  2591    path: {
  2592      label: "then"
  2593      style.stroke-width: 4
  2594    }
  2595  }
  2596  nostar: { class: dragon_ball }
  2597  1star: "*" { class: dragon_ball; style.fill: red }
  2598  2star: { label: "**"; class: dragon_ball }
  2599  
  2600  nostar -> 1star: { class: path }
  2601  `,
  2602  			assertions: func(t *testing.T, g *d2graph.Graph) {
  2603  				tassert.Equal(t, 3, len(g.Objects))
  2604  				tassert.Equal(t, "dragon_ball", g.Objects[0].Classes[0])
  2605  				tassert.Equal(t, "", g.Objects[0].Label.Value)
  2606  				// Class field overrides primary
  2607  				tassert.Equal(t, "", g.Objects[1].Label.Value)
  2608  				tassert.Equal(t, "**", g.Objects[2].Label.Value)
  2609  				tassert.Equal(t, "orange", g.Objects[0].Style.Fill.Value)
  2610  				tassert.Equal(t, "red", g.Objects[1].Style.Fill.Value)
  2611  
  2612  				tassert.Equal(t, "4", g.Edges[0].Style.StrokeWidth.Value)
  2613  				tassert.Equal(t, "then", g.Edges[0].Label.Value)
  2614  			},
  2615  		},
  2616  		{
  2617  			name: "array-classes",
  2618  			text: `classes: {
  2619    dragon_ball: {
  2620      label: ""
  2621      shape: circle
  2622      style.fill: orange
  2623    }
  2624    path: {
  2625      label: "then"
  2626      style.stroke-width: 4
  2627    }
  2628  	path2: {
  2629      style.stroke-width: 2
  2630  	}
  2631  }
  2632  nostar: { class: [dragon_ball; path] }
  2633  1star: { class: [path; dragon_ball] }
  2634  
  2635  nostar -> 1star: { class: [path; path2] }
  2636  `,
  2637  			assertions: func(t *testing.T, g *d2graph.Graph) {
  2638  				tassert.Equal(t, "then", g.Objects[0].Label.Value)
  2639  				tassert.Equal(t, "", g.Objects[1].Label.Value)
  2640  				tassert.Equal(t, "circle", g.Objects[0].Shape.Value)
  2641  				tassert.Equal(t, "circle", g.Objects[1].Shape.Value)
  2642  				tassert.Equal(t, "2", g.Edges[0].Style.StrokeWidth.Value)
  2643  			},
  2644  		},
  2645  		{
  2646  			name: "comma-array-class",
  2647  
  2648  			text: `classes: {
  2649    dragon_ball: {
  2650      label: ""
  2651      shape: circle
  2652      style.fill: orange
  2653    }
  2654    path: {
  2655      label: "then"
  2656      style.stroke-width: 4
  2657    }
  2658  }
  2659  nostar: { class: [dragon_ball, path] }`,
  2660  			expErr: `d2/testdata/d2compiler/TestCompile/comma-array-class.d2:12:11: class "dragon_ball, path" not found. Did you mean to use ";" to separate array items?`,
  2661  		},
  2662  		{
  2663  			name: "reordered-classes",
  2664  			text: `classes: {
  2665    x: {
  2666      shape: circle
  2667    }
  2668  }
  2669  a.class: x
  2670  classes.x.shape: diamond
  2671  `,
  2672  			assertions: func(t *testing.T, g *d2graph.Graph) {
  2673  				tassert.Equal(t, 1, len(g.Objects))
  2674  				tassert.Equal(t, "diamond", g.Objects[0].Shape.Value)
  2675  			},
  2676  		},
  2677  		{
  2678  			name: "nested-array-classes",
  2679  			text: `classes: {
  2680    one target: {
  2681  		target-arrowhead.label: 1
  2682    }
  2683  	association: {
  2684  		target-arrowhead.shape: arrow
  2685  	}
  2686  }
  2687  
  2688  a -> b: { class: [one target; association] }
  2689  a -> b: { class: [association; one target] }
  2690  `,
  2691  			assertions: func(t *testing.T, g *d2graph.Graph) {
  2692  				// They have the same, regardless of order of class application
  2693  				// since the classes modify attributes exclusive of each other
  2694  				tassert.Equal(t, "1", g.Edges[0].DstArrowhead.Label.Value)
  2695  				tassert.Equal(t, "1", g.Edges[1].DstArrowhead.Label.Value)
  2696  				tassert.Equal(t, "arrow", g.Edges[0].DstArrowhead.Shape.Value)
  2697  				tassert.Equal(t, "arrow", g.Edges[1].DstArrowhead.Shape.Value)
  2698  			},
  2699  		},
  2700  		{
  2701  			name: "var_in_glob",
  2702  			text: `vars: {
  2703    v: {
  2704      ok
  2705    }
  2706  }
  2707  
  2708  x1 -> x2
  2709  
  2710  x*: {
  2711    ...${v}
  2712  }
  2713  `,
  2714  			assertions: func(t *testing.T, g *d2graph.Graph) {
  2715  				tassert.Equal(t, 4, len(g.Objects))
  2716  				tassert.Equal(t, "x1.ok", g.Objects[0].AbsID())
  2717  				tassert.Equal(t, "x2.ok", g.Objects[1].AbsID())
  2718  				tassert.Equal(t, "x1", g.Objects[2].AbsID())
  2719  				tassert.Equal(t, "x2", g.Objects[3].AbsID())
  2720  			},
  2721  		},
  2722  		{
  2723  			name: "class-shape-class",
  2724  			text: `classes: {
  2725    classClass: {
  2726      shape: class
  2727    }
  2728  }
  2729  
  2730  object: {
  2731    class: classClass
  2732    length(): int
  2733  }
  2734  `,
  2735  		},
  2736  		{
  2737  			name: "no-class-primary",
  2738  			text: `x.class
  2739  `,
  2740  			expErr: `d2/testdata/d2compiler/TestCompile/no-class-primary.d2:1:3: class missing value`,
  2741  		},
  2742  		{
  2743  			name: "no-class-inside-classes",
  2744  			text: `classes: {
  2745    x: {
  2746      class: y
  2747    }
  2748  }
  2749  `,
  2750  			expErr: `d2/testdata/d2compiler/TestCompile/no-class-inside-classes.d2:3:5: "class" cannot appear within "classes"`,
  2751  		},
  2752  		{
  2753  			// This is okay
  2754  			name: "missing-class",
  2755  			text: `x.class: yo
  2756  `,
  2757  		},
  2758  		{
  2759  			name: "classes-unreserved",
  2760  			text: `classes: {
  2761    mango: {
  2762      seed
  2763    }
  2764  }
  2765  `,
  2766  			expErr: `d2/testdata/d2compiler/TestCompile/classes-unreserved.d2:3:5: seed is an invalid class field, must be reserved keyword`,
  2767  		},
  2768  		{
  2769  			name: "classes-internal-edge",
  2770  			text: `classes: {
  2771    mango: {
  2772  		width: 100
  2773    }
  2774    jango: {
  2775      height: 100
  2776    }
  2777    mango -> jango
  2778  }
  2779  `,
  2780  			expErr: `d2/testdata/d2compiler/TestCompile/classes-internal-edge.d2:8:3: classes cannot contain an edge`,
  2781  		},
  2782  		{
  2783  			name: "reserved-composite",
  2784  			text: `shape: sequence_diagram {
  2785    alice -> bob: What does it mean\nto be well-adjusted?
  2786    bob -> alice: The ability to play bridge or\ngolf as if they were games.
  2787  }
  2788  `,
  2789  			expErr: `d2/testdata/d2compiler/TestCompile/reserved-composite.d2:1:1: reserved field shape does not accept composite`,
  2790  		},
  2791  		{
  2792  			name: "text_no_label",
  2793  			text: `a: "ok" {
  2794  	shape: text
  2795  }
  2796  b: " \n " {
  2797  	shape: text
  2798  }
  2799  c: "" {
  2800  	shape: text
  2801  }
  2802  d: "" {
  2803  	shape: circle
  2804  }
  2805  e: " \n "
  2806  f: |md  |
  2807  g: |md
  2808  
  2809  |
  2810  `,
  2811  			expErr: `d2/testdata/d2compiler/TestCompile/text_no_label.d2:14:1: block string cannot be empty
  2812  d2/testdata/d2compiler/TestCompile/text_no_label.d2:15:1: block string cannot be empty
  2813  d2/testdata/d2compiler/TestCompile/text_no_label.d2:4:1: shape text must have a non-empty label
  2814  d2/testdata/d2compiler/TestCompile/text_no_label.d2:7:1: shape text must have a non-empty label`,
  2815  		},
  2816  		{
  2817  			name: "var-not-color",
  2818  			text: `vars: {
  2819    d2-config: {
  2820      theme-overrides: {
  2821        B1: potato
  2822  			potato: B1
  2823      }
  2824    }
  2825  }
  2826  a
  2827  `,
  2828  			expErr: `d2/testdata/d2compiler/TestCompile/var-not-color.d2:4:7: expected "B1" to be a valid named color ("orange") or a hex code ("#f0ff3a")
  2829  d2/testdata/d2compiler/TestCompile/var-not-color.d2:5:4: "potato" is not a valid theme code`,
  2830  		},
  2831  		{
  2832  			name: "no_arrowheads_in_shape",
  2833  
  2834  			text: `x.target-arrowhead.shape: cf-one
  2835  y.source-arrowhead.shape: cf-one
  2836  `,
  2837  			expErr: `d2/testdata/d2compiler/TestCompile/no_arrowheads_in_shape.d2:1:3: "target-arrowhead" can only be used on connections
  2838  d2/testdata/d2compiler/TestCompile/no_arrowheads_in_shape.d2:2:3: "source-arrowhead" can only be used on connections`,
  2839  		},
  2840  	}
  2841  
  2842  	for _, tc := range testCases {
  2843  		tc := tc
  2844  		t.Run(tc.name, func(t *testing.T) {
  2845  			t.Parallel()
  2846  
  2847  			d2Path := fmt.Sprintf("d2/testdata/d2compiler/%v.d2", t.Name())
  2848  			g, _, err := d2compiler.Compile(d2Path, strings.NewReader(tc.text), nil)
  2849  			if tc.expErr != "" {
  2850  				if err == nil {
  2851  					t.Fatalf("expected error with: %q", tc.expErr)
  2852  				}
  2853  				ds, err := diff.Strings(tc.expErr, err.Error())
  2854  				if err != nil {
  2855  					t.Fatal(err)
  2856  				}
  2857  				if ds != "" {
  2858  					t.Fatalf("unexpected error: %s", ds)
  2859  				}
  2860  			} else if err != nil {
  2861  				t.Fatal(err)
  2862  			}
  2863  
  2864  			if tc.expErr == "" && tc.assertions != nil {
  2865  				t.Run("assertions", func(t *testing.T) {
  2866  					tc.assertions(t, g)
  2867  				})
  2868  			}
  2869  
  2870  			got := struct {
  2871  				Graph *d2graph.Graph `json:"graph"`
  2872  				Err   error          `json:"err"`
  2873  			}{
  2874  				Graph: g,
  2875  				Err:   err,
  2876  			}
  2877  
  2878  			err = diff.TestdataJSON(filepath.Join("..", "testdata", "d2compiler", t.Name()), got)
  2879  			assert.Success(t, err)
  2880  		})
  2881  	}
  2882  }
  2883  
  2884  func TestCompile2(t *testing.T) {
  2885  	t.Parallel()
  2886  
  2887  	t.Run("boards", testBoards)
  2888  	t.Run("seqdiagrams", testSeqDiagrams)
  2889  	t.Run("nulls", testNulls)
  2890  	t.Run("vars", testVars)
  2891  	t.Run("globs", testGlobs)
  2892  }
  2893  
  2894  func testBoards(t *testing.T) {
  2895  	t.Parallel()
  2896  
  2897  	tca := []struct {
  2898  		name string
  2899  		run  func(t *testing.T)
  2900  	}{
  2901  		{
  2902  			name: "root",
  2903  			run: func(t *testing.T) {
  2904  				g, _ := assertCompile(t, `base
  2905  
  2906  layers: {
  2907    one: {
  2908      santa
  2909    }
  2910    two: {
  2911      clause
  2912    }
  2913  }
  2914  `, "")
  2915  				assert.Equal(t, 2, len(g.Layers))
  2916  				assert.Equal(t, "one", g.Layers[0].Name)
  2917  				assert.Equal(t, "two", g.Layers[1].Name)
  2918  			},
  2919  		},
  2920  		{
  2921  			name: "recursive",
  2922  			run: func(t *testing.T) {
  2923  				g, _ := assertCompile(t, `base
  2924  
  2925  layers: {
  2926    one: {
  2927      santa
  2928    }
  2929    two: {
  2930      clause
  2931  		steps: {
  2932  			seinfeld: {
  2933  				reindeer
  2934  			}
  2935  			missoula: {
  2936  				montana
  2937  			}
  2938  		}
  2939    }
  2940  }
  2941  `, "")
  2942  				assert.Equal(t, 2, len(g.Layers))
  2943  				assert.Equal(t, "one", g.Layers[0].Name)
  2944  				assert.Equal(t, "two", g.Layers[1].Name)
  2945  				assert.Equal(t, 2, len(g.Layers[1].Steps))
  2946  			},
  2947  		},
  2948  		{
  2949  			name: "isFolderOnly",
  2950  			run: func(t *testing.T) {
  2951  				g, _ := assertCompile(t, `
  2952  layers: {
  2953    one: {
  2954      santa
  2955    }
  2956    two: {
  2957      clause
  2958  		scenarios: {
  2959  			seinfeld: {
  2960  			}
  2961  			missoula: {
  2962  				steps: {
  2963  					missus: one two three
  2964  				}
  2965  			}
  2966  		}
  2967    }
  2968  }
  2969  `, "")
  2970  				assert.True(t, g.IsFolderOnly)
  2971  				assert.Equal(t, 2, len(g.Layers))
  2972  				assert.Equal(t, "one", g.Layers[0].Name)
  2973  				assert.Equal(t, "two", g.Layers[1].Name)
  2974  				assert.Equal(t, 2, len(g.Layers[1].Scenarios))
  2975  				assert.False(t, g.Layers[1].Scenarios[0].IsFolderOnly)
  2976  				assert.False(t, g.Layers[1].Scenarios[1].IsFolderOnly)
  2977  			},
  2978  		},
  2979  		{
  2980  			name: "isFolderOnly-shapes",
  2981  			run: func(t *testing.T) {
  2982  				g, _ := assertCompile(t, `
  2983  direction: right
  2984  
  2985  steps: {
  2986    1: {
  2987      RJ
  2988    }
  2989  }
  2990  `, "")
  2991  				assert.True(t, g.IsFolderOnly)
  2992  			},
  2993  		},
  2994  		{
  2995  			name: "scenarios_edge_index",
  2996  			run: func(t *testing.T) {
  2997  				assertCompile(t, `a -> x
  2998  
  2999  scenarios: {
  3000    1: {
  3001      (a -> x)[0].style.opacity: 0.1
  3002    }
  3003  }
  3004  `, "")
  3005  			},
  3006  		},
  3007  		{
  3008  			name: "errs/duplicate_board",
  3009  			run: func(t *testing.T) {
  3010  				assertCompile(t, `base
  3011  
  3012  layers: {
  3013    one: {
  3014      santa
  3015    }
  3016  }
  3017  steps: {
  3018  	one: {
  3019  		clause
  3020  	}
  3021  }
  3022  `, `d2/testdata/d2compiler/TestCompile2/boards/errs/duplicate_board.d2:9:2: board name one already used by another board`)
  3023  			},
  3024  		},
  3025  	}
  3026  
  3027  	for _, tc := range tca {
  3028  		tc := tc
  3029  		t.Run(tc.name, func(t *testing.T) {
  3030  			t.Parallel()
  3031  			tc.run(t)
  3032  		})
  3033  	}
  3034  }
  3035  
  3036  func testSeqDiagrams(t *testing.T) {
  3037  	t.Parallel()
  3038  
  3039  	t.Run("errs", func(t *testing.T) {
  3040  		t.Parallel()
  3041  
  3042  		tca := []struct {
  3043  			name string
  3044  			skip bool
  3045  			run  func(t *testing.T)
  3046  		}{
  3047  			{
  3048  				name: "sequence_diagram_edge_between_edge_groups",
  3049  				// New sequence diagram scoping implementation is disabled.
  3050  				skip: true,
  3051  				run: func(t *testing.T) {
  3052  					assertCompile(t, `
  3053  Office chatter: {
  3054    shape: sequence_diagram
  3055    alice: Alice
  3056    bob: Bobby
  3057    awkward small talk: {
  3058      alice -> bob: uhm, hi
  3059      bob -> alice: oh, hello
  3060      icebreaker attempt: {
  3061        alice -> bob: what did you have for lunch?
  3062      }
  3063      unfortunate outcome: {
  3064        bob -> alice: that's personal
  3065      }
  3066    }
  3067    awkward small talk.icebreaker attempt.alice -> awkward small talk.unfortunate outcome.bob
  3068  }
  3069  `, "d2/testdata/d2compiler/TestCompile2/seqdiagrams/errs/sequence_diagram_edge_between_edge_groups.d2:16:3: edges between edge groups are not allowed")
  3070  				},
  3071  			},
  3072  		}
  3073  
  3074  		for _, tc := range tca {
  3075  			tc := tc
  3076  			t.Run(tc.name, func(t *testing.T) {
  3077  				t.Parallel()
  3078  				if tc.skip {
  3079  					t.SkipNow()
  3080  				}
  3081  				tc.run(t)
  3082  			})
  3083  		}
  3084  	})
  3085  }
  3086  
  3087  func testNulls(t *testing.T) {
  3088  	t.Parallel()
  3089  
  3090  	t.Run("basic", func(t *testing.T) {
  3091  		t.Parallel()
  3092  
  3093  		tca := []struct {
  3094  			name string
  3095  			skip bool
  3096  			run  func(t *testing.T)
  3097  		}{
  3098  			{
  3099  				name: "shape",
  3100  				run: func(t *testing.T) {
  3101  					g, _ := assertCompile(t, `
  3102  a
  3103  a: null
  3104  `, "")
  3105  					assert.Equal(t, 0, len(g.Objects))
  3106  				},
  3107  			},
  3108  			{
  3109  				name: "edge",
  3110  				run: func(t *testing.T) {
  3111  					g, _ := assertCompile(t, `
  3112  a -> b
  3113  (a -> b)[0]: null
  3114  `, "")
  3115  					assert.Equal(t, 2, len(g.Objects))
  3116  					assert.Equal(t, 0, len(g.Edges))
  3117  				},
  3118  			},
  3119  			{
  3120  				name: "attribute",
  3121  				run: func(t *testing.T) {
  3122  					g, _ := assertCompile(t, `
  3123  a.style.opacity: 0.2
  3124  a.style.opacity: null
  3125  			`, "")
  3126  					assert.Equal(t, (*d2graph.Scalar)(nil), g.Objects[0].Attributes.Style.Opacity)
  3127  				},
  3128  			},
  3129  		}
  3130  
  3131  		for _, tc := range tca {
  3132  			tc := tc
  3133  			t.Run(tc.name, func(t *testing.T) {
  3134  				t.Parallel()
  3135  				if tc.skip {
  3136  					t.SkipNow()
  3137  				}
  3138  				tc.run(t)
  3139  			})
  3140  		}
  3141  	})
  3142  
  3143  	t.Run("reappear", func(t *testing.T) {
  3144  		t.Parallel()
  3145  
  3146  		tca := []struct {
  3147  			name string
  3148  			skip bool
  3149  			run  func(t *testing.T)
  3150  		}{
  3151  			{
  3152  				name: "shape",
  3153  				run: func(t *testing.T) {
  3154  					g, _ := assertCompile(t, `
  3155  a
  3156  a: null
  3157  a
  3158  `, "")
  3159  					assert.Equal(t, 1, len(g.Objects))
  3160  				},
  3161  			},
  3162  			{
  3163  				name: "edge",
  3164  				run: func(t *testing.T) {
  3165  					g, _ := assertCompile(t, `
  3166  a -> b
  3167  (a -> b)[0]: null
  3168  a -> b
  3169  `, "")
  3170  					assert.Equal(t, 2, len(g.Objects))
  3171  					assert.Equal(t, 1, len(g.Edges))
  3172  				},
  3173  			},
  3174  			{
  3175  				name: "attribute-reset",
  3176  				run: func(t *testing.T) {
  3177  					g, _ := assertCompile(t, `
  3178  a.style.opacity: 0.2
  3179  a: null
  3180  a
  3181  `, "")
  3182  					assert.Equal(t, 1, len(g.Objects))
  3183  					assert.Equal(t, (*d2graph.Scalar)(nil), g.Objects[0].Attributes.Style.Opacity)
  3184  				},
  3185  			},
  3186  			{
  3187  				name: "edge-reset",
  3188  				run: func(t *testing.T) {
  3189  					g, _ := assertCompile(t, `
  3190  a -> b
  3191  a: null
  3192  a
  3193  `, "")
  3194  					assert.Equal(t, 2, len(g.Objects))
  3195  					assert.Equal(t, 0, len(g.Edges))
  3196  				},
  3197  			},
  3198  			{
  3199  				name: "children-reset",
  3200  				run: func(t *testing.T) {
  3201  					g, _ := assertCompile(t, `
  3202  a.b.c
  3203  a.b: null
  3204  a.b
  3205  `, "")
  3206  					assert.Equal(t, 2, len(g.Objects))
  3207  				},
  3208  			},
  3209  		}
  3210  
  3211  		for _, tc := range tca {
  3212  			tc := tc
  3213  			t.Run(tc.name, func(t *testing.T) {
  3214  				t.Parallel()
  3215  				if tc.skip {
  3216  					t.SkipNow()
  3217  				}
  3218  				tc.run(t)
  3219  			})
  3220  		}
  3221  	})
  3222  
  3223  	t.Run("implicit", func(t *testing.T) {
  3224  		t.Parallel()
  3225  
  3226  		tca := []struct {
  3227  			name string
  3228  			skip bool
  3229  			run  func(t *testing.T)
  3230  		}{
  3231  			{
  3232  				name: "delete-connection",
  3233  				run: func(t *testing.T) {
  3234  					g, _ := assertCompile(t, `
  3235  x -> y
  3236  y: null
  3237  `, "")
  3238  					assert.Equal(t, 1, len(g.Objects))
  3239  					assert.Equal(t, 0, len(g.Edges))
  3240  				},
  3241  			},
  3242  			{
  3243  				name: "delete-multiple-connections",
  3244  				run: func(t *testing.T) {
  3245  					g, _ := assertCompile(t, `
  3246  x -> y
  3247  z -> y
  3248  y -> a
  3249  y: null
  3250  `, "")
  3251  					assert.Equal(t, 3, len(g.Objects))
  3252  					assert.Equal(t, 0, len(g.Edges))
  3253  				},
  3254  			},
  3255  			{
  3256  				name: "no-delete-connection",
  3257  				run: func(t *testing.T) {
  3258  					g, _ := assertCompile(t, `
  3259  y: null
  3260  x -> y
  3261  `, "")
  3262  					assert.Equal(t, 2, len(g.Objects))
  3263  					assert.Equal(t, 1, len(g.Edges))
  3264  				},
  3265  			},
  3266  			{
  3267  				name: "delete-children",
  3268  				run: func(t *testing.T) {
  3269  					g, _ := assertCompile(t, `
  3270  x.y.z
  3271  a.b.c
  3272  
  3273  x: null
  3274  a.b: null
  3275  `, "")
  3276  					assert.Equal(t, 1, len(g.Objects))
  3277  				},
  3278  			},
  3279  		}
  3280  
  3281  		for _, tc := range tca {
  3282  			tc := tc
  3283  			t.Run(tc.name, func(t *testing.T) {
  3284  				t.Parallel()
  3285  				if tc.skip {
  3286  					t.SkipNow()
  3287  				}
  3288  				tc.run(t)
  3289  			})
  3290  		}
  3291  	})
  3292  
  3293  	t.Run("multiboard", func(t *testing.T) {
  3294  		t.Parallel()
  3295  
  3296  		tca := []struct {
  3297  			name string
  3298  			skip bool
  3299  			run  func(t *testing.T)
  3300  		}{
  3301  			{
  3302  				name: "scenario",
  3303  				run: func(t *testing.T) {
  3304  					g, _ := assertCompile(t, `
  3305  x
  3306  
  3307  scenarios: {
  3308    a: {
  3309      x: null
  3310    }
  3311  }
  3312  `, "")
  3313  					assert.Equal(t, 0, len(g.Scenarios[0].Objects))
  3314  				},
  3315  			},
  3316  		}
  3317  
  3318  		for _, tc := range tca {
  3319  			tc := tc
  3320  			t.Run(tc.name, func(t *testing.T) {
  3321  				t.Parallel()
  3322  				if tc.skip {
  3323  					t.SkipNow()
  3324  				}
  3325  				tc.run(t)
  3326  			})
  3327  		}
  3328  	})
  3329  }
  3330  
  3331  func testVars(t *testing.T) {
  3332  	t.Parallel()
  3333  
  3334  	t.Run("basic", func(t *testing.T) {
  3335  		t.Parallel()
  3336  
  3337  		tca := []struct {
  3338  			name string
  3339  			skip bool
  3340  			run  func(t *testing.T)
  3341  		}{
  3342  			{
  3343  				name: "shape-label",
  3344  				run: func(t *testing.T) {
  3345  					g, _ := assertCompile(t, `
  3346  vars: {
  3347    x: im a var
  3348  }
  3349  hi: ${x}
  3350  `, "")
  3351  					assert.Equal(t, 1, len(g.Objects))
  3352  					assert.Equal(t, "im a var", g.Objects[0].Label.Value)
  3353  				},
  3354  			},
  3355  			{
  3356  				name: "style",
  3357  				run: func(t *testing.T) {
  3358  					g, _ := assertCompile(t, `
  3359  vars: {
  3360    primary-color: red
  3361  }
  3362  hi: {
  3363    style.fill: ${primary-color}
  3364  }
  3365  `, "")
  3366  					assert.Equal(t, 1, len(g.Objects))
  3367  					assert.Equal(t, "red", g.Objects[0].Style.Fill.Value)
  3368  				},
  3369  			},
  3370  			{
  3371  				name: "number",
  3372  				run: func(t *testing.T) {
  3373  					g, _ := assertCompile(t, `
  3374  vars: {
  3375  	columns: 2
  3376  }
  3377  hi: {
  3378  	grid-columns: ${columns}
  3379  	x
  3380  }
  3381  `, "")
  3382  					assert.Equal(t, "2", g.Objects[0].GridColumns.Value)
  3383  				},
  3384  			},
  3385  			{
  3386  				name: "nested",
  3387  				run: func(t *testing.T) {
  3388  					g, _ := assertCompile(t, `
  3389  vars: {
  3390  	colors: {
  3391      primary: {
  3392        button: red
  3393      }
  3394    }
  3395  }
  3396  hi: {
  3397    style.fill: ${colors.primary.button}
  3398  }
  3399  `, "")
  3400  					assert.Equal(t, "red", g.Objects[0].Style.Fill.Value)
  3401  				},
  3402  			},
  3403  			{
  3404  				name: "combined",
  3405  				run: func(t *testing.T) {
  3406  					g, _ := assertCompile(t, `
  3407  vars: {
  3408    x: im a var
  3409  }
  3410  hi: 1 ${x} 2
  3411  `, "")
  3412  					assert.Equal(t, "1 im a var 2", g.Objects[0].Label.Value)
  3413  				},
  3414  			},
  3415  			{
  3416  				name: "double-quoted",
  3417  				run: func(t *testing.T) {
  3418  					g, _ := assertCompile(t, `
  3419  vars: {
  3420    x: im a var
  3421  }
  3422  hi: "1 ${x} 2"
  3423  `, "")
  3424  					assert.Equal(t, "1 im a var 2", g.Objects[0].Label.Value)
  3425  				},
  3426  			},
  3427  			{
  3428  				name: "single-quoted",
  3429  				run: func(t *testing.T) {
  3430  					g, _ := assertCompile(t, `
  3431  vars: {
  3432    x: im a var
  3433  }
  3434  hi: '1 ${x} 2'
  3435  `, "")
  3436  					assert.Equal(t, "1 ${x} 2", g.Objects[0].Label.Value)
  3437  				},
  3438  			},
  3439  			{
  3440  				name: "edge-label",
  3441  				run: func(t *testing.T) {
  3442  					g, _ := assertCompile(t, `
  3443  vars: {
  3444    x: im a var
  3445  }
  3446  a -> b: ${x}
  3447  `, "")
  3448  					assert.Equal(t, 1, len(g.Edges))
  3449  					assert.Equal(t, "im a var", g.Edges[0].Label.Value)
  3450  				},
  3451  			},
  3452  			{
  3453  				name: "edge-map",
  3454  				run: func(t *testing.T) {
  3455  					g, _ := assertCompile(t, `
  3456  vars: {
  3457    x: im a var
  3458  }
  3459  a -> b: {
  3460    target-arrowhead.label: ${x}
  3461  }
  3462  `, "")
  3463  					assert.Equal(t, 1, len(g.Edges))
  3464  					assert.Equal(t, "im a var", g.Edges[0].DstArrowhead.Label.Value)
  3465  				},
  3466  			},
  3467  			{
  3468  				name: "quoted-var",
  3469  				run: func(t *testing.T) {
  3470  					g, _ := assertCompile(t, `
  3471  vars: {
  3472    primaryColors: {
  3473      button: {
  3474        active: "#4baae5"
  3475      }
  3476    }
  3477  }
  3478  
  3479  button: {
  3480    style: {
  3481      border-radius: 5
  3482      fill: ${primaryColors.button.active}
  3483    }
  3484  }
  3485  `, "")
  3486  					assert.Equal(t, `#4baae5`, g.Objects[0].Style.Fill.Value)
  3487  				},
  3488  			},
  3489  			{
  3490  				name: "quoted-var-quoted-sub",
  3491  				run: func(t *testing.T) {
  3492  					g, _ := assertCompile(t, `
  3493  vars: {
  3494    x: "hi"
  3495  }
  3496  
  3497  y: "hey ${x}"
  3498  `, "")
  3499  					assert.Equal(t, `hey hi`, g.Objects[0].Label.Value)
  3500  				},
  3501  			},
  3502  			{
  3503  				name: "parent-scope",
  3504  				run: func(t *testing.T) {
  3505  					g, _ := assertCompile(t, `
  3506  vars: {
  3507    x: im root var
  3508  }
  3509  a: {
  3510    vars: {
  3511      b: im nested var
  3512    }
  3513    hi: ${x}
  3514  }
  3515  `, "")
  3516  					assert.Equal(t, "im root var", g.Objects[1].Label.Value)
  3517  				},
  3518  			},
  3519  			{
  3520  				name: "map",
  3521  				run: func(t *testing.T) {
  3522  					g, _ := assertCompile(t, `
  3523  vars: {
  3524    cool-style: {
  3525  		fill: red
  3526    }
  3527    arrows: {
  3528      target-arrowhead.label: yay
  3529    }
  3530  }
  3531  hi.style: ${cool-style}
  3532  a -> b: ${arrows}
  3533  `, "")
  3534  					assert.Equal(t, "red", g.Objects[0].Style.Fill.Value)
  3535  					assert.Equal(t, "yay", g.Edges[0].DstArrowhead.Label.Value)
  3536  				},
  3537  			},
  3538  			{
  3539  				name: "primary-and-composite",
  3540  				run: func(t *testing.T) {
  3541  					g, _ := assertCompile(t, `
  3542  vars: {
  3543  	x: all {
  3544  		a: b
  3545    }
  3546  }
  3547  z: ${x}
  3548  `, "")
  3549  					assert.Equal(t, "z", g.Objects[1].ID)
  3550  					assert.Equal(t, "all", g.Objects[1].Label.Value)
  3551  					assert.Equal(t, 1, len(g.Objects[1].Children))
  3552  				},
  3553  			},
  3554  			{
  3555  				name: "spread",
  3556  				run: func(t *testing.T) {
  3557  					g, _ := assertCompile(t, `
  3558  vars: {
  3559  	x: all {
  3560  		a: b
  3561      b: c
  3562    }
  3563  }
  3564  z: {
  3565    ...${x}
  3566    c
  3567  }
  3568  `, "")
  3569  					assert.Equal(t, "z", g.Objects[2].ID)
  3570  					assert.Equal(t, 4, len(g.Objects))
  3571  					assert.Equal(t, 3, len(g.Objects[2].Children))
  3572  				},
  3573  			},
  3574  			{
  3575  				name: "array",
  3576  				run: func(t *testing.T) {
  3577  					g, _ := assertCompile(t, `
  3578  vars: {
  3579  	base-constraints: [UNQ; NOT NULL]
  3580  }
  3581  a: {
  3582    shape: sql_table
  3583  	b: int {constraint: ${base-constraints}}
  3584  }
  3585  `, "")
  3586  					assert.Equal(t, "a", g.Objects[0].ID)
  3587  					assert.Equal(t, 2, len(g.Objects[0].SQLTable.Columns[0].Constraint))
  3588  				},
  3589  			},
  3590  			{
  3591  				name: "spread-array",
  3592  				run: func(t *testing.T) {
  3593  					g, _ := assertCompile(t, `
  3594  vars: {
  3595  	base-constraints: [UNQ; NOT NULL]
  3596  }
  3597  a: {
  3598    shape: sql_table
  3599  	b: int {constraint: [PK; ...${base-constraints}]}
  3600  }
  3601  `, "")
  3602  					assert.Equal(t, "a", g.Objects[0].ID)
  3603  					assert.Equal(t, 3, len(g.Objects[0].SQLTable.Columns[0].Constraint))
  3604  				},
  3605  			},
  3606  			{
  3607  				name: "sub-array",
  3608  				run: func(t *testing.T) {
  3609  					g, _ := assertCompile(t, `
  3610  vars: {
  3611  	x: all
  3612  }
  3613  z.class: [a; ${x}]
  3614  `, "")
  3615  					assert.Equal(t, "z", g.Objects[0].ID)
  3616  					assert.Equal(t, "all", g.Objects[0].Attributes.Classes[1])
  3617  				},
  3618  			},
  3619  			{
  3620  				name: "multi-part-array",
  3621  				run: func(t *testing.T) {
  3622  					g, _ := assertCompile(t, `
  3623  vars: {
  3624  	x: all
  3625  }
  3626  z.class: [a; ${x}together]
  3627  `, "")
  3628  					assert.Equal(t, "z", g.Objects[0].ID)
  3629  					assert.Equal(t, "alltogether", g.Objects[0].Attributes.Classes[1])
  3630  				},
  3631  			},
  3632  			{
  3633  				name: "double-quote-primary",
  3634  				run: func(t *testing.T) {
  3635  					g, _ := assertCompile(t, `
  3636  vars: {
  3637  	x: always {
  3638      a: b
  3639    }
  3640  }
  3641  z: "${x} be my maybe"
  3642  `, "")
  3643  					assert.Equal(t, "z", g.Objects[0].ID)
  3644  					assert.Equal(t, "always be my maybe", g.Objects[0].Label.Value)
  3645  				},
  3646  			},
  3647  			{
  3648  				name: "spread-nested",
  3649  				run: func(t *testing.T) {
  3650  					g, _ := assertCompile(t, `
  3651  vars: {
  3652    disclaimer: {
  3653      I am not a lawyer
  3654    }
  3655  }
  3656  custom-disclaimer: DRAFT DISCLAIMER {
  3657    ...${disclaimer}
  3658  }
  3659  `, "")
  3660  					assert.Equal(t, 2, len(g.Objects))
  3661  				},
  3662  			},
  3663  			{
  3664  				name: "spread-edge",
  3665  				run: func(t *testing.T) {
  3666  					g, _ := assertCompile(t, `
  3667  vars: {
  3668    connections: {
  3669      x -> a
  3670    }
  3671  }
  3672  hi: {
  3673    ...${connections}
  3674  }
  3675  `, "")
  3676  					assert.Equal(t, 3, len(g.Objects))
  3677  					assert.Equal(t, 1, len(g.Edges))
  3678  				},
  3679  			},
  3680  		}
  3681  
  3682  		for _, tc := range tca {
  3683  			tc := tc
  3684  			t.Run(tc.name, func(t *testing.T) {
  3685  				t.Parallel()
  3686  				if tc.skip {
  3687  					t.SkipNow()
  3688  				}
  3689  				tc.run(t)
  3690  			})
  3691  		}
  3692  	})
  3693  
  3694  	t.Run("override", func(t *testing.T) {
  3695  		t.Parallel()
  3696  
  3697  		tca := []struct {
  3698  			name string
  3699  			skip bool
  3700  			run  func(t *testing.T)
  3701  		}{
  3702  			{
  3703  				name: "label",
  3704  				run: func(t *testing.T) {
  3705  					g, _ := assertCompile(t, `
  3706  vars: {
  3707    x: im a var
  3708  }
  3709  hi: ${x}
  3710  hi: not a var
  3711  `, "")
  3712  					assert.Equal(t, 1, len(g.Objects))
  3713  					assert.Equal(t, "not a var", g.Objects[0].Label.Value)
  3714  				},
  3715  			},
  3716  			{
  3717  				name: "map",
  3718  				run: func(t *testing.T) {
  3719  					g, _ := assertCompile(t, `
  3720  vars: {
  3721    x: im root var
  3722  }
  3723  a: {
  3724    vars: {
  3725      x: im nested var
  3726    }
  3727    hi: ${x}
  3728  }
  3729  `, "")
  3730  					assert.Equal(t, "im nested var", g.Objects[1].Label.Value)
  3731  				},
  3732  			},
  3733  			{
  3734  				name: "var-in-var",
  3735  				run: func(t *testing.T) {
  3736  					g, _ := assertCompile(t, `
  3737  vars: {
  3738  	surname: Smith
  3739  }
  3740  a: {
  3741    vars: {
  3742  		trade1: Black${surname}
  3743  		trade2: Metal${surname}
  3744    }
  3745    hi: ${trade1}
  3746  }
  3747  `, "")
  3748  					assert.Equal(t, "BlackSmith", g.Objects[1].Label.Value)
  3749  				},
  3750  			},
  3751  			{
  3752  				name: "recursive-var",
  3753  				run: func(t *testing.T) {
  3754  					g, _ := assertCompile(t, `
  3755  vars: {
  3756    x: a
  3757  }
  3758  hi: {
  3759    vars: {
  3760      x: ${x}-b
  3761    }
  3762    yo: ${x}
  3763  }
  3764  `, "")
  3765  					assert.Equal(t, "a-b", g.Objects[1].Label.Value)
  3766  				},
  3767  			},
  3768  			{
  3769  				name: "null",
  3770  				run: func(t *testing.T) {
  3771  					assertCompile(t, `
  3772  vars: {
  3773  	surname: Smith
  3774  }
  3775  a: {
  3776    vars: {
  3777  		surname: null
  3778    }
  3779    hi: John ${surname}
  3780  }
  3781  `, `d2/testdata/d2compiler/TestCompile2/vars/override/null.d2:9:3: could not resolve variable "surname"`)
  3782  				},
  3783  			},
  3784  			{
  3785  				name: "nested-null",
  3786  				run: func(t *testing.T) {
  3787  					assertCompile(t, `
  3788  vars: {
  3789  	surnames: {
  3790      john: smith
  3791    }
  3792  }
  3793  a: {
  3794    vars: {
  3795  		surnames: {
  3796        john: null
  3797      }
  3798    }
  3799    hi: John ${surname}
  3800  }
  3801  `, `d2/testdata/d2compiler/TestCompile2/vars/override/nested-null.d2:13:3: could not resolve variable "surname"`)
  3802  				},
  3803  			},
  3804  		}
  3805  
  3806  		for _, tc := range tca {
  3807  			tc := tc
  3808  			t.Run(tc.name, func(t *testing.T) {
  3809  				t.Parallel()
  3810  				if tc.skip {
  3811  					t.SkipNow()
  3812  				}
  3813  				tc.run(t)
  3814  			})
  3815  		}
  3816  	})
  3817  
  3818  	t.Run("boards", func(t *testing.T) {
  3819  		t.Parallel()
  3820  
  3821  		tca := []struct {
  3822  			name string
  3823  			skip bool
  3824  			run  func(t *testing.T)
  3825  		}{
  3826  			{
  3827  				name: "layer",
  3828  				run: func(t *testing.T) {
  3829  					g, _ := assertCompile(t, `
  3830  vars: {
  3831    x: im a var
  3832  }
  3833  
  3834  layers: {
  3835    l: {
  3836      hi: ${x}
  3837    }
  3838  }
  3839  `, "")
  3840  					assert.Equal(t, 1, len(g.Layers[0].Objects))
  3841  					assert.Equal(t, "im a var", g.Layers[0].Objects[0].Label.Value)
  3842  				},
  3843  			},
  3844  			{
  3845  				name: "layer-2",
  3846  				run: func(t *testing.T) {
  3847  					g, _ := assertCompile(t, `
  3848  vars: {
  3849    x: root var x
  3850    y: root var y
  3851  }
  3852  
  3853  layers: {
  3854    l: {
  3855      vars: {
  3856        x: layer var x
  3857      }
  3858      hi: ${x}
  3859      hello: ${y}
  3860    }
  3861  }
  3862  `, "")
  3863  					assert.Equal(t, "hi", g.Layers[0].Objects[0].ID)
  3864  					assert.Equal(t, "layer var x", g.Layers[0].Objects[0].Label.Value)
  3865  					assert.Equal(t, "hello", g.Layers[0].Objects[1].ID)
  3866  					assert.Equal(t, "root var y", g.Layers[0].Objects[1].Label.Value)
  3867  				},
  3868  			},
  3869  			{
  3870  				name: "scenario",
  3871  				run: func(t *testing.T) {
  3872  					g, _ := assertCompile(t, `
  3873  vars: {
  3874    x: im a var
  3875  }
  3876  
  3877  scenarios: {
  3878    l: {
  3879      hi: ${x}
  3880    }
  3881  }
  3882  `, "")
  3883  					assert.Equal(t, 1, len(g.Scenarios[0].Objects))
  3884  					assert.Equal(t, "im a var", g.Scenarios[0].Objects[0].Label.Value)
  3885  				},
  3886  			},
  3887  			{
  3888  				name: "overlay",
  3889  				run: func(t *testing.T) {
  3890  					g, _ := assertCompile(t, `
  3891  vars: {
  3892    x: im x var
  3893  }
  3894  
  3895  scenarios: {
  3896    l: {
  3897      vars: {
  3898        y: im y var
  3899      }
  3900      x: ${x}
  3901      y: ${y}
  3902    }
  3903  }
  3904  layers: {
  3905    l2: {
  3906      vars: {
  3907        y: im y var
  3908      }
  3909      x: ${x}
  3910      y: ${y}
  3911    }
  3912  }
  3913  `, "")
  3914  					assert.Equal(t, 2, len(g.Scenarios[0].Objects))
  3915  					assert.Equal(t, "im x var", g.Scenarios[0].Objects[0].Label.Value)
  3916  					assert.Equal(t, "im y var", g.Scenarios[0].Objects[1].Label.Value)
  3917  					assert.Equal(t, 2, len(g.Layers[0].Objects))
  3918  					assert.Equal(t, "im x var", g.Layers[0].Objects[0].Label.Value)
  3919  					assert.Equal(t, "im y var", g.Layers[0].Objects[1].Label.Value)
  3920  				},
  3921  			},
  3922  			{
  3923  				name: "replace",
  3924  				run: func(t *testing.T) {
  3925  					g, _ := assertCompile(t, `
  3926  vars: {
  3927    x: im x var
  3928  }
  3929  
  3930  scenarios: {
  3931    l: {
  3932      vars: {
  3933        x: im replaced x var
  3934      }
  3935      x: ${x}
  3936    }
  3937  }
  3938  `, "")
  3939  					assert.Equal(t, 1, len(g.Scenarios[0].Objects))
  3940  					assert.Equal(t, "im replaced x var", g.Scenarios[0].Objects[0].Label.Value)
  3941  				},
  3942  			},
  3943  		}
  3944  
  3945  		for _, tc := range tca {
  3946  			tc := tc
  3947  			t.Run(tc.name, func(t *testing.T) {
  3948  				t.Parallel()
  3949  				if tc.skip {
  3950  					t.SkipNow()
  3951  				}
  3952  				tc.run(t)
  3953  			})
  3954  		}
  3955  	})
  3956  
  3957  	t.Run("config", func(t *testing.T) {
  3958  		t.Parallel()
  3959  
  3960  		tca := []struct {
  3961  			name string
  3962  			skip bool
  3963  			run  func(t *testing.T)
  3964  		}{
  3965  			{
  3966  				name: "basic",
  3967  				run: func(t *testing.T) {
  3968  					_, config := assertCompile(t, `
  3969  vars: {
  3970  	d2-config: {
  3971      sketch: true
  3972    }
  3973  }
  3974  
  3975  x -> y
  3976  `, "")
  3977  					assert.Equal(t, true, *config.Sketch)
  3978  				},
  3979  			},
  3980  			{
  3981  				name: "invalid",
  3982  				run: func(t *testing.T) {
  3983  					assertCompile(t, `
  3984  vars: {
  3985  	d2-config: {
  3986      sketch: lol
  3987    }
  3988  }
  3989  
  3990  x -> y
  3991  `, `d2/testdata/d2compiler/TestCompile2/vars/config/invalid.d2:4:5: expected a boolean for "sketch", got "lol"`)
  3992  				},
  3993  			},
  3994  			{
  3995  				name: "not-root",
  3996  				run: func(t *testing.T) {
  3997  					assertCompile(t, `
  3998  x: {
  3999    vars: {
  4000    	d2-config: {
  4001        sketch: false
  4002      }
  4003    }
  4004  }
  4005  `, `d2/testdata/d2compiler/TestCompile2/vars/config/not-root.d2:4:4: "d2-config" can only appear at root vars`)
  4006  				},
  4007  			},
  4008  		}
  4009  
  4010  		for _, tc := range tca {
  4011  			tc := tc
  4012  			t.Run(tc.name, func(t *testing.T) {
  4013  				t.Parallel()
  4014  				if tc.skip {
  4015  					t.SkipNow()
  4016  				}
  4017  				tc.run(t)
  4018  			})
  4019  		}
  4020  	})
  4021  
  4022  	t.Run("errors", func(t *testing.T) {
  4023  		t.Parallel()
  4024  
  4025  		tca := []struct {
  4026  			name string
  4027  			skip bool
  4028  			run  func(t *testing.T)
  4029  		}{
  4030  			{
  4031  				name: "missing",
  4032  				run: func(t *testing.T) {
  4033  					assertCompile(t, `
  4034  vars: {
  4035    x: hey
  4036  }
  4037  hi: ${z}
  4038  `, `d2/testdata/d2compiler/TestCompile2/vars/errors/missing.d2:5:1: could not resolve variable "z"`)
  4039  				},
  4040  			},
  4041  			{
  4042  				name: "multi-part-map",
  4043  				run: func(t *testing.T) {
  4044  					assertCompile(t, `
  4045  vars: {
  4046    x: {
  4047      a: b
  4048    }
  4049  }
  4050  hi: 1 ${x}
  4051  `, `d2/testdata/d2compiler/TestCompile2/vars/errors/multi-part-map.d2:7:1: cannot substitute composite variable "x" as part of a string`)
  4052  				},
  4053  			},
  4054  			{
  4055  				name: "quoted-map",
  4056  				run: func(t *testing.T) {
  4057  					assertCompile(t, `
  4058  vars: {
  4059    x: {
  4060      a: b
  4061    }
  4062  }
  4063  hi: "${x}"
  4064  `, `d2/testdata/d2compiler/TestCompile2/vars/errors/quoted-map.d2:7:1: cannot substitute map variable "x" in quotes`)
  4065  				},
  4066  			},
  4067  			{
  4068  				name: "nested-missing",
  4069  				run: func(t *testing.T) {
  4070  					assertCompile(t, `
  4071  vars: {
  4072    x: {
  4073      y: hey
  4074    }
  4075  }
  4076  hi: ${x.z}
  4077  `, `d2/testdata/d2compiler/TestCompile2/vars/errors/nested-missing.d2:7:1: could not resolve variable "x.z"`)
  4078  				},
  4079  			},
  4080  			{
  4081  				name: "out-of-scope",
  4082  				run: func(t *testing.T) {
  4083  					assertCompile(t, `
  4084  a: {
  4085    vars: {
  4086      x: hey
  4087    }
  4088  }
  4089  hi: ${x}
  4090  `, `d2/testdata/d2compiler/TestCompile2/vars/errors/out-of-scope.d2:7:1: could not resolve variable "x"`)
  4091  				},
  4092  			},
  4093  			{
  4094  				name: "recursive-var",
  4095  				run: func(t *testing.T) {
  4096  					assertCompile(t, `
  4097  vars: {
  4098    x: ${x}
  4099  }
  4100  hi: ${x}
  4101  `, `d2/testdata/d2compiler/TestCompile2/vars/errors/recursive-var.d2:3:3: could not resolve variable "x"`)
  4102  				},
  4103  			},
  4104  
  4105  			{
  4106  				name: "spread-non-map",
  4107  				run: func(t *testing.T) {
  4108  					assertCompile(t, `
  4109  vars: {
  4110  	x: all
  4111  }
  4112  z: {
  4113    ...${x}
  4114    c
  4115  }
  4116  `, `d2/testdata/d2compiler/TestCompile2/vars/errors/spread-non-map.d2:6:3: cannot spread non-composite`)
  4117  				},
  4118  			},
  4119  			{
  4120  				name: "missing-array",
  4121  				run: func(t *testing.T) {
  4122  					assertCompile(t, `
  4123  vars: {
  4124  	x: b
  4125  }
  4126  z: {
  4127    class: [...${a}]
  4128  }
  4129  `, `d2/testdata/d2compiler/TestCompile2/vars/errors/missing-array.d2:6:3: could not resolve variable "a"`)
  4130  				},
  4131  			},
  4132  			{
  4133  				name: "spread-non-array",
  4134  				run: func(t *testing.T) {
  4135  					assertCompile(t, `
  4136  vars: {
  4137  	x: {
  4138      a: b
  4139    }
  4140  }
  4141  z: {
  4142    class: [...${x}]
  4143  }
  4144  `, `d2/testdata/d2compiler/TestCompile2/vars/errors/spread-non-array.d2:8:11: cannot spread non-array into array`)
  4145  				},
  4146  			},
  4147  			{
  4148  				name: "spread-non-solo",
  4149  				// NOTE: this doesn't get parsed correctly and so the error message isn't exactly right, but the important thing is that it errors
  4150  				run: func(t *testing.T) {
  4151  					assertCompile(t, `
  4152  vars: {
  4153  	x: {
  4154      a: b
  4155    }
  4156  }
  4157  z: {
  4158  	d: ...${x}
  4159    c
  4160  }
  4161  `, `d2/testdata/d2compiler/TestCompile2/vars/errors/spread-non-solo.d2:8:2: cannot substitute composite variable "x" as part of a string`)
  4162  				},
  4163  			},
  4164  			{
  4165  				name: "spread-mid-string",
  4166  				run: func(t *testing.T) {
  4167  					assertCompile(t, `
  4168  vars: {
  4169    test: hello
  4170  }
  4171  
  4172  mybox: {
  4173    label: prefix${test}suffix
  4174  }
  4175  `, "")
  4176  				},
  4177  			},
  4178  			{
  4179  				name: "undeclared-var-usage",
  4180  				run: func(t *testing.T) {
  4181  					assertCompile(t, `
  4182  x: { ...${v} }
  4183  `, `d2/testdata/d2compiler/TestCompile2/vars/errors/undeclared-var-usage.d2:2:4: could not resolve variable "v"`)
  4184  				},
  4185  			},
  4186  			{
  4187  				name: "split-var-usage",
  4188  				run: func(t *testing.T) {
  4189  					assertCompile(t, `
  4190  x1
  4191  
  4192  vars: {
  4193    v: {
  4194      style.fill: green
  4195    }
  4196  }
  4197  
  4198  x1: { ...${v} }
  4199  `, ``)
  4200  				},
  4201  			},
  4202  		}
  4203  
  4204  		for _, tc := range tca {
  4205  			tc := tc
  4206  			t.Run(tc.name, func(t *testing.T) {
  4207  				t.Parallel()
  4208  				if tc.skip {
  4209  					t.SkipNow()
  4210  				}
  4211  				tc.run(t)
  4212  			})
  4213  		}
  4214  	})
  4215  }
  4216  
  4217  func testGlobs(t *testing.T) {
  4218  	t.Parallel()
  4219  
  4220  	tca := []struct {
  4221  		name string
  4222  		skip bool
  4223  		run  func(t *testing.T)
  4224  	}{
  4225  		{
  4226  			name: "alixander-lazy-globs-review/1",
  4227  			run: func(t *testing.T) {
  4228  				assertCompile(t, `
  4229  ***.style.fill: yellow
  4230  **.shape: circle
  4231  *.style.multiple: true
  4232  
  4233  x: {
  4234    y
  4235  }
  4236  
  4237  layers: {
  4238    next: {
  4239      a
  4240    }
  4241  }
  4242  `, "")
  4243  			},
  4244  		},
  4245  		{
  4246  			name: "alixander-lazy-globs-review/2",
  4247  			run: func(t *testing.T) {
  4248  				assertCompile(t, `
  4249  **.style.fill: yellow
  4250  
  4251  scenarios: {
  4252    b: {
  4253      a -> b
  4254    }
  4255  }
  4256  `, "")
  4257  			},
  4258  		},
  4259  		{
  4260  			name: "alixander-lazy-globs-review/3",
  4261  			run: func(t *testing.T) {
  4262  				assertCompile(t, `
  4263  ***: {
  4264    c: d
  4265  }
  4266  
  4267  ***: {
  4268    style.fill: red
  4269  }
  4270  
  4271  table: {
  4272    shape: sql_table
  4273    a: b
  4274  }
  4275  
  4276  class: {
  4277    shape: class
  4278    a: b
  4279  }
  4280  `, "")
  4281  			},
  4282  		},
  4283  		{
  4284  			name: "double-glob-err-val",
  4285  			run: func(t *testing.T) {
  4286  				assertCompile(t, `
  4287  **: {
  4288    label: hi
  4289    label.near: center
  4290  }
  4291  
  4292  x: {
  4293    a -> b
  4294  }
  4295  `, `d2/testdata/d2compiler/TestCompile2/globs/double-glob-err-val.d2:4:3: invalid "near" field`)
  4296  			},
  4297  		},
  4298  		{
  4299  			name: "double-glob-override-err-val",
  4300  			run: func(t *testing.T) {
  4301  				assertCompile(t, `
  4302  (** -> **)[*]: {
  4303  	label.near: top-center
  4304  }
  4305  (** -> **)[*]: {
  4306  	label.near: invalid
  4307  }
  4308  
  4309  x: {
  4310    a -> b
  4311  }
  4312  `, `d2/testdata/d2compiler/TestCompile2/globs/double-glob-override-err-val.d2:6:2: invalid "near" field`)
  4313  			},
  4314  		},
  4315  		{
  4316  			name: "creating-node-bug",
  4317  			run: func(t *testing.T) {
  4318  				g, _ := assertCompile(t, `
  4319  *.*a -> *.*b
  4320  
  4321  container_1: {
  4322  	a
  4323  }
  4324  
  4325  container_2: {
  4326  	b
  4327  }
  4328  `, ``)
  4329  				assert.Equal(t, 4, len(g.Objects))
  4330  			},
  4331  		},
  4332  	}
  4333  
  4334  	for _, tc := range tca {
  4335  		tc := tc
  4336  		t.Run(tc.name, func(t *testing.T) {
  4337  			t.Parallel()
  4338  			if tc.skip {
  4339  				t.SkipNow()
  4340  			}
  4341  			tc.run(t)
  4342  		})
  4343  	}
  4344  }
  4345  
  4346  func assertCompile(t *testing.T, text string, expErr string) (*d2graph.Graph, *d2target.Config) {
  4347  	d2Path := fmt.Sprintf("d2/testdata/d2compiler/%v.d2", t.Name())
  4348  	g, config, err := d2compiler.Compile(d2Path, strings.NewReader(text), nil)
  4349  	if expErr != "" {
  4350  		assert.Error(t, err)
  4351  		assert.ErrorString(t, err, expErr)
  4352  	} else {
  4353  		assert.Success(t, err)
  4354  	}
  4355  
  4356  	got := struct {
  4357  		Graph *d2graph.Graph `json:"graph"`
  4358  		Err   error          `json:"err"`
  4359  	}{
  4360  		Graph: g,
  4361  		Err:   err,
  4362  	}
  4363  
  4364  	err = diff.TestdataJSON(filepath.Join("..", "testdata", "d2compiler", t.Name()), got)
  4365  	assert.Success(t, err)
  4366  	return g, config
  4367  }
  4368  

View as plain text