diff --git a/d2ast/d2ast.go b/d2ast/d2ast.go index a5be3fa98..d49b1b268 100644 --- a/d2ast/d2ast.go +++ b/d2ast/d2ast.go @@ -423,6 +423,7 @@ func (s *BlockString) value() {} func (a *Array) value() {} func (m *Map) value() {} func (i *Import) value() {} +func (i *Substitution) value() {} func (n *Null) scalar() {} func (b *Boolean) scalar() {} @@ -901,6 +902,7 @@ type ValueBox struct { Array *Array `json:"array,omitempty"` Map *Map `json:"map,omitempty"` Import *Import `json:"import,omitempty"` + Substitution *Substitution `json:"substitution,omitempty"` } func (vb ValueBox) Unbox() Value { @@ -925,6 +927,8 @@ func (vb ValueBox) Unbox() Value { return vb.Map case vb.Import != nil: return vb.Import + case vb.Substitution != nil: + return vb.Substitution default: return nil } @@ -953,6 +957,8 @@ func MakeValueBox(v Value) ValueBox { vb.Map = v case *Import: vb.Import = v + case *Substitution: + vb.Substitution = v } return vb } diff --git a/d2compiler/compile.go b/d2compiler/compile.go index fdb24e551..4c11ac5dd 100644 --- a/d2compiler/compile.go +++ b/d2compiler/compile.go @@ -1,6 +1,7 @@ package d2compiler import ( + "encoding/json" "encoding/xml" "fmt" "io" @@ -64,6 +65,8 @@ func compileIR(ast *d2ast.Map, m *d2ir.Map) (*d2graph.Graph, error) { g := d2graph.NewGraph() g.AST = ast c.compileBoard(g, m) + b, _ := json.MarshalIndent(m, "", " ") + println("\033[1;31m--- DEBUG:", string(b), "\033[m") if len(c.err.Errors) > 0 { return nil, c.err } @@ -277,6 +280,26 @@ func (c *compiler) compileField(obj *d2graph.Object, f *d2ir.Field) { } } return + } else if f.Name == "vars" { + if f.Map() != nil { + if len(f.Map().Edges) > 0 { + c.errorf(f.Map().Edges[0].LastRef().AST(), "vars cannot contain an edge") + } + // for _, varField := range f.Map().Fields { + // if varField.Map() != nil { + // c.errorf(varField.LastRef().AST(), "vars must be simple") + // } + // 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 diff --git a/d2compiler/compile_test.go b/d2compiler/compile_test.go index f0dbaa1bb..08fef43fd 100644 --- a/d2compiler/compile_test.go +++ b/d2compiler/compile_test.go @@ -2744,6 +2744,7 @@ func TestCompile2(t *testing.T) { t.Run("boards", testBoards) t.Run("seqdiagrams", testSeqDiagrams) t.Run("nulls", testNulls) + t.Run("vars", testVars) } func testBoards(t *testing.T) { @@ -3168,6 +3169,78 @@ scenarios: { }) } +func testVars(t *testing.T) { + t.Parallel() + + t.Run("basic", func(t *testing.T) { + t.Parallel() + + tca := []struct { + name string + skip bool + run func(t *testing.T) + }{ + { + name: "label", + run: func(t *testing.T) { + g := assertCompile(t, ` +vars: { + x: im a var +} +hi: ${x} +`, "") + assert.Equal(t, 1, len(g.Objects)) + assert.Equal(t, "im a var", g.Objects[0].Label.Value) + }, + }, + } + + for _, tc := range tca { + tc := tc + t.Run(tc.name, func(t *testing.T) { + t.Parallel() + if tc.skip { + t.SkipNow() + } + tc.run(t) + }) + } + }) + + t.Run("errors", func(t *testing.T) { + t.Parallel() + + tca := []struct { + name string + skip bool + run func(t *testing.T) + }{ + { + name: "missing", + run: func(t *testing.T) { + assertCompile(t, ` +vars: { + x: hey +} +hi: ${z} +`, "d2/testdata/d2compiler/TestCompile2/vars/errors/missing.d2:5:1: could not resolve variable z") + }, + }, + } + + for _, tc := range tca { + tc := tc + t.Run(tc.name, func(t *testing.T) { + t.Parallel() + if tc.skip { + t.SkipNow() + } + tc.run(t) + }) + } + }) +} + func assertCompile(t *testing.T, text string, expErr string) *d2graph.Graph { d2Path := fmt.Sprintf("d2/testdata/d2compiler/%v.d2", t.Name()) g, err := d2compiler.Compile(d2Path, strings.NewReader(text), nil) diff --git a/d2ir/compile.go b/d2ir/compile.go index f5d92d043..27c11233b 100644 --- a/d2ir/compile.go +++ b/d2ir/compile.go @@ -53,6 +53,8 @@ func Compile(ast *d2ast.Map, opts *CompileOptions) (*Map, error) { c.compileMap(m, ast, ast) c.compileClasses(m) + c.compileVars(m) + c.compileSubstitutions(m) if !c.err.Empty() { return nil, c.err } @@ -96,6 +98,81 @@ func (c *compiler) compileClasses(m *Map) { } } +func (c *compiler) compileSubstitutions(m *Map) { + vars := m.GetField("vars") + for _, f := range m.Fields { + // No substitutions within vars itself + if f.Name == "vars" { + continue + } + for _, ref := range f.References { + if ref.Context.Key != nil && ref.Context.Key.Value.Substitution != nil { + var resolved *Field + m := vars + for _, p := range ref.Context.Key.Value.Substitution.Path { + r := m.Map().GetField(p.Unbox().ScalarString()) + if r == nil { + resolved = nil + break + } + m = r + resolved = r + } + if resolved == nil { + c.errorf(ref.Context.Key, "could not resolve variable %s", strings.Join(ref.Context.Key.Value.Substitution.IDA(), ".")) + } else { + // TODO do i need this + // ref.Context.Key.Value = d2ast.MakeValueBox(resolved.Primary().Value) + + // TODO maps + f.Primary_ = &Scalar{ + parent: f, + Value: resolved.Primary().Value, + } + } + ref.Context.Key.Value.Substitution = nil + } + } + } +} + +func (c *compiler) compileVars(m *Map) { + vars := m.GetField("vars") + if vars == nil || vars.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() + lVars := l.GetField("vars") + + if lVars == nil { + lVars = vars.Copy(l).(*Field) + l.Fields = append(l.Fields, lVars) + } else { + base := vars.Copy(l).(*Field) + OverlayMap(base.Map(), lVars.Map()) + l.DeleteField("vars") + l.Fields = append(l.Fields, base) + } + + c.compileVars(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)) @@ -244,6 +321,10 @@ func (c *compiler) compileField(dst *Map, kp *d2ast.KeyPath, refctx *RefContext) c.compileClasses(f.Map()) } } + } else if refctx.Key.Value.Substitution != nil { + // b, _ := json.MarshalIndent(refctx.Key.Value.Substitution.IDA(), "", " ") + // println("\033[1;31m--- DEBUG:", string(b), "\033[m") + // println("\033[1;31m--- DEBUG:", "=======what===============", "\033[m") } 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" { diff --git a/d2parser/parse.go b/d2parser/parse.go index fa64531a3..490505e4d 100644 --- a/d2parser/parse.go +++ b/d2parser/parse.go @@ -1596,6 +1596,9 @@ func (p *parser) parseValue() d2ast.ValueBox { case '@': box.Import = p.parseImport(false) return box + case '$': + box.Substitution = p.parseSubstitution(false) + return box } p.replay(r) diff --git a/testdata/d2compiler/TestCompile2/vars/basic/label.exp.json b/testdata/d2compiler/TestCompile2/vars/basic/label.exp.json new file mode 100644 index 000000000..0270f685c --- /dev/null +++ b/testdata/d2compiler/TestCompile2/vars/basic/label.exp.json @@ -0,0 +1,168 @@ +{ + "graph": { + "name": "", + "isFolderOnly": false, + "ast": { + "range": "d2/testdata/d2compiler/TestCompile2/vars/basic/label.d2,0:0:0-5:0:34", + "nodes": [ + { + "map_key": { + "range": "d2/testdata/d2compiler/TestCompile2/vars/basic/label.d2,1:0:1-3:1:24", + "key": { + "range": "d2/testdata/d2compiler/TestCompile2/vars/basic/label.d2,1:0:1-1:4:5", + "path": [ + { + "unquoted_string": { + "range": "d2/testdata/d2compiler/TestCompile2/vars/basic/label.d2,1:0:1-1:4:5", + "value": [ + { + "string": "vars", + "raw_string": "vars" + } + ] + } + } + ] + }, + "primary": {}, + "value": { + "map": { + "range": "d2/testdata/d2compiler/TestCompile2/vars/basic/label.d2,1:6:7-3:1:24", + "nodes": [ + { + "map_key": { + "range": "d2/testdata/d2compiler/TestCompile2/vars/basic/label.d2,2:2:11-2:13:22", + "key": { + "range": "d2/testdata/d2compiler/TestCompile2/vars/basic/label.d2,2:2:11-2:3:12", + "path": [ + { + "unquoted_string": { + "range": "d2/testdata/d2compiler/TestCompile2/vars/basic/label.d2,2:2:11-2:3:12", + "value": [ + { + "string": "x", + "raw_string": "x" + } + ] + } + } + ] + }, + "primary": {}, + "value": { + "unquoted_string": { + "range": "d2/testdata/d2compiler/TestCompile2/vars/basic/label.d2,2:5:14-2:13:22", + "value": [ + { + "string": "im a var", + "raw_string": "im a var" + } + ] + } + } + } + } + ] + } + } + } + }, + { + "map_key": { + "range": "d2/testdata/d2compiler/TestCompile2/vars/basic/label.d2,4:0:25-4:8:33", + "key": { + "range": "d2/testdata/d2compiler/TestCompile2/vars/basic/label.d2,4:0:25-4:2:27", + "path": [ + { + "unquoted_string": { + "range": "d2/testdata/d2compiler/TestCompile2/vars/basic/label.d2,4:0:25-4:2:27", + "value": [ + { + "string": "hi", + "raw_string": "hi" + } + ] + } + } + ] + }, + "primary": {}, + "value": {} + } + } + ] + }, + "root": { + "id": "", + "id_val": "", + "attributes": { + "label": { + "value": "" + }, + "labelDimensions": { + "width": 0, + "height": 0 + }, + "style": {}, + "near_key": null, + "shape": { + "value": "" + }, + "direction": { + "value": "" + }, + "constraint": null + }, + "zIndex": 0 + }, + "edges": null, + "objects": [ + { + "id": "hi", + "id_val": "hi", + "references": [ + { + "key": { + "range": "d2/testdata/d2compiler/TestCompile2/vars/basic/label.d2,4:0:25-4:2:27", + "path": [ + { + "unquoted_string": { + "range": "d2/testdata/d2compiler/TestCompile2/vars/basic/label.d2,4:0:25-4:2:27", + "value": [ + { + "string": "hi", + "raw_string": "hi" + } + ] + } + } + ] + }, + "key_path_index": 0, + "map_key_edge_index": -1 + } + ], + "attributes": { + "label": { + "value": "im a var" + }, + "labelDimensions": { + "width": 0, + "height": 0 + }, + "style": {}, + "near_key": null, + "shape": { + "value": "rectangle" + }, + "direction": { + "value": "" + }, + "constraint": null + }, + "zIndex": 0 + } + ] + }, + "err": null +} diff --git a/testdata/d2compiler/TestCompile2/vars/errors/missing.exp.json b/testdata/d2compiler/TestCompile2/vars/errors/missing.exp.json new file mode 100644 index 000000000..64d495784 --- /dev/null +++ b/testdata/d2compiler/TestCompile2/vars/errors/missing.exp.json @@ -0,0 +1,11 @@ +{ + "graph": null, + "err": { + "errs": [ + { + "range": "d2/testdata/d2compiler/TestCompile2/vars/errors/missing.d2,4:0:20-4:8:28", + "errmsg": "d2/testdata/d2compiler/TestCompile2/vars/errors/missing.d2:5:1: could not resolve variable z" + } + ] + } +}