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 }