1 # -*- coding: iso-8859-1 -*- 2 """ 3 MoinMoin - Graphviz Parser 4 Based loosely on GNUPLOT parser by MoinMoin:KwonChanYoung 5 6 @copyright: 2008 Wayne Tucker 7 @copyright: 2011, 2012 Paul Boddie <paul@boddie.org.uk> 8 @license: GNU GPL, see COPYING for details. 9 """ 10 11 """ 12 BASIC USAGE: 13 14 embed a visualization of a graph in a wiki page: 15 16 {{{#!graphviz 17 digraph G { 18 A -> B; 19 }; 20 }}} 21 22 ADVANCED USAGE: 23 24 This parser will check the first lines of the Graphviz data for C++ style 25 comments instructing it to use a different filter (dot, neato, twopi, 26 circo, or fdp - see http://graphviz.org/ for more info), use a different 27 format for the output (see the FORMATS list in the Parser class below), 28 or to generate and pass a client-side image map. 29 30 Options: 31 filter - the filter to use (see Parser.FILTERS) 32 format - the output format (see Parser.FORMATS) 33 cmapx - the map name to use for the client-side image map. Must match 34 the graph name in the graph definition and shouldn't conflict 35 with any other graphs that are used on the same page. 36 37 embed a visualization of a graph in a wiki page, using the dot filter and 38 providing a client-side image map (the filter=dot and format=png options are 39 redundant since those are the defaults for this parser): 40 41 {{{#!graphviz 42 //filter=dot 43 //format=png 44 //cmapx=DocumentationMap 45 digraph DocumentationMap { 46 FrontPage [href="FrontPage", root=true]; 47 HelpOnEditing [href="HelpOnEditing"]; 48 SyntaxReference [href="SyntaxReference"]; 49 WikiSandBox [href="WikiSandBox", color="grey"]; 50 MoinMoin [href="http://moinmo.in"]; 51 FrontPage -> WikiSandBox; 52 FrontPage -> MoinMoin; 53 WikiSandBox -> HelpOnEditing; 54 WikiSandBox -> SyntaxReference; 55 SyntaxReference -> FrontPage; 56 }; 57 }}} 58 59 60 KNOWN BUGS: 61 - Hasn't been thoroughly checked for potential methods of injecting 62 arbitrary HTML into the output. 63 - Only compatible with HTML rendering 64 - May not use all of the MoinMoin interfaces properly - this is a 65 quick hack based on looking at an example and digging through the 66 MoinMoin source. The MoinMoin development docs haven't been 67 consulted (yet). 68 - Comments must start at the beginning of the graphviz block, and at the 69 beginning of their respective lines. They must also not contain 70 any extra whitespace surrounding the = sign. 71 72 """ 73 74 # Change this to the directory that the Graphviz binaries (dot, neato, etc.) 75 # are installed in. 76 77 BINARY_PATH = '/usr/bin' 78 79 import os 80 import subprocess 81 import sha 82 import re 83 84 from MoinMoin import config 85 from MoinMoin.action import AttachFile 86 from MoinMoin import log 87 from MoinMoin import wikiutil 88 89 logging = log.getLogger(__name__) 90 91 class GraphVizError(RuntimeError): 92 pass 93 94 Dependencies = [] 95 96 class Parser: 97 """Uses the Graphviz programs to create a visualization of a graph.""" 98 99 FILTERS = ['dot', 'neato', 'twopi', 'circo', 'fdp'] 100 IMAGE_FORMATS = ['png', 'gif'] 101 OBJECT_FORMATS = ['svg', 'svgz'] 102 OUTPUT_FORMATS = IMAGE_FORMATS + OBJECT_FORMATS 103 FORMATS = OUTPUT_FORMATS + \ 104 ['ps', 'fig', 'mif', 'hpgl', 'pcl', 'dia', 'imap', 'cmapx'] 105 extensions = [] 106 Dependencies = Dependencies 107 108 attach_regexp = re.compile( 109 r"graphviz_" 110 r"(?P<digest>.*?)" 111 r"(?:" # begin optional section 112 r"_(?P<width>.*?)_(?P<height>.*?)" # dimensions 113 r")?" # end optional section 114 r"\.(?P<format>.*)" 115 r"$") 116 117 attr_regexp = re.compile( 118 r"(?P<attr>width|height)" 119 r"\s*=\s*" 120 r"""(?P<quote>['"])""" # start quote 121 r"(?P<value>.*?)" 122 r"""(?P=quote)""", # matching quote 123 re.UNICODE) 124 125 def __init__(self, raw, request, **kw): 126 self.raw = raw 127 self.request = request 128 129 def format(self, formatter): 130 131 "Using the 'formatter', return the formatted page output." 132 133 request = self.request 134 page = request.page 135 _ = request.getText 136 137 request.flush() # to identify error text 138 139 self.filter = Parser.FILTERS[0] 140 format = 'png' 141 cmapx = None 142 width = None 143 height = None 144 145 raw_lines = self.raw.splitlines() 146 for l in raw_lines: 147 if not l[0:2] == '//': 148 break 149 150 parts = l[2:].split("=") 151 directive = parts[0] 152 value = "=".join(parts[1:]) 153 154 if directive == 'filter': 155 filter = value.lower() 156 if filter in Parser.FILTERS: 157 self.filter = filter 158 else: 159 logging.warn('unknown filter %s' % filter) 160 161 elif directive == 'format': 162 value = value.lower() 163 if value in Parser.FORMATS: 164 format = value 165 166 elif directive == 'cmapx': 167 cmapx = wikiutil.escape(value) 168 169 if not format in Parser.OUTPUT_FORMATS: 170 raise NotImplementedError, "only formats %s are currently supported" % \ 171 Parser.OUTPUT_FORMATS 172 173 if cmapx: 174 if not format in Parser.IMAGE_FORMATS: 175 logging.warn('format %s is incompatible with cmapx option' % format) 176 cmapx = None 177 178 digest = sha.new(self.raw).hexdigest() 179 180 self.pagename = formatter.page.page_name 181 self.attach_dir = AttachFile.getAttachDir(request, self.pagename, create=1) 182 self.delete_old_graphs(formatter) 183 184 attrs = self.find_graph(digest, format) 185 if not attrs: 186 attrs = self.graphviz(self.raw, digest, format) 187 188 chart = self.get_chartname(digest, format, attrs) 189 url = AttachFile.getAttachUrl(self.pagename, chart, request) 190 191 if format in Parser.IMAGE_FORMATS: 192 if cmapx: 193 request.write('\n' + self.graphviz(self.raw, digest, "cmapx") + '\n') 194 request.write(formatter.image(src="%s" % url, usemap="#%s" % cmapx, **self.get_format_attrs(attrs))) 195 else: 196 request.write(formatter.image(src="%s" % url, alt="graphviz image", **self.get_format_attrs(attrs))) 197 else: 198 request.write(formatter.transclusion(1, data=url, **self.get_format_attrs(attrs))) 199 request.write(formatter.text(_("graphviz image"))) 200 request.write(formatter.transclusion(0)) 201 202 def find_graph(self, digest, format): 203 204 "Find an existing graph using 'digest' and 'format'." 205 206 attach_files = AttachFile._get_files(self.request, self.pagename) 207 208 for chart in attach_files: 209 match = self.attach_regexp.match(chart) 210 211 if match and \ 212 match.group("digest") == digest and \ 213 match.group("format") == format: 214 215 return match.groupdict() 216 217 return None 218 219 def get_chartname(self, digest, format, attrs): 220 221 "Return the chart name for the 'digest', 'format' and 'attrs'." 222 223 wh = self.get_dimensions(attrs) 224 if wh: 225 dimensions = "_%s_%s" % wh 226 else: 227 dimensions = "" 228 return "graphviz_%s%s.%s" % (digest, dimensions, format) 229 230 def delete_old_graphs(self, formatter): 231 232 "Using the 'formatter' for page information, delete old graphs." 233 234 page_info = formatter.page.lastEditInfo() 235 try: 236 page_date = page_info['time'] 237 except KeyError, ex: 238 return 239 240 attach_files = AttachFile._get_files(self.request, self.pagename) 241 242 for chart in attach_files: 243 match = self.attach_regexp.match(chart) 244 245 if match and match.group("format") in Parser.FORMATS: 246 fullpath = os.path.join(self.attach_dir, chart).encode(config.charset) 247 st = os.stat(fullpath) 248 chart_date = self.request.user.getFormattedDateTime(st.st_mtime) 249 if chart_date < page_date: 250 os.remove(fullpath) 251 252 def graphviz(self, graph_def, digest, format): 253 254 "Using the 'graph_def' and 'digest', generate output in the given 'format'." 255 256 p = subprocess.Popen(['%s/%s' % (BINARY_PATH, self.filter), '-T%s' % format], shell=False, \ 257 stdin=subprocess.PIPE, \ 258 stdout=subprocess.PIPE, \ 259 stderr=subprocess.PIPE) 260 261 p.stdin.write(graph_def) 262 p.stdin.flush() 263 p.stdin.close() 264 265 p.wait() 266 267 # Graph data always goes via standard output so that we can extract the 268 # width and height if possible. 269 270 output, attrs = self.process_output(p.stdout, format) 271 errors = p.stderr.read() 272 273 if len(errors) > 0: 274 raise GraphVizError, errors 275 276 # Copy to a file, returning the width and height if possible. 277 278 if format != "cmapx": 279 chart = self.get_chartname(digest, format, attrs) 280 filename = os.path.join(self.attach_dir, chart).encode(config.charset) 281 282 f = open(filename, "wb") 283 try: 284 f.write(output) 285 finally: 286 f.close() 287 288 return attrs 289 290 # Otherwise, return the output. 291 292 else: 293 return output 294 295 def process_output(self, output, format): 296 297 "Process graph 'output' in the given 'format'." 298 299 # Return the raw output if SVG is not being produced. 300 301 if format != "svg": 302 return output.read(), {} 303 304 # Otherwise, return the processed SVG output. 305 306 processed = [] 307 found = False 308 attrs = {} 309 310 for line in output.xreadlines(): 311 if not found and line.startswith("<svg "): 312 for match in self.attr_regexp.finditer(line): 313 attrs[match.group("attr")] = match.group("value") 314 found = True 315 processed.append(line) 316 317 return "".join(processed), attrs 318 319 def get_dimensions(self, attrs): 320 321 "Return a (width, height) tuple using the 'attrs' dictionary." 322 323 if attrs.has_key("width") and attrs.has_key("height"): 324 return attrs["width"], attrs["height"] 325 else: 326 return None 327 328 def get_format_attrs(self, attrs): 329 330 "Return a dictionary based on 'attrs' with only formatting attributes." 331 332 dattrs = {} 333 for key in ("width", "height"): 334 if attrs.has_key(key): 335 dattrs[key] = attrs[key] 336 return dattrs