diff --git a/ci/release/changelogs/next.md b/ci/release/changelogs/next.md
index 4aa513a11..a312ab5be 100644
--- a/ci/release/changelogs/next.md
+++ b/ci/release/changelogs/next.md
@@ -1,5 +1,7 @@
#### Features ๐
+- Multi-board SVG outputs with internal links go to their output paths [#1116](https://github.com/terrastruct/d2/pull/1116)
+
#### Improvements ๐งน
#### Bugfixes โ๏ธ
diff --git a/d2cli/main.go b/d2cli/main.go
index ae2446c95..400d81aa5 100644
--- a/d2cli/main.go
+++ b/d2cli/main.go
@@ -358,6 +358,15 @@ func compile(ctx context.Context, ms *xmain.State, plugin d2plugin.Plugin, rende
return pdf, true, nil
} else {
compileDur := time.Since(start)
+ if animateInterval <= 0 {
+ // Rename all the "root.layers.x" to the paths that the boards get output to
+ linkToOutput, err := resolveLinks("root", outputPath, diagram)
+ if err != nil {
+ return nil, false, err
+ }
+ relink(diagram, linkToOutput)
+ }
+
boards, err := render(ctx, ms, compileDur, plugin, renderOpts, inputPath, outputPath, bundle, forceAppendix, page, ruler, diagram)
if err != nil {
return nil, false, err
@@ -382,6 +391,99 @@ func compile(ctx context.Context, ms *xmain.State, plugin d2plugin.Plugin, rende
}
}
+func resolveLinks(currDiagramPath, outputPath string, diagram *d2target.Diagram) (linkToOutput map[string]string, err error) {
+ if diagram.Name != "" {
+ ext := filepath.Ext(outputPath)
+ outputPath = strings.TrimSuffix(outputPath, ext)
+ outputPath = filepath.Join(outputPath, diagram.Name)
+ outputPath += ext
+ }
+
+ boardOutputPath := outputPath
+ if len(diagram.Layers) > 0 || len(diagram.Scenarios) > 0 || len(diagram.Steps) > 0 {
+ ext := filepath.Ext(boardOutputPath)
+ boardOutputPath = strings.TrimSuffix(boardOutputPath, ext)
+ boardOutputPath = filepath.Join(boardOutputPath, "index")
+ boardOutputPath += ext
+ }
+
+ layersOutputPath := outputPath
+ if len(diagram.Scenarios) > 0 || len(diagram.Steps) > 0 {
+ ext := filepath.Ext(layersOutputPath)
+ layersOutputPath = strings.TrimSuffix(layersOutputPath, ext)
+ layersOutputPath = filepath.Join(layersOutputPath, "layers")
+ layersOutputPath += ext
+ }
+ scenariosOutputPath := outputPath
+ if len(diagram.Layers) > 0 || len(diagram.Steps) > 0 {
+ ext := filepath.Ext(scenariosOutputPath)
+ scenariosOutputPath = strings.TrimSuffix(scenariosOutputPath, ext)
+ scenariosOutputPath = filepath.Join(scenariosOutputPath, "scenarios")
+ scenariosOutputPath += ext
+ }
+ stepsOutputPath := outputPath
+ if len(diagram.Layers) > 0 || len(diagram.Scenarios) > 0 {
+ ext := filepath.Ext(stepsOutputPath)
+ stepsOutputPath = strings.TrimSuffix(stepsOutputPath, ext)
+ stepsOutputPath = filepath.Join(stepsOutputPath, "steps")
+ stepsOutputPath += ext
+ }
+
+ linkToOutput = map[string]string{currDiagramPath: boardOutputPath}
+
+ for _, dl := range diagram.Layers {
+ m, err := resolveLinks(strings.Join([]string{currDiagramPath, "layers", dl.Name}, "."), layersOutputPath, dl)
+ if err != nil {
+ return nil, err
+ }
+ for k, v := range m {
+ linkToOutput[k] = v
+ }
+ }
+ for _, dl := range diagram.Scenarios {
+ m, err := resolveLinks(strings.Join([]string{currDiagramPath, "scenarios", dl.Name}, "."), scenariosOutputPath, dl)
+ if err != nil {
+ return nil, err
+ }
+ for k, v := range m {
+ linkToOutput[k] = v
+ }
+ }
+ for _, dl := range diagram.Steps {
+ m, err := resolveLinks(strings.Join([]string{currDiagramPath, "steps", dl.Name}, "."), stepsOutputPath, dl)
+ if err != nil {
+ return nil, err
+ }
+ for k, v := range m {
+ linkToOutput[k] = v
+ }
+ }
+
+ return linkToOutput, nil
+}
+
+func relink(d *d2target.Diagram, linkToOutput map[string]string) {
+ for i, shape := range d.Shapes {
+ if shape.Link != "" {
+ for k, v := range linkToOutput {
+ if shape.Link == k {
+ d.Shapes[i].Link = v
+ break
+ }
+ }
+ }
+ }
+ for _, board := range d.Layers {
+ relink(board, linkToOutput)
+ }
+ for _, board := range d.Scenarios {
+ relink(board, linkToOutput)
+ }
+ for _, board := range d.Steps {
+ relink(board, linkToOutput)
+ }
+}
+
func render(ctx context.Context, ms *xmain.State, compileDur time.Duration, plugin d2plugin.Plugin, opts d2svg.RenderOpts, inputPath, outputPath string, bundle, forceAppendix bool, page playwright.Page, ruler *textmeasure.Ruler, diagram *d2target.Diagram) ([][]byte, error) {
if diagram.Name != "" {
ext := filepath.Ext(outputPath)
diff --git a/e2etests-cli/main_test.go b/e2etests-cli/main_test.go
index 402ce7f3e..6bad9de65 100644
--- a/e2etests-cli/main_test.go
+++ b/e2etests-cli/main_test.go
@@ -82,6 +82,33 @@ steps: {
assert.Testdata(t, ".svg", svg)
},
},
+ {
+ name: "linked-path",
+ run: func(t *testing.T, ctx context.Context, dir string, env *xos.Env) {
+ writeFile(t, dir, "linked.d2", `cat: how does the cat go? {
+ link: layers.cat
+}
+layers: {
+ cat: {
+ home: {
+ link: _
+ }
+ the cat -> meow: goes
+
+ scenarios: {
+ big cat: {
+ the cat -> roar: goes
+ }
+ }
+ }
+}
+`)
+ err := runTestMain(t, ctx, dir, env, "linked.d2")
+ assert.Success(t, err)
+
+ assert.TestdataDir(t, filepath.Join(dir, "linked"))
+ },
+ },
{
name: "with-font",
run: func(t *testing.T, ctx context.Context, dir string, env *xos.Env) {
diff --git a/e2etests-cli/testdata/TestCLI_E2E/internal_linked_pdf.exp.pdf b/e2etests-cli/testdata/TestCLI_E2E/internal_linked_pdf.exp.pdf
index 5842a8b83..4f63dfc5b 100644
Binary files a/e2etests-cli/testdata/TestCLI_E2E/internal_linked_pdf.exp.pdf and b/e2etests-cli/testdata/TestCLI_E2E/internal_linked_pdf.exp.pdf differ
diff --git a/e2etests-cli/testdata/TestCLI_E2E/linked-path/cat/big cat.exp.svg b/e2etests-cli/testdata/TestCLI_E2E/linked-path/cat/big cat.exp.svg
new file mode 100644
index 000000000..9e77770f3
--- /dev/null
+++ b/e2etests-cli/testdata/TestCLI_E2E/linked-path/cat/big cat.exp.svg
@@ -0,0 +1,102 @@
+
diff --git a/e2etests-cli/testdata/TestCLI_E2E/linked-path/cat/index.exp.svg b/e2etests-cli/testdata/TestCLI_E2E/linked-path/cat/index.exp.svg
new file mode 100644
index 000000000..0e66f755b
--- /dev/null
+++ b/e2etests-cli/testdata/TestCLI_E2E/linked-path/cat/index.exp.svg
@@ -0,0 +1,117 @@
+home
+
+
+
+
+
+
+
+
+
+
+
+the catmeow goes
+
+
+
diff --git a/e2etests-cli/testdata/TestCLI_E2E/linked-path/index.exp.svg b/e2etests-cli/testdata/TestCLI_E2E/linked-path/index.exp.svg
new file mode 100644
index 000000000..c5328ae3b
--- /dev/null
+++ b/e2etests-cli/testdata/TestCLI_E2E/linked-path/index.exp.svg
@@ -0,0 +1,110 @@
+how does the cat go?
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+