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 .It Ar layout
Lists available layout engine options with short help. Lists available layout engine options with short help.
.It Ar layout Op Ar name .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 .It Ar fmt Ar file.d2
Format Format
.Ar file.d2 .Ar file.d2

View file

@ -9,5 +9,5 @@ import (
) )
func main() { 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) err = g.SetDimensions(nil, ruler, nil)
assert.Nil(t, err) assert.Nil(t, err)
err = d2dagrelayout.Layout(ctx, g) err = d2dagrelayout.DefaultLayout(ctx, g)
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }

View file

@ -239,7 +239,7 @@ func run(t *testing.T, tc testCase) {
err = g.SetDimensions(nil, ruler, nil) err = g.SetDimensions(nil, ruler, nil)
assert.JSON(t, nil, err) assert.JSON(t, nil, err)
err = d2sequence.Layout(ctx, g, d2dagrelayout.Layout) err = d2sequence.Layout(ctx, g, d2dagrelayout.DefaultLayout)
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }

View file

@ -30,6 +30,16 @@ var setupJS string
//go:embed dagre.js //go:embed dagre.js
var dagreJS string var dagreJS string
type ConfigurableOpts struct {
NodeSep int `json:"nodesep"`
EdgeSep int `json:"edgesep"`
}
var DefaultOpts = ConfigurableOpts{
NodeSep: 60,
EdgeSep: 40,
}
type DagreNode struct { type DagreNode struct {
ID string `json:"id"` ID string `json:"id"`
X float64 `json:"x"` X float64 `json:"x"`
@ -42,16 +52,23 @@ type DagreEdge struct {
Points []*geo.Point `json:"points"` 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 // for a top to bottom graph: ranksep is y spacing, nodesep is x spacing, edgesep is x spacing
ranksep int ranksep int
edgesep int
nodesep int
// graph direction: tb (top to bottom)| bt | lr | rl // graph direction: tb (top to bottom)| bt | lr | rl
rankdir string 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") defer xdefer.Errorf(&err, "failed to dagre layout")
debugJS := false debugJS := false
@ -63,9 +80,11 @@ func Layout(ctx context.Context, g *d2graph.Graph) (err error) {
return err return err
} }
rootAttrs := dagreGraphAttrs{ rootAttrs := dagreOpts{
edgesep: 40, ConfigurableOpts: ConfigurableOpts{
nodesep: 60, EdgeSep: opts.EdgeSep,
NodeSep: opts.NodeSep,
},
} }
isHorizontal := false isHorizontal := false
switch g.Root.Attributes.Direction.Value { switch g.Root.Attributes.Direction.Value {
@ -266,7 +285,7 @@ func Layout(ctx context.Context, g *d2graph.Graph) (err error) {
return nil return nil
} }
func setGraphAttrs(attrs dagreGraphAttrs) string { func setGraphAttrs(attrs dagreOpts) string {
return fmt.Sprintf(`g.setGraph({ return fmt.Sprintf(`g.setGraph({
ranksep: %d, ranksep: %d,
edgesep: %d, edgesep: %d,
@ -275,8 +294,8 @@ func setGraphAttrs(attrs dagreGraphAttrs) string {
}); });
`, `,
attrs.ranksep, attrs.ranksep,
attrs.edgesep, attrs.ConfigurableOpts.EdgeSep,
attrs.nodesep, attrs.ConfigurableOpts.NodeSep,
attrs.rankdir, attrs.rankdir,
) )
} }

View file

@ -38,7 +38,7 @@ type ELKNode struct {
Height float64 `json:"height"` Height float64 `json:"height"`
Children []*ELKNode `json:"children,omitempty"` Children []*ELKNode `json:"children,omitempty"`
Labels []*ELKLabel `json:"labels,omitempty"` Labels []*ELKLabel `json:"labels,omitempty"`
LayoutOptions *ELKLayoutOptions `json:"layoutOptions,omitempty"` LayoutOptions *elkOpts `json:"layoutOptions,omitempty"`
} }
type ELKLabel struct { type ELKLabel struct {
@ -47,7 +47,7 @@ type ELKLabel struct {
Y float64 `json:"y"` Y float64 `json:"y"`
Width float64 `json:"width"` Width float64 `json:"width"`
Height float64 `json:"height"` Height float64 `json:"height"`
LayoutOptions *ELKLayoutOptions `json:"layoutOptions,omitempty"` LayoutOptions *elkOpts `json:"layoutOptions,omitempty"`
} }
type ELKPoint struct { type ELKPoint struct {
@ -72,25 +72,45 @@ type ELKEdge struct {
type ELKGraph struct { type ELKGraph struct {
ID string `json:"id"` ID string `json:"id"`
LayoutOptions *ELKLayoutOptions `json:"layoutOptions"` LayoutOptions *elkOpts `json:"layoutOptions"`
Children []*ELKNode `json:"children,omitempty"` Children []*ELKNode `json:"children,omitempty"`
Edges []*ELKEdge `json:"edges,omitempty"` Edges []*ELKEdge `json:"edges,omitempty"`
} }
type ELKLayoutOptions struct { type ConfigurableOpts struct {
Algorithm string `json:"elk.algorithm,omitempty"` Algorithm string `json:"elk.algorithm,omitempty"`
HierarchyHandling string `json:"elk.hierarchyHandling,omitempty"` NodeSpacing int `json:"spacing.nodeNodeBetweenLayers,omitempty"`
NodeSpacing float64 `json:"spacing.nodeNodeBetweenLayers,omitempty"`
Padding string `json:"elk.padding,omitempty"` Padding string `json:"elk.padding,omitempty"`
EdgeNodeSpacing float64 `json:"spacing.edgeNodeBetweenLayers,omitempty"` EdgeNodeSpacing int `json:"spacing.edgeNodeBetweenLayers,omitempty"`
Direction string `json:"elk.direction"` SelfLoopSpacing int `json:"elk.spacing.nodeSelfLoop"`
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"`
} }
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") defer xdefer.Errorf(&err, "failed to ELK layout")
vm := goja.New() vm := goja.New()
@ -109,13 +129,15 @@ func Layout(ctx context.Context, g *d2graph.Graph) (err error) {
elkGraph := &ELKGraph{ elkGraph := &ELKGraph{
ID: "root", ID: "root",
LayoutOptions: &ELKLayoutOptions{ LayoutOptions: &elkOpts{
Algorithm: "layered",
HierarchyHandling: "INCLUDE_CHILDREN", HierarchyHandling: "INCLUDE_CHILDREN",
NodeSpacing: 100.0,
EdgeNodeSpacing: 50.0,
SelfLoopSpacing: 50.0,
ConsiderModelOrder: "NODES_AND_EDGES", ConsiderModelOrder: "NODES_AND_EDGES",
ConfigurableOpts: ConfigurableOpts{
Algorithm: opts.Algorithm,
NodeSpacing: opts.NodeSpacing,
EdgeNodeSpacing: opts.EdgeNodeSpacing,
SelfLoopSpacing: opts.SelfLoopSpacing,
},
}, },
} }
switch g.Root.Attributes.Direction.Value { 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 { if len(obj.ChildrenArray) > 0 {
n.LayoutOptions = &ELKLayoutOptions{ n.LayoutOptions = &elkOpts{
Padding: "[top=75,left=75,bottom=75,right=75]",
ForceNodeModelOrder: true, 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, Text: edge.Attributes.Label.Value,
Width: float64(edge.LabelDimensions.Width), Width: float64(edge.LabelDimensions.Width),
Height: float64(edge.LabelDimensions.Height), Height: float64(edge.LabelDimensions.Height),
LayoutOptions: &ELKLayoutOptions{ LayoutOptions: &elkOpts{
InlineEdgeLabels: true, 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/d2compiler"
"oss.terrastruct.com/d2/d2exporter" "oss.terrastruct.com/d2/d2exporter"
"oss.terrastruct.com/d2/d2graph" "oss.terrastruct.com/d2/d2graph"
"oss.terrastruct.com/d2/d2layouts/d2dagrelayout"
"oss.terrastruct.com/d2/d2layouts/d2near" "oss.terrastruct.com/d2/d2layouts/d2near"
"oss.terrastruct.com/d2/d2layouts/d2sequence" "oss.terrastruct.com/d2/d2layouts/d2sequence"
"oss.terrastruct.com/d2/d2renderers/d2fonts" "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) { func getLayout(opts *CompileOptions) (func(context.Context, *d2graph.Graph) error, error) {
if opts.Layout != nil { if opts.Layout != nil {
return opts.Layout, nil return opts.Layout, nil
} else if os.Getenv("D2_LAYOUT") == "dagre" && dagreLayout != nil { } else if os.Getenv("D2_LAYOUT") == "dagre" {
return dagreLayout, nil defaultLayout := func(ctx context.Context, g *d2graph.Graph) error {
return d2dagrelayout.Layout(ctx, g, nil)
}
return defaultLayout, nil
} else { } else {
return nil, errors.New("no available layout") 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" "time"
"oss.terrastruct.com/util-go/xdefer" "oss.terrastruct.com/util-go/xdefer"
"oss.terrastruct.com/util-go/xmain"
"oss.terrastruct.com/d2/d2graph" "oss.terrastruct.com/d2/d2graph"
) )
@ -37,6 +38,45 @@ import (
// the error to stderr. // the error to stderr.
type execPlugin struct { type execPlugin struct {
path string 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) { 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 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 := bytes.Buffer{}
buffer.Write(graphBytes) buffer.Write(graphBytes)

View file

@ -10,6 +10,7 @@ import (
"os/exec" "os/exec"
"oss.terrastruct.com/util-go/xexec" "oss.terrastruct.com/util-go/xexec"
"oss.terrastruct.com/util-go/xmain"
"oss.terrastruct.com/d2/d2graph" "oss.terrastruct.com/d2/d2graph"
) )
@ -19,10 +20,32 @@ import (
// See plugin_* files for the plugins available for bundling. // See plugin_* files for the plugins available for bundling.
var plugins []Plugin 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 { type Plugin interface {
// Info returns the current info information of the plugin. // Info returns the current info information of the plugin.
Info(context.Context) (*PluginInfo, error) Info(context.Context) (*PluginInfo, error)
Flags(context.Context) ([]PluginSpecificFlag, error)
HydrateOpts([]byte) error
// Layout runs the plugin's autolayout algorithm on the input graph // Layout runs the plugin's autolayout algorithm on the input graph
// and returns a new graph with the computed placements. // and returns a new graph with the computed placements.
Layout(context.Context, *d2graph.Graph) error 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 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 ( import (
"context" "context"
"encoding/json"
"fmt"
"oss.terrastruct.com/d2/d2graph" "oss.terrastruct.com/d2/d2graph"
"oss.terrastruct.com/d2/d2layouts/d2dagrelayout" "oss.terrastruct.com/d2/d2layouts/d2dagrelayout"
"oss.terrastruct.com/util-go/xmain"
) )
var DagrePlugin = dagrePlugin{} var DagrePlugin = dagrePlugin{}
func init() { 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{ return &PluginInfo{
Name: "dagre", Name: "dagre",
ShortHelp: "The directed graph layout library 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 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. Flags correspond to ones found at https://github.com/dagrejs/dagre/wiki. See dagre's reference for more on each.
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:
`, %s
`, opts.Defaults()),
}, nil }, nil
} }
func (p dagrePlugin) Layout(ctx context.Context, g *d2graph.Graph) error { 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) { func (p dagrePlugin) PostProcess(ctx context.Context, in []byte) ([]byte, error) {

View file

@ -4,31 +4,103 @@ package d2plugin
import ( import (
"context" "context"
"encoding/json"
"fmt"
"oss.terrastruct.com/d2/d2graph" "oss.terrastruct.com/d2/d2graph"
"oss.terrastruct.com/d2/d2layouts/d2elklayout" "oss.terrastruct.com/d2/d2layouts/d2elklayout"
"oss.terrastruct.com/util-go/xmain"
) )
var ELKPlugin = elkPlugin{} var ELKPlugin = elkPlugin{}
func init() { 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{ return &PluginInfo{
Name: "elk", Name: "elk",
ShortHelp: "Eclipse Layout Kernel (ELK) with the Layered algorithm.", 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. 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 }, nil
} }
func (p elkPlugin) Layout(ctx context.Context, g *d2graph.Graph) error { 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) { 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 { switch subcmd {
case "info": case "info":
return info(ctx, p, ms) return info(ctx, p, ms)
case "flags":
return flags(ctx, p, ms)
case "layout": case "layout":
return layout(ctx, p, ms) return layout(ctx, p, ms)
case "postprocess": case "postprocess":
@ -64,6 +66,22 @@ func info(ctx context.Context, p Plugin, ms *xmain.State) error {
return nil 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 { func layout(ctx context.Context, p Plugin, ms *xmain.State) error {
in, err := io.ReadAll(ms.Stdin) in, err := io.ReadAll(ms.Stdin)
if err != nil { 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{ diagram, _, err := d2lib.Compile(ctx, tc.script, &d2lib.CompileOptions{
Ruler: ruler, Ruler: ruler,
ThemeID: 0, ThemeID: 0,
Layout: d2dagrelayout.Layout, Layout: d2dagrelayout.DefaultLayout,
FontFamily: go2.Pointer(d2fonts.HandDrawn), FontFamily: go2.Pointer(d2fonts.HandDrawn),
}) })
if !tassert.Nil(t, err) { 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{ diagram, _, err := d2lib.Compile(ctx, tc.script, &d2lib.CompileOptions{
Ruler: ruler, Ruler: ruler,
ThemeID: 0, ThemeID: 0,
Layout: d2dagrelayout.Layout, Layout: d2dagrelayout.DefaultLayout,
}) })
if !tassert.Nil(t, err) { if !tassert.Nil(t, err) {
return return

View file

@ -5,6 +5,7 @@ import (
"io/ioutil" "io/ioutil"
"path/filepath" "path/filepath"
"oss.terrastruct.com/d2/d2graph"
"oss.terrastruct.com/d2/d2layouts/d2dagrelayout" "oss.terrastruct.com/d2/d2layouts/d2dagrelayout"
"oss.terrastruct.com/d2/d2lib" "oss.terrastruct.com/d2/d2lib"
"oss.terrastruct.com/d2/d2renderers/d2svg" "oss.terrastruct.com/d2/d2renderers/d2svg"
@ -15,8 +16,11 @@ import (
// Remember to add if err != nil checks in production. // Remember to add if err != nil checks in production.
func main() { func main() {
ruler, _ := textmeasure.NewRuler() 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{ diagram, _, _ := d2lib.Compile(context.Background(), "x -> y", &d2lib.CompileOptions{
Layout: d2dagrelayout.Layout, Layout: defaultLayout,
Ruler: ruler, Ruler: ruler,
ThemeID: d2themescatalog.GrapeSoda.ID, ThemeID: d2themescatalog.GrapeSoda.ID,
}) })

View file

@ -17,7 +17,7 @@ func main() {
// From one.go // From one.go
ruler, _ := textmeasure.NewRuler() ruler, _ := textmeasure.NewRuler()
_, graph, _ := d2lib.Compile(context.Background(), "x -> y", &d2lib.CompileOptions{ _, graph, _ := d2lib.Compile(context.Background(), "x -> y", &d2lib.CompileOptions{
Layout: d2dagrelayout.Layout, Layout: d2dagrelayout.DefaultLayout,
Ruler: ruler, Ruler: ruler,
ThemeID: d2themescatalog.GrapeSoda.ID, ThemeID: d2themescatalog.GrapeSoda.ID,
}) })

View file

@ -19,7 +19,7 @@ func main() {
graph, _ := d2compiler.Compile("", strings.NewReader("x -> y"), nil) graph, _ := d2compiler.Compile("", strings.NewReader("x -> y"), nil)
ruler, _ := textmeasure.NewRuler() ruler, _ := textmeasure.NewRuler()
_ = graph.SetDimensions(nil, ruler, nil) _ = 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) diagram, _ := d2exporter.Export(context.Background(), graph, d2themescatalog.NeutralDefault.ID, nil)
out, _ := d2svg.Render(diagram, &d2svg.RenderOpts{ out, _ := d2svg.Render(diagram, &d2svg.RenderOpts{
Pad: d2svg.DEFAULT_PADDING, Pad: d2svg.DEFAULT_PADDING,

View file

@ -130,9 +130,9 @@ func run(t *testing.T, tc testCase) {
for _, layoutName := range layoutsTested { for _, layoutName := range layoutsTested {
var layout func(context.Context, *d2graph.Graph) error var layout func(context.Context, *d2graph.Graph) error
if layoutName == "dagre" { if layoutName == "dagre" {
layout = d2dagrelayout.Layout layout = d2dagrelayout.DefaultLayout
} else if layoutName == "elk" { } else if layoutName == "elk" {
layout = d2elklayout.Layout layout = d2elklayout.DefaultLayout
} }
diagram, _, err := d2lib.Compile(ctx, tc.script, &d2lib.CompileOptions{ diagram, _, err := d2lib.Compile(ctx, tc.script, &d2lib.CompileOptions{
Ruler: ruler, Ruler: ruler,

View file

@ -32,7 +32,7 @@ Flags:
Subcommands: Subcommands:
%[1]s layout - Lists available layout engine options with short help %[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 %[1]s fmt file.d2 - Format file.d2
See more docs and the source code at https://oss.terrastruct.com/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 D2_LAYOUT=dagre d2 in.d2 out.svg
Subcommands: 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 See more docs at https://oss.terrastruct.com/d2
`, strings.Join(pluginLines, "\n"), ms.Name) `, strings.Join(pluginLines, "\n"), ms.Name)

52
main.go
View file

@ -2,6 +2,7 @@ package main
import ( import (
"context" "context"
"encoding/json"
"errors" "errors"
"fmt" "fmt"
"io" "io"
@ -75,6 +76,11 @@ func run(ctx context.Context, ms *xmain.State) (err error) {
return err return err
} }
err = populateLayoutOpts(ctx, ms)
if err != nil {
return err
}
err = ms.Opts.Flags.Parse(ms.Opts.Args) err = ms.Opts.Flags.Parse(ms.Opts.Args)
if !errors.Is(err, pflag.ErrHelp) && err != nil { if !errors.Is(err, pflag.ErrHelp) && err != nil {
return xmain.UsageErrorf("failed to parse flags: %v", err) 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 return err
} }
err = parseLayoutOpts(ctx, ms, plugin)
if err != nil {
return err
}
pluginLocation := "bundled" pluginLocation := "bundled"
if path != "" { if path != "" {
pluginLocation = fmt.Sprintf("executable plugin at %s", humanPath(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 { func DiscardSlog(ctx context.Context) context.Context {
return ctxlog.With(ctx, slog.Make(sloghuman.Sink(io.Discard))) 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
}