mirror of
https://github.com/gonum/gonum.git
synced 2025-10-13 02:43:59 +08:00
encoding/dot: support edge ports on Unmarshal/decode
This commit is contained in:
@@ -26,6 +26,18 @@ type DOTIDSetter interface {
|
|||||||
SetDOTID(id string)
|
SetDOTID(id string)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// PortSetter is implemented by graph.Edge and graph.Line that can set
|
||||||
|
// the DOT port and compass directions of an edge.
|
||||||
|
type PortSetter interface {
|
||||||
|
// SetFromPort sets the From port and
|
||||||
|
// compass direction of the receiver.
|
||||||
|
SetFromPort(port, compass string) error
|
||||||
|
|
||||||
|
// SetToPort sets the To port and compass
|
||||||
|
// direction of the receiver.
|
||||||
|
SetToPort(port, compass string) error
|
||||||
|
}
|
||||||
|
|
||||||
// Unmarshal parses the Graphviz DOT-encoded data and stores the result in dst.
|
// Unmarshal parses the Graphviz DOT-encoded data and stores the result in dst.
|
||||||
func Unmarshal(data []byte, dst encoding.Builder) error {
|
func Unmarshal(data []byte, dst encoding.Builder) error {
|
||||||
file, err := dot.ParseBytes(data)
|
file, err := dot.ParseBytes(data)
|
||||||
@@ -169,6 +181,30 @@ func (gen *generator) addStmt(dst encoding.Builder, stmt ast.Stmt) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// applyPortsToEdge applies the available port metadata from an ast.Edge
|
||||||
|
// to a graph.Edge
|
||||||
|
func applyPortsToEdge(from ast.Vertex, to *ast.Edge, edge graph.Edge) {
|
||||||
|
if ps, isPortSetter := edge.(PortSetter); isPortSetter {
|
||||||
|
if n, vertexIsNode := from.(*ast.Node); vertexIsNode {
|
||||||
|
if n.Port != nil {
|
||||||
|
err := ps.SetFromPort(n.Port.ID, n.Port.CompassPoint.String())
|
||||||
|
if err != nil {
|
||||||
|
panic(fmt.Errorf("unable to unmarshal edge port (:%s:%s)", n.Port.ID, n.Port.CompassPoint.String()))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if n, vertexIsNode := to.Vertex.(*ast.Node); vertexIsNode {
|
||||||
|
if n.Port != nil {
|
||||||
|
err := ps.SetToPort(n.Port.ID, n.Port.CompassPoint.String())
|
||||||
|
if err != nil {
|
||||||
|
panic(fmt.Errorf("unable to unmarshal edge DOT port (:%s:%s)", n.Port.ID, n.Port.CompassPoint.String()))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// addEdgeStmt adds the given edge statement to the graph.
|
// addEdgeStmt adds the given edge statement to the graph.
|
||||||
func (gen *generator) addEdgeStmt(dst encoding.Builder, stmt *ast.EdgeStmt) {
|
func (gen *generator) addEdgeStmt(dst encoding.Builder, stmt *ast.EdgeStmt) {
|
||||||
fs := gen.addVertex(dst, stmt.From)
|
fs := gen.addVertex(dst, stmt.From)
|
||||||
@@ -177,6 +213,8 @@ func (gen *generator) addEdgeStmt(dst encoding.Builder, stmt *ast.EdgeStmt) {
|
|||||||
for _, t := range ts {
|
for _, t := range ts {
|
||||||
edge := dst.NewEdge(f, t)
|
edge := dst.NewEdge(f, t)
|
||||||
dst.SetEdge(edge)
|
dst.SetEdge(edge)
|
||||||
|
applyPortsToEdge(stmt.From, stmt.To, edge)
|
||||||
|
|
||||||
e, ok := edge.(encoding.AttributeSetter)
|
e, ok := edge.(encoding.AttributeSetter)
|
||||||
if !ok {
|
if !ok {
|
||||||
continue
|
continue
|
||||||
@@ -223,6 +261,7 @@ func (gen *generator) addEdge(dst encoding.Builder, to *ast.Edge) []graph.Node {
|
|||||||
for _, t := range ts {
|
for _, t := range ts {
|
||||||
edge := dst.NewEdge(f, t)
|
edge := dst.NewEdge(f, t)
|
||||||
dst.SetEdge(edge)
|
dst.SetEdge(edge)
|
||||||
|
applyPortsToEdge(to.Vertex, to.To, edge)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -34,6 +34,14 @@ func TestRoundTrip(t *testing.T) {
|
|||||||
want: undirectedID,
|
want: undirectedID,
|
||||||
directed: false,
|
directed: false,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
want: directedWithPorts,
|
||||||
|
directed: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
want: undirectedWithPorts,
|
||||||
|
directed: false,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
for i, g := range golden {
|
for i, g := range golden {
|
||||||
var dst encoding.Builder
|
var dst encoding.Builder
|
||||||
@@ -120,6 +128,42 @@ const undirectedID = `graph H {
|
|||||||
A -- B;
|
A -- B;
|
||||||
}`
|
}`
|
||||||
|
|
||||||
|
const directedWithPorts = `digraph {
|
||||||
|
// Node definitions.
|
||||||
|
A;
|
||||||
|
B;
|
||||||
|
C;
|
||||||
|
D;
|
||||||
|
E;
|
||||||
|
F;
|
||||||
|
|
||||||
|
// Edge definitions.
|
||||||
|
A:foo -> B:bar;
|
||||||
|
A -> C:bar;
|
||||||
|
B:foo -> C;
|
||||||
|
D:foo:n -> E:bar:s;
|
||||||
|
D:e -> F:bar:w;
|
||||||
|
E:_ -> F:c;
|
||||||
|
}`
|
||||||
|
|
||||||
|
const undirectedWithPorts = `graph {
|
||||||
|
// Node definitions.
|
||||||
|
A;
|
||||||
|
B;
|
||||||
|
C;
|
||||||
|
D;
|
||||||
|
E;
|
||||||
|
F;
|
||||||
|
|
||||||
|
// Edge definitions.
|
||||||
|
A:foo -- B:bar;
|
||||||
|
A -- C:bar;
|
||||||
|
B:foo -- C;
|
||||||
|
D:foo:n -- E:bar:s;
|
||||||
|
D:e -- F:bar:w;
|
||||||
|
E:_ -- F:c;
|
||||||
|
}`
|
||||||
|
|
||||||
// Below follows a minimal implementation of a graph capable of validating the
|
// Below follows a minimal implementation of a graph capable of validating the
|
||||||
// round-trip encoding and decoding of DOT graphs with nodes and edges
|
// round-trip encoding and decoding of DOT graphs with nodes and edges
|
||||||
// containing DOT attributes.
|
// containing DOT attributes.
|
||||||
@@ -257,12 +301,18 @@ func (n *dotNode) Attributes() []encoding.Attribute {
|
|||||||
}}
|
}}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type dotPortLabels struct {
|
||||||
|
Port, Compass string
|
||||||
|
}
|
||||||
|
|
||||||
// dotEdge extends simple.Edge with a label field to test round-trip encoding and
|
// dotEdge extends simple.Edge with a label field to test round-trip encoding and
|
||||||
// decoding of edge DOT label attributes.
|
// decoding of edge DOT label attributes.
|
||||||
type dotEdge struct {
|
type dotEdge struct {
|
||||||
graph.Edge
|
graph.Edge
|
||||||
// Edge label.
|
// Edge label.
|
||||||
Label string
|
Label string
|
||||||
|
FromPortLabels dotPortLabels
|
||||||
|
ToPortLabels dotPortLabels
|
||||||
}
|
}
|
||||||
|
|
||||||
// SetAttribute sets a DOT attribute.
|
// SetAttribute sets a DOT attribute.
|
||||||
@@ -285,6 +335,26 @@ func (e *dotEdge) Attributes() []encoding.Attribute {
|
|||||||
}}
|
}}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (e *dotEdge) SetFromPort(port, compass string) error {
|
||||||
|
e.FromPortLabels.Port = port
|
||||||
|
e.FromPortLabels.Compass = compass
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *dotEdge) SetToPort(port, compass string) error {
|
||||||
|
e.ToPortLabels.Port = port
|
||||||
|
e.ToPortLabels.Compass = compass
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *dotEdge) FromPort() (port, compass string) {
|
||||||
|
return e.FromPortLabels.Port, e.FromPortLabels.Compass
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *dotEdge) ToPort() (port, compass string) {
|
||||||
|
return e.ToPortLabels.Port, e.ToPortLabels.Compass
|
||||||
|
}
|
||||||
|
|
||||||
// attributes is a helper for global attributes.
|
// attributes is a helper for global attributes.
|
||||||
type attributes []encoding.Attribute
|
type attributes []encoding.Attribute
|
||||||
|
|
||||||
|
@@ -351,7 +351,7 @@ func (p *Port) String() string {
|
|||||||
if len(p.ID) > 0 {
|
if len(p.ID) > 0 {
|
||||||
fmt.Fprintf(buf, ":%s", p.ID)
|
fmt.Fprintf(buf, ":%s", p.ID)
|
||||||
}
|
}
|
||||||
if p.CompassPoint != CompassPointDefault {
|
if p.CompassPoint != CompassPointNone {
|
||||||
fmt.Fprintf(buf, ":%s", p.CompassPoint)
|
fmt.Fprintf(buf, ":%s", p.CompassPoint)
|
||||||
}
|
}
|
||||||
return buf.String()
|
return buf.String()
|
||||||
@@ -362,7 +362,7 @@ type CompassPoint uint
|
|||||||
|
|
||||||
// Compass points.
|
// Compass points.
|
||||||
const (
|
const (
|
||||||
CompassPointDefault CompassPoint = iota // _
|
CompassPointNone CompassPoint = iota //
|
||||||
CompassPointNorth // n
|
CompassPointNorth // n
|
||||||
CompassPointNorthEast // ne
|
CompassPointNorthEast // ne
|
||||||
CompassPointEast // e
|
CompassPointEast // e
|
||||||
@@ -372,13 +372,14 @@ const (
|
|||||||
CompassPointWest // w
|
CompassPointWest // w
|
||||||
CompassPointNorthWest // nw
|
CompassPointNorthWest // nw
|
||||||
CompassPointCenter // c
|
CompassPointCenter // c
|
||||||
|
CompassPointDefault // _
|
||||||
)
|
)
|
||||||
|
|
||||||
// String returns the string representation of the compass point.
|
// String returns the string representation of the compass point.
|
||||||
func (c CompassPoint) String() string {
|
func (c CompassPoint) String() string {
|
||||||
switch c {
|
switch c {
|
||||||
case CompassPointDefault:
|
case CompassPointNone:
|
||||||
return "_"
|
return ""
|
||||||
case CompassPointNorth:
|
case CompassPointNorth:
|
||||||
return "n"
|
return "n"
|
||||||
case CompassPointNorthEast:
|
case CompassPointNorthEast:
|
||||||
@@ -397,6 +398,8 @@ func (c CompassPoint) String() string {
|
|||||||
return "nw"
|
return "nw"
|
||||||
case CompassPointCenter:
|
case CompassPointCenter:
|
||||||
return "c"
|
return "c"
|
||||||
|
case CompassPointDefault:
|
||||||
|
return "_"
|
||||||
}
|
}
|
||||||
panic(fmt.Sprintf("invalid compass point (%d)", uint(c)))
|
panic(fmt.Sprintf("invalid compass point (%d)", uint(c)))
|
||||||
}
|
}
|
||||||
|
@@ -55,10 +55,7 @@ func TestParseFile(t *testing.T) {
|
|||||||
out: "../internal/testdata/attr_sep.golden",
|
out: "../internal/testdata/attr_sep.golden",
|
||||||
},
|
},
|
||||||
{in: "../internal/testdata/subgraph_vertex.dot"},
|
{in: "../internal/testdata/subgraph_vertex.dot"},
|
||||||
{
|
{in: "../internal/testdata/port.dot"},
|
||||||
in: "../internal/testdata/port.dot",
|
|
||||||
out: "../internal/testdata/port.golden",
|
|
||||||
},
|
|
||||||
}
|
}
|
||||||
for _, g := range golden {
|
for _, g := range golden {
|
||||||
file, err := dot.ParseFile(g.in)
|
file, err := dot.ParseFile(g.in)
|
||||||
|
@@ -299,7 +299,7 @@ func getCompassPoint(s string) (ast.CompassPoint, bool) {
|
|||||||
case "c":
|
case "c":
|
||||||
return ast.CompassPointCenter, true
|
return ast.CompassPointCenter, true
|
||||||
}
|
}
|
||||||
return ast.CompassPointDefault, false
|
return ast.CompassPointNone, false
|
||||||
}
|
}
|
||||||
|
|
||||||
// === [ Identifiers ] =========================================================
|
// === [ Identifiers ] =========================================================
|
||||||
|
@@ -54,10 +54,7 @@ func TestParseFile(t *testing.T) {
|
|||||||
out: "../testdata/attr_sep.golden",
|
out: "../testdata/attr_sep.golden",
|
||||||
},
|
},
|
||||||
{in: "../testdata/subgraph_vertex.dot"},
|
{in: "../testdata/subgraph_vertex.dot"},
|
||||||
{
|
{in: "../testdata/port.dot"},
|
||||||
in: "../testdata/port.dot",
|
|
||||||
out: "../testdata/port.golden",
|
|
||||||
},
|
|
||||||
{in: "../testdata/quoted_id.dot"},
|
{in: "../testdata/quoted_id.dot"},
|
||||||
{
|
{
|
||||||
in: "../testdata/backslash_newline_id.dot",
|
in: "../testdata/backslash_newline_id.dot",
|
||||||
|
@@ -54,10 +54,7 @@ func TestParseFile(t *testing.T) {
|
|||||||
out: "../testdata/attr_sep.golden",
|
out: "../testdata/attr_sep.golden",
|
||||||
},
|
},
|
||||||
{in: "../testdata/subgraph_vertex.dot"},
|
{in: "../testdata/subgraph_vertex.dot"},
|
||||||
{
|
{in: "../testdata/port.dot"},
|
||||||
in: "../testdata/port.dot",
|
|
||||||
out: "../testdata/port.golden",
|
|
||||||
},
|
|
||||||
{in: "../testdata/quoted_id.dot"},
|
{in: "../testdata/quoted_id.dot"},
|
||||||
{
|
{
|
||||||
in: "../testdata/backslash_newline_id.dot",
|
in: "../testdata/backslash_newline_id.dot",
|
||||||
|
11
graph/formats/dot/internal/testdata/port.golden
vendored
11
graph/formats/dot/internal/testdata/port.golden
vendored
@@ -1,11 +0,0 @@
|
|||||||
digraph {
|
|
||||||
A:ne -> B:sw
|
|
||||||
C:foo -> D:bar:se
|
|
||||||
E -> F
|
|
||||||
G:n
|
|
||||||
H:e
|
|
||||||
I:s
|
|
||||||
J:w
|
|
||||||
K:nw
|
|
||||||
L:c
|
|
||||||
}
|
|
Reference in New Issue
Block a user