d2compiler: Integrate d2ir (wip)

This commit is contained in:
Anmol Sethi 2023-01-23 22:45:21 -08:00
parent 5af31670d1
commit 7d011bab47
No known key found for this signature in database
GPG key ID: 25BC68888A99A8BA
4 changed files with 98 additions and 75 deletions

View file

@ -40,60 +40,62 @@ func Compile(path string, r io.RuneReader, opts *CompileOptions) (*d2graph.Graph
return nil, err return nil, err
} }
g, err := compileIR(pe, ir.CopyBase(nil)) g, err := compileIR(pe, ir)
if err != nil { if err != nil {
return nil, err return nil, err
} }
g.AST = ast g.AST = ast
err = compileLayersField(pe, g, ir, "layers")
if err != nil {
return nil, err
}
err = compileLayersField(pe, g, ir, "scenarios")
if err != nil {
return nil, err
}
err = compileLayersField(pe, g, ir, "steps")
return g, err return g, err
} }
func compileLayersField(pe d2parser.ParseError, g *d2graph.Graph, ir *d2ir.Map, fieldName string) error {
layers := ir.GetField(fieldName)
if layers.Map() == nil {
return nil
}
for _, f := range layers.Map().Fields {
if f.Map() == nil {
continue
}
g2, err := compileIR(pe, f.Map())
if err != nil {
return err
}
g2.Name = f.Name
g.Layers = append(g.Layers, g2)
}
return nil
}
func compileIR(pe d2parser.ParseError, m *d2ir.Map) (*d2graph.Graph, error) { func compileIR(pe d2parser.ParseError, m *d2ir.Map) (*d2graph.Graph, error) {
g := d2graph.NewGraph()
c := &compiler{ c := &compiler{
err: pe, err: pe,
} }
g := c.compileLayer(m)
if len(c.err.Errors) > 0 {
return nil, c.err
}
return g, nil
}
func (c *compiler) compileLayer(ir *d2ir.Map) *d2graph.Graph {
g := d2graph.NewGraph()
m := ir.CopyRoot()
c.compileMap(g.Root, m) c.compileMap(g.Root, m)
if len(c.err.Errors) == 0 { if len(c.err.Errors) == 0 {
c.validateKeys(g.Root, m) c.validateKeys(g.Root, m)
} }
c.validateNear(g) c.validateNear(g)
if len(c.err.Errors) > 0 { c.compileLayersField(g, ir, "layers")
return nil, c.err c.compileLayersField(g, ir, "scenarios")
c.compileLayersField(g, ir, "steps")
return g
}
func (c *compiler) compileLayersField(g *d2graph.Graph, ir *d2ir.Map, fieldName string) {
layers := ir.GetField(fieldName)
if layers.Map() == nil {
return
}
for _, f := range layers.Map().Fields {
if f.Map() == nil {
continue
}
g2 := c.compileLayer(f.Map())
g2.Name = f.Name
switch fieldName {
case "layers":
g.Layers = append(g.Layers, g2)
case "scenarios":
g.Scenarios = append(g.Scenarios, g2)
case "steps":
g.Steps = append(g.Steps, g2)
}
} }
return g, nil
} }
type compiler struct { type compiler struct {
@ -139,6 +141,9 @@ func (c *compiler) compileField(obj *d2graph.Object, f *d2ir.Field) {
return return
} }
c.compileStyle(obj.Attributes, f.Map()) c.compileStyle(obj.Attributes, f.Map())
if obj.Attributes.Style.Animated != nil {
c.errorf(obj.Attributes.Style.Animated.MapKey, `key "animated" can only be applied to edges`)
}
return return
} }
@ -204,7 +209,8 @@ func (c *compiler) compileReserved(attrs *d2graph.Attributes, f *d2ir.Field) {
c.compileLabel(attrs, f) c.compileLabel(attrs, f)
case "shape": case "shape":
in := d2target.IsShape(scalar.ScalarString()) in := d2target.IsShape(scalar.ScalarString())
if !in { _, isArrowhead := d2target.Arrowheads[scalar.ScalarString()]
if !in && !isArrowhead {
c.errorf(scalar, "unknown shape %q", scalar.ScalarString()) c.errorf(scalar, "unknown shape %q", scalar.ScalarString())
return return
} }
@ -227,6 +233,7 @@ func (c *compiler) compileReserved(attrs *d2graph.Attributes, f *d2ir.Field) {
c.errorf(scalar, "bad near key %#v: %s", scalar.ScalarString(), err) c.errorf(scalar, "bad near key %#v: %s", scalar.ScalarString(), err)
return return
} }
nearKey.Range = scalar.GetRange()
attrs.NearKey = nearKey attrs.NearKey = nearKey
case "tooltip": case "tooltip":
attrs.Tooltip = scalar.ScalarString() attrs.Tooltip = scalar.ScalarString()
@ -378,10 +385,6 @@ func (c *compiler) compileEdgeField(edge *d2graph.Edge, f *d2ir.Field) {
return return
} }
if f.Primary() != nil {
c.compileLabel(edge.Attributes, f)
}
if f.Name == "source-arrowhead" || f.Name == "target-arrowhead" { if f.Name == "source-arrowhead" || f.Name == "target-arrowhead" {
if f.Map() != nil { if f.Map() != nil {
c.compileArrowheads(edge, f) c.compileArrowheads(edge, f)
@ -399,6 +402,10 @@ func (c *compiler) compileArrowheads(edge *d2graph.Edge, f *d2ir.Field) {
attrs = edge.DstArrowhead attrs = edge.DstArrowhead
} }
if f.Primary() != nil {
c.compileLabel(attrs, f)
}
for _, f2 := range f.Map().Fields { for _, f2 := range f.Map().Fields {
keyword := strings.ToLower(f2.Name) keyword := strings.ToLower(f2.Name)
_, isReserved := d2graph.SimpleReservedKeywords[keyword] _, isReserved := d2graph.SimpleReservedKeywords[keyword]
@ -552,10 +559,6 @@ func (c *compiler) validateKey(obj *d2graph.Object, f *d2ir.Field) {
if obj.Attributes.Shape.Value != d2target.ShapeImage { if obj.Attributes.Shape.Value != d2target.ShapeImage {
c.errorf(f.LastPrimaryKey(), "height is only applicable to image shapes.") c.errorf(f.LastPrimaryKey(), "height is only applicable to image shapes.")
} }
case "3d":
if obj.Attributes.Shape.Value != "" && !strings.EqualFold(obj.Attributes.Shape.Value, d2target.ShapeSquare) && !strings.EqualFold(obj.Attributes.Shape.Value, d2target.ShapeRectangle) {
c.errorf(f.LastPrimaryKey(), `key "3d" can only be applied to squares and rectangles`)
}
case "shape": case "shape":
switch obj.Attributes.Shape.Value { switch obj.Attributes.Shape.Value {
case d2target.ShapeSQLTable, d2target.ShapeClass: case d2target.ShapeSQLTable, d2target.ShapeClass:
@ -578,8 +581,14 @@ func (c *compiler) validateKey(obj *d2graph.Object, f *d2ir.Field) {
return return
} }
if obj.Attributes.Style.ThreeDee != nil {
if !strings.EqualFold(obj.Attributes.Shape.Value, d2target.ShapeSquare) && !strings.EqualFold(obj.Attributes.Shape.Value, d2target.ShapeRectangle) {
c.errorf(obj.Attributes.Style.ThreeDee.MapKey, `key "3d" can only be applied to squares and rectangles`)
}
}
if obj.Attributes.Shape.Value == d2target.ShapeImage { if obj.Attributes.Shape.Value == d2target.ShapeImage {
c.errorf(obj.Attributes.Shape.MapKey, "image shapes cannot have children.") c.errorf(f.LastRef().AST(), "image shapes cannot have children.")
return return
} }

View file

@ -263,7 +263,7 @@ d2/testdata/d2compiler/TestCompile/no_dimensions_on_containers.d2:37:3: height c
_ _
} }
`, `,
expErr: `d2/testdata/d2compiler/TestCompile/blank_underscore.d2:3:3: invalid use of parent "_"`, expErr: `d2/testdata/d2compiler/TestCompile/blank_underscore.d2:3:3: field key must contain more than underscores`,
}, },
{ {
name: "image_non_style", name: "image_non_style",
@ -451,7 +451,7 @@ x: {
text: ` text: `
_.x _.x
`, `,
expErr: `d2/testdata/d2compiler/TestCompile/underscore_parent_root.d2:2:1: parent "_" cannot be used in the root scope`, expErr: `d2/testdata/d2compiler/TestCompile/underscore_parent_root.d2:2:1: invalid underscore: no parent`,
}, },
{ {
name: "underscore_parent_middle_path", name: "underscore_parent_middle_path",
@ -461,7 +461,7 @@ x: {
y._.z y._.z
} }
`, `,
expErr: `d2/testdata/d2compiler/TestCompile/underscore_parent_middle_path.d2:3:3: parent "_" can only be used in the beginning of paths, e.g. "_.x"`, 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"`,
}, },
{ {
name: "underscore_parent_sandwich_path", name: "underscore_parent_sandwich_path",
@ -471,7 +471,7 @@ x: {
_.z._ _.z._
} }
`, `,
expErr: `d2/testdata/d2compiler/TestCompile/underscore_parent_sandwich_path.d2:3:3: parent "_" can only be used in the beginning of paths, e.g. "_.x"`, 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"`,
}, },
{ {
name: "underscore_edge", name: "underscore_edge",
@ -1074,7 +1074,7 @@ x -> y: {
space -> stars space -> stars
} }
`, `,
expErr: `d2/testdata/d2compiler/TestCompile/nested_edge.d2:1:1: edges cannot be nested within another edge`, expErr: `d2/testdata/d2compiler/TestCompile/nested_edge.d2:2:3: cannot create edge inside edge`,
}, },
{ {
name: "shape_edge_style", name: "shape_edge_style",
@ -1340,7 +1340,7 @@ x -> y: {
z z
} }
`, `,
expErr: `d2/testdata/d2compiler/TestCompile/edge_map_non_reserved.d2:2:1: edge map keys must be reserved keywords`, expErr: `d2/testdata/d2compiler/TestCompile/edge_map_non_reserved.d2:3:3: edge map keys must be reserved keywords`,
}, },
{ {
name: "url_link", name: "url_link",
@ -1385,7 +1385,7 @@ x -> y: {
text: `x.near: txop-center text: `x.near: txop-center
`, `,
expErr: `d2/testdata/d2compiler/TestCompile/near_bad_constant.d2:1:1: 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`, 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`,
}, },
{ {
name: "near_bad_container", name: "near_bad_container",
@ -1395,7 +1395,7 @@ x -> y: {
y y
} }
`, `,
expErr: `d2/testdata/d2compiler/TestCompile/near_bad_container.d2:1:1: constant near keys cannot be set on shapes with children`, expErr: `d2/testdata/d2compiler/TestCompile/near_bad_container.d2:2:9: constant near keys cannot be set on shapes with children`,
}, },
{ {
name: "near_bad_connected", name: "near_bad_connected",
@ -1405,14 +1405,14 @@ x -> y: {
} }
x -> y x -> y
`, `,
expErr: `d2/testdata/d2compiler/TestCompile/near_bad_connected.d2:1:1: constant near keys cannot be set on connected shapes`, expErr: `d2/testdata/d2compiler/TestCompile/near_bad_connected.d2:2:9: constant near keys cannot be set on connected shapes`,
}, },
{ {
name: "nested_near_constant", name: "nested_near_constant",
text: `x.y.near: top-center text: `x.y.near: top-center
`, `,
expErr: `d2/testdata/d2compiler/TestCompile/nested_near_constant.d2:1:1: constant near keys can only be set on root level shapes`, expErr: `d2/testdata/d2compiler/TestCompile/nested_near_constant.d2:1:11: constant near keys can only be set on root level shapes`,
}, },
{ {
name: "reserved_icon_near_style", name: "reserved_icon_near_style",
@ -1458,9 +1458,8 @@ y
} }
`, `,
expErr: `d2/testdata/d2compiler/TestCompile/errors/reserved_icon_style.d2:3:9: bad icon url "::????:::%%orange": parse "::????:::%%orange": missing protocol scheme expErr: `d2/testdata/d2compiler/TestCompile/errors/reserved_icon_style.d2:3:9: bad icon url "::????:::%%orange": parse "::????:::%%orange": missing protocol scheme
d2/testdata/d2compiler/TestCompile/errors/reserved_icon_style.d2:4:18: expected "opacity" to be a number between 0.0 and 1.0
d2/testdata/d2compiler/TestCompile/errors/reserved_icon_style.d2:5:18: expected "opacity" to be a number between 0.0 and 1.0 d2/testdata/d2compiler/TestCompile/errors/reserved_icon_style.d2:5:18: expected "opacity" to be a number between 0.0 and 1.0
d2/testdata/d2compiler/TestCompile/errors/reserved_icon_style.d2:1:1: 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`, 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`,
}, },
{ {
name: "errors/missing_shape_icon", name: "errors/missing_shape_icon",
@ -1562,7 +1561,7 @@ b`, g.Objects[0].Attributes.Label.Value)
GetType(): string GetType(): string
style: { style: {
opacity: 0.4 opacity: 0.4
color: blue font-color: blue
} }
} }
`, `,
@ -1661,7 +1660,7 @@ x.y -> a.b: {
{ {
name: "3d_oval", name: "3d_oval",
text: `SVP1.style.shape: oval text: `SVP1.shape: oval
SVP1.style.3d: true`, SVP1.style.3d: true`,
expErr: `d2/testdata/d2compiler/TestCompile/3d_oval.d2:2:1: key "3d" can only be applied to squares and rectangles`, expErr: `d2/testdata/d2compiler/TestCompile/3d_oval.d2:2:1: key "3d" can only be applied to squares and rectangles`,
}, { }, {

View file

@ -15,17 +15,9 @@ func (c *compiler) errorf(n d2ast.Node, f string, v ...interface{}) {
func Compile(ast *d2ast.Map) (*Map, error) { func Compile(ast *d2ast.Map) (*Map, error) {
c := &compiler{} c := &compiler{}
m := &Map{ m := &Map{}
parent: &Field{ m.initRoot()
Name: "", m.parent.(*Field).References[0].Context.Scope = ast
References: []*FieldReference{{
Context: &RefContext{
Scope: ast,
},
}},
},
}
m.parent.(*Field).References[0].Context.ScopeMap = m
c.compileMap(m, ast) c.compileMap(m, ast)
c.compileScenarios(m) c.compileScenarios(m)
c.compileSteps(m) c.compileSteps(m)

View file

@ -152,6 +152,17 @@ type Map struct {
Edges []*Edge `json:"edges"` Edges []*Edge `json:"edges"`
} }
func (m *Map) initRoot() {
m.parent = &Field{
Name: "",
References: []*FieldReference{{
Context: &RefContext{
ScopeMap: m,
},
}},
}
}
func (m *Map) Copy(newParent Node) Node { func (m *Map) Copy(newParent Node) Node {
tmp := *m tmp := *m
m = &tmp m = &tmp
@ -187,6 +198,13 @@ func (m *Map) CopyBase(newParent Node) *Map {
return m2 return m2
} }
// CopyRoot copies the map such that it is now the root of a diagram.
func (m *Map) CopyRoot() *Map {
m = m.CopyBase(nil)
m.initRoot()
return m
}
// Root reports whether the Map is the root of the D2 tree. // Root reports whether the Map is the root of the D2 tree.
func (m *Map) Root() bool { func (m *Map) Root() bool {
// m.parent exists even on the root map as we store the root AST in // m.parent exists even on the root map as we store the root AST in
@ -213,9 +231,16 @@ func NodeLayerKind(n Node) LayerKind {
var f *Field var f *Field
switch n := n.(type) { switch n := n.(type) {
case *Field: case *Field:
f = n if n.Name == "" {
return LayerLayer
}
f = ParentField(n)
case *Map: case *Map:
f = ParentField(n) f = ParentField(n)
if f.Name == "" {
return LayerLayer
}
f = ParentField(f)
} }
if f == nil { if f == nil {
return "" return ""
@ -227,11 +252,9 @@ func NodeLayerKind(n Node) LayerKind {
return LayerScenario return LayerScenario
case "steps": case "steps":
return LayerStep return LayerStep
case "": default:
// root return ""
return LayerLayer
} }
return ""
} }
type Field struct { type Field struct {