Merge pull request #563 from alixander/layout-configs
Pass layout configs
This commit is contained in:
commit
8708bceef0
21 changed files with 398 additions and 90 deletions
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -9,5 +9,5 @@ import (
|
|||
)
|
||||
|
||||
func main() {
|
||||
xmain.Main(d2plugin.Serve(d2plugin.DagrePlugin))
|
||||
xmain.Main(d2plugin.Serve(&d2plugin.DagrePlugin))
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
},
|
||||
})
|
||||
|
|
|
|||
|
|
@ -1,7 +0,0 @@
|
|||
package d2lib
|
||||
|
||||
import "oss.terrastruct.com/d2/d2layouts/d2dagrelayout"
|
||||
|
||||
func init() {
|
||||
dagreLayout = d2dagrelayout.Layout
|
||||
}
|
||||
11
d2lib/d2.go
11
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
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
})
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
})
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
4
help.go
4
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)
|
||||
|
|
|
|||
52
main.go
52
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
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue