vitess.io/vitess@v0.16.2/go/tools/graphviz/graph.go (about)

     1  /*
     2  Copyright 2022 The Vitess Authors.
     3  
     4  Licensed under the Apache License, Version 2.0 (the "License");
     5  you may not use this file except in compliance with the License.
     6  You may obtain a copy of the License at
     7  
     8      http://www.apache.org/licenses/LICENSE-2.0
     9  
    10  Unless required by applicable law or agreed to in writing, software
    11  distributed under the License is distributed on an "AS IS" BASIS,
    12  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    13  See the License for the specific language governing permissions and
    14  limitations under the License.
    15  */
    16  
    17  package graphviz
    18  
    19  import (
    20  	"fmt"
    21  	"os"
    22  	"os/exec"
    23  	"runtime"
    24  	"strconv"
    25  	"strings"
    26  )
    27  
    28  type (
    29  	Graph struct {
    30  		lastID int
    31  		nodes  []*Node
    32  		edges  []*Edge
    33  	}
    34  	Node struct {
    35  		id      int
    36  		Name    string
    37  		attrs   []string
    38  		tooltip string
    39  	}
    40  	Edge struct {
    41  		From, To *Node
    42  	}
    43  )
    44  
    45  func escape(s string) string {
    46  	s = strings.ReplaceAll(s, "<", "\\<")
    47  	s = strings.ReplaceAll(s, ">", "\\>")
    48  	s = strings.ReplaceAll(s, "\"", "\\\"")
    49  	s = strings.ReplaceAll(s, "|", "\\|")
    50  	s = strings.ReplaceAll(s, "{", "\\{")
    51  	s = strings.ReplaceAll(s, "}", "\\}")
    52  	return s
    53  }
    54  
    55  func (n *Node) AddAttribute(s string) {
    56  	n.attrs = append(n.attrs, escape(s))
    57  }
    58  func (n *Node) AddTooltip(s string) {
    59  	n.tooltip = escape(s)
    60  }
    61  
    62  func (g *Graph) produceDot() string {
    63  	var dot strings.Builder
    64  	dot.WriteString(`digraph {
    65  node [shape=record, fontsize=10]
    66  `)
    67  	for _, node := range g.nodes {
    68  		labels := node.Name
    69  		for i, attr := range node.attrs {
    70  			if i == 0 {
    71  				labels += "|{" + attr
    72  			} else {
    73  				labels += "|" + attr
    74  			}
    75  
    76  		}
    77  		labels += "}"
    78  		if node.tooltip != "" {
    79  			dot.WriteString(fmt.Sprintf(`n%d [label="%s", tooltip="%s"]`, node.id, labels, node.tooltip))
    80  		} else {
    81  			dot.WriteString(fmt.Sprintf(`n%d [label="%s"]`, node.id, labels))
    82  		}
    83  		dot.WriteString(";\n")
    84  	}
    85  	for _, edge := range g.edges {
    86  		dot.WriteString(fmt.Sprintf(`n%d -> n%d`, edge.From.id, edge.To.id))
    87  		dot.WriteString(";\n")
    88  	}
    89  	dot.WriteString("}")
    90  	return dot.String()
    91  }
    92  
    93  func (g *Graph) AddNode(name string) *Node {
    94  	n := &Node{
    95  		id:   g.lastID,
    96  		Name: name,
    97  	}
    98  	g.lastID++
    99  	g.nodes = append(g.nodes, n)
   100  	return n
   101  }
   102  
   103  func (g *Graph) AddEdge(from, to *Node) *Edge {
   104  	e := &Edge{
   105  		From: from,
   106  		To:   to,
   107  	}
   108  	g.edges = append(g.edges, e)
   109  	return e
   110  }
   111  
   112  const htmlTemplate = `
   113  <!DOCTYPE html>
   114  <html>
   115  <head>
   116      <meta charset="UTF-8">
   117      <title>GraphViz Viewer</title>
   118      <script src="https://cdn.jsdelivr.net/npm/@hpcc-js/wasm/dist/index.min.js"></script>
   119  	<script src="https://cdn.jsdelivr.net/npm/svg-pan-zoom/dist/svg-pan-zoom.min.js"></script>
   120      <script>
   121          var hpccWasm = window["@hpcc-js/wasm"];
   122      </script>
   123  	<style>
   124  		#graph svg {
   125  			width: 90vw;
   126  			height: 90vh;
   127  			border-width: 1px;
   128  			border-style: dotted;
   129  		}
   130  	</style>
   131  </head>
   132  <body>
   133      <div id="graph"></div>
   134      <script>
   135  		const dot = %s;
   136          hpccWasm.graphviz.layout(dot, "svg", "dot").then(svg => {
   137              const div = document.getElementById("graph");
   138              div.innerHTML = svg;
   139  			svgPanZoom(div.querySelector('svg'), { controlIconsEnabled: true });
   140          });
   141      </script>
   142  </body>
   143  </html>
   144  `
   145  
   146  func (g *Graph) Render() error {
   147  
   148  	dot := g.produceDot()
   149  
   150  	browsers := func() []string {
   151  		var cmds []string
   152  		if userBrowser := os.Getenv("BROWSER"); userBrowser != "" {
   153  			cmds = append(cmds, userBrowser)
   154  		}
   155  		switch runtime.GOOS {
   156  		case "darwin":
   157  			cmds = append(cmds, "/usr/bin/open")
   158  		case "windows":
   159  			cmds = append(cmds, "cmd /c start")
   160  		default:
   161  			// Commands opening browsers are prioritized over xdg-open, so browser()
   162  			// command can be used on linux to open the .svg file generated by the -web
   163  			// command (the .svg file includes embedded javascript so is best viewed in
   164  			// a browser).
   165  			cmds = append(cmds, []string{"chrome", "google-chrome", "chromium", "firefox", "sensible-browser"}...)
   166  			if os.Getenv("DISPLAY") != "" {
   167  				// xdg-open is only for use in a desktop environment.
   168  				cmds = append(cmds, "xdg-open")
   169  			}
   170  		}
   171  		return cmds
   172  	}
   173  
   174  	tmpfile, err := os.CreateTemp("", "graphviz_*.html")
   175  	if err != nil {
   176  		return err
   177  	}
   178  
   179  	_, err = fmt.Fprintf(tmpfile, htmlTemplate, strconv.Quote(dot))
   180  	if err != nil {
   181  		return err
   182  	}
   183  	err = tmpfile.Close()
   184  	if err != nil {
   185  		return err
   186  	}
   187  
   188  	for _, b := range browsers() {
   189  		args := strings.Split(b, " ")
   190  		if len(args) == 0 {
   191  			continue
   192  		}
   193  		viewer := exec.Command(args[0], append(args[1:], tmpfile.Name())...)
   194  		viewer.Stderr = os.Stderr
   195  		if err := viewer.Start(); err == nil {
   196  			return nil
   197  		}
   198  	}
   199  
   200  	return fmt.Errorf("failed to open browser for SVG debugging")
   201  }
   202  
   203  func New() *Graph {
   204  	return &Graph{}
   205  }