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 }