diff --git a/ci/release/changelogs/next.md b/ci/release/changelogs/next.md index a8d6aaefb..65780a72c 100644 --- a/ci/release/changelogs/next.md +++ b/ci/release/changelogs/next.md @@ -1,53 +1,9 @@ #### Features ๐Ÿš€ -- 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) - - `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) -- Icons: border-radius should work on icon [#2409](https://github.com/terrastruct/d2/issues/2409) -- Diagram legends are implemented [#2416](https://github.com/terrastruct/d2/pull/2416) -- Ability to fade background shape on multiple style [#2509](https://github.com/terrastruct/d2/pull/2509) - #### Improvements ๐Ÿงน -- CLI: - - Support `validate` command. [#2415](https://github.com/terrastruct/d2/pull/2415) - - Watch mode ignores backup files (e.g. files created by certain editors like Helix). [#2131](https://github.com/terrastruct/d2/issues/2131) - - Support for `--omit-version` flag. [#2377](https://github.com/terrastruct/d2/issues/2377) - - Casing is ignored for plugin names [#2486](https://github.com/terrastruct/d2/pull/2486) -- Compiler: - - `link`s can be set to root path, e.g. `/xyz`. [#2357](https://github.com/terrastruct/d2/issues/2357) - - When importing a file, attempt resolving substitutions at the imported file scope first [#2482](https://github.com/terrastruct/d2/pull/2482) -- Parser: - - impose max key length. It's almost certainly a mistake if an ID gets too long, e.g. missing quotes [#2465](https://github.com/terrastruct/d2/pull/2465) -- Render: - - horizontal padding added for connection labels [#2461](https://github.com/terrastruct/d2/pull/2461) - #### Bugfixes โ›‘๏ธ -- Compiler: - - fixes panic when `sql_shape` shape value had mixed casing [#2349](https://github.com/terrastruct/d2/pull/2349) - - fixes panic when importing from a file with spread substitutions in `vars` [#2427](https://github.com/terrastruct/d2/pull/2427) - - fixes support for `center` in `d2-config` [#2360](https://github.com/terrastruct/d2/pull/2360) - - fixes panic when comment lines appear in arrays [#2378](https://github.com/terrastruct/d2/pull/2378) - - fixes inconsistencies when objects were double quoted [#2390](https://github.com/terrastruct/d2/pull/2390) - - fixes globs not applying to spread substitutions [#2426](https://github.com/terrastruct/d2/issues/2426) - - fixes panic when classes were mixed with layers incorrectly [#2448](https://github.com/terrastruct/d2/pull/2448) - - fixes panic when gradient colors are used in sketch mode [#2481](https://github.com/terrastruct/d2/pull/2487) - - fixes panic using glob ampersand filters with composite values [#2489](https://github.com/terrastruct/d2/pull/2489) -- Formatter: - - fixes substitutions in quotes surrounded by text [#2462](https://github.com/terrastruct/d2/pull/2462) -- CLI: fetch and render remote images of mimetype octet-stream correctly [#2370](https://github.com/terrastruct/d2/pull/2370) -- Composition: - - spread importing scenarios/steps was not inheriting correctly [#2460](https://github.com/terrastruct/d2/pull/2460) - - imported fields were not merging with current fields/edges [#2464](https://github.com/terrastruct/d2/pull/2464) -- Markdown: fixes nested var substitutions not working [#2456](https://github.com/terrastruct/d2/pull/2456) -- d2js: handle unicode characters [#2393](https://github.com/terrastruct/d2/pull/2393) - --- For the latest d2.js changes, see separate [changelog](https://github.com/terrastruct/d2/blob/master/d2js/js/CHANGELOG.md). diff --git a/ci/release/changelogs/v0.7.0.md b/ci/release/changelogs/v0.7.0.md new file mode 100644 index 000000000..b1828ca77 --- /dev/null +++ b/ci/release/changelogs/v0.7.0.md @@ -0,0 +1,59 @@ +#### Features ๐Ÿš€ + +- 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) + - `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) +- Icons: + - border-radius should work on icon [#2409](https://github.com/terrastruct/d2/issues/2409) +- Misc: + - Diagram legends are implemented [#2416](https://github.com/terrastruct/d2/pull/2416) + +#### Improvements ๐Ÿงน + +- CLI: + - Support `validate` command. [#2415](https://github.com/terrastruct/d2/pull/2415) + - Watch mode ignores backup files (e.g. files created by certain editors like Helix). [#2131](https://github.com/terrastruct/d2/issues/2131) + - Support for `--omit-version` flag. [#2377](https://github.com/terrastruct/d2/issues/2377) + - Casing is ignored for plugin names [#2486](https://github.com/terrastruct/d2/pull/2486) +- Compiler: + - `link`s can be set to root path, e.g. `/xyz`. [#2357](https://github.com/terrastruct/d2/issues/2357) + - When importing a file, attempt resolving substitutions at the imported file scope first [#2482](https://github.com/terrastruct/d2/pull/2482) + - validate gradient color stops. [#2492](https://github.com/terrastruct/d2/pull/2492) +- Parser: + - impose max key length. It's almost certainly a mistake if an ID gets too long, e.g. missing quotes [#2465](https://github.com/terrastruct/d2/pull/2465) +- Render: + - horizontal padding added for connection labels [#2461](https://github.com/terrastruct/d2/pull/2461) + +#### Bugfixes โ›‘๏ธ + +- Compiler: + - fixes panic when `sql_shape` shape value had mixed casing [#2349](https://github.com/terrastruct/d2/pull/2349) + - fixes panic when importing from a file with spread substitutions in `vars` [#2427](https://github.com/terrastruct/d2/pull/2427) + - fixes support for `center` in `d2-config` [#2360](https://github.com/terrastruct/d2/pull/2360) + - fixes panic when comment lines appear in arrays [#2378](https://github.com/terrastruct/d2/pull/2378) + - fixes inconsistencies when objects were double quoted [#2390](https://github.com/terrastruct/d2/pull/2390) + - fixes globs not applying to spread substitutions [#2426](https://github.com/terrastruct/d2/issues/2426) + - fixes panic when classes were mixed with layers incorrectly [#2448](https://github.com/terrastruct/d2/pull/2448) + - fixes panic when gradient colors are used in sketch mode [#2481](https://github.com/terrastruct/d2/pull/2487) + - fixes panic using glob ampersand filters with composite values [#2489](https://github.com/terrastruct/d2/pull/2489) + - fixes leaf ampersand filter when used with imports [#2494](https://github.com/terrastruct/d2/pull/2494) +- Formatter: + - fixes substitutions in quotes surrounded by text [#2462](https://github.com/terrastruct/d2/pull/2462) +- CLI: + - fetch and render remote images of mimetype octet-stream correctly [#2370](https://github.com/terrastruct/d2/pull/2370) +- Composition: + - spread importing scenarios/steps was not inheriting correctly [#2460](https://github.com/terrastruct/d2/pull/2460) + - imported fields were not merging with current fields/edges [#2464](https://github.com/terrastruct/d2/pull/2464) +- Markdown: + - fixes nested var substitutions not working [#2456](https://github.com/terrastruct/d2/pull/2456) + +--- + +For the latest d2.js changes, see separate [changelog](https://github.com/terrastruct/d2/blob/master/d2js/js/CHANGELOG.md). diff --git a/ci/release/release.sh b/ci/release/release.sh index c85120722..f9bc7e5b6 100755 --- a/ci/release/release.sh +++ b/ci/release/release.sh @@ -3,24 +3,4 @@ set -eu cd -- "$(dirname "$0")/../.." . "./ci/sub/lib.sh" - -NPM_VERSION="" - -for arg in "$@"; do - case "$arg" in - --npm-version=*) - NPM_VERSION="${arg#*=}" - ;; - esac -done - -if [ -z "$NPM_VERSION" ]; then - flag_errusage "--npm-version is required" -fi - ./ci/sub/release/release.sh "$@" - -if [ -n "$NPM_VERSION" ]; then - ./ci/release/release-js.sh --version="$NPM_VERSION" -fi - diff --git a/d2compiler/compile.go b/d2compiler/compile.go index 51aae2a2a..8c3599409 100644 --- a/d2compiler/compile.go +++ b/d2compiler/compile.go @@ -208,55 +208,48 @@ func findFieldAST(ast *d2ast.Map, f *d2ir.Field) *d2ast.Map { curr = d2ir.ParentField(curr) } - currAST := ast - for len(path) > 0 { - head := path[0] - found := false - for _, n := range currAST.Nodes { - if n.MapKey == nil { - continue - } - if n.MapKey.Key == nil { - continue - } - if len(n.MapKey.Key.Path) != 1 { - continue - } - head2 := n.MapKey.Key.Path[0].Unbox().ScalarString() - if head == head2 { - currAST = n.MapKey.Value.Map - // The BaseAST is only used for making edits to the AST (through d2oracle) - // If there's no Map for a given board, either it's an empty layer or set to an import - // Either way, in order to make edits, it needs to be expanded into a Map to add lines to - if currAST == nil { - n.MapKey.Value.Map = &d2ast.Map{ - Range: d2ast.MakeRange(",1:0:0-1:0:0"), - } - if n.MapKey.Value.Import != nil { - imp := &d2ast.Import{ - Range: d2ast.MakeRange(",1:0:0-1:0:0"), - Spread: true, - Pre: n.MapKey.Value.Import.Pre, - Path: n.MapKey.Value.Import.Path, - } - n.MapKey.Value.Map.Nodes = append(n.MapKey.Value.Map.Nodes, d2ast.MapNodeBox{ - Import: imp, - }) + return _findFieldAST(ast, path) +} - } - currAST = n.MapKey.Value.Map - } - found = true - break - } - } - if !found { - return nil - } - path = path[1:] +func _findFieldAST(ast *d2ast.Map, path []string) *d2ast.Map { + if len(path) == 0 { + return ast } - return currAST + head := path[0] + remainingPath := path[1:] + + for i := range ast.Nodes { + if ast.Nodes[i].MapKey == nil || ast.Nodes[i].MapKey.Key == nil || len(ast.Nodes[i].MapKey.Key.Path) != 1 { + continue + } + + head2 := ast.Nodes[i].MapKey.Key.Path[0].Unbox().ScalarString() + if head == head2 { + if ast.Nodes[i].MapKey.Value.Map == nil { + ast.Nodes[i].MapKey.Value.Map = &d2ast.Map{ + Range: d2ast.MakeRange(",1:0:0-1:0:0"), + } + if ast.Nodes[i].MapKey.Value.Import != nil { + imp := &d2ast.Import{ + Range: d2ast.MakeRange(",1:0:0-1:0:0"), + Spread: true, + Pre: ast.Nodes[i].MapKey.Value.Import.Pre, + Path: ast.Nodes[i].MapKey.Value.Import.Path, + } + ast.Nodes[i].MapKey.Value.Map.Nodes = append(ast.Nodes[i].MapKey.Value.Map.Nodes, d2ast.MapNodeBox{ + Import: imp, + }) + } + } + + if result := _findFieldAST(ast.Nodes[i].MapKey.Value.Map, remainingPath); result != nil { + return result + } + } + } + + return nil } type compiler struct { diff --git a/d2compiler/compile_test.go b/d2compiler/compile_test.go index 702295099..8a3224aab 100644 --- a/d2compiler/compile_test.go +++ b/d2compiler/compile_test.go @@ -1726,6 +1726,33 @@ k expErr: `d2/testdata/d2compiler/TestCompile/composite-glob-filter.d2:3:3: glob filters cannot be composites d2/testdata/d2compiler/TestCompile/composite-glob-filter.d2:3:3: glob filters cannot be composites`, }, + { + name: "imported-glob-leaf-filter", + + text: ` +***: { + &leaf: true + style: { + font-size: 30 + } +} +a: { + ...@x +} +`, + files: map[string]string{ + "x.d2": ` +b +`, + }, + assertions: func(t *testing.T, g *d2graph.Graph) { + assert.Equal(t, 2, len(g.Objects)) + assert.Equal(t, "b", g.Objects[0].Label.Value) + assert.Equal(t, "a", g.Objects[1].Label.Value) + assert.Equal(t, "30", g.Objects[0].Style.FontSize.Value) + assert.Equal(t, (*d2graph.Scalar)(nil), g.Objects[1].Style.FontSize) + }, + }, { name: "import-nested-var", @@ -3929,6 +3956,14 @@ svc_1.t2 -> b: do with B tassert.Equal(t, "d2/testdata/d2compiler/TestCompile/meow.d2", g.Layers[0].Layers[0].AST.Range.Path) }, }, + { + name: "invalid_gradient_color_stop", + text: ` + x + x.style.fill: "linear-gradient(#ggg, #000)" + `, + expErr: `d2/testdata/d2compiler/TestCompile/invalid_gradient_color_stop.d2:3:19: expected "fill" to be a valid named color ("orange"), a hex code ("#f0ff3a"), or a gradient ("linear-gradient(red, blue)")`, + }, } for _, tc := range testCases { diff --git a/d2ir/compile.go b/d2ir/compile.go index 91c0739e8..6467fef1a 100644 --- a/d2ir/compile.go +++ b/d2ir/compile.go @@ -981,7 +981,7 @@ func (c *compiler) _ampersandPropertyFilter(propName string, value string, node c.errorf(key, `&leaf must be "true" or "false", got %q`, value) return false } - isLeaf := node.Map() == nil || !node.Map().IsContainer() + isLeaf := node.Map() == nil || !c.IsContainer(node.Map()) return isLeaf == boolVal case "connected": boolVal, err := strconv.ParseBool(value) diff --git a/d2ir/d2ir.go b/d2ir/d2ir.go index 290e679de..d336d7894 100644 --- a/d2ir/d2ir.go +++ b/d2ir/d2ir.go @@ -705,7 +705,7 @@ func (m *Map) FieldCountRecursive() int { return acc } -func (m *Map) IsContainer() bool { +func (c *compiler) IsContainer(m *Map) bool { if m == nil { return false } @@ -714,6 +714,20 @@ func (m *Map) IsContainer() bool { for _, ref := range f.References { if ref.Primary() && ref.Context_.Key != nil && ref.Context_.Key.Value.Map != nil { for _, n := range ref.Context_.Key.Value.Map.Nodes { + if n.MapKey == nil { + if n.Import != nil { + impn, ok := c.peekImport(n.Import) + if ok { + for _, f := range impn.Fields { + _, isReserved := d2ast.ReservedKeywords[f.Name.ScalarString()] + if !(isReserved && f.Name.IsUnquoted()) { + return true + } + } + } + } + continue + } if len(n.MapKey.Edges) > 0 { return true } @@ -1333,7 +1347,7 @@ func (m *Map) createEdge(eid *EdgeID, refctx *RefContext, gctx *globContext, c * if refctx.Edge.Src.HasMultiGlob() { // If src has a double glob we only select leafs, those without children. - if src.Map().IsContainer() { + if c.IsContainer(src.Map()) { continue } if NodeBoardKind(src) != "" || ParentBoard(src) != ParentBoard(dst) { @@ -1342,7 +1356,7 @@ func (m *Map) createEdge(eid *EdgeID, refctx *RefContext, gctx *globContext, c * } if refctx.Edge.Dst.HasMultiGlob() { // If dst has a double glob we only select leafs, those without children. - if dst.Map().IsContainer() { + if c.IsContainer(dst.Map()) { continue } if NodeBoardKind(dst) != "" || ParentBoard(src) != ParentBoard(dst) { diff --git a/d2ir/import.go b/d2ir/import.go index 2d6630203..dff3edbee 100644 --- a/d2ir/import.go +++ b/d2ir/import.go @@ -124,6 +124,53 @@ func (c *compiler) __import(imp *d2ast.Import) (*Map, bool) { return ir, true } +func (c *compiler) peekImport(imp *d2ast.Import) (*Map, bool) { + impPath := imp.PathWithPre() + if impPath == "" && imp.Range != (d2ast.Range{}) { + return nil, false + } + + if len(c.importStack) > 0 { + if path.Ext(impPath) != ".d2" { + impPath += ".d2" + } + + if !filepath.IsAbs(impPath) { + impPath = path.Join(path.Dir(c.importStack[len(c.importStack)-1]), impPath) + } + } + + var f fs.File + var err error + if c.fs == nil { + f, err = os.Open(impPath) + } else { + f, err = c.fs.Open(impPath) + } + if err != nil { + return nil, false + } + defer f.Close() + + // Use a separate parse error to avoid polluting the main one + localErr := &d2parser.ParseError{} + ast, err := d2parser.Parse(impPath, f, &d2parser.ParseOptions{ + UTF16Pos: c.utf16Pos, + ParseError: localErr, + }) + if err != nil { + return nil, false + } + + ir := &Map{} + ir.initRoot() + ir.parent.(*Field).References[0].Context_.Scope = ast + + c.compileMap(ir, ast, ast) + + return ir, true +} + func nilScopeMap(n Node) { switch n := n.(type) { case *Map: diff --git a/d2oracle/edit.go b/d2oracle/edit.go index 6256403d4..bdb548548 100644 --- a/d2oracle/edit.go +++ b/d2oracle/edit.go @@ -3324,3 +3324,135 @@ func filterReservedPath(path []*d2ast.StringBox) (filtered []*d2ast.StringBox) { } return } + +func UpdateImport(dsl, path string, newPath *string) (_ string, err error) { + if newPath == nil { + defer xdefer.Errorf(&err, "failed to remove import %#v", path) + } else { + defer xdefer.Errorf(&err, "failed to update import from %#v to %#v", path, *newPath) + } + + ast, err := d2parser.Parse("", strings.NewReader(dsl), nil) + if err != nil { + return "", err + } + + _updateImport(ast, path, newPath) + + return d2format.Format(ast), nil +} + +func _updateImport(m *d2ast.Map, oldPath string, newPath *string) { + for i := 0; i < len(m.Nodes); i++ { + node := m.Nodes[i] + + if node.Import != nil { + importPath := node.Import.PathWithPre() + if matchesImportPath(importPath, oldPath) { + if newPath == nil { + if node.Import.Spread { + m.Nodes = append(m.Nodes[:i], m.Nodes[i+1:]...) + i-- + } else { + node.Import = nil + } + } else { + updateImportPath(node.Import, getNewImportPath(importPath, oldPath, *newPath)) + } + continue + } + } + + if node.MapKey != nil { + if node.MapKey.Value.Import != nil { + importPath := node.MapKey.Value.Import.PathWithPre() + if matchesImportPath(importPath, oldPath) { + if newPath == nil { + if node.MapKey.Value.Import.Spread && node.MapKey.Value.Map == nil { + m.Nodes = append(m.Nodes[:i], m.Nodes[i+1:]...) + i-- + } else { + node.MapKey.Value.Import = nil + } + } else { + updateImportPath(node.MapKey.Value.Import, getNewImportPath(importPath, oldPath, *newPath)) + } + } + } + + primaryImport := node.MapKey.Primary.Unbox() + if primaryImport != nil { + value, ok := primaryImport.(d2ast.Value) + if ok { + importBox := d2ast.MakeValueBox(value) + if importBox.Import != nil { + importPath := importBox.Import.PathWithPre() + if matchesImportPath(importPath, oldPath) { + if newPath == nil { + node.MapKey.Primary = d2ast.ScalarBox{} + } else { + updateImportPath(importBox.Import, getNewImportPath(importPath, oldPath, *newPath)) + } + } + } + } + } + + if node.MapKey.Value.Map != nil { + _updateImport(node.MapKey.Value.Map, oldPath, newPath) + } + } + } +} + +func updateImportPath(imp *d2ast.Import, newPath string) { + var pre string + pathPart := newPath + + for i, r := range newPath { + if r != '.' && r != '/' { + pre = newPath[:i] + pathPart = newPath[i:] + break + } + } + + if pre == "" && len(newPath) > 0 && (newPath[0] == '.' || newPath[0] == '/') { + pre = newPath + pathPart = "" + } + + imp.Pre = pre + + if pathPart != "" { + if len(imp.Path) > 0 { + imp.Path[0] = d2ast.MakeValueBox(d2ast.RawString(pathPart, true)).StringBox() + } else { + imp.Path = []*d2ast.StringBox{ + d2ast.MakeValueBox(d2ast.RawString(pathPart, true)).StringBox(), + } + } + } else if len(imp.Path) == 0 { + imp.Path = []*d2ast.StringBox{ + d2ast.MakeValueBox(d2ast.RawString("", true)).StringBox(), + } + } +} + +func matchesImportPath(importPath, oldPath string) bool { + isDir := strings.HasSuffix(oldPath, "/") + if isDir { + return strings.HasPrefix(importPath, oldPath) + } + return importPath == oldPath +} + +func getNewImportPath(importPath, oldPath, newPath string) string { + isOldDir := strings.HasSuffix(oldPath, "/") + isNewDir := strings.HasSuffix(newPath, "/") + if isOldDir && isNewDir { + relPath := importPath[len(oldPath):] + return newPath + relPath + } + return newPath +} diff --git a/d2oracle/edit_test.go b/d2oracle/edit_test.go index 9e6466568..c7e8db0ec 100644 --- a/d2oracle/edit_test.go +++ b/d2oracle/edit_test.go @@ -2775,6 +2775,40 @@ scenarios: { Metricbeat.style.stroke: red } } +`, + }, + { + name: "set-style-in-layer", + text: `hey + +layers: { + k: { + b: {style.stroke: "#969db4"} + } +} + +layers: { + x: { + y + } +} +`, + boardPath: []string{"x"}, + key: `y.style.fill`, + value: go2.Pointer(`#ff0000`), + exp: `hey + +layers: { + k: { + b: {style.stroke: "#969db4"} + } +} + +layers: { + x: { + y: {style.fill: "#ff0000"} + } +} `, }, } @@ -9563,3 +9597,288 @@ scenarios: { }) } } + +func TestUpdateImport(t *testing.T) { + t.Parallel() + + testCases := []struct { + name string + boardPath []string + text string + fsTexts map[string]string + path string + newPath *string + + expErr string + exp string + assertions func(t *testing.T, g *d2graph.Graph) + }{ + { + name: "remove_import", + text: `x: @meow +y +`, + path: "meow", + newPath: nil, + exp: `x +y +`, + }, + { + name: "remove_spread_import", + text: `x +...@meow +y`, + path: "meow", + newPath: nil, + exp: `x + +y +`, + }, + { + name: "update_import", + text: `x: @meow +y +`, + path: "meow", + newPath: go2.Pointer("woof"), + exp: `x: @woof +y +`, + }, + { + name: "update_import_with_dir", + text: `x: @foo/meow +y +`, + path: "foo/meow", + newPath: go2.Pointer("bar/woof"), + exp: `x: @bar/woof +y +`, + }, + { + name: "update_spread_import", + text: `x +...@meow +y +`, + path: "meow", + newPath: go2.Pointer("woof"), + exp: `x +...@woof +y +`, + }, + { + name: "no_matching_import", + text: `x: @cat +y +`, + path: "meow", + newPath: go2.Pointer("woof"), + exp: `x: @cat +y +`, + }, + { + name: "nested_import", + text: `container: { + x: @meow + y +} +`, + path: "meow", + newPath: go2.Pointer("woof"), + exp: `container: { + x: @woof + y +} +`, + }, + { + name: "remove_nested_import", + text: `container: { + x: @meow + y +} +`, + path: "meow", + newPath: nil, + exp: `container: { + x + y +} +`, + }, + { + name: "multiple_imports", + text: `x: @meow +y: @meow +z +`, + path: "meow", + newPath: go2.Pointer("woof"), + exp: `x: @woof +y: @woof +z +`, + }, + { + name: "mixed_imports", + text: `x: @meow +y +...@meow +z +`, + path: "meow", + newPath: go2.Pointer("woof"), + exp: `x: @woof +y +...@woof +z +`, + }, + { + name: "in_layer", + text: `x + +layers: { + y: { + z: @meow + } +} +`, + path: "meow", + newPath: go2.Pointer("woof"), + exp: `x + +layers: { + y: { + z: @woof + } +} +`, + }, + { + name: "layer_import", + text: `x + +layers: { + y: { + ...@meow + } +} +`, + path: "meow", + newPath: go2.Pointer("woof"), + exp: `x + +layers: { + y: { + ...@woof + } +} +`, + }, + { + name: "update_directory_import", + text: `x: @foo/bar +y: @foo/baz +z +`, + path: "foo/", + newPath: go2.Pointer("woof/"), + exp: `x: @woof/bar +y: @woof/baz +z +`, + }, + { + name: "remove_directory_import", + text: `x: @foo/bar +y: @foo/baz +z +`, + path: "foo/", + newPath: nil, + exp: `x +y +z +`, + }, + { + name: "update_deep_directory_paths", + text: `x: @foo/bar/baz +y: @foo/qux/quux +z +`, + path: "foo/", + newPath: go2.Pointer("woof/"), + exp: `x: @woof/bar/baz +y: @woof/qux/quux +z +`, + }, + { + name: "update_relative_import-1", + text: `x: @../meow +y +`, + path: "../meow", + newPath: go2.Pointer("../woof"), + exp: `x: @../woof +y +`, + }, + { + name: "update_relative_import-2", + text: `x: @../meow +y +`, + path: "../meow", + newPath: go2.Pointer("woof"), + exp: `x: @woof +y +`, + }, + { + name: "update_relative_import-3", + text: `x: @../meow +y +`, + path: "../meow", + newPath: go2.Pointer("../meow/woof"), + exp: `x: @../meow/woof +y +`, + }, + { + name: "update_relative_import-4", + text: `x: @../meow +y +`, + path: "../meow", + newPath: go2.Pointer("../g/woof"), + exp: `x: @../g/woof +y +`, + }, + } + + for _, tc := range testCases { + tc := tc + t.Run(tc.name, func(t *testing.T) { + t.Parallel() + + got, err := d2oracle.UpdateImport(tc.text, tc.path, tc.newPath) + if err != nil { + t.Fatal(err) + } + if got != tc.exp { + t.Fatalf("tc.exp != newText:\n%s", got) + } + }) + } +} diff --git a/d2oracle/get.go b/d2oracle/get.go index c39e88469..15df0767a 100644 --- a/d2oracle/get.go +++ b/d2oracle/get.go @@ -48,51 +48,35 @@ func ReplaceBoardNode(ast, ast2 *d2ast.Map, boardPath []string) bool { return false } - findMap := func(root *d2ast.Map, name string) *d2ast.Map { - for _, n := range root.Nodes { - if n.MapKey != nil && n.MapKey.Key != nil && n.MapKey.Key.Path[0].Unbox().ScalarString() == name { - return n.MapKey.Value.Map - } - } - return nil - } + return replaceBoardNodeInMap(ast, ast2, boardPath, "layers") || + replaceBoardNodeInMap(ast, ast2, boardPath, "scenarios") || + replaceBoardNodeInMap(ast, ast2, boardPath, "steps") +} - layersMap := findMap(ast, "layers") - scenariosMap := findMap(ast, "scenarios") - stepsMap := findMap(ast, "steps") +func replaceBoardNodeInMap(ast, ast2 *d2ast.Map, boardPath []string, boardType string) bool { + var matches []*d2ast.Map - if layersMap != nil { - m := findMap(layersMap, boardPath[0]) - if m != nil { - if len(boardPath) > 1 { - return ReplaceBoardNode(m, ast2, boardPath[1:]) - } else { - m.Nodes = ast2.Nodes - return true - } + for _, n := range ast.Nodes { + if n.MapKey != nil && n.MapKey.Key != nil && + n.MapKey.Key.Path[0].Unbox().ScalarString() == boardType && + n.MapKey.Value.Map != nil { + matches = append(matches, n.MapKey.Value.Map) } } - if scenariosMap != nil { - m := findMap(scenariosMap, boardPath[0]) - if m != nil { - if len(boardPath) > 1 { - return ReplaceBoardNode(m, ast2, boardPath[1:]) - } else { - m.Nodes = ast2.Nodes - return true - } - } - } - - if stepsMap != nil { - m := findMap(stepsMap, boardPath[0]) - if m != nil { - if len(boardPath) > 1 { - return ReplaceBoardNode(m, ast2, boardPath[1:]) - } else { - m.Nodes = ast2.Nodes - return true + for _, boardMap := range matches { + for _, n := range boardMap.Nodes { + if n.MapKey != nil && n.MapKey.Key != nil && + n.MapKey.Key.Path[0].Unbox().ScalarString() == boardPath[0] && + n.MapKey.Value.Map != nil { + if len(boardPath) > 1 { + if ReplaceBoardNode(n.MapKey.Value.Map, ast2, boardPath[1:]) { + return true + } + } else { + n.MapKey.Value.Map.Nodes = ast2.Nodes + return true + } } } } diff --git a/d2renderers/d2sketch/testdata/all_shapes/sketch.exp.svg b/d2renderers/d2sketch/testdata/all_shapes/sketch.exp.svg index daba1652f..a07f5b49f 100644 --- a/d2renderers/d2sketch/testdata/all_shapes/sketch.exp.svg +++ b/d2renderers/d2sketch/testdata/all_shapes/sketch.exp.svg @@ -1,4 +1,4 @@ -