diff --git a/ci/release/changelogs/next.md b/ci/release/changelogs/next.md index 902660519..6841ef632 100644 --- a/ci/release/changelogs/next.md +++ b/ci/release/changelogs/next.md @@ -2,7 +2,9 @@ - Icons: connections can include icons [#12](https://github.com/terrastruct/d2/issues/12) - Syntax: `suspend`/`unsuspend` to define models and instantiate them [#2394](https://github.com/terrastruct/d2/pull/2394) -- Globs: support for filtering edges based on properties of endpoint nodes (e.g., `&src.style.fill: blue`) [#2395](https://github.com/terrastruct/d2/pull/2395) +- Globs: + - support for filtering edges based on properties of endpoint nodes (e.g., `&src.style.fill: blue`) [#2395](https://github.com/terrastruct/d2/pull/2395) + - `level` filter implemented [#2473](https://github.com/terrastruct/d2/pull/2473) - Render: - markdown, latex, and code can be used as object labels [#2204](https://github.com/terrastruct/d2/pull/2204) - `shape: c4-person` to render a person shape like what the C4 model prescribes [#2397](https://github.com/terrastruct/d2/pull/2397) diff --git a/d2compiler/compile_test.go b/d2compiler/compile_test.go index e373a4cd9..0871d4b33 100644 --- a/d2compiler/compile_test.go +++ b/d2compiler/compile_test.go @@ -5641,6 +5641,33 @@ d: { assert.Equal(t, "yellow", g.Objects[4].Attributes.Style.Stroke.Value) }, }, + { + name: "level-filter", + run: func(t *testing.T) { + g, _ := assertCompile(t, ` +**: { + &level: 0 + style.fill: red +} +**: { + &level: 1 + style.stroke: yellow +} +a.b.c +`, ``) + assert.Equal(t, "a", g.Objects[0].ID) + assert.Equal(t, "red", g.Objects[0].Attributes.Style.Fill.Value) + assert.Equal(t, (*d2graph.Scalar)(nil), g.Objects[0].Attributes.Style.Stroke) + + assert.Equal(t, "b", g.Objects[1].ID) + assert.Equal(t, "yellow", g.Objects[1].Attributes.Style.Stroke.Value) + assert.Equal(t, (*d2graph.Scalar)(nil), g.Objects[1].Attributes.Style.Fill) + + assert.Equal(t, "c", g.Objects[2].ID) + assert.Equal(t, (*d2graph.Scalar)(nil), g.Objects[2].Attributes.Style.Fill) + assert.Equal(t, (*d2graph.Scalar)(nil), g.Objects[2].Attributes.Style.Stroke) + }, + }, { name: "connected-filter", run: func(t *testing.T) { diff --git a/d2ir/compile.go b/d2ir/compile.go index 45672d4bb..47031ddfe 100644 --- a/d2ir/compile.go +++ b/d2ir/compile.go @@ -851,6 +851,26 @@ func (c *compiler) ampersandFilter(refctx *RefContext) bool { f := refctx.ScopeMap.Parent().(*Field) isLeaf := f.Map() == nil || !f.Map().IsContainer() return isLeaf == boolVal + case "level": + raw := refctx.Key.Value.ScalarBox().Unbox().ScalarString() + levelVal, err := strconv.Atoi(raw) + if err != nil { + c.errorf(refctx.Key, `&level must be a non-negative integer, got %q`, raw) + return false + } + if levelVal < 0 { + c.errorf(refctx.Key, `&level must be a non-negative integer, got %d`, levelVal) + return false + } + + f := refctx.ScopeMap.Parent().(*Field) + level := 0 + parent := ParentField(f) + for parent != nil && parent.Name.ScalarString() != "root" && NodeBoardKind(parent) == "" { + level++ + parent = ParentField(parent) + } + return level == levelVal case "connected": raw := refctx.Key.Value.ScalarBox().Unbox().ScalarString() boolVal, err := strconv.ParseBool(raw) diff --git a/testdata/d2compiler/TestCompile2/globs/level-filter.exp.json b/testdata/d2compiler/TestCompile2/globs/level-filter.exp.json new file mode 100644 index 000000000..0c1939d87 --- /dev/null +++ b/testdata/d2compiler/TestCompile2/globs/level-filter.exp.json @@ -0,0 +1,508 @@ +{ + "graph": { + "name": "", + "isFolderOnly": false, + "ast": { + "range": "d2/testdata/d2compiler/TestCompile2/globs/level-filter.d2,0:0:0-10:0:88", + "nodes": [ + { + "map_key": { + "range": "d2/testdata/d2compiler/TestCompile2/globs/level-filter.d2,1:0:1-4:1:38", + "key": { + "range": "d2/testdata/d2compiler/TestCompile2/globs/level-filter.d2,1:0:1-1:2:3", + "path": [ + { + "unquoted_string": { + "range": "d2/testdata/d2compiler/TestCompile2/globs/level-filter.d2,1:0:1-1:2:3", + "value": [ + { + "string": "**", + "raw_string": "**" + } + ], + "pattern": [ + "*", + "", + "*" + ] + } + } + ] + }, + "primary": {}, + "value": { + "map": { + "range": "d2/testdata/d2compiler/TestCompile2/globs/level-filter.d2,1:4:5-4:1:38", + "nodes": [ + { + "map_key": { + "range": "d2/testdata/d2compiler/TestCompile2/globs/level-filter.d2,2:2:9-2:11:18", + "ampersand": true, + "key": { + "range": "d2/testdata/d2compiler/TestCompile2/globs/level-filter.d2,2:3:10-2:8:15", + "path": [ + { + "unquoted_string": { + "range": "d2/testdata/d2compiler/TestCompile2/globs/level-filter.d2,2:3:10-2:8:15", + "value": [ + { + "string": "level", + "raw_string": "level" + } + ] + } + } + ] + }, + "primary": {}, + "value": { + "number": { + "range": "d2/testdata/d2compiler/TestCompile2/globs/level-filter.d2,2:10:17-2:11:18", + "raw": "0", + "value": "0" + } + } + } + }, + { + "map_key": { + "range": "d2/testdata/d2compiler/TestCompile2/globs/level-filter.d2,3:2:21-3:17:36", + "key": { + "range": "d2/testdata/d2compiler/TestCompile2/globs/level-filter.d2,3:2:21-3:12:31", + "path": [ + { + "unquoted_string": { + "range": "d2/testdata/d2compiler/TestCompile2/globs/level-filter.d2,3:2:21-3:7:26", + "value": [ + { + "string": "style", + "raw_string": "style" + } + ] + } + }, + { + "unquoted_string": { + "range": "d2/testdata/d2compiler/TestCompile2/globs/level-filter.d2,3:8:27-3:12:31", + "value": [ + { + "string": "fill", + "raw_string": "fill" + } + ] + } + } + ] + }, + "primary": {}, + "value": { + "unquoted_string": { + "range": "d2/testdata/d2compiler/TestCompile2/globs/level-filter.d2,3:14:33-3:17:36", + "value": [ + { + "string": "red", + "raw_string": "red" + } + ] + } + } + } + } + ] + } + } + } + }, + { + "map_key": { + "range": "d2/testdata/d2compiler/TestCompile2/globs/level-filter.d2,5:0:39-8:1:81", + "key": { + "range": "d2/testdata/d2compiler/TestCompile2/globs/level-filter.d2,5:0:39-5:2:41", + "path": [ + { + "unquoted_string": { + "range": "d2/testdata/d2compiler/TestCompile2/globs/level-filter.d2,5:0:39-5:2:41", + "value": [ + { + "string": "**", + "raw_string": "**" + } + ], + "pattern": [ + "*", + "", + "*" + ] + } + } + ] + }, + "primary": {}, + "value": { + "map": { + "range": "d2/testdata/d2compiler/TestCompile2/globs/level-filter.d2,5:4:43-8:1:81", + "nodes": [ + { + "map_key": { + "range": "d2/testdata/d2compiler/TestCompile2/globs/level-filter.d2,6:2:47-6:11:56", + "ampersand": true, + "key": { + "range": "d2/testdata/d2compiler/TestCompile2/globs/level-filter.d2,6:3:48-6:8:53", + "path": [ + { + "unquoted_string": { + "range": "d2/testdata/d2compiler/TestCompile2/globs/level-filter.d2,6:3:48-6:8:53", + "value": [ + { + "string": "level", + "raw_string": "level" + } + ] + } + } + ] + }, + "primary": {}, + "value": { + "number": { + "range": "d2/testdata/d2compiler/TestCompile2/globs/level-filter.d2,6:10:55-6:11:56", + "raw": "1", + "value": "1" + } + } + } + }, + { + "map_key": { + "range": "d2/testdata/d2compiler/TestCompile2/globs/level-filter.d2,7:2:59-7:22:79", + "key": { + "range": "d2/testdata/d2compiler/TestCompile2/globs/level-filter.d2,7:2:59-7:14:71", + "path": [ + { + "unquoted_string": { + "range": "d2/testdata/d2compiler/TestCompile2/globs/level-filter.d2,7:2:59-7:7:64", + "value": [ + { + "string": "style", + "raw_string": "style" + } + ] + } + }, + { + "unquoted_string": { + "range": "d2/testdata/d2compiler/TestCompile2/globs/level-filter.d2,7:8:65-7:14:71", + "value": [ + { + "string": "stroke", + "raw_string": "stroke" + } + ] + } + } + ] + }, + "primary": {}, + "value": { + "unquoted_string": { + "range": "d2/testdata/d2compiler/TestCompile2/globs/level-filter.d2,7:16:73-7:22:79", + "value": [ + { + "string": "yellow", + "raw_string": "yellow" + } + ] + } + } + } + } + ] + } + } + } + }, + { + "map_key": { + "range": "d2/testdata/d2compiler/TestCompile2/globs/level-filter.d2,9:0:82-9:5:87", + "key": { + "range": "d2/testdata/d2compiler/TestCompile2/globs/level-filter.d2,9:0:82-9:5:87", + "path": [ + { + "unquoted_string": { + "range": "d2/testdata/d2compiler/TestCompile2/globs/level-filter.d2,9:0:82-9:1:83", + "value": [ + { + "string": "a", + "raw_string": "a" + } + ] + } + }, + { + "unquoted_string": { + "range": "d2/testdata/d2compiler/TestCompile2/globs/level-filter.d2,9:2:84-9:3:85", + "value": [ + { + "string": "b", + "raw_string": "b" + } + ] + } + }, + { + "unquoted_string": { + "range": "d2/testdata/d2compiler/TestCompile2/globs/level-filter.d2,9:4:86-9:5:87", + "value": [ + { + "string": "c", + "raw_string": "c" + } + ] + } + } + ] + }, + "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": "a", + "id_val": "a", + "references": [ + { + "key": { + "range": "d2/testdata/d2compiler/TestCompile2/globs/level-filter.d2,9:0:82-9:5:87", + "path": [ + { + "unquoted_string": { + "range": "d2/testdata/d2compiler/TestCompile2/globs/level-filter.d2,9:0:82-9:1:83", + "value": [ + { + "string": "a", + "raw_string": "a" + } + ] + } + }, + { + "unquoted_string": { + "range": "d2/testdata/d2compiler/TestCompile2/globs/level-filter.d2,9:2:84-9:3:85", + "value": [ + { + "string": "b", + "raw_string": "b" + } + ] + } + }, + { + "unquoted_string": { + "range": "d2/testdata/d2compiler/TestCompile2/globs/level-filter.d2,9:4:86-9:5:87", + "value": [ + { + "string": "c", + "raw_string": "c" + } + ] + } + } + ] + }, + "key_path_index": 0, + "map_key_edge_index": -1 + } + ], + "attributes": { + "label": { + "value": "a" + }, + "labelDimensions": { + "width": 0, + "height": 0 + }, + "style": { + "fill": { + "value": "red" + } + }, + "near_key": null, + "shape": { + "value": "rectangle" + }, + "direction": { + "value": "" + }, + "constraint": null + }, + "zIndex": 0 + }, + { + "id": "b", + "id_val": "b", + "references": [ + { + "key": { + "range": "d2/testdata/d2compiler/TestCompile2/globs/level-filter.d2,9:0:82-9:5:87", + "path": [ + { + "unquoted_string": { + "range": "d2/testdata/d2compiler/TestCompile2/globs/level-filter.d2,9:0:82-9:1:83", + "value": [ + { + "string": "a", + "raw_string": "a" + } + ] + } + }, + { + "unquoted_string": { + "range": "d2/testdata/d2compiler/TestCompile2/globs/level-filter.d2,9:2:84-9:3:85", + "value": [ + { + "string": "b", + "raw_string": "b" + } + ] + } + }, + { + "unquoted_string": { + "range": "d2/testdata/d2compiler/TestCompile2/globs/level-filter.d2,9:4:86-9:5:87", + "value": [ + { + "string": "c", + "raw_string": "c" + } + ] + } + } + ] + }, + "key_path_index": 1, + "map_key_edge_index": -1 + } + ], + "attributes": { + "label": { + "value": "b" + }, + "labelDimensions": { + "width": 0, + "height": 0 + }, + "style": { + "stroke": { + "value": "yellow" + } + }, + "near_key": null, + "shape": { + "value": "rectangle" + }, + "direction": { + "value": "" + }, + "constraint": null + }, + "zIndex": 0 + }, + { + "id": "c", + "id_val": "c", + "references": [ + { + "key": { + "range": "d2/testdata/d2compiler/TestCompile2/globs/level-filter.d2,9:0:82-9:5:87", + "path": [ + { + "unquoted_string": { + "range": "d2/testdata/d2compiler/TestCompile2/globs/level-filter.d2,9:0:82-9:1:83", + "value": [ + { + "string": "a", + "raw_string": "a" + } + ] + } + }, + { + "unquoted_string": { + "range": "d2/testdata/d2compiler/TestCompile2/globs/level-filter.d2,9:2:84-9:3:85", + "value": [ + { + "string": "b", + "raw_string": "b" + } + ] + } + }, + { + "unquoted_string": { + "range": "d2/testdata/d2compiler/TestCompile2/globs/level-filter.d2,9:4:86-9:5:87", + "value": [ + { + "string": "c", + "raw_string": "c" + } + ] + } + } + ] + }, + "key_path_index": 2, + "map_key_edge_index": -1 + } + ], + "attributes": { + "label": { + "value": "c" + }, + "labelDimensions": { + "width": 0, + "height": 0 + }, + "style": {}, + "near_key": null, + "shape": { + "value": "rectangle" + }, + "direction": { + "value": "" + }, + "constraint": null + }, + "zIndex": 0 + } + ] + }, + "err": null +}