Merge pull request #563 from alixander/layout-configs

Pass layout configs
This commit is contained in:
Alexander Wang 2022-12-30 14:31:05 -08:00 committed by GitHub
commit 8708bceef0
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
21 changed files with 398 additions and 90 deletions

View file

@ -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

View file

@ -9,5 +9,5 @@ import (
)
func main() {
xmain.Main(d2plugin.Serve(d2plugin.DagrePlugin))
xmain.Main(d2plugin.Serve(&d2plugin.DagrePlugin))
}

View file

@ -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)
}

View file

@ -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)
}

View file

@ -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,
)
}

View file

@ -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,
},
})

View file

@ -1,7 +0,0 @@
package d2lib
import "oss.terrastruct.com/d2/d2layouts/d2dagrelayout"
func init() {
dagreLayout = d2dagrelayout.Layout
}

View file

@ -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

View file

@ -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)

View file

@ -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
}

View file

@ -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) {

View file

@ -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 elements 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 nodes 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) {

View file

@ -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 {

View file

@ -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) {

View file

@ -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

View file

@ -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,
})

View file

@ -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,
})

View file

@ -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,

View file

@ -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,

View file

@ -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)

52
main.go
View file

@ -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
}