Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

classes #772

Merged
merged 1 commit into from
Apr 7, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions ci/release/changelogs/next.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
#### Features 🚀

- Container with constant key near attribute now can have descendant objects and connections [#1071](https://github.com/terrastruct/d2/pull/1071)
- Classes are implemented. See [docs](https://d2lang.com/todo). [#772](https://github.com/terrastruct/d2/pull/772)
- Multi-board SVG outputs with internal links go to their output paths [#1116](https://github.com/terrastruct/d2/pull/1116)
- New grid layout to place nodes in rows and columns [#1122](https://github.com/terrastruct/d2/pull/1122)

Expand All @@ -17,3 +18,7 @@
- Namespace transitions so that multiple animated D2 diagrams can exist on the same page [#1123](https://github.com/terrastruct/d2/issues/1123)
- Fix a bug in vertical alignment of appendix lines [#1104](https://github.com/terrastruct/d2/issues/1104)
- Fix precision difference for sketch mode running on different architectures [#921](https://github.com/terrastruct/d2/issues/921)

#### Breaking changes

- `class` and `classes` are now reserved keywords.
72 changes: 61 additions & 11 deletions d2compiler/compile.go
Original file line number Diff line number Diff line change
Expand Up @@ -116,15 +116,26 @@ func (c *compiler) compileBoardsField(g *d2graph.Graph, ir *d2ir.Map, fieldName
}

type compiler struct {
inEdgeGroup bool
err d2parser.ParseError
err d2parser.ParseError
}

func (c *compiler) errorf(n d2ast.Node, f string, v ...interface{}) {
c.err.Errors = append(c.err.Errors, d2parser.Errorf(n, f, v...).(d2ast.Error))
}

func (c *compiler) compileMap(obj *d2graph.Object, m *d2ir.Map) {
class := m.GetField("class")
if class != nil {
className := class.Primary()
if className == nil {
c.errorf(class.LastRef().AST(), "class missing value")
} else {
classMap := m.GetClassMap(className.String())
if classMap != nil {
c.compileMap(obj, classMap)
alixander marked this conversation as resolved.
Show resolved Hide resolved
}
}
}
shape := m.GetField("shape")
if shape != nil {
c.compileField(obj, shape)
Expand Down Expand Up @@ -159,7 +170,27 @@ func (c *compiler) compileField(obj *d2graph.Object, f *d2ir.Field) {
return
}
_, isReserved := d2graph.SimpleReservedKeywords[keyword]
if isReserved {
if f.Name == "classes" {
if f.Map() != nil {
if len(f.Map().Edges) > 0 {
c.errorf(f.Map().Edges[0].LastRef().AST(), "classes cannot contain an edge")
}
for _, classesField := range f.Map().Fields {
if classesField.Map() == nil {
continue
}
for _, cf := range classesField.Map().Fields {
if _, ok := d2graph.ReservedKeywords[cf.Name]; !ok {
c.errorf(cf.LastRef().AST(), "%s is an invalid class field, must be reserved keyword", cf.Name)
}
if cf.Name == "class" {
c.errorf(cf.LastRef().AST(), `"class" cannot appear within "classes"`)
}
}
}
}
return
} else if isReserved {
c.compileReserved(obj.Attributes, f)
return
} else if f.Name == "style" {
Expand Down Expand Up @@ -389,6 +420,9 @@ func (c *compiler) compileReserved(attrs *d2graph.Attributes, f *d2ir.Field) {
attrs.GridColumns = &d2graph.Scalar{}
attrs.GridColumns.Value = scalar.ScalarString()
attrs.GridColumns.MapKey = f.LastPrimaryKey()
case "class":
attrs.Classes = append(attrs.Classes, scalar.ScalarString())
case "classes":
}

if attrs.Link != nil && attrs.Tooltip != nil {
Expand Down Expand Up @@ -480,14 +514,7 @@ func (c *compiler) compileEdge(obj *d2graph.Object, e *d2ir.Edge) {
c.compileLabel(edge.Attributes, e)
}
if e.Map() != nil {
for _, f := range e.Map().Fields {
_, ok := d2graph.ReservedKeywords[f.Name]
if !ok {
c.errorf(f.References[0].AST(), `edge map keys must be reserved keywords`)
continue
}
c.compileEdgeField(edge, f)
}
c.compileEdgeMap(edge, e.Map())
}

edge.Attributes.Label.MapKey = e.LastPrimaryKey()
Expand All @@ -504,6 +531,29 @@ func (c *compiler) compileEdge(obj *d2graph.Object, e *d2ir.Edge) {
}
}

func (c *compiler) compileEdgeMap(edge *d2graph.Edge, m *d2ir.Map) {
class := m.GetField("class")
if class != nil {
className := class.Primary()
if className == nil {
c.errorf(class.LastRef().AST(), "class missing value")
} else {
classMap := m.GetClassMap(className.String())
if classMap != nil {
c.compileEdgeMap(edge, classMap)
alixander marked this conversation as resolved.
Show resolved Hide resolved
}
}
}
for _, f := range m.Fields {
_, ok := d2graph.ReservedKeywords[f.Name]
if !ok {
c.errorf(f.References[0].AST(), `edge map keys must be reserved keywords`)
continue
}
c.compileEdgeField(edge, f)
}
}

func (c *compiler) compileEdgeField(edge *d2graph.Edge, f *d2ir.Field) {
keyword := strings.ToLower(f.Name)
_, isStyleReserved := d2graph.StyleKeywords[keyword]
Expand Down
94 changes: 94 additions & 0 deletions d2compiler/compile_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2320,6 +2320,100 @@ d2/testdata/d2compiler/TestCompile/grid_edge.d2:6:2: edges in grid diagrams are
expErr: `d2/testdata/d2compiler/TestCompile/grid_nested.d2:2:2: "grid-rows" can only be used on containers with one level of nesting right now. ("hey.d" has nested "invalid descendant")
d2/testdata/d2compiler/TestCompile/grid_nested.d2:3:2: "grid-columns" can only be used on containers with one level of nesting right now. ("hey.d" has nested "invalid descendant")`,
},
{
name: "classes",
text: `classes: {
dragon_ball: {
label: ""
shape: circle
style.fill: orange
}
path: {
label: "then"
style.stroke-width: 4
}
}
nostar: { class: dragon_ball }
1star: "*" { class: dragon_ball; style.fill: red }
2star: { label: "**"; class: dragon_ball }

nostar -> 1star: { class: path }
`,
assertions: func(t *testing.T, g *d2graph.Graph) {
tassert.Equal(t, 3, len(g.Objects))
tassert.Equal(t, "dragon_ball", g.Objects[0].Attributes.Classes[0])
tassert.Equal(t, "", g.Objects[0].Attributes.Label.Value)
// Class field overrides primary
tassert.Equal(t, "", g.Objects[1].Attributes.Label.Value)
tassert.Equal(t, "**", g.Objects[2].Attributes.Label.Value)
tassert.Equal(t, "orange", g.Objects[0].Attributes.Style.Fill.Value)
tassert.Equal(t, "red", g.Objects[1].Attributes.Style.Fill.Value)

tassert.Equal(t, "4", g.Edges[0].Attributes.Style.StrokeWidth.Value)
tassert.Equal(t, "then", g.Edges[0].Attributes.Label.Value)
},
},
{
name: "reordered-classes",
text: `classes: {
x: {
shape: circle
}
}
a.class: x
classes.x.shape: diamond
`,
assertions: func(t *testing.T, g *d2graph.Graph) {
tassert.Equal(t, 1, len(g.Objects))
tassert.Equal(t, "diamond", g.Objects[0].Attributes.Shape.Value)
},
},
{
name: "no-class-primary",
text: `x.class
`,
expErr: `d2/testdata/d2compiler/TestCompile/no-class-primary.d2:1:3: class missing value`,
},
{
name: "no-class-inside-classes",
text: `classes: {
x: {
class: y
}
}
`,
expErr: `d2/testdata/d2compiler/TestCompile/no-class-inside-classes.d2:3:5: "class" cannot appear within "classes"`,
},
{
// This is okay
name: "missing-class",
text: `x.class: yo
`,
},
{
name: "classes-unreserved",
text: `classes: {
mango: {
seed
}
}
`,
expErr: `d2/testdata/d2compiler/TestCompile/classes-unreserved.d2:3:5: seed is an invalid class field, must be reserved keyword`,
},
{
name: "classes-internal-edge",
text: `classes: {
mango: {
width: 100
}
jango: {
height: 100
}
mango -> jango
}
`,
expErr: `d2/testdata/d2compiler/TestCompile/classes-internal-edge.d2:8:3: classes cannot contain an edge`,
},
}

for _, tc := range testCases {
Expand Down
2 changes: 2 additions & 0 deletions d2exporter/export.go
Original file line number Diff line number Diff line change
Expand Up @@ -129,6 +129,7 @@ func toShape(obj *d2graph.Object, theme *d2themes.Theme) d2target.Shape {
shape := d2target.BaseShape()
shape.SetType(obj.Attributes.Shape.Value)
shape.ID = obj.AbsID()
shape.Classes = obj.Attributes.Classes
shape.ZIndex = obj.ZIndex
shape.Level = int(obj.Level())
shape.Pos = d2target.NewPoint(int(obj.TopLeft.X), int(obj.TopLeft.Y))
Expand Down Expand Up @@ -194,6 +195,7 @@ func toShape(obj *d2graph.Object, theme *d2themes.Theme) d2target.Shape {
func toConnection(edge *d2graph.Edge, theme *d2themes.Theme) d2target.Connection {
connection := d2target.BaseConnection()
connection.ID = edge.AbsID()
connection.Classes = edge.Attributes.Classes
connection.ZIndex = edge.ZIndex
text := edge.Text()

Expand Down
6 changes: 6 additions & 0 deletions d2graph/d2graph.go
Original file line number Diff line number Diff line change
Expand Up @@ -135,6 +135,10 @@ type Attributes struct {

GridRows *Scalar `json:"gridRows,omitempty"`
GridColumns *Scalar `json:"gridColumns,omitempty"`

// These names are attached to the rendered elements in SVG
// so that users can target them however they like outside of D2
Classes []string `json:"classes,omitempty"`
}

// TODO references at the root scope should have their Scope set to root graph AST
Expand Down Expand Up @@ -1556,6 +1560,8 @@ var SimpleReservedKeywords = map[string]struct{}{
"left": {},
"grid-rows": {},
"grid-columns": {},
"class": {},
"classes": {},
}

// ReservedKeywordHolders are reserved keywords that are meaningless on its own and exist solely to hold a set of reserved keywords
Expand Down
42 changes: 42 additions & 0 deletions d2ir/compile.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,12 +22,50 @@ func Compile(ast *d2ast.Map) (*Map, error) {
m.initRoot()
m.parent.(*Field).References[0].Context.Scope = ast
c.compileMap(m, ast)
c.compileClasses(m)
if !c.err.Empty() {
return nil, c.err
}
return m, nil
}

func (c *compiler) compileClasses(m *Map) {
classes := m.GetField("classes")
if classes == nil || classes.Map() == nil {
return
}

layersField := m.GetField("layers")
if layersField == nil {
return
}
layers := layersField.Map()
if layers == nil {
return
}

for _, lf := range layers.Fields {
if lf.Map() == nil || lf.Primary() != nil {
c.errorf(lf.References[0].Context.Key, "invalid layer")
continue
}
l := lf.Map()
lClasses := l.GetField("classes")

if lClasses == nil {
lClasses = classes.Copy(l).(*Field)
l.Fields = append(l.Fields, lClasses)
alixander marked this conversation as resolved.
Show resolved Hide resolved
} else {
base := classes.Copy(l).(*Field)
OverlayMap(base.Map(), lClasses.Map())
l.DeleteField("classes")
l.Fields = append(l.Fields, base)
}

c.compileClasses(l)
}
}

func (c *compiler) overlay(base *Map, f *Field) {
if f.Map() == nil || f.Primary() != nil {
c.errorf(f.References[0].Context.Key, "invalid %s", NodeBoardKind(f))
Expand Down Expand Up @@ -103,6 +141,10 @@ func (c *compiler) compileField(dst *Map, kp *d2ast.KeyPath, refctx *RefContext)
}
}
c.compileMap(f.Map(), refctx.Key.Value.Map)
switch NodeBoardKind(f) {
case BoardScenario, BoardStep:
c.compileClasses(f.Map())
}
} else if refctx.Key.Value.ScalarBox().Unbox() != nil {
// If the link is a board, we need to transform it into an absolute path.
if f.Name == "link" {
Expand Down
Loading