github.com/gopherd/gonum@v0.0.4/graph/encoding/dot/encode.go (about)

     1  // Copyright ©2015 The Gonum Authors. All rights reserved.
     2  // Use of this source code is governed by a BSD-style
     3  // license that can be found in the LICENSE file.
     4  
     5  package dot
     6  
     7  import (
     8  	"bytes"
     9  	"errors"
    10  	"fmt"
    11  	"regexp"
    12  	"strconv"
    13  	"strings"
    14  
    15  	"github.com/gopherd/gonum/graph"
    16  	"github.com/gopherd/gonum/graph/encoding"
    17  	"github.com/gopherd/gonum/graph/internal/ordered"
    18  )
    19  
    20  // Node is a DOT graph node.
    21  type Node interface {
    22  	// DOTID returns a DOT node ID.
    23  	//
    24  	// An ID is one of the following:
    25  	//
    26  	//  - a string of alphabetic ([a-zA-Z\x80-\xff]) characters, underscores ('_').
    27  	//    digits ([0-9]), not beginning with a digit.
    28  	//  - a numeral [-]?(.[0-9]+ | [0-9]+(.[0-9]*)?).
    29  	//  - a double-quoted string ("...") possibly containing escaped quotes (\").
    30  	//  - an HTML string (<...>).
    31  	DOTID() string
    32  }
    33  
    34  // Attributers are graph.Graph values that specify top-level DOT
    35  // attributes.
    36  type Attributers interface {
    37  	DOTAttributers() (graph, node, edge encoding.Attributer)
    38  }
    39  
    40  // Porter defines the behavior of graph.Edge values that can specify
    41  // connection ports for their end points. The returned port corresponds
    42  // to the DOT node port to be used by the edge, compass corresponds
    43  // to DOT compass point to which the edge will be aimed.
    44  type Porter interface {
    45  	// FromPort returns the port and compass for
    46  	// the From node of a graph.Edge.
    47  	FromPort() (port, compass string)
    48  
    49  	// ToPort returns the port and compass for
    50  	// the To node of a graph.Edge.
    51  	ToPort() (port, compass string)
    52  }
    53  
    54  // Structurer represents a graph.Graph that can define subgraphs.
    55  type Structurer interface {
    56  	Structure() []Graph
    57  }
    58  
    59  // MultiStructurer represents a graph.Multigraph that can define subgraphs.
    60  type MultiStructurer interface {
    61  	Structure() []Multigraph
    62  }
    63  
    64  // Graph wraps named graph.Graph values.
    65  type Graph interface {
    66  	graph.Graph
    67  	DOTID() string
    68  }
    69  
    70  // Multigraph wraps named graph.Multigraph values.
    71  type Multigraph interface {
    72  	graph.Multigraph
    73  	DOTID() string
    74  }
    75  
    76  // Subgrapher wraps graph.Node values that represent subgraphs.
    77  type Subgrapher interface {
    78  	Subgraph() graph.Graph
    79  }
    80  
    81  // MultiSubgrapher wraps graph.Node values that represent subgraphs.
    82  type MultiSubgrapher interface {
    83  	Subgraph() graph.Multigraph
    84  }
    85  
    86  // Marshal returns the DOT encoding for the graph g, applying the prefix and
    87  // indent to the encoding. Name is used to specify the graph name. If name is
    88  // empty and g implements Graph, the returned string from DOTID will be used.
    89  //
    90  // Graph serialization will work for a graph.Graph without modification,
    91  // however, advanced GraphViz DOT features provided by Marshal depend on
    92  // implementation of the Node, Attributer, Porter, Attributers, Structurer,
    93  // Subgrapher and Graph interfaces.
    94  //
    95  // Attributes and IDs are quoted if needed during marshalling.
    96  func Marshal(g graph.Graph, name, prefix, indent string) ([]byte, error) {
    97  	var p simpleGraphPrinter
    98  	p.indent = indent
    99  	p.prefix = prefix
   100  	p.visited = make(map[edge]bool)
   101  	err := p.print(g, name, false, false)
   102  	if err != nil {
   103  		return nil, err
   104  	}
   105  	return p.buf.Bytes(), nil
   106  }
   107  
   108  // MarshalMulti returns the DOT encoding for the multigraph g, applying the
   109  // prefix and indent to the encoding. Name is used to specify the graph name. If
   110  // name is empty and g implements Graph, the returned string from DOTID will be
   111  // used.
   112  //
   113  // Graph serialization will work for a graph.Multigraph without modification,
   114  // however, advanced GraphViz DOT features provided by Marshal depend on
   115  // implementation of the Node, Attributer, Porter, Attributers, Structurer,
   116  // MultiSubgrapher and Multigraph interfaces.
   117  //
   118  // Attributes and IDs are quoted if needed during marshalling.
   119  func MarshalMulti(g graph.Multigraph, name, prefix, indent string) ([]byte, error) {
   120  	var p multiGraphPrinter
   121  	p.indent = indent
   122  	p.prefix = prefix
   123  	p.visited = make(map[line]bool)
   124  	err := p.print(g, name, false, false)
   125  	if err != nil {
   126  		return nil, err
   127  	}
   128  	return p.buf.Bytes(), nil
   129  }
   130  
   131  type printer struct {
   132  	buf bytes.Buffer
   133  
   134  	prefix string
   135  	indent string
   136  	depth  int
   137  }
   138  
   139  type edge struct {
   140  	inGraph  string
   141  	from, to int64
   142  }
   143  
   144  func (p *simpleGraphPrinter) print(g graph.Graph, name string, needsIndent, isSubgraph bool) error {
   145  	if name == "" {
   146  		if g, ok := g.(Graph); ok {
   147  			name = g.DOTID()
   148  		}
   149  	}
   150  
   151  	_, isDirected := g.(graph.Directed)
   152  	p.printFrontMatter(name, needsIndent, isSubgraph, isDirected, true)
   153  
   154  	if a, ok := g.(Attributers); ok {
   155  		p.writeAttributeComplex(a)
   156  	}
   157  	if s, ok := g.(Structurer); ok {
   158  		for _, g := range s.Structure() {
   159  			_, subIsDirected := g.(graph.Directed)
   160  			if subIsDirected != isDirected {
   161  				return errors.New("dot: mismatched graph type")
   162  			}
   163  			p.buf.WriteByte('\n')
   164  			p.print(g, g.DOTID(), true, true)
   165  		}
   166  	}
   167  
   168  	nodes := graph.NodesOf(g.Nodes())
   169  	ordered.ByID(nodes)
   170  
   171  	havePrintedNodeHeader := false
   172  	for _, n := range nodes {
   173  		if s, ok := n.(Subgrapher); ok {
   174  			// If the node is not linked to any other node
   175  			// the graph needs to be written now.
   176  			if g.From(n.ID()).Len() == 0 {
   177  				g := s.Subgraph()
   178  				_, subIsDirected := g.(graph.Directed)
   179  				if subIsDirected != isDirected {
   180  					return errors.New("dot: mismatched graph type")
   181  				}
   182  				if !havePrintedNodeHeader {
   183  					p.newline()
   184  					p.buf.WriteString("// Node definitions.")
   185  					havePrintedNodeHeader = true
   186  				}
   187  				p.newline()
   188  				p.print(g, graphID(g, n), false, true)
   189  			}
   190  			continue
   191  		}
   192  		if !havePrintedNodeHeader {
   193  			p.newline()
   194  			p.buf.WriteString("// Node definitions.")
   195  			havePrintedNodeHeader = true
   196  		}
   197  		p.newline()
   198  		p.writeNode(n)
   199  		if a, ok := n.(encoding.Attributer); ok {
   200  			p.writeAttributeList(a)
   201  		}
   202  		p.buf.WriteByte(';')
   203  	}
   204  
   205  	havePrintedEdgeHeader := false
   206  	for _, n := range nodes {
   207  		nid := n.ID()
   208  		to := graph.NodesOf(g.From(nid))
   209  		ordered.ByID(to)
   210  		for _, t := range to {
   211  			tid := t.ID()
   212  			f := edge{inGraph: name, from: nid, to: tid}
   213  			if isDirected {
   214  				if p.visited[f] {
   215  					continue
   216  				}
   217  				p.visited[f] = true
   218  			} else {
   219  				if p.visited[f] {
   220  					continue
   221  				}
   222  				p.visited[f] = true
   223  				p.visited[edge{inGraph: name, from: tid, to: nid}] = true
   224  			}
   225  
   226  			if !havePrintedEdgeHeader {
   227  				p.buf.WriteByte('\n')
   228  				p.buf.WriteString(strings.TrimRight(p.prefix, " \t\n")) // Trim whitespace suffix.
   229  				p.newline()
   230  				p.buf.WriteString("// Edge definitions.")
   231  				havePrintedEdgeHeader = true
   232  			}
   233  			p.newline()
   234  
   235  			if s, ok := n.(Subgrapher); ok {
   236  				g := s.Subgraph()
   237  				_, subIsDirected := g.(graph.Directed)
   238  				if subIsDirected != isDirected {
   239  					return errors.New("dot: mismatched graph type")
   240  				}
   241  				p.print(g, graphID(g, n), false, true)
   242  			} else {
   243  				p.writeNode(n)
   244  			}
   245  			e := g.Edge(nid, tid)
   246  			porter, edgeIsPorter := e.(Porter)
   247  			if edgeIsPorter {
   248  				if e.From().ID() == nid {
   249  					p.writePorts(porter.FromPort())
   250  				} else {
   251  					p.writePorts(porter.ToPort())
   252  				}
   253  			}
   254  
   255  			if isDirected {
   256  				p.buf.WriteString(" -> ")
   257  			} else {
   258  				p.buf.WriteString(" -- ")
   259  			}
   260  
   261  			if s, ok := t.(Subgrapher); ok {
   262  				g := s.Subgraph()
   263  				_, subIsDirected := g.(graph.Directed)
   264  				if subIsDirected != isDirected {
   265  					return errors.New("dot: mismatched graph type")
   266  				}
   267  				p.print(g, graphID(g, t), false, true)
   268  			} else {
   269  				p.writeNode(t)
   270  			}
   271  			if edgeIsPorter {
   272  				if e.From().ID() == nid {
   273  					p.writePorts(porter.ToPort())
   274  				} else {
   275  					p.writePorts(porter.FromPort())
   276  				}
   277  			}
   278  
   279  			if a, ok := g.Edge(nid, tid).(encoding.Attributer); ok {
   280  				p.writeAttributeList(a)
   281  			}
   282  
   283  			p.buf.WriteByte(';')
   284  		}
   285  	}
   286  
   287  	p.closeBlock("}")
   288  
   289  	return nil
   290  }
   291  
   292  func (p *printer) printFrontMatter(name string, needsIndent, isSubgraph, isDirected, isStrict bool) {
   293  	p.buf.WriteString(p.prefix)
   294  	if needsIndent {
   295  		for i := 0; i < p.depth; i++ {
   296  			p.buf.WriteString(p.indent)
   297  		}
   298  	}
   299  
   300  	if !isSubgraph && isStrict {
   301  		p.buf.WriteString("strict ")
   302  	}
   303  
   304  	if isSubgraph {
   305  		p.buf.WriteString("sub")
   306  	} else if isDirected {
   307  		p.buf.WriteString("di")
   308  	}
   309  	p.buf.WriteString("graph")
   310  
   311  	if name != "" {
   312  		p.buf.WriteByte(' ')
   313  		p.buf.WriteString(quoteID(name))
   314  	}
   315  
   316  	p.openBlock(" {")
   317  }
   318  
   319  func (p *printer) writeNode(n graph.Node) {
   320  	p.buf.WriteString(quoteID(nodeID(n)))
   321  }
   322  
   323  func (p *printer) writePorts(port, cp string) {
   324  	if port != "" {
   325  		p.buf.WriteByte(':')
   326  		p.buf.WriteString(quoteID(port))
   327  	}
   328  	if cp != "" {
   329  		p.buf.WriteByte(':')
   330  		p.buf.WriteString(cp)
   331  	}
   332  }
   333  
   334  func nodeID(n graph.Node) string {
   335  	switch n := n.(type) {
   336  	case Node:
   337  		return n.DOTID()
   338  	default:
   339  		return fmt.Sprint(n.ID())
   340  	}
   341  }
   342  
   343  func graphID(g interface{}, n graph.Node) string {
   344  	switch g := g.(type) {
   345  	case Node:
   346  		return g.DOTID()
   347  	default:
   348  		return nodeID(n)
   349  	}
   350  }
   351  
   352  func (p *printer) writeAttributeList(a encoding.Attributer) {
   353  	attributes := a.Attributes()
   354  	switch len(attributes) {
   355  	case 0:
   356  	case 1:
   357  		p.buf.WriteString(" [")
   358  		p.buf.WriteString(quoteID(attributes[0].Key))
   359  		p.buf.WriteByte('=')
   360  		p.buf.WriteString(quoteID(attributes[0].Value))
   361  		p.buf.WriteString("]")
   362  	default:
   363  		p.openBlock(" [")
   364  		for _, att := range attributes {
   365  			p.newline()
   366  			p.buf.WriteString(quoteID(att.Key))
   367  			p.buf.WriteByte('=')
   368  			p.buf.WriteString(quoteID(att.Value))
   369  		}
   370  		p.closeBlock("]")
   371  	}
   372  }
   373  
   374  var attType = []string{"graph", "node", "edge"}
   375  
   376  func (p *printer) writeAttributeComplex(ca Attributers) {
   377  	g, n, e := ca.DOTAttributers()
   378  	haveWrittenBlock := false
   379  	for i, a := range []encoding.Attributer{g, n, e} {
   380  		if a == nil {
   381  			continue
   382  		}
   383  		attributes := a.Attributes()
   384  		if len(attributes) == 0 {
   385  			continue
   386  		}
   387  		if haveWrittenBlock {
   388  			p.buf.WriteByte(';')
   389  		}
   390  		p.newline()
   391  		p.buf.WriteString(attType[i])
   392  		p.openBlock(" [")
   393  		for _, att := range attributes {
   394  			p.newline()
   395  			p.buf.WriteString(quoteID(att.Key))
   396  			p.buf.WriteByte('=')
   397  			p.buf.WriteString(quoteID(att.Value))
   398  		}
   399  		p.closeBlock("]")
   400  		haveWrittenBlock = true
   401  	}
   402  	if haveWrittenBlock {
   403  		p.buf.WriteString(";\n")
   404  	}
   405  }
   406  
   407  func (p *printer) newline() {
   408  	p.buf.WriteByte('\n')
   409  	p.buf.WriteString(p.prefix)
   410  	for i := 0; i < p.depth; i++ {
   411  		p.buf.WriteString(p.indent)
   412  	}
   413  }
   414  
   415  func (p *printer) openBlock(b string) {
   416  	p.buf.WriteString(b)
   417  	p.depth++
   418  }
   419  
   420  func (p *printer) closeBlock(b string) {
   421  	p.depth--
   422  	p.newline()
   423  	p.buf.WriteString(b)
   424  }
   425  
   426  type simpleGraphPrinter struct {
   427  	printer
   428  	visited map[edge]bool
   429  }
   430  
   431  type multiGraphPrinter struct {
   432  	printer
   433  	visited map[line]bool
   434  }
   435  
   436  type line struct {
   437  	inGraph string
   438  	from    int64
   439  	to      int64
   440  	id      int64
   441  }
   442  
   443  func (p *multiGraphPrinter) print(g graph.Multigraph, name string, needsIndent, isSubgraph bool) error {
   444  	if name == "" {
   445  		if g, ok := g.(Multigraph); ok {
   446  			name = g.DOTID()
   447  		}
   448  	}
   449  
   450  	_, isDirected := g.(graph.Directed)
   451  	p.printFrontMatter(name, needsIndent, isSubgraph, isDirected, false)
   452  
   453  	if a, ok := g.(Attributers); ok {
   454  		p.writeAttributeComplex(a)
   455  	}
   456  	if s, ok := g.(MultiStructurer); ok {
   457  		for _, g := range s.Structure() {
   458  			_, subIsDirected := g.(graph.Directed)
   459  			if subIsDirected != isDirected {
   460  				return errors.New("dot: mismatched graph type")
   461  			}
   462  			p.buf.WriteByte('\n')
   463  			p.print(g, g.DOTID(), true, true)
   464  		}
   465  	}
   466  
   467  	nodes := graph.NodesOf(g.Nodes())
   468  	ordered.ByID(nodes)
   469  
   470  	havePrintedNodeHeader := false
   471  	for _, n := range nodes {
   472  		if s, ok := n.(MultiSubgrapher); ok {
   473  			// If the node is not linked to any other node
   474  			// the graph needs to be written now.
   475  			if g.From(n.ID()).Len() == 0 {
   476  				g := s.Subgraph()
   477  				_, subIsDirected := g.(graph.Directed)
   478  				if subIsDirected != isDirected {
   479  					return errors.New("dot: mismatched graph type")
   480  				}
   481  				if !havePrintedNodeHeader {
   482  					p.newline()
   483  					p.buf.WriteString("// Node definitions.")
   484  					havePrintedNodeHeader = true
   485  				}
   486  				p.newline()
   487  				p.print(g, graphID(g, n), false, true)
   488  			}
   489  			continue
   490  		}
   491  		if !havePrintedNodeHeader {
   492  			p.newline()
   493  			p.buf.WriteString("// Node definitions.")
   494  			havePrintedNodeHeader = true
   495  		}
   496  		p.newline()
   497  		p.writeNode(n)
   498  		if a, ok := n.(encoding.Attributer); ok {
   499  			p.writeAttributeList(a)
   500  		}
   501  		p.buf.WriteByte(';')
   502  	}
   503  
   504  	havePrintedEdgeHeader := false
   505  	for _, n := range nodes {
   506  		nid := n.ID()
   507  		to := graph.NodesOf(g.From(nid))
   508  		ordered.ByID(to)
   509  
   510  		for _, t := range to {
   511  			tid := t.ID()
   512  
   513  			lines := graph.LinesOf(g.Lines(nid, tid))
   514  			ordered.LinesByIDs(lines)
   515  
   516  			for _, l := range lines {
   517  				lid := l.ID()
   518  				f := line{inGraph: name, from: nid, to: tid, id: lid}
   519  				if isDirected {
   520  					if p.visited[f] {
   521  						continue
   522  					}
   523  					p.visited[f] = true
   524  				} else {
   525  					if p.visited[f] {
   526  						continue
   527  					}
   528  					p.visited[f] = true
   529  					p.visited[line{inGraph: name, from: tid, to: nid, id: lid}] = true
   530  				}
   531  
   532  				if !havePrintedEdgeHeader {
   533  					p.buf.WriteByte('\n')
   534  					p.buf.WriteString(strings.TrimRight(p.prefix, " \t\n")) // Trim whitespace suffix.
   535  					p.newline()
   536  					p.buf.WriteString("// Edge definitions.")
   537  					havePrintedEdgeHeader = true
   538  				}
   539  				p.newline()
   540  
   541  				if s, ok := n.(MultiSubgrapher); ok {
   542  					g := s.Subgraph()
   543  					_, subIsDirected := g.(graph.Directed)
   544  					if subIsDirected != isDirected {
   545  						return errors.New("dot: mismatched graph type")
   546  					}
   547  					p.print(g, graphID(g, n), false, true)
   548  				} else {
   549  					p.writeNode(n)
   550  				}
   551  
   552  				porter, edgeIsPorter := l.(Porter)
   553  				if edgeIsPorter {
   554  					if l.From().ID() == nid {
   555  						p.writePorts(porter.FromPort())
   556  					} else {
   557  						p.writePorts(porter.ToPort())
   558  					}
   559  				}
   560  
   561  				if isDirected {
   562  					p.buf.WriteString(" -> ")
   563  				} else {
   564  					p.buf.WriteString(" -- ")
   565  				}
   566  
   567  				if s, ok := t.(MultiSubgrapher); ok {
   568  					g := s.Subgraph()
   569  					_, subIsDirected := g.(graph.Directed)
   570  					if subIsDirected != isDirected {
   571  						return errors.New("dot: mismatched graph type")
   572  					}
   573  					p.print(g, graphID(g, t), false, true)
   574  				} else {
   575  					p.writeNode(t)
   576  				}
   577  				if edgeIsPorter {
   578  					if l.From().ID() == nid {
   579  						p.writePorts(porter.ToPort())
   580  					} else {
   581  						p.writePorts(porter.FromPort())
   582  					}
   583  				}
   584  
   585  				if a, ok := l.(encoding.Attributer); ok {
   586  					p.writeAttributeList(a)
   587  				}
   588  
   589  				p.buf.WriteByte(';')
   590  			}
   591  		}
   592  	}
   593  
   594  	p.closeBlock("}")
   595  
   596  	return nil
   597  }
   598  
   599  // quoteID quotes the given string if needed in the context of an ID. If s is
   600  // already quoted, or if s does not contain any spaces or special characters
   601  // that need escaping, the original string is returned.
   602  func quoteID(s string) string {
   603  	// To use a keyword as an ID, it must be quoted.
   604  	if isKeyword(s) {
   605  		return strconv.Quote(s)
   606  	}
   607  	// Quote if s is not an ID. This includes strings containing spaces, except
   608  	// if those spaces are used within HTML string IDs (e.g. <foo >).
   609  	if !isID(s) {
   610  		return strconv.Quote(s)
   611  	}
   612  	return s
   613  }
   614  
   615  // isKeyword reports whether the given string is a keyword in the DOT language.
   616  func isKeyword(s string) bool {
   617  	// ref: https://www.graphviz.org/doc/info/lang.html
   618  	keywords := []string{"node", "edge", "graph", "digraph", "subgraph", "strict"}
   619  	for _, keyword := range keywords {
   620  		if strings.EqualFold(s, keyword) {
   621  			return true
   622  		}
   623  	}
   624  	return false
   625  }
   626  
   627  // FIXME: see if we rewrite this in another way to remove our regexp dependency.
   628  
   629  // Regular expression to match identifier and numeral IDs.
   630  var (
   631  	reIdent   = regexp.MustCompile(`^[a-zA-Z\200-\377_][0-9a-zA-Z\200-\377_]*$`)
   632  	reNumeral = regexp.MustCompile(`^[-]?(\.[0-9]+|[0-9]+(\.[0-9]*)?)$`)
   633  )
   634  
   635  // isID reports whether the given string is an ID.
   636  //
   637  // An ID is one of the following:
   638  //
   639  // 1. Any string of alphabetic ([a-zA-Z\200-\377]) characters, underscores ('_')
   640  //    or digits ([0-9]), not beginning with a digit;
   641  // 2. a numeral [-]?(.[0-9]+ | [0-9]+(.[0-9]*)? );
   642  // 3. any double-quoted string ("...") possibly containing escaped quotes (\");
   643  // 4. an HTML string (<...>).
   644  func isID(s string) bool {
   645  	// 1. an identifier.
   646  	if reIdent.MatchString(s) {
   647  		return true
   648  	}
   649  	// 2. a numeral.
   650  	if reNumeral.MatchString(s) {
   651  		return true
   652  	}
   653  	// 3. double-quote string ID.
   654  	if len(s) >= 2 && strings.HasPrefix(s, `"`) && strings.HasSuffix(s, `"`) {
   655  		// Check that escape sequences within the double-quotes are valid.
   656  		if _, err := strconv.Unquote(s); err == nil {
   657  			return true
   658  		}
   659  	}
   660  	// 4. HTML ID.
   661  	return isHTMLID(s)
   662  }
   663  
   664  // isHTMLID reports whether the given string an HTML ID.
   665  func isHTMLID(s string) bool {
   666  	// HTML IDs have the format /^<.*>$/
   667  	return len(s) >= 2 && strings.HasPrefix(s, "<") && strings.HasSuffix(s, ">")
   668  }