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