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 @license: GNU GPL, see COPYING for details. 8 """ 9 10 """ 11 BASIC USAGE: 12 13 embed a visualization of a graph in a wiki page: 14 15 {{{#!graphviz 16 digraph G { 17 A -> B; 18 }; 19 }}} 20 21 ADVANCED USAGE: 22 23 This parser will check the first lines of the Graphviz data for C++ style 24 comments instructing it to use a different filter (dot, neato, twopi, 25 circo, or fdp - see http://graphviz.org/ for more info), use a different 26 format for the output (see the FORMATS list in the Parser class below), 27 or to generate and pass a client-side image map. 28 29 Options: 30 filter - the filter to use (see Parser.FILTERS) 31 format - the output format (see Parser.FORMATS) 32 cmapx - the map name to use for the client-side image map. Must match 33 the graph name in the graph definition and shouldn't conflict 34 with any other graphs that are used on the same page. 35 36 embed a visualization of a graph in a wiki page, using the dot filter and 37 providing a client-side image map (the filter=dot and format=png options are 38 redundant since those are the defaults for this parser): 39 40 {{{#!graphviz 41 //filter=dot 42 //format=png 43 //cmapx=DocumentationMap 44 digraph DocumentationMap { 45 FrontPage [href="FrontPage", root=true]; 46 HelpOnEditing [href="HelpOnEditing"]; 47 SyntaxReference [href="SyntaxReference"]; 48 WikiSandBox [href="WikiSandBox", color="grey"]; 49 MoinMoin [href="http://moinmo.in"]; 50 FrontPage -> WikiSandBox; 51 FrontPage -> MoinMoin; 52 WikiSandBox -> HelpOnEditing; 53 WikiSandBox -> SyntaxReference; 54 SyntaxReference -> FrontPage; 55 }; 56 }}} 57 58 59 KNOWN BUGS: 60 - Hasn't been thoroughly checked for potential methods of injecting 61 arbitrary HTML into the output. 62 - Only compatible with HTML rendering 63 - May not use all of the MoinMoin interfaces properly - this is a 64 quick hack based on looking at an example and digging through the 65 MoinMoin source. The MoinMoin development docs haven't been 66 consulted (yet). 67 - Only image formats (png and gif) are currently implemented 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 sys 81 import base64 82 import string 83 import exceptions 84 import codecs 85 import subprocess 86 import time 87 import sha 88 89 from MoinMoin import config 90 from MoinMoin.action import AttachFile 91 from MoinMoin import log 92 from MoinMoin import wikiutil 93 94 logging = log.getLogger(__name__) 95 96 class GraphVizError(exceptions.RuntimeError): 97 pass 98 99 100 Dependencies = [] 101 102 class Parser: 103 """Uses the Graphviz programs to create a visualization of a graph.""" 104 105 FILTERS = ['dot', 'neato', 'twopi', 'circo', 'fdp'] 106 IMAGE_FORMATS = ['png', 'gif'] 107 FORMATS = IMAGE_FORMATS + ['ps', 'svg', 'svgz', 'fig', 'mif', \ 108 'hpgl', 'pcl', 'dia', 'imap', 'cmapx'] 109 extensions = [] 110 Dependencies = Dependencies 111 112 def __init__(self, raw, request, **kw): 113 self.raw = raw 114 self.request = request 115 116 def format(self, formatter): 117 """ Send the text. """ 118 self.request.flush() # to identify error text 119 120 self.filter = Parser.FILTERS[0] 121 self.format = 'png' 122 self.cmapx = None 123 124 raw_lines = self.raw.splitlines() 125 for l in raw_lines: 126 if not l[0:2] == '//': 127 break 128 if l.lower().startswith('//filter='): 129 tmp = l.split('=', 1)[1].lower() 130 if tmp in Parser.FILTERS: 131 self.filter = tmp 132 else: 133 logging.warn('unknown filter %s' % tmp) 134 elif l.lower().startswith('//format='): 135 tmp = l.split('=', 1)[1] 136 if tmp in Parser.FORMATS: 137 self.format = tmp 138 elif l.lower().startswith('//cmapx='): 139 self.cmapx = wikiutil.escape(l.split('=', 1)[1]) 140 141 if not self.format in Parser.IMAGE_FORMATS: 142 raise NotImplementedError, "only formats %s are currently supported" % Parser.IMAGE_FORMATS 143 144 if self.cmapx: 145 if not self.format in Parser.IMAGE_FORMATS: 146 logging.warn('format %s is incompatible with cmapx option' % self.format) 147 self.cmapx = None 148 149 img_name = 'graphviz_%s.%s' % (sha.new(self.raw).hexdigest(), self.format) 150 151 self.pagename = formatter.page.page_name 152 url = AttachFile.getAttachUrl(self.pagename, img_name, self.request) 153 self.attach_dir=AttachFile.getAttachDir(self.request,self.pagename,create=1) 154 155 self.delete_old_graphs(formatter) 156 157 if not os.path.isfile(self.attach_dir + '/' + img_name): 158 self.graphviz(self.raw, fn='%s/%s' % (self.attach_dir, img_name)) 159 160 if self.format in Parser.IMAGE_FORMATS: 161 if self.cmapx: 162 self.request.write('\n' + self.graphviz(self.raw, format='cmapx') + '\n') 163 self.request.write(formatter.image(src="%s" % url, usemap="#%s" % self.cmapx)) 164 else: 165 self.request.write(formatter.image(src="%s" % url, alt="graphviz image")) 166 else: 167 # TODO: read the docs and figure out how to do this correctly 168 self.request.write(formatter.attachment_link(True, url=url)) 169 170 def delete_old_graphs(self, formatter): 171 page_info = formatter.page.lastEditInfo() 172 try: 173 page_date = page_info['time'] 174 except exceptions.KeyError, ex: 175 return 176 attach_files = AttachFile._get_files(self.request, self.pagename) 177 for chart in attach_files: 178 if chart.find('graphviz_') == 0 and chart[chart.rfind('.')+1:] in Parser.FORMATS: 179 fullpath = os.path.join(self.attach_dir, chart).encode(config.charset) 180 st = os.stat(fullpath) 181 chart_date = self.request.user.getFormattedDateTime(st.st_mtime) 182 if chart_date < page_date : 183 os.remove(fullpath) 184 else : 185 continue 186 187 def graphviz(self, graph_def, fn=None, format=None): 188 if not format: 189 format = self.format 190 if fn: 191 p = subprocess.Popen(['%s/%s' % (BINARY_PATH, self.filter), '-T%s' % format, '-o', fn], shell=False, \ 192 stdin=subprocess.PIPE, \ 193 stderr=subprocess.PIPE) 194 else: 195 p = subprocess.Popen(['%s/%s' % (BINARY_PATH, self.filter), '-T%s' % format], shell=False, \ 196 stdin=subprocess.PIPE, \ 197 stdout=subprocess.PIPE, \ 198 stderr=subprocess.PIPE) 199 200 p.stdin.write(graph_def) 201 p.stdin.flush() 202 p.stdin.close() 203 204 p.wait() 205 206 if not fn: 207 output = p.stdout.read() 208 209 errors = p.stderr.read() 210 if len(errors) > 0: 211 raise GraphVizError, errors 212 213 p = None 214 215 if fn: 216 return None 217 else: 218 return output 219