blob: f4381f2dc3f1f2898d81614deb11298ebf5e8677 [file] [log] [blame]
// Copyright ©2015 The Gonum Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package dot
import (
"bytes"
"errors"
"fmt"
"sort"
"strings"
"gonum.org/v1/gonum/graph"
"gonum.org/v1/gonum/graph/encoding"
"gonum.org/v1/gonum/graph/internal/ordered"
)
// Node is a DOT graph node.
type Node interface {
// DOTID returns a DOT node ID.
//
// An ID is one of the following:
//
// - a string of alphabetic ([a-zA-Z\x80-\xff]) characters, underscores ('_').
// digits ([0-9]), not beginning with a digit.
// - a numeral [-]?(.[0-9]+ | [0-9]+(.[0-9]*)?).
// - a double-quoted string ("...") possibly containing escaped quotes (\").
// - an HTML string (<...>).
DOTID() string
}
// Attributers are graph.Graph values that specify top-level DOT
// attributes.
type Attributers interface {
DOTAttributers() (graph, node, edge encoding.Attributer)
}
// Porter defines the behavior of graph.Edge values that can specify
// connection ports for their end points. The returned port corresponds
// to the the DOT node port to be used by the edge, compass corresponds
// to DOT compass point to which the edge will be aimed.
type Porter interface {
// FromPort returns the port and compass for the
// From node of a graph.Edge.
FromPort() (port, compass string)
// ToPort returns the port and compass for the
// To node of a graph.Edge.
ToPort() (port, compass string)
}
// Structurer represents a graph.Graph that can define subgraphs.
type Structurer interface {
Structure() []Graph
}
// MultiStructurer represents a graph.Multigraph that can define subgraphs.
type MultiStructurer interface {
Structure() []Multigraph
}
// Graph wraps named graph.Graph values.
type Graph interface {
graph.Graph
DOTID() string
}
// Multigraph wraps named graph.Multigraph values.
type Multigraph interface {
graph.Multigraph
DOTID() string
}
// Subgrapher wraps graph.Node values that represent subgraphs.
type Subgrapher interface {
Subgraph() graph.Graph
}
// MultiSubgrapher wraps graph.Node values that represent subgraphs.
type MultiSubgrapher interface {
Subgraph() graph.Multigraph
}
// Marshal returns the DOT encoding for the graph g, applying the prefix
// and indent to the encoding. Name is used to specify the graph name. If
// name is empty and g implements Graph, the returned string from DOTID
// will be used.
//
// Graph serialization will work for a graph.Graph without modification,
// however, advanced GraphViz DOT features provided by Marshal depend on
// implementation of the Node, Attributer, Porter, Attributers, Structurer,
// Subgrapher and Graph interfaces.
func Marshal(g graph.Graph, name, prefix, indent string) ([]byte, error) {
var p simpleGraphPrinter
p.indent = indent
p.prefix = prefix
p.visited = make(map[edge]bool)
err := p.print(g, name, false, false)
if err != nil {
return nil, err
}
return p.buf.Bytes(), nil
}
// MarshalMulti returns the DOT encoding for the multigraph g, applying the
// prefix and indent to the encoding. Name is used to specify the graph name. If
// name is empty and g implements Graph, the returned string from DOTID
// will be used. If strict is true the output bytes will be prefixed with
// the DOT "strict" keyword.
//
// Graph serialization will work for a graph.Multigraph without modification,
// however, advanced GraphViz DOT features provided by Marshal depend on
// implementation of the Node, Attributer, Porter, Attributers, Structurer,
// MultiSubgrapher and Multigraph interfaces.
func MarshalMulti(g graph.Multigraph, name, prefix, indent string) ([]byte, error) {
var p multiGraphPrinter
p.indent = indent
p.prefix = prefix
p.visited = make(map[line]bool)
err := p.print(g, name, false, false)
if err != nil {
return nil, err
}
return p.buf.Bytes(), nil
}
type printer struct {
buf bytes.Buffer
prefix string
indent string
depth int
err error
}
type edge struct {
inGraph string
from, to int64
}
func (p *simpleGraphPrinter) print(g graph.Graph, name string, needsIndent, isSubgraph bool) error {
if name == "" {
if g, ok := g.(Graph); ok {
name = g.DOTID()
}
}
_, isDirected := g.(graph.Directed)
p.printFrontMatter(name, needsIndent, isSubgraph, isDirected, true)
if a, ok := g.(Attributers); ok {
p.writeAttributeComplex(a)
}
if s, ok := g.(Structurer); ok {
for _, g := range s.Structure() {
_, subIsDirected := g.(graph.Directed)
if subIsDirected != isDirected {
return errors.New("dot: mismatched graph type")
}
p.buf.WriteByte('\n')
p.print(g, g.DOTID(), true, true)
}
}
nodes := graph.NodesOf(g.Nodes())
sort.Sort(ordered.ByID(nodes))
havePrintedNodeHeader := false
for _, n := range nodes {
if s, ok := n.(Subgrapher); ok {
// If the node is not linked to any other node
// the graph needs to be written now.
if g.From(n.ID()).Len() == 0 {
g := s.Subgraph()
_, subIsDirected := g.(graph.Directed)
if subIsDirected != isDirected {
return errors.New("dot: mismatched graph type")
}
if !havePrintedNodeHeader {
p.newline()
p.buf.WriteString("// Node definitions.")
havePrintedNodeHeader = true
}
p.newline()
p.print(g, graphID(g, n), false, true)
}
continue
}
if !havePrintedNodeHeader {
p.newline()
p.buf.WriteString("// Node definitions.")
havePrintedNodeHeader = true
}
p.newline()
p.writeNode(n)
if a, ok := n.(encoding.Attributer); ok {
p.writeAttributeList(a)
}
p.buf.WriteByte(';')
}
havePrintedEdgeHeader := false
for _, n := range nodes {
nid := n.ID()
to := graph.NodesOf(g.From(nid))
sort.Sort(ordered.ByID(to))
for _, t := range to {
tid := t.ID()
if isDirected {
if p.visited[edge{inGraph: name, from: nid, to: tid}] {
continue
}
p.visited[edge{inGraph: name, from: nid, to: tid}] = true
} else {
if p.visited[edge{inGraph: name, from: nid, to: tid}] {
continue
}
p.visited[edge{inGraph: name, from: nid, to: tid}] = true
p.visited[edge{inGraph: name, from: tid, to: n.ID()}] = true
}
if !havePrintedEdgeHeader {
p.buf.WriteByte('\n')
p.buf.WriteString(strings.TrimRight(p.prefix, " \t\n")) // Trim whitespace suffix.
p.newline()
p.buf.WriteString("// Edge definitions.")
havePrintedEdgeHeader = true
}
p.newline()
if s, ok := n.(Subgrapher); ok {
g := s.Subgraph()
_, subIsDirected := g.(graph.Directed)
if subIsDirected != isDirected {
return errors.New("dot: mismatched graph type")
}
p.print(g, graphID(g, n), false, true)
} else {
p.writeNode(n)
}
e := g.Edge(nid, tid)
porter, edgeIsPorter := e.(Porter)
if edgeIsPorter {
if e.From().ID() == nid {
p.writePorts(porter.FromPort())
} else {
p.writePorts(porter.ToPort())
}
}
if isDirected {
p.buf.WriteString(" -> ")
} else {
p.buf.WriteString(" -- ")
}
if s, ok := t.(Subgrapher); ok {
g := s.Subgraph()
_, subIsDirected := g.(graph.Directed)
if subIsDirected != isDirected {
return errors.New("dot: mismatched graph type")
}
p.print(g, graphID(g, t), false, true)
} else {
p.writeNode(t)
}
if edgeIsPorter {
if e.From().ID() == nid {
p.writePorts(porter.ToPort())
} else {
p.writePorts(porter.FromPort())
}
}
if a, ok := g.Edge(nid, tid).(encoding.Attributer); ok {
p.writeAttributeList(a)
}
p.buf.WriteByte(';')
}
}
p.closeBlock("}")
return nil
}
func (p *printer) printFrontMatter(name string, needsIndent, isSubgraph, isDirected, isStrict bool) error {
p.buf.WriteString(p.prefix)
if needsIndent {
for i := 0; i < p.depth; i++ {
p.buf.WriteString(p.indent)
}
}
if !isSubgraph && isStrict {
p.buf.WriteString("strict ")
}
if isSubgraph {
p.buf.WriteString("sub")
} else if isDirected {
p.buf.WriteString("di")
}
p.buf.WriteString("graph")
if name != "" {
p.buf.WriteByte(' ')
p.buf.WriteString(name)
}
p.openBlock(" {")
return nil
}
func (p *printer) writeNode(n graph.Node) {
p.buf.WriteString(nodeID(n))
}
func (p *printer) writePorts(port, cp string) {
if port != "" {
p.buf.WriteByte(':')
p.buf.WriteString(port)
}
if cp != "" {
p.buf.WriteByte(':')
p.buf.WriteString(cp)
}
}
func nodeID(n graph.Node) string {
switch n := n.(type) {
case Node:
return n.DOTID()
default:
return fmt.Sprint(n.ID())
}
}
func graphID(g interface{}, n graph.Node) string {
switch g := g.(type) {
case Node:
return g.DOTID()
default:
return nodeID(n)
}
}
func (p *printer) writeAttributeList(a encoding.Attributer) {
attributes := a.Attributes()
switch len(attributes) {
case 0:
case 1:
p.buf.WriteString(" [")
p.buf.WriteString(attributes[0].Key)
p.buf.WriteByte('=')
p.buf.WriteString(attributes[0].Value)
p.buf.WriteString("]")
default:
p.openBlock(" [")
for _, att := range attributes {
p.newline()
p.buf.WriteString(att.Key)
p.buf.WriteByte('=')
p.buf.WriteString(att.Value)
}
p.closeBlock("]")
}
}
var attType = []string{"graph", "node", "edge"}
func (p *printer) writeAttributeComplex(ca Attributers) {
g, n, e := ca.DOTAttributers()
haveWrittenBlock := false
for i, a := range []encoding.Attributer{g, n, e} {
attributes := a.Attributes()
if len(attributes) == 0 {
continue
}
if haveWrittenBlock {
p.buf.WriteByte(';')
}
p.newline()
p.buf.WriteString(attType[i])
p.openBlock(" [")
for _, att := range attributes {
p.newline()
p.buf.WriteString(att.Key)
p.buf.WriteByte('=')
p.buf.WriteString(att.Value)
}
p.closeBlock("]")
haveWrittenBlock = true
}
if haveWrittenBlock {
p.buf.WriteString(";\n")
}
}
func (p *printer) newline() {
p.buf.WriteByte('\n')
p.buf.WriteString(p.prefix)
for i := 0; i < p.depth; i++ {
p.buf.WriteString(p.indent)
}
}
func (p *printer) openBlock(b string) {
p.buf.WriteString(b)
p.depth++
}
func (p *printer) closeBlock(b string) {
p.depth--
p.newline()
p.buf.WriteString(b)
}
type simpleGraphPrinter struct {
printer
visited map[edge]bool
}
type multiGraphPrinter struct {
printer
visited map[line]bool
}
type line struct {
inGraph string
id int64
}
func (p *multiGraphPrinter) print(g graph.Multigraph, name string, needsIndent, isSubgraph bool) error {
if name == "" {
if g, ok := g.(Multigraph); ok {
name = g.DOTID()
}
}
_, isDirected := g.(graph.Directed)
p.printFrontMatter(name, needsIndent, isSubgraph, isDirected, false)
if a, ok := g.(Attributers); ok {
p.writeAttributeComplex(a)
}
if s, ok := g.(MultiStructurer); ok {
for _, g := range s.Structure() {
_, subIsDirected := g.(graph.Directed)
if subIsDirected != isDirected {
return errors.New("dot: mismatched graph type")
}
p.buf.WriteByte('\n')
p.print(g, g.DOTID(), true, true)
}
}
nodes := graph.NodesOf(g.Nodes())
sort.Sort(ordered.ByID(nodes))
havePrintedNodeHeader := false
for _, n := range nodes {
if s, ok := n.(MultiSubgrapher); ok {
// If the node is not linked to any other node
// the graph needs to be written now.
if g.From(n.ID()).Len() == 0 {
g := s.Subgraph()
_, subIsDirected := g.(graph.Directed)
if subIsDirected != isDirected {
return errors.New("dot: mismatched graph type")
}
if !havePrintedNodeHeader {
p.newline()
p.buf.WriteString("// Node definitions.")
havePrintedNodeHeader = true
}
p.newline()
p.print(g, graphID(g, n), false, true)
}
continue
}
if !havePrintedNodeHeader {
p.newline()
p.buf.WriteString("// Node definitions.")
havePrintedNodeHeader = true
}
p.newline()
p.writeNode(n)
if a, ok := n.(encoding.Attributer); ok {
p.writeAttributeList(a)
}
p.buf.WriteByte(';')
}
havePrintedEdgeHeader := false
for _, n := range nodes {
nid := n.ID()
to := graph.NodesOf(g.From(nid))
sort.Sort(ordered.ByID(to))
for _, t := range to {
tid := t.ID()
lines := graph.LinesOf(g.Lines(nid, tid))
sort.Sort(ordered.LinesByIDs(lines))
for _, l := range lines {
lid := l.ID()
if p.visited[line{inGraph: name, id: lid}] {
continue
}
p.visited[line{inGraph: name, id: lid}] = true
if !havePrintedEdgeHeader {
p.buf.WriteByte('\n')
p.buf.WriteString(strings.TrimRight(p.prefix, " \t\n")) // Trim whitespace suffix.
p.newline()
p.buf.WriteString("// Edge definitions.")
havePrintedEdgeHeader = true
}
p.newline()
if s, ok := n.(MultiSubgrapher); ok {
g := s.Subgraph()
_, subIsDirected := g.(graph.Directed)
if subIsDirected != isDirected {
return errors.New("dot: mismatched graph type")
}
p.print(g, graphID(g, n), false, true)
} else {
p.writeNode(n)
}
porter, edgeIsPorter := l.(Porter)
if edgeIsPorter {
if l.From().ID() == nid {
p.writePorts(porter.FromPort())
} else {
p.writePorts(porter.ToPort())
}
}
if isDirected {
p.buf.WriteString(" -> ")
} else {
p.buf.WriteString(" -- ")
}
if s, ok := t.(MultiSubgrapher); ok {
g := s.Subgraph()
_, subIsDirected := g.(graph.Directed)
if subIsDirected != isDirected {
return errors.New("dot: mismatched graph type")
}
p.print(g, graphID(g, t), false, true)
} else {
p.writeNode(t)
}
if edgeIsPorter {
if l.From().ID() == nid {
p.writePorts(porter.ToPort())
} else {
p.writePorts(porter.FromPort())
}
}
if a, ok := l.(encoding.Attributer); ok {
p.writeAttributeList(a)
}
p.buf.WriteByte(';')
}
}
}
p.closeBlock("}")
return nil
}