Skip to content

Commit

Permalink
graph/encoding/dot: (un)quote attributes if needed during (un)marshal
Browse files Browse the repository at this point in the history
  • Loading branch information
mewmew authored and kortschak committed Jan 14, 2019
1 parent 9b1d387 commit 24f0d08
Show file tree
Hide file tree
Showing 5 changed files with 288 additions and 74 deletions.
59 changes: 44 additions & 15 deletions graph/encoding/dot/decode.go
Expand Up @@ -6,6 +6,8 @@ package dot

import (
"fmt"
"strconv"
"strings"

"gonum.org/v1/gonum/graph"
"gonum.org/v1/gonum/graph/encoding"
Expand Down Expand Up @@ -41,6 +43,10 @@ type PortSetter interface {
// Unmarshal parses the Graphviz DOT-encoded data and stores the result in dst.
// If the number of graphs encoded in data is not one, an error is returned and
// dst will hold the first graph in data.
//
// Attributes and IDs are quoted if needed during marshalling, to conform with
// valid DOT syntax. Quoted IDs and attributes are unquoted during unmarshaling,
// so the data is kept in raw form.
func Unmarshal(data []byte, dst encoding.Builder) error {
file, err := dot.ParseBytes(data)
if err != nil {
Expand All @@ -57,6 +63,10 @@ func Unmarshal(data []byte, dst encoding.Builder) error {
// stores the result in dst.
// If the number of graphs encoded in data is not one, an error is returned and
// dst will hold the first graph in data.
//
// Attributes and IDs are quoted if needed during marshalling, to conform with
// valid DOT syntax. Quoted IDs and attributes are unquoted during unmarshaling,
// so the data is kept in raw form.
func UnmarshalMulti(data []byte, dst encoding.MultiBuilder) error {
file, err := dot.ParseBytes(data)
if err != nil {
Expand Down Expand Up @@ -88,7 +98,7 @@ func copyGraph(dst encoding.Builder, src *ast.Graph) (err error) {
},
}
if dst, ok := dst.(DOTIDSetter); ok {
dst.SetDOTID(src.ID)
dst.SetDOTID(unquoteID(src.ID))
}
if a, ok := dst.(AttributeSetters); ok {
gen.graphAttr, gen.nodeAttr, gen.edgeAttr = a.DOTAttributeSetters()
Expand Down Expand Up @@ -118,7 +128,7 @@ func copyMultigraph(dst encoding.MultiBuilder, src *ast.Graph) (err error) {
},
}
if dst, ok := dst.(DOTIDSetter); ok {
dst.SetDOTID(src.ID)
dst.SetDOTID(unquoteID(src.ID))
}
if a, ok := dst.(AttributeSetters); ok {
gen.graphAttr, gen.nodeAttr, gen.edgeAttr = a.DOTAttributeSetters()
Expand Down Expand Up @@ -154,7 +164,7 @@ func (gen *generator) node(dst graph.NodeAdder, id string) graph.Node {
}
n := dst.NewNode()
if n, ok := n.(DOTIDSetter); ok {
n.SetDOTID(id)
n.SetDOTID(unquoteID(id))
}
dst.AddNode(n)
gen.ids[id] = n
Expand All @@ -180,8 +190,8 @@ func (gen *simpleGraph) addStmt(dst encoding.Builder, stmt ast.Stmt) {
}
for _, attr := range stmt.Attrs {
a := encoding.Attribute{
Key: attr.Key,
Value: attr.Val,
Key: unquoteID(attr.Key),
Value: unquoteID(attr.Val),
}
if err := n.SetAttribute(a); err != nil {
panic(fmt.Errorf("unable to unmarshal node DOT attribute (%s=%s): %v", a.Key, a.Value, err))
Expand Down Expand Up @@ -216,8 +226,8 @@ func (gen *simpleGraph) addStmt(dst encoding.Builder, stmt ast.Stmt) {
}
for _, attr := range stmt.Attrs {
a := encoding.Attribute{
Key: attr.Key,
Value: attr.Val,
Key: unquoteID(attr.Key),
Value: unquoteID(attr.Val),
}
if err := n.SetAttribute(a); err != nil {
panic(fmt.Errorf("unable to unmarshal global %s DOT attribute (%s=%s): %v", dst, a.Key, a.Value, err))
Expand All @@ -240,7 +250,7 @@ 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())
err := ps.SetFromPort(unquoteID(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()))
}
Expand All @@ -249,7 +259,7 @@ func applyPortsToEdge(from ast.Vertex, to *ast.Edge, edge graph.Edge) {

if n, vertexIsNode := to.Vertex.(*ast.Node); vertexIsNode {
if n.Port != nil {
err := ps.SetToPort(n.Port.ID, n.Port.CompassPoint.String())
err := ps.SetToPort(unquoteID(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()))
}
Expand Down Expand Up @@ -372,8 +382,8 @@ func (gen *multiGraph) addStmt(dst encoding.MultiBuilder, stmt ast.Stmt) {
}
for _, attr := range stmt.Attrs {
a := encoding.Attribute{
Key: attr.Key,
Value: attr.Val,
Key: unquoteID(attr.Key),
Value: unquoteID(attr.Val),
}
if err := n.SetAttribute(a); err != nil {
panic(fmt.Errorf("unable to unmarshal node DOT attribute (%s=%s): %v", a.Key, a.Value, err))
Expand Down Expand Up @@ -408,8 +418,8 @@ func (gen *multiGraph) addStmt(dst encoding.MultiBuilder, stmt ast.Stmt) {
}
for _, attr := range stmt.Attrs {
a := encoding.Attribute{
Key: attr.Key,
Value: attr.Val,
Key: unquoteID(attr.Key),
Value: unquoteID(attr.Val),
}
if err := n.SetAttribute(a); err != nil {
panic(fmt.Errorf("unable to unmarshal global %s DOT attribute (%s=%s): %v", dst, a.Key, a.Value, err))
Expand Down Expand Up @@ -485,11 +495,30 @@ func addEdgeAttrs(edge graph.Edge, attrs []*ast.Attr) {
}
for _, attr := range attrs {
a := encoding.Attribute{
Key: attr.Key,
Value: attr.Val,
Key: unquoteID(attr.Key),
Value: unquoteID(attr.Val),
}
if err := e.SetAttribute(a); err != nil {
panic(fmt.Errorf("unable to unmarshal edge DOT attribute (%s=%s): %v", a.Key, a.Value, err))
}
}
}

// unquoteID unquotes the given string if needed in the context of an ID. If s
// is not already quoted the original string is returned.
func unquoteID(s string) string {
// To make round-trips idempotent, don't unquote quoted HTML-like strings
//
// /^"<.*>"$/
if len(s) >= 4 && strings.HasPrefix(s, `"<`) && strings.HasSuffix(s, `>"`) {
return s
}
// Unquote quoted string if possible.
if t, err := strconv.Unquote(s); err == nil {
return t
}
// On error, either s is not quoted or s is quoted but contains invalid
// characters, in both cases we return the original string rather than
// panicking.
return s
}
48 changes: 48 additions & 0 deletions graph/encoding/dot/decode_test.go
Expand Up @@ -43,6 +43,14 @@ func TestRoundTrip(t *testing.T) {
want: undirectedWithPorts,
directed: false,
},
{
want: directedAttrs,
directed: true,
},
{
want: undirectedAttrs,
directed: false,
},
}
for i, g := range golden {
var dst encoding.Builder
Expand Down Expand Up @@ -165,6 +173,46 @@ const undirectedWithPorts = `strict graph {
E:_ -- F:c;
}`

const directedAttrs = `strict digraph {
node [
shape=circle
style=filled
label="NODE"
];
edge [
penwidth=5
color=gray
label=3.14
];
// Node definitions.
A [label=<br>];
B [label=-14];
// Edge definitions.
A -> B [label="hello world"];
}`

const undirectedAttrs = `strict graph {
node [
shape=circle
style=filled
label="NODE"
];
edge [
penwidth=5
color=gray
label=3.14
];
// Node definitions.
A [label=<br>];
B [label=-14];
// Edge definitions.
A -- B [label="hello world"];
}`

func TestChainedEdgeAttributes(t *testing.T) {
golden := []struct {
in, want string
Expand Down

0 comments on commit 24f0d08

Please sign in to comment.