+
+Since interactive features obviously won't work on static export formats like PNG, they will be included automatically in an appendix when exporting to those formats, like so:
+
+
+
+This release also gives more power to configure layouts. `width` and `height` are D2 keywords which previouslly only worked on images, but now work on any non-containers. Additionally, all the layout engines have configurations exposed. D2 sets sensible defaults to each layout engine without any input, so this is meant to be an advanced feature for users who want that extra control.
+
+Happy new years!
+
+#### Features 🚀
+
+- Tooltips can be set on shapes. See [https://d2lang.com/tour/interactive](https://d2lang.com/tour/interactive). [#548](https://github.com/terrastruct/d2/pull/548)
+- Links can be set on shapes. See [https://d2lang.com/tour/interactive](https://d2lang.com/tour/interactive). [#548](https://github.com/terrastruct/d2/pull/548)
+- The `width` and `height` attributes are no longer restricted to images and can be applied to non-container shapes. [#498](https://github.com/terrastruct/d2/pull/498)
+- Layout engine options are exposed and configurable. See individual layout pages on [https://d2lang.com/tour/layouts](https://d2lang.com/tour/layouts) for list of configurations. [#563](https://github.com/terrastruct/d2/pull/563)
+
+#### Improvements 🧹
+
+- Watch mode renders fit to screen. [#560](https://github.com/terrastruct/d2/pull/560)
+
+#### Bugfixes ⛑️
+
+- Fixes rendering `class` and `table` with empty headers. [#498](https://github.com/terrastruct/d2/pull/498)
+- Fixes rendering of `sql_table` with no columns. [#553](https://github.com/terrastruct/d2/pull/553)
+- Diagram bounding boxes account for stroke widths. [#574](https://github.com/terrastruct/d2/pull/574)
+- Restricts where `near` key constant values can be used, with good error messages, instead of erroring (e.g. setting `near: top-center` on a container would cause bad layouts or error). [#538](https://github.com/terrastruct/d2/pull/538)
+- Fixes panic when images with empty labels are rendered with ELK. [#555](https://github.com/terrastruct/d2/pull/555)
+
+#### Breaking changes
+
+- For usages of D2 as a library, `d2dagrelayout.Layout` and `d2elklayout.Layout` now accept a third parameter for options. If you would like to keep the defaults, please change your code to call `dagrelayout.DefaultLayout` and `d2elklayout.DefaultLayout` respectively.
+
diff --git a/ci/release/template/man/d2.1 b/ci/release/template/man/d2.1
index a484ebe8f..b1bcb709c 100644
--- a/ci/release/template/man/d2.1
+++ b/ci/release/template/man/d2.1
@@ -82,7 +82,7 @@ Print version information and exit.
.It Ar layout
Lists available layout engine options with short help.
.It Ar layout Op Ar name
-Display long help for a particular layout engine.
+Display long help for a particular layout engine, including its configuration options.
.It Ar fmt Ar file.d2
Format
.Ar file.d2
diff --git a/ci/sub b/ci/sub
index b1ec0a8d4..9a29d9ea6 160000
--- a/ci/sub
+++ b/ci/sub
@@ -1 +1 @@
-Subproject commit b1ec0a8d430a62b7556211ed8bcd7b6e41e2362c
+Subproject commit 9a29d9ea640834905c4010c0b3d14b7301ebb6d8
diff --git a/cmd/d2plugin-dagre/main.go b/cmd/d2plugin-dagre/main.go
index 5dbed2235..96130300e 100644
--- a/cmd/d2plugin-dagre/main.go
+++ b/cmd/d2plugin-dagre/main.go
@@ -9,5 +9,5 @@ import (
)
func main() {
- xmain.Main(d2plugin.Serve(d2plugin.DagrePlugin))
+ xmain.Main(d2plugin.Serve(&d2plugin.DagrePlugin))
}
diff --git a/d2chaos/d2chaos_test.go b/d2chaos/d2chaos_test.go
index d5ac6a17d..8b73e4e82 100644
--- a/d2chaos/d2chaos_test.go
+++ b/d2chaos/d2chaos_test.go
@@ -123,7 +123,7 @@ func test(t *testing.T, textPath, text string) {
err = g.SetDimensions(nil, ruler, nil)
assert.Nil(t, err)
- err = d2dagrelayout.Layout(ctx, g)
+ err = d2dagrelayout.DefaultLayout(ctx, g)
if err != nil {
t.Fatal(err)
}
diff --git a/d2compiler/compile.go b/d2compiler/compile.go
index 51014adc0..21e4c1cc4 100644
--- a/d2compiler/compile.go
+++ b/d2compiler/compile.go
@@ -836,7 +836,7 @@ func (c *compiler) validateKey(obj *d2graph.Object, m *d2ast.Map, mk *d2ast.Key)
switch strings.ToLower(obj.Attributes.Shape.Value) {
case d2target.ShapeSQLTable, d2target.ShapeClass:
default:
- if len(obj.Children) > 0 && (reserved == "width" || reserved == "height") {
+ if len(obj.Children) > 0 && !(len(obj.Children) == 1 && obj.ChildrenArray[0].ID == "style") && (reserved == "width" || reserved == "height") {
c.errorf(mk.Range.Start, mk.Range.End, fmt.Sprintf("%s cannot be used on container: %s", reserved, obj.AbsID()))
}
}
diff --git a/d2compiler/compile_test.go b/d2compiler/compile_test.go
index ddb51cf8c..81db413f2 100644
--- a/d2compiler/compile_test.go
+++ b/d2compiler/compile_test.go
@@ -208,6 +208,15 @@ d2/testdata/d2compiler/TestCompile/no_dimensions_on_containers.d2:25:3: width ca
d2/testdata/d2compiler/TestCompile/no_dimensions_on_containers.d2:26:3: height cannot be used on container: containers.oval container
d2/testdata/d2compiler/TestCompile/no_dimensions_on_containers.d2:36:3: width cannot be used on container: containers.hexagon container
d2/testdata/d2compiler/TestCompile/no_dimensions_on_containers.d2:37:3: height cannot be used on container: containers.hexagon container
+`,
+ },
+ {
+ name: "dimension_with_style",
+
+ text: `x: {
+ width: 200
+ style.multiple: true
+}
`,
},
{
diff --git a/d2exporter/export_test.go b/d2exporter/export_test.go
index ca813d2b1..613090be5 100644
--- a/d2exporter/export_test.go
+++ b/d2exporter/export_test.go
@@ -239,7 +239,7 @@ func run(t *testing.T, tc testCase) {
err = g.SetDimensions(nil, ruler, nil)
assert.JSON(t, nil, err)
- err = d2sequence.Layout(ctx, g, d2dagrelayout.Layout)
+ err = d2sequence.Layout(ctx, g, d2dagrelayout.DefaultLayout)
if err != nil {
t.Fatal(err)
}
diff --git a/d2graph/d2graph.go b/d2graph/d2graph.go
index 7bd59ec6a..21361304a 100644
--- a/d2graph/d2graph.go
+++ b/d2graph/d2graph.go
@@ -1067,6 +1067,22 @@ func (g *Graph) SetDimensions(mtexts []*d2target.MText, ruler *textmeasure.Ruler
if obj.Attributes.Height != nil {
desiredHeight, _ = strconv.Atoi(obj.Attributes.Height.Value)
}
+
+ if obj.Attributes.Label.Value == "" &&
+ obj.Attributes.Shape.Value != d2target.ShapeImage &&
+ obj.Attributes.Shape.Value != d2target.ShapeSQLTable &&
+ obj.Attributes.Shape.Value != d2target.ShapeClass {
+ obj.Width = DEFAULT_SHAPE_PADDING
+ obj.Height = DEFAULT_SHAPE_PADDING
+ if desiredWidth != 0 {
+ obj.Width = float64(desiredWidth)
+ }
+ if desiredHeight != 0 {
+ obj.Height = float64(desiredHeight)
+ }
+ continue
+ }
+
shapeType := strings.ToLower(obj.Attributes.Shape.Value)
labelDims, err := obj.GetLabelSize(mtexts, ruler, fontFamily)
diff --git a/d2layouts/d2dagrelayout/layout.go b/d2layouts/d2dagrelayout/layout.go
index 68d8ec0f6..9969d2080 100644
--- a/d2layouts/d2dagrelayout/layout.go
+++ b/d2layouts/d2dagrelayout/layout.go
@@ -30,6 +30,16 @@ var setupJS string
//go:embed dagre.js
var dagreJS string
+type ConfigurableOpts struct {
+ NodeSep int `json:"nodesep"`
+ EdgeSep int `json:"edgesep"`
+}
+
+var DefaultOpts = ConfigurableOpts{
+ NodeSep: 60,
+ EdgeSep: 40,
+}
+
type DagreNode struct {
ID string `json:"id"`
X float64 `json:"x"`
@@ -42,16 +52,23 @@ type DagreEdge struct {
Points []*geo.Point `json:"points"`
}
-type dagreGraphAttrs struct {
+type dagreOpts struct {
// for a top to bottom graph: ranksep is y spacing, nodesep is x spacing, edgesep is x spacing
ranksep int
- edgesep int
- nodesep int
// graph direction: tb (top to bottom)| bt | lr | rl
rankdir string
+
+ ConfigurableOpts
}
-func Layout(ctx context.Context, g *d2graph.Graph) (err error) {
+func DefaultLayout(ctx context.Context, g *d2graph.Graph) (err error) {
+ return Layout(ctx, g, nil)
+}
+
+func Layout(ctx context.Context, g *d2graph.Graph, opts *ConfigurableOpts) (err error) {
+ if opts == nil {
+ opts = &DefaultOpts
+ }
defer xdefer.Errorf(&err, "failed to dagre layout")
debugJS := false
@@ -63,9 +80,11 @@ func Layout(ctx context.Context, g *d2graph.Graph) (err error) {
return err
}
- rootAttrs := dagreGraphAttrs{
- edgesep: 40,
- nodesep: 60,
+ rootAttrs := dagreOpts{
+ ConfigurableOpts: ConfigurableOpts{
+ EdgeSep: opts.EdgeSep,
+ NodeSep: opts.NodeSep,
+ },
}
isHorizontal := false
switch g.Root.Attributes.Direction.Value {
@@ -266,7 +285,7 @@ func Layout(ctx context.Context, g *d2graph.Graph) (err error) {
return nil
}
-func setGraphAttrs(attrs dagreGraphAttrs) string {
+func setGraphAttrs(attrs dagreOpts) string {
return fmt.Sprintf(`g.setGraph({
ranksep: %d,
edgesep: %d,
@@ -275,8 +294,8 @@ func setGraphAttrs(attrs dagreGraphAttrs) string {
});
`,
attrs.ranksep,
- attrs.edgesep,
- attrs.nodesep,
+ attrs.ConfigurableOpts.EdgeSep,
+ attrs.ConfigurableOpts.NodeSep,
attrs.rankdir,
)
}
diff --git a/d2layouts/d2elklayout/layout.go b/d2layouts/d2elklayout/layout.go
index ada0ca0e7..d9b333385 100644
--- a/d2layouts/d2elklayout/layout.go
+++ b/d2layouts/d2elklayout/layout.go
@@ -31,23 +31,23 @@ var elkJS string
var setupJS string
type ELKNode struct {
- ID string `json:"id"`
- X float64 `json:"x"`
- Y float64 `json:"y"`
- Width float64 `json:"width"`
- Height float64 `json:"height"`
- Children []*ELKNode `json:"children,omitempty"`
- Labels []*ELKLabel `json:"labels,omitempty"`
- LayoutOptions *ELKLayoutOptions `json:"layoutOptions,omitempty"`
+ ID string `json:"id"`
+ X float64 `json:"x"`
+ Y float64 `json:"y"`
+ Width float64 `json:"width"`
+ Height float64 `json:"height"`
+ Children []*ELKNode `json:"children,omitempty"`
+ Labels []*ELKLabel `json:"labels,omitempty"`
+ LayoutOptions *elkOpts `json:"layoutOptions,omitempty"`
}
type ELKLabel struct {
- Text string `json:"text"`
- X float64 `json:"x"`
- Y float64 `json:"y"`
- Width float64 `json:"width"`
- Height float64 `json:"height"`
- LayoutOptions *ELKLayoutOptions `json:"layoutOptions,omitempty"`
+ Text string `json:"text"`
+ X float64 `json:"x"`
+ Y float64 `json:"y"`
+ Width float64 `json:"width"`
+ Height float64 `json:"height"`
+ LayoutOptions *elkOpts `json:"layoutOptions,omitempty"`
}
type ELKPoint struct {
@@ -71,26 +71,46 @@ type ELKEdge struct {
}
type ELKGraph struct {
- ID string `json:"id"`
- LayoutOptions *ELKLayoutOptions `json:"layoutOptions"`
- Children []*ELKNode `json:"children,omitempty"`
- Edges []*ELKEdge `json:"edges,omitempty"`
+ ID string `json:"id"`
+ LayoutOptions *elkOpts `json:"layoutOptions"`
+ Children []*ELKNode `json:"children,omitempty"`
+ Edges []*ELKEdge `json:"edges,omitempty"`
}
-type ELKLayoutOptions struct {
- Algorithm string `json:"elk.algorithm,omitempty"`
- HierarchyHandling string `json:"elk.hierarchyHandling,omitempty"`
- NodeSpacing float64 `json:"spacing.nodeNodeBetweenLayers,omitempty"`
- Padding string `json:"elk.padding,omitempty"`
- EdgeNodeSpacing float64 `json:"spacing.edgeNodeBetweenLayers,omitempty"`
- Direction string `json:"elk.direction"`
- SelfLoopSpacing float64 `json:"elk.spacing.nodeSelfLoop"`
- InlineEdgeLabels bool `json:"elk.edgeLabels.inline,omitempty"`
- ConsiderModelOrder string `json:"elk.layered.considerModelOrder.strategy,omitempty"`
- ForceNodeModelOrder bool `json:"elk.layered.crossingMinimization.forceNodeModelOrder,omitempty"`
+type ConfigurableOpts struct {
+ Algorithm string `json:"elk.algorithm,omitempty"`
+ NodeSpacing int `json:"spacing.nodeNodeBetweenLayers,omitempty"`
+ Padding string `json:"elk.padding,omitempty"`
+ EdgeNodeSpacing int `json:"spacing.edgeNodeBetweenLayers,omitempty"`
+ SelfLoopSpacing int `json:"elk.spacing.nodeSelfLoop"`
}
-func Layout(ctx context.Context, g *d2graph.Graph) (err error) {
+var DefaultOpts = ConfigurableOpts{
+ Algorithm: "layered",
+ NodeSpacing: 100.0,
+ Padding: "[top=75,left=75,bottom=75,right=75]",
+ EdgeNodeSpacing: 50.0,
+ SelfLoopSpacing: 50.0,
+}
+
+type elkOpts struct {
+ Direction string `json:"elk.direction"`
+ HierarchyHandling string `json:"elk.hierarchyHandling,omitempty"`
+ InlineEdgeLabels bool `json:"elk.edgeLabels.inline,omitempty"`
+ ForceNodeModelOrder bool `json:"elk.layered.crossingMinimization.forceNodeModelOrder,omitempty"`
+ ConsiderModelOrder string `json:"elk.layered.considerModelOrder.strategy,omitempty"`
+
+ ConfigurableOpts
+}
+
+func DefaultLayout(ctx context.Context, g *d2graph.Graph) (err error) {
+ return Layout(ctx, g, nil)
+}
+
+func Layout(ctx context.Context, g *d2graph.Graph, opts *ConfigurableOpts) (err error) {
+ if opts == nil {
+ opts = &DefaultOpts
+ }
defer xdefer.Errorf(&err, "failed to ELK layout")
vm := goja.New()
@@ -109,13 +129,15 @@ func Layout(ctx context.Context, g *d2graph.Graph) (err error) {
elkGraph := &ELKGraph{
ID: "root",
- LayoutOptions: &ELKLayoutOptions{
- Algorithm: "layered",
+ LayoutOptions: &elkOpts{
HierarchyHandling: "INCLUDE_CHILDREN",
- NodeSpacing: 100.0,
- EdgeNodeSpacing: 50.0,
- SelfLoopSpacing: 50.0,
ConsiderModelOrder: "NODES_AND_EDGES",
+ ConfigurableOpts: ConfigurableOpts{
+ Algorithm: opts.Algorithm,
+ NodeSpacing: opts.NodeSpacing,
+ EdgeNodeSpacing: opts.EdgeNodeSpacing,
+ SelfLoopSpacing: opts.SelfLoopSpacing,
+ },
},
}
switch g.Root.Attributes.Direction.Value {
@@ -160,9 +182,11 @@ func Layout(ctx context.Context, g *d2graph.Graph) (err error) {
}
if len(obj.ChildrenArray) > 0 {
- n.LayoutOptions = &ELKLayoutOptions{
- Padding: "[top=75,left=75,bottom=75,right=75]",
+ n.LayoutOptions = &elkOpts{
ForceNodeModelOrder: true,
+ ConfigurableOpts: ConfigurableOpts{
+ Padding: opts.Padding,
+ },
}
}
@@ -193,7 +217,7 @@ func Layout(ctx context.Context, g *d2graph.Graph) (err error) {
Text: edge.Attributes.Label.Value,
Width: float64(edge.LabelDimensions.Width),
Height: float64(edge.LabelDimensions.Height),
- LayoutOptions: &ELKLayoutOptions{
+ LayoutOptions: &elkOpts{
InlineEdgeLabels: true,
},
})
diff --git a/d2lib/c.go b/d2lib/c.go
deleted file mode 100644
index c62ef79ae..000000000
--- a/d2lib/c.go
+++ /dev/null
@@ -1,7 +0,0 @@
-package d2lib
-
-import "oss.terrastruct.com/d2/d2layouts/d2dagrelayout"
-
-func init() {
- dagreLayout = d2dagrelayout.Layout
-}
diff --git a/d2lib/d2.go b/d2lib/d2.go
index 936124b9f..bfe9713a9 100644
--- a/d2lib/d2.go
+++ b/d2lib/d2.go
@@ -9,6 +9,7 @@ import (
"oss.terrastruct.com/d2/d2compiler"
"oss.terrastruct.com/d2/d2exporter"
"oss.terrastruct.com/d2/d2graph"
+ "oss.terrastruct.com/d2/d2layouts/d2dagrelayout"
"oss.terrastruct.com/d2/d2layouts/d2near"
"oss.terrastruct.com/d2/d2layouts/d2sequence"
"oss.terrastruct.com/d2/d2renderers/d2fonts"
@@ -74,12 +75,12 @@ func Compile(ctx context.Context, input string, opts *CompileOptions) (*d2target
func getLayout(opts *CompileOptions) (func(context.Context, *d2graph.Graph) error, error) {
if opts.Layout != nil {
return opts.Layout, nil
- } else if os.Getenv("D2_LAYOUT") == "dagre" && dagreLayout != nil {
- return dagreLayout, nil
+ } else if os.Getenv("D2_LAYOUT") == "dagre" {
+ defaultLayout := func(ctx context.Context, g *d2graph.Graph) error {
+ return d2dagrelayout.Layout(ctx, g, nil)
+ }
+ return defaultLayout, nil
} else {
return nil, errors.New("no available layout")
}
}
-
-// See c.go
-var dagreLayout func(context.Context, *d2graph.Graph) error
diff --git a/d2plugin/exec.go b/d2plugin/exec.go
index b4929dcc2..b76a802d0 100644
--- a/d2plugin/exec.go
+++ b/d2plugin/exec.go
@@ -7,9 +7,11 @@ import (
"errors"
"fmt"
"os/exec"
+ "strconv"
"time"
"oss.terrastruct.com/util-go/xdefer"
+ "oss.terrastruct.com/util-go/xmain"
"oss.terrastruct.com/d2/d2graph"
)
@@ -37,9 +39,65 @@ import (
// the error to stderr.
type execPlugin struct {
path string
+ opts map[string]string
+ info *PluginInfo
}
-func (p execPlugin) Info(ctx context.Context) (_ *PluginInfo, err error) {
+func (p *execPlugin) Flags(ctx context.Context) (_ []PluginSpecificFlag, err error) {
+ ctx, cancel := context.WithTimeout(ctx, time.Second*10)
+ defer cancel()
+ cmd := exec.CommandContext(ctx, p.path, "flags")
+ defer xdefer.Errorf(&err, "failed to run %v", cmd.Args)
+
+ stdout, err := cmd.Output()
+ if err != nil {
+ ee := &exec.ExitError{}
+ if errors.As(err, &ee) && len(ee.Stderr) > 0 {
+ return nil, fmt.Errorf("%v\nstderr:\n%s", ee, ee.Stderr)
+ }
+ return nil, err
+ }
+
+ var flags []PluginSpecificFlag
+
+ err = json.Unmarshal(stdout, &flags)
+ if err != nil {
+ return nil, fmt.Errorf("failed to unmarshal json: %w", err)
+ }
+
+ return flags, nil
+}
+
+func (p *execPlugin) HydrateOpts(opts []byte) error {
+ if opts != nil {
+ var execOpts map[string]interface{}
+ err := json.Unmarshal(opts, &execOpts)
+ if err != nil {
+ return xmain.UsageErrorf("non-exec layout options given for exec")
+ }
+
+ allString := make(map[string]string)
+ for k, v := range execOpts {
+ switch vt := v.(type) {
+ case string:
+ allString[k] = vt
+ case int64:
+ allString[k] = strconv.Itoa(int(vt))
+ case float64:
+ allString[k] = strconv.Itoa(int(vt))
+ }
+ }
+
+ p.opts = allString
+ }
+ return nil
+}
+
+func (p *execPlugin) Info(ctx context.Context) (_ *PluginInfo, err error) {
+ if p.info != nil {
+ return p.info, nil
+ }
+
ctx, cancel := context.WithTimeout(ctx, time.Second*10)
defer cancel()
cmd := exec.CommandContext(ctx, p.path, "info")
@@ -61,10 +119,11 @@ func (p execPlugin) Info(ctx context.Context) (_ *PluginInfo, err error) {
return nil, fmt.Errorf("failed to unmarshal json: %w", err)
}
+ p.info = &info
return &info, nil
}
-func (p execPlugin) Layout(ctx context.Context, g *d2graph.Graph) error {
+func (p *execPlugin) Layout(ctx context.Context, g *d2graph.Graph) error {
ctx, cancel := context.WithTimeout(ctx, time.Minute)
defer cancel()
@@ -73,7 +132,11 @@ func (p execPlugin) Layout(ctx context.Context, g *d2graph.Graph) error {
return err
}
- cmd := exec.CommandContext(ctx, p.path, "layout")
+ args := []string{"layout"}
+ for k, v := range p.opts {
+ args = append(args, fmt.Sprintf("--%s", k), v)
+ }
+ cmd := exec.CommandContext(ctx, p.path, args...)
buffer := bytes.Buffer{}
buffer.Write(graphBytes)
@@ -95,7 +158,7 @@ func (p execPlugin) Layout(ctx context.Context, g *d2graph.Graph) error {
return nil
}
-func (p execPlugin) PostProcess(ctx context.Context, in []byte) ([]byte, error) {
+func (p *execPlugin) PostProcess(ctx context.Context, in []byte) ([]byte, error) {
ctx, cancel := context.WithTimeout(ctx, time.Minute)
defer cancel()
diff --git a/d2plugin/plugin.go b/d2plugin/plugin.go
index 56798c88f..a6eefdf7e 100644
--- a/d2plugin/plugin.go
+++ b/d2plugin/plugin.go
@@ -7,9 +7,11 @@ package d2plugin
import (
"context"
+ "encoding/json"
"os/exec"
"oss.terrastruct.com/util-go/xexec"
+ "oss.terrastruct.com/util-go/xmain"
"oss.terrastruct.com/d2/d2graph"
)
@@ -19,10 +21,40 @@ import (
// See plugin_* files for the plugins available for bundling.
var plugins []Plugin
+type PluginSpecificFlag struct {
+ Name string
+ Type string
+ Default interface{}
+ Usage string
+ // Must match the tag in the opt
+ Tag string
+}
+
+func (f *PluginSpecificFlag) AddToOpts(opts *xmain.Opts) {
+ switch f.Type {
+ case "string":
+ opts.String("", f.Name, "", f.Default.(string), f.Usage)
+ case "int64":
+ var val int64
+ switch defaultType := f.Default.(type) {
+ case int64:
+ val = defaultType
+ case float64:
+ // json unmarshals numbers to float64
+ val = int64(defaultType)
+ }
+ opts.Int64("", f.Name, "", val, f.Usage)
+ }
+}
+
type Plugin interface {
// Info returns the current info information of the plugin.
Info(context.Context) (*PluginInfo, error)
+ Flags(context.Context) ([]PluginSpecificFlag, error)
+
+ HydrateOpts([]byte) error
+
// Layout runs the plugin's autolayout algorithm on the input graph
// and returns a new graph with the computed placements.
Layout(context.Context, *d2graph.Graph) error
@@ -48,36 +80,54 @@ type PluginInfo struct {
const binaryPrefix = "d2plugin-"
-func ListPlugins(ctx context.Context) ([]*PluginInfo, error) {
+func ListPlugins(ctx context.Context) ([]Plugin, error) {
// 1. Run Info on all bundled plugins in the global plugins array.
// - set Type for each bundled plugin to "bundled".
// 2. Iterate through directories in $PATH and look for executables within these
// directories with the prefix d2plugin-*
// 3. Run each plugin binary with the argument info. e.g. d2plugin-dagre info
- var infoSlice []*PluginInfo
-
- for _, p := range plugins {
- info, err := p.Info(ctx)
- if err != nil {
- return nil, err
- }
- info.Type = "bundled"
- infoSlice = append(infoSlice, info)
- }
+ var ps []Plugin
+ ps = append(ps, plugins...)
matches, err := xexec.SearchPath(binaryPrefix)
if err != nil {
return nil, err
}
+BINARY_PLUGINS_LOOP:
for _, path := range matches {
p := &execPlugin{path: path}
info, err := p.Info(ctx)
if err != nil {
return nil, err
}
- info.Type = "binary"
- info.Path = path
+ for _, p2 := range ps {
+ info2, err := p2.Info(ctx)
+ if err != nil {
+ return nil, err
+ }
+ if info.Name == info2.Name {
+ continue BINARY_PLUGINS_LOOP
+ }
+ }
+ ps = append(ps, p)
+ }
+ return ps, nil
+}
+
+func ListPluginInfos(ctx context.Context, ps []Plugin) ([]*PluginInfo, error) {
+ var infoSlice []*PluginInfo
+ for _, p := range ps {
+ info, err := p.Info(ctx)
+ if err != nil {
+ return nil, err
+ }
+ if ep, ok := p.(*execPlugin); ok {
+ info.Type = "binary"
+ info.Path = ep.path
+ } else {
+ info.Type = "bundled"
+ }
infoSlice = append(infoSlice, info)
}
@@ -91,21 +141,53 @@ func ListPlugins(ctx context.Context) ([]*PluginInfo, error) {
// **NOTE** When D2 upgrades to go 1.19, remember to ignore exec.ErrDot
// 3. If such a binary is found, it builds an execPlugin in exec.go
// to get a plugin implementation around the binary and returns it.
-func FindPlugin(ctx context.Context, name string) (Plugin, string, error) {
- for _, p := range plugins {
+func FindPlugin(ctx context.Context, ps []Plugin, name string) (Plugin, error) {
+ for _, p := range ps {
info, err := p.Info(ctx)
if err != nil {
- return nil, "", err
+ return nil, err
}
if info.Name == name {
- return p, "", nil
+ return p, nil
+ }
+ }
+ return nil, exec.ErrNotFound
+}
+
+func ListPluginFlags(ctx context.Context, ps []Plugin) ([]PluginSpecificFlag, error) {
+ var out []PluginSpecificFlag
+ for _, p := range ps {
+ flags, err := p.Flags(ctx)
+ if err != nil {
+ return nil, err
+ }
+ out = append(out, flags...)
+ }
+
+ return out, nil
+}
+
+func HydratePluginOpts(ctx context.Context, ms *xmain.State, plugin Plugin) error {
+ opts := make(map[string]interface{})
+ flags, err := plugin.Flags(ctx)
+ if err != nil {
+ return err
+ }
+ for _, f := range flags {
+ switch f.Type {
+ case "string":
+ val, _ := ms.Opts.Flags.GetString(f.Name)
+ opts[f.Tag] = val
+ case "int64":
+ val, _ := ms.Opts.Flags.GetInt64(f.Name)
+ opts[f.Tag] = val
}
}
- path, err := exec.LookPath(binaryPrefix + name)
+ b, err := json.Marshal(opts)
if err != nil {
- return nil, "", err
+ return err
}
- return &execPlugin{path: path}, path, nil
+ return plugin.HydrateOpts(b)
}
diff --git a/d2plugin/plugin_dagre.go b/d2plugin/plugin_dagre.go
index 228011ac0..c3f16a8f1 100644
--- a/d2plugin/plugin_dagre.go
+++ b/d2plugin/plugin_dagre.go
@@ -4,36 +4,82 @@ package d2plugin
import (
"context"
+ "encoding/json"
+ "fmt"
"oss.terrastruct.com/d2/d2graph"
"oss.terrastruct.com/d2/d2layouts/d2dagrelayout"
+ "oss.terrastruct.com/util-go/xmain"
)
var DagrePlugin = dagrePlugin{}
func init() {
- plugins = append(plugins, DagrePlugin)
+ plugins = append(plugins, &DagrePlugin)
}
-type dagrePlugin struct{}
+type dagrePlugin struct {
+ opts *d2dagrelayout.ConfigurableOpts
+}
+
+func (p dagrePlugin) Flags(context.Context) ([]PluginSpecificFlag, error) {
+ return []PluginSpecificFlag{
+ {
+ Name: "dagre-nodesep",
+ Type: "int64",
+ Default: int64(d2dagrelayout.DefaultOpts.NodeSep),
+ Usage: "number of pixels that separate nodes horizontally.",
+ Tag: "nodesep",
+ },
+ {
+ Name: "dagre-edgesep",
+ Type: "int64",
+ Default: int64(d2dagrelayout.DefaultOpts.EdgeSep),
+ Usage: "number of pixels that separate edges horizontally.",
+ Tag: "edgesep",
+ },
+ }, nil
+}
+
+func (p *dagrePlugin) HydrateOpts(opts []byte) error {
+ if opts != nil {
+ var dagreOpts d2dagrelayout.ConfigurableOpts
+ err := json.Unmarshal(opts, &dagreOpts)
+ if err != nil {
+ return xmain.UsageErrorf("non-dagre layout options given for dagre")
+ }
+
+ p.opts = &dagreOpts
+ }
+ return nil
+}
+
+func (p dagrePlugin) Info(ctx context.Context) (*PluginInfo, error) {
+ opts := xmain.NewOpts(nil, nil, nil)
+ flags, err := p.Flags(ctx)
+ if err != nil {
+ return nil, err
+ }
+ for _, f := range flags {
+ f.AddToOpts(opts)
+ }
-func (p dagrePlugin) Info(context.Context) (*PluginInfo, error) {
return &PluginInfo{
Name: "dagre",
ShortHelp: "The directed graph layout library Dagre",
- LongHelp: `dagre is a directed graph layout library for JavaScript.
+ LongHelp: fmt.Sprintf(`dagre is a directed graph layout library for JavaScript.
See https://github.com/dagrejs/dagre
-The implementation of this plugin is at: https://github.com/terrastruct/d2/tree/master/d2plugin/d2dagrelayout
-note: dagre is the primary layout algorithm for text to diagram generator Mermaid.js.
- See https://github.com/mermaid-js/mermaid
- We have a useful comparison at https://text-to-diagram.com/?example=basic&a=d2&b=mermaid
-`,
+Flags correspond to ones found at https://github.com/dagrejs/dagre/wiki. See dagre's reference for more on each.
+
+Flags:
+%s
+`, opts.Defaults()),
}, nil
}
func (p dagrePlugin) Layout(ctx context.Context, g *d2graph.Graph) error {
- return d2dagrelayout.Layout(ctx, g)
+ return d2dagrelayout.Layout(ctx, g, p.opts)
}
func (p dagrePlugin) PostProcess(ctx context.Context, in []byte) ([]byte, error) {
diff --git a/d2plugin/plugin_elk.go b/d2plugin/plugin_elk.go
index 6a8567f5e..631e953ba 100644
--- a/d2plugin/plugin_elk.go
+++ b/d2plugin/plugin_elk.go
@@ -4,31 +4,103 @@ package d2plugin
import (
"context"
+ "encoding/json"
+ "fmt"
"oss.terrastruct.com/d2/d2graph"
"oss.terrastruct.com/d2/d2layouts/d2elklayout"
+ "oss.terrastruct.com/util-go/xmain"
)
var ELKPlugin = elkPlugin{}
func init() {
- plugins = append(plugins, ELKPlugin)
+ plugins = append(plugins, &ELKPlugin)
}
-type elkPlugin struct{}
+type elkPlugin struct {
+ opts *d2elklayout.ConfigurableOpts
+}
-func (p elkPlugin) Info(context.Context) (*PluginInfo, error) {
+func (p elkPlugin) Flags(context.Context) ([]PluginSpecificFlag, error) {
+ return []PluginSpecificFlag{
+ {
+ Name: "elk-algorithm",
+ Type: "string",
+ Default: d2elklayout.DefaultOpts.Algorithm,
+ Usage: "layout algorithm",
+ Tag: "elk.algorithm",
+ },
+ {
+ Name: "elk-nodeNodeBetweenLayers",
+ Type: "int64",
+ Default: int64(d2elklayout.DefaultOpts.NodeSpacing),
+ Usage: "the spacing to be preserved between any pair of nodes of two adjacent layers",
+ Tag: "spacing.nodeNodeBetweenLayers",
+ },
+ {
+ Name: "elk-padding",
+ Type: "string",
+ Default: d2elklayout.DefaultOpts.Padding,
+ Usage: "the padding to be left to a parent element’s border when placing child elements",
+ Tag: "elk.padding",
+ },
+ {
+ Name: "elk-edgeNodeBetweenLayers",
+ Type: "int64",
+ Default: int64(d2elklayout.DefaultOpts.EdgeNodeSpacing),
+ Usage: "the spacing to be preserved between nodes and edges that are routed next to the node’s layer",
+ Tag: "spacing.edgeNodeBetweenLayers",
+ },
+ {
+ Name: "elk-nodeSelfLoop",
+ Type: "int64",
+ Default: int64(d2elklayout.DefaultOpts.SelfLoopSpacing),
+ Usage: "spacing to be preserved between a node and its self loops",
+ Tag: "elk.spacing.nodeSelfLoop",
+ },
+ }, nil
+}
+
+func (p *elkPlugin) HydrateOpts(opts []byte) error {
+ if opts != nil {
+ var elkOpts d2elklayout.ConfigurableOpts
+ err := json.Unmarshal(opts, &elkOpts)
+ if err != nil {
+ return xmain.UsageErrorf("non-ELK layout options given for ELK")
+ }
+
+ p.opts = &elkOpts
+ }
+ return nil
+}
+
+func (p elkPlugin) Info(ctx context.Context) (*PluginInfo, error) {
+ opts := xmain.NewOpts(nil, nil, nil)
+ flags, err := p.Flags(ctx)
+ if err != nil {
+ return nil, err
+ }
+ for _, f := range flags {
+ f.AddToOpts(opts)
+ }
return &PluginInfo{
Name: "elk",
ShortHelp: "Eclipse Layout Kernel (ELK) with the Layered algorithm.",
- LongHelp: `ELK is a layout engine offered by Eclipse.
+ LongHelp: fmt.Sprintf(`ELK is a layout engine offered by Eclipse.
Originally written in Java, it has been ported to Javascript and cross-compiled into D2.
-See https://github.com/kieler/elkjs for more.`,
+See https://github.com/kieler/elkjs for more.
+
+Flags correspond to ones found at https://www.eclipse.org/elk/reference.html. See ELK's reference for more on each.
+
+Flags:
+%s
+`, opts.Defaults()),
}, nil
}
func (p elkPlugin) Layout(ctx context.Context, g *d2graph.Graph) error {
- return d2elklayout.Layout(ctx, g)
+ return d2elklayout.Layout(ctx, g, p.opts)
}
func (p elkPlugin) PostProcess(ctx context.Context, in []byte) ([]byte, error) {
diff --git a/d2plugin/serve.go b/d2plugin/serve.go
index b13680746..acc697a96 100644
--- a/d2plugin/serve.go
+++ b/d2plugin/serve.go
@@ -21,6 +21,13 @@ import (
// Also see execPlugin in exec.go for the d2 binary plugin protocol.
func Serve(p Plugin) xmain.RunFunc {
return func(ctx context.Context, ms *xmain.State) (err error) {
+ fs, err := p.Flags(ctx)
+ if err != nil {
+ return err
+ }
+ for _, f := range fs {
+ f.AddToOpts(ms.Opts)
+ }
err = ms.Opts.Flags.Parse(ms.Opts.Args)
if !errors.Is(err, pflag.ErrHelp) && err != nil {
return xmain.UsageErrorf("failed to parse flags: %v", err)
@@ -34,10 +41,17 @@ func Serve(p Plugin) xmain.RunFunc {
return xmain.UsageErrorf("expected first argument to be subcmd name")
}
+ err = HydratePluginOpts(ctx, ms, p)
+ if err != nil {
+ return err
+ }
+
subcmd := ms.Opts.Flags.Arg(0)
switch subcmd {
case "info":
return info(ctx, p, ms)
+ case "flags":
+ return flags(ctx, p, ms)
case "layout":
return layout(ctx, p, ms)
case "postprocess":
@@ -64,6 +78,22 @@ func info(ctx context.Context, p Plugin, ms *xmain.State) error {
return nil
}
+func flags(ctx context.Context, p Plugin, ms *xmain.State) error {
+ flags, err := p.Flags(ctx)
+ if err != nil {
+ return err
+ }
+ b, err := json.Marshal(flags)
+ if err != nil {
+ return err
+ }
+ _, err = ms.Stdout.Write(b)
+ if err != nil {
+ return err
+ }
+ return nil
+}
+
func layout(ctx context.Context, p Plugin, ms *xmain.State) error {
in, err := io.ReadAll(ms.Stdin)
if err != nil {
diff --git a/d2renderers/d2sketch/rough.js b/d2renderers/d2sketch/rough.js
index 04eabe71a..b19e8c7d7 100644
--- a/d2renderers/d2sketch/rough.js
+++ b/d2renderers/d2sketch/rough.js
@@ -1261,7 +1261,7 @@ class B {
n = [];
if (!t) return this._drawable("path", n, s);
const i = (function (t, e) {
- t = (t || "").replace(/\n/g, " ").replace(/(-\s)/g, "-").replace("/(ss)/g", " ");
+ t = (t || "").replace(/\n/g, " ").replace(/(-\s)/g, "-").replace("/(\s\s)/g", " ");
let s = new o(t);
if (e.simplification) {
const t = new r(s.linearPoints, s.closed).fit(e.simplification);
diff --git a/d2renderers/d2sketch/sketch_test.go b/d2renderers/d2sketch/sketch_test.go
index e033bedfe..c2b8dadea 100644
--- a/d2renderers/d2sketch/sketch_test.go
+++ b/d2renderers/d2sketch/sketch_test.go
@@ -314,7 +314,7 @@ func run(t *testing.T, tc testCase) {
diagram, _, err := d2lib.Compile(ctx, tc.script, &d2lib.CompileOptions{
Ruler: ruler,
ThemeID: 0,
- Layout: d2dagrelayout.Layout,
+ Layout: d2dagrelayout.DefaultLayout,
FontFamily: go2.Pointer(d2fonts.HandDrawn),
})
if !tassert.Nil(t, err) {
diff --git a/d2renderers/d2sketch/testdata/all_shapes/sketch.exp.svg b/d2renderers/d2sketch/testdata/all_shapes/sketch.exp.svg
index 5b8429973..efb2c8aba 100644
--- a/d2renderers/d2sketch/testdata/all_shapes/sketch.exp.svg
+++ b/d2renderers/d2sketch/testdata/all_shapes/sketch.exp.svg
@@ -1,8 +1,9 @@