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/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/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/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..cce95b5db 100644 --- a/d2plugin/exec.go +++ b/d2plugin/exec.go @@ -10,6 +10,7 @@ import ( "time" "oss.terrastruct.com/util-go/xdefer" + "oss.terrastruct.com/util-go/xmain" "oss.terrastruct.com/d2/d2graph" ) @@ -37,6 +38,45 @@ import ( // the error to stderr. type execPlugin struct { path string + opts map[string]string +} + +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]string + err := json.Unmarshal(opts, &execOpts) + if err != nil { + return xmain.UsageErrorf("non-exec layout options given for exec") + } + + p.opts = execOpts + } + return nil } func (p execPlugin) Info(ctx context.Context) (_ *PluginInfo, err error) { @@ -73,7 +113,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, k, v) + } + cmd := exec.CommandContext(ctx, p.path, args...) buffer := bytes.Buffer{} buffer.Write(graphBytes) diff --git a/d2plugin/plugin.go b/d2plugin/plugin.go index 56798c88f..765d9f60e 100644 --- a/d2plugin/plugin.go +++ b/d2plugin/plugin.go @@ -10,6 +10,7 @@ import ( "os/exec" "oss.terrastruct.com/util-go/xexec" + "oss.terrastruct.com/util-go/xmain" "oss.terrastruct.com/d2/d2graph" ) @@ -19,10 +20,32 @@ 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": + opts.Int64("", f.Name, "", f.Default.(int64), 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 @@ -109,3 +132,15 @@ func FindPlugin(ctx context.Context, name string) (Plugin, string, error) { return &execPlugin{path: path}, path, nil } + +func ListPluginFlags(ctx context.Context) ([]PluginSpecificFlag, error) { + var out []PluginSpecificFlag + for _, p := range plugins { + flags, err := p.Flags(ctx) + if err != nil { + return nil, err + } + out = append(out, flags...) + } + return out, nil +} 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..cf857b3ee 100644 --- a/d2plugin/serve.go +++ b/d2plugin/serve.go @@ -38,6 +38,8 @@ func Serve(p Plugin) xmain.RunFunc { 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 +66,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/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/d2svg/appendix/appendix_test.go b/d2renderers/d2svg/appendix/appendix_test.go index a4105c38e..74a6f327f 100644 --- a/d2renderers/d2svg/appendix/appendix_test.go +++ b/d2renderers/d2svg/appendix/appendix_test.go @@ -121,7 +121,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, }) if !tassert.Nil(t, err) { return diff --git a/docs/examples/lib/1-d2lib/d2lib.go b/docs/examples/lib/1-d2lib/d2lib.go index 3b0114c88..af2b98674 100644 --- a/docs/examples/lib/1-d2lib/d2lib.go +++ b/docs/examples/lib/1-d2lib/d2lib.go @@ -5,6 +5,7 @@ import ( "io/ioutil" "path/filepath" + "oss.terrastruct.com/d2/d2graph" "oss.terrastruct.com/d2/d2layouts/d2dagrelayout" "oss.terrastruct.com/d2/d2lib" "oss.terrastruct.com/d2/d2renderers/d2svg" @@ -15,8 +16,11 @@ import ( // Remember to add if err != nil checks in production. func main() { ruler, _ := textmeasure.NewRuler() + defaultLayout := func(ctx context.Context, g *d2graph.Graph) error { + return d2dagrelayout.Layout(ctx, g, nil) + } diagram, _, _ := d2lib.Compile(context.Background(), "x -> y", &d2lib.CompileOptions{ - Layout: d2dagrelayout.Layout, + Layout: defaultLayout, Ruler: ruler, ThemeID: d2themescatalog.GrapeSoda.ID, }) diff --git a/docs/examples/lib/2-d2oracle/d2oracle.go b/docs/examples/lib/2-d2oracle/d2oracle.go index 1f265a05e..617943e15 100644 --- a/docs/examples/lib/2-d2oracle/d2oracle.go +++ b/docs/examples/lib/2-d2oracle/d2oracle.go @@ -17,7 +17,7 @@ func main() { // From one.go ruler, _ := textmeasure.NewRuler() _, graph, _ := d2lib.Compile(context.Background(), "x -> y", &d2lib.CompileOptions{ - Layout: d2dagrelayout.Layout, + Layout: d2dagrelayout.DefaultLayout, Ruler: ruler, ThemeID: d2themescatalog.GrapeSoda.ID, }) diff --git a/docs/examples/lib/3-lowlevel/lowlevel.go b/docs/examples/lib/3-lowlevel/lowlevel.go index 1ad6443b0..1912137f4 100644 --- a/docs/examples/lib/3-lowlevel/lowlevel.go +++ b/docs/examples/lib/3-lowlevel/lowlevel.go @@ -19,7 +19,7 @@ func main() { graph, _ := d2compiler.Compile("", strings.NewReader("x -> y"), nil) ruler, _ := textmeasure.NewRuler() _ = graph.SetDimensions(nil, ruler, nil) - _ = d2dagrelayout.Layout(context.Background(), graph) + _ = d2dagrelayout.Layout(context.Background(), graph, nil) diagram, _ := d2exporter.Export(context.Background(), graph, d2themescatalog.NeutralDefault.ID, nil) out, _ := d2svg.Render(diagram, &d2svg.RenderOpts{ Pad: d2svg.DEFAULT_PADDING, diff --git a/e2etests/e2e_test.go b/e2etests/e2e_test.go index 994770426..dcca4bba2 100644 --- a/e2etests/e2e_test.go +++ b/e2etests/e2e_test.go @@ -130,9 +130,9 @@ func run(t *testing.T, tc testCase) { for _, layoutName := range layoutsTested { var layout func(context.Context, *d2graph.Graph) error if layoutName == "dagre" { - layout = d2dagrelayout.Layout + layout = d2dagrelayout.DefaultLayout } else if layoutName == "elk" { - layout = d2elklayout.Layout + layout = d2elklayout.DefaultLayout } diagram, _, err := d2lib.Compile(ctx, tc.script, &d2lib.CompileOptions{ Ruler: ruler, diff --git a/help.go b/help.go index bd6b455d0..27f9bc22c 100644 --- a/help.go +++ b/help.go @@ -32,7 +32,7 @@ Flags: Subcommands: %[1]s layout - Lists available layout engine options with short help - %[1]s layout [name] - Display long help for a particular layout engine + %[1]s layout [name] - Display long help for a particular layout engine, including its configuration options %[1]s fmt file.d2 - Format file.d2 See more docs and the source code at https://oss.terrastruct.com/d2 @@ -75,7 +75,7 @@ Example: D2_LAYOUT=dagre d2 in.d2 out.svg Subcommands: - %s layout [layout name] - Display long help for a particular layout engine + %s layout [layout name] - Display long help for a particular layout engine, including its configuration options See more docs at https://oss.terrastruct.com/d2 `, strings.Join(pluginLines, "\n"), ms.Name) diff --git a/main.go b/main.go index 0377ac538..d9c5655b4 100644 --- a/main.go +++ b/main.go @@ -2,6 +2,7 @@ package main import ( "context" + "encoding/json" "errors" "fmt" "io" @@ -75,6 +76,11 @@ func run(ctx context.Context, ms *xmain.State) (err error) { return err } + err = populateLayoutOpts(ctx, ms) + if err != nil { + return err + } + 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) @@ -144,6 +150,11 @@ func run(ctx context.Context, ms *xmain.State) (err error) { return err } + err = parseLayoutOpts(ctx, ms, plugin) + if err != nil { + return err + } + pluginLocation := "bundled" if path != "" { pluginLocation = fmt.Sprintf("executable plugin at %s", humanPath(path)) @@ -288,3 +299,44 @@ func renameExt(fp string, newExt string) string { func DiscardSlog(ctx context.Context) context.Context { return ctxlog.With(ctx, slog.Make(sloghuman.Sink(io.Discard))) } + +func populateLayoutOpts(ctx context.Context, ms *xmain.State) error { + pluginFlags, err := d2plugin.ListPluginFlags(ctx) + if err != nil { + return err + } + + for _, f := range pluginFlags { + f.AddToOpts(ms.Opts) + // Don't pollute the main d2 flagset with these. It'll be a lot + ms.Opts.Flags.MarkHidden(f.Name) + } + + return nil +} + +func parseLayoutOpts(ctx context.Context, ms *xmain.State, plugin d2plugin.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 + } + } + + b, err := json.Marshal(opts) + if err != nil { + return err + } + + err = plugin.HydrateOpts(b) + return err +}