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 }