This commit is contained in:
Alexander Wang 2025-03-04 10:14:32 -07:00
parent 27d60d9e7d
commit 79e7b69552
No known key found for this signature in database
GPG key ID: BE3937D0D52D8927
6 changed files with 1375 additions and 1 deletions

View file

@ -96,6 +96,8 @@ func (c *compiler) compileBoard(g *d2graph.Graph, ir *d2ir.Map) *d2graph.Graph {
c.validateEdges(g) c.validateEdges(g)
c.validatePositionsCompatibility(g) c.validatePositionsCompatibility(g)
c.compileLegend(g, ir)
c.compileBoardsField(g, ir, "layers") c.compileBoardsField(g, ir, "layers")
c.compileBoardsField(g, ir, "scenarios") c.compileBoardsField(g, ir, "scenarios")
c.compileBoardsField(g, ir, "steps") c.compileBoardsField(g, ir, "steps")
@ -110,6 +112,42 @@ func (c *compiler) compileBoard(g *d2graph.Graph, ir *d2ir.Map) *d2graph.Graph {
return g return g
} }
func (c *compiler) compileLegend(g *d2graph.Graph, m *d2ir.Map) {
varsField := m.GetField(d2ast.FlatUnquotedString("vars"))
if varsField == nil || varsField.Map() == nil {
return
}
legendField := varsField.Map().GetField(d2ast.FlatUnquotedString("d2-legend"))
if legendField == nil || legendField.Map() == nil {
return
}
legendGraph := d2graph.NewGraph()
c.compileMap(legendGraph.Root, legendField.Map())
c.setDefaultShapes(legendGraph)
objects := make([]*d2graph.Object, 0)
for _, obj := range legendGraph.Objects {
if obj.Style.Opacity != nil {
if opacity, err := strconv.ParseFloat(obj.Style.Opacity.Value, 64); err == nil && opacity == 0 {
continue
}
}
objects = append(objects, obj)
}
legend := &d2graph.Legend{
Objects: objects,
Edges: legendGraph.Edges,
}
if len(legend.Objects) > 0 || len(legend.Edges) > 0 {
g.Legend = legend
}
}
func (c *compiler) compileBoardsField(g *d2graph.Graph, ir *d2ir.Map, fieldName string) { func (c *compiler) compileBoardsField(g *d2graph.Graph, ir *d2ir.Map, fieldName string) {
boards := ir.GetField(d2ast.FlatUnquotedString(fieldName)) boards := ir.GetField(d2ast.FlatUnquotedString(fieldName))
if boards.Map() == nil { if boards.Map() == nil {

View file

@ -720,6 +720,142 @@ x: {
} }
}, },
}, },
{
name: "legend",
text: `
vars: {
d2-legend: {
User: "A person who interacts with the system" {
shape: person
style: {
fill: "#f5f5f5"
}
}
Database: "Stores application data" {
shape: cylinder
style.fill: "#b5d3ff"
}
HiddenShape: "This should not appear in the legend" {
style.opacity: 0
}
User -> Database: "Reads data" {
style.stroke: "blue"
}
Database -> User: "Returns results" {
style.stroke-dash: 5
}
}
}
user: User
db: Database
user -> db: Uses
`,
assertions: func(t *testing.T, g *d2graph.Graph) {
if g.Legend == nil {
t.Fatal("Expected Legend to be non-nil")
return
}
// 2. Verify the correct objects are in the legend
if len(g.Legend.Objects) != 2 {
t.Errorf("Expected 2 objects in legend, got %d", len(g.Legend.Objects))
}
// Check for User object
hasUser := false
hasDatabase := false
for _, obj := range g.Legend.Objects {
if obj.ID == "User" {
hasUser = true
if obj.Shape.Value != "person" {
t.Errorf("User shape incorrect, expected 'person', got: %s", obj.Shape.Value)
}
} else if obj.ID == "Database" {
hasDatabase = true
if obj.Shape.Value != "cylinder" {
t.Errorf("Database shape incorrect, expected 'cylinder', got: %s", obj.Shape.Value)
}
} else if obj.ID == "HiddenShape" {
t.Errorf("HiddenShape should not be in legend due to opacity: 0")
}
}
if !hasUser {
t.Errorf("User object missing from legend")
}
if !hasDatabase {
t.Errorf("Database object missing from legend")
}
// 3. Verify the correct edges are in the legend
if len(g.Legend.Edges) != 2 {
t.Errorf("Expected 2 edges in legend, got %d", len(g.Legend.Edges))
}
// Check for expected edges
hasReadsEdge := false
hasReturnsEdge := false
for _, edge := range g.Legend.Edges {
if edge.Label.Value == "Reads data" {
hasReadsEdge = true
// Check edge properties
if edge.Style.Stroke == nil {
t.Errorf("Reads edge stroke is nil")
} else if edge.Style.Stroke.Value != "blue" {
t.Errorf("Reads edge stroke incorrect, expected 'blue', got: %s", edge.Style.Stroke.Value)
}
} else if edge.Label.Value == "Returns results" {
hasReturnsEdge = true
// Check edge properties
if edge.Style.StrokeDash == nil {
t.Errorf("Returns edge stroke-dash is nil")
} else if edge.Style.StrokeDash.Value != "5" {
t.Errorf("Returns edge stroke-dash incorrect, expected '5', got: %s", edge.Style.StrokeDash.Value)
}
} else if edge.Label.Value == "Hidden connection" {
t.Errorf("Hidden connection should not be in legend due to opacity: 0")
}
}
if !hasReadsEdge {
t.Errorf("'Reads data' edge missing from legend")
}
if !hasReturnsEdge {
t.Errorf("'Returns results' edge missing from legend")
}
// 4. Verify the regular diagram content is still there
userObj, hasUserObj := g.Root.HasChild([]string{"user"})
if !hasUserObj {
t.Errorf("Main diagram missing 'user' object")
} else if userObj.Label.Value != "User" {
t.Errorf("User label incorrect, expected 'User', got: %s", userObj.Label.Value)
}
dbObj, hasDBObj := g.Root.HasChild([]string{"db"})
if !hasDBObj {
t.Errorf("Main diagram missing 'db' object")
} else if dbObj.Label.Value != "Database" {
t.Errorf("DB label incorrect, expected 'Database', got: %s", dbObj.Label.Value)
}
// Check the main edge
if len(g.Edges) == 0 {
t.Errorf("No edges found in main diagram")
} else {
mainEdge := g.Edges[0]
if mainEdge.Label.Value != "Uses" {
t.Errorf("Main edge label incorrect, expected 'Uses', got: %s", mainEdge.Label.Value)
}
}
},
},
{ {
name: "underscore_edge_nested", name: "underscore_edge_nested",

View file

@ -47,6 +47,26 @@ func Export(ctx context.Context, g *d2graph.Graph, fontFamily *d2fonts.FontFamil
diagram.Connections[i] = toConnection(g.Edges[i], g.Theme) diagram.Connections[i] = toConnection(g.Edges[i], g.Theme)
} }
if g.Legend != nil {
legend := &d2target.Legend{}
if len(g.Legend.Objects) > 0 {
legend.Shapes = make([]d2target.Shape, len(g.Legend.Objects))
for i, obj := range g.Legend.Objects {
legend.Shapes[i] = toShape(obj, g)
}
}
if len(g.Legend.Edges) > 0 {
legend.Connections = make([]d2target.Connection, len(g.Legend.Edges))
for i, edge := range g.Legend.Edges {
legend.Connections[i] = toConnection(edge, g.Theme)
}
}
diagram.Legend = legend
}
return diagram, nil return diagram, nil
} }

View file

@ -49,6 +49,7 @@ type Graph struct {
BaseAST *d2ast.Map `json:"-"` BaseAST *d2ast.Map `json:"-"`
Root *Object `json:"root"` Root *Object `json:"root"`
Legend *Legend `json:"legend,omitempty"`
Edges []*Edge `json:"edges"` Edges []*Edge `json:"edges"`
Objects []*Object `json:"objects"` Objects []*Object `json:"objects"`
@ -67,6 +68,11 @@ type Graph struct {
Data map[string]interface{} `json:"data,omitempty"` Data map[string]interface{} `json:"data,omitempty"`
} }
type Legend struct {
Objects []*Object `json:"objects,omitempty"`
Edges []*Edge `json:"edges,omitempty"`
}
func NewGraph() *Graph { func NewGraph() *Graph {
d := &Graph{} d := &Graph{}
d.Root = &Object{ d.Root = &Object{

View file

@ -86,7 +86,8 @@ type Diagram struct {
Shapes []Shape `json:"shapes"` Shapes []Shape `json:"shapes"`
Connections []Connection `json:"connections"` Connections []Connection `json:"connections"`
Root Shape `json:"root"` Root Shape `json:"root"`
Legend *Legend `json:"legend,omitempty"`
// Maybe Icon can be used as a watermark in the root shape // Maybe Icon can be used as a watermark in the root shape
Layers []*Diagram `json:"layers,omitempty"` Layers []*Diagram `json:"layers,omitempty"`
@ -94,6 +95,11 @@ type Diagram struct {
Steps []*Diagram `json:"steps,omitempty"` Steps []*Diagram `json:"steps,omitempty"`
} }
type Legend struct {
Shapes []Shape `json:"shapes,omitempty"`
Connections []Connection `json:"connections,omitempty"`
}
func (d *Diagram) GetBoard(boardPath []string) *Diagram { func (d *Diagram) GetBoard(boardPath []string) *Diagram {
if len(boardPath) == 0 { if len(boardPath) == 0 {
return d return d

1168
testdata/d2compiler/TestCompile/legend.exp.json generated vendored Normal file

File diff suppressed because it is too large Load diff