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 # Change this to the directory that the Graphviz binaries (dot, neato, etc.) 12 # are installed in. 13 14 BINARY_PATH = '/usr/bin' 15 16 from os.path import join 17 import os 18 import subprocess 19 import sha 20 import re 21 22 from MoinMoin import config 23 from MoinMoin.action import AttachFile 24 from MoinMoin import log 25 from MoinMoin import wikiutil 26 27 logging = log.getLogger(__name__) 28 29 class GraphVizError(RuntimeError): 30 pass 31 32 Dependencies = ["pages"] 33 34 class Parser: 35 36 "Uses the Graphviz programs to create a visualization of a graph." 37 38 extensions = [] 39 Dependencies = Dependencies 40 41 FILTERS = ['dot', 'neato', 'twopi', 'circo', 'fdp'] 42 IMAGE_FORMATS = ['png', 'gif'] 43 SVG_FORMATS = ['svg', 'svgz'] 44 OUTPUT_FORMATS = IMAGE_FORMATS + SVG_FORMATS + \ 45 ['ps', 'fig', 'mif', 'hpgl', 'pcl', 'dia', 'imap'] 46 47 attach_regexp = re.compile( 48 r"graphviz_" 49 r"(?P<digest>.*?)" 50 r"(?:" # begin optional section 51 r"_(?P<width>.*?)_(?P<height>.*?)" # dimensions 52 r")?" # end optional section 53 r"\.(?P<format>.*)" 54 r"$") 55 56 attr_regexp = re.compile( 57 r"(?P<attr>width|height)" 58 r"\s*=\s*" 59 r"""(?P<quote>['"])""" # start quote 60 r"(?P<value>.*?)" 61 r"""(?P=quote)""", # matching quote 62 re.UNICODE) 63 64 def __init__(self, raw, request, **kw): 65 self.raw = raw 66 self.request = request 67 68 def format(self, formatter): 69 70 "Using the 'formatter', return the formatted page output." 71 72 request = self.request 73 page = request.page 74 _ = request.getText 75 76 request.flush() # to identify error text 77 78 filter = self.FILTERS[0] 79 format = 'png' 80 cmapx = None 81 width = None 82 height = None 83 84 raw_lines = self.raw.splitlines() 85 for l in raw_lines: 86 if not l[0:2] == '//': 87 break 88 89 parts = l[2:].split("=") 90 directive = parts[0] 91 value = "=".join(parts[1:]) 92 93 if directive == 'filter': 94 filter = value.lower() 95 if filter not in self.FILTERS: 96 logging.warn('unknown filter %s' % filter) 97 98 elif directive == 'format': 99 value = value.lower() 100 if value in self.OUTPUT_FORMATS: 101 format = value 102 103 elif directive == 'cmapx': 104 cmapx = wikiutil.escape(value) 105 106 if not format in self.OUTPUT_FORMATS: 107 raise NotImplementedError, "only formats %s are currently supported" % \ 108 self.OUTPUT_FORMATS 109 110 if cmapx and not format in self.IMAGE_FORMATS: 111 logging.warn('format %s is incompatible with cmapx option' % format) 112 cmapx = None 113 114 digest = sha.new(self.raw).hexdigest() 115 116 # Make sure that an attachments directory exists and that old graphs are 117 # deleted. 118 119 self.attach_dir = AttachFile.getAttachDir(request, page.page_name, create=1) 120 self.delete_old_graphs(formatter) 121 122 # Find the details of the graph, rendering a new graph if necessary. 123 124 attrs = self.find_graph(digest, format) 125 if not attrs: 126 attrs = self.graphviz(filter, self.raw, digest, format) 127 128 chart = self.get_chartname(digest, format, attrs) 129 url = AttachFile.getAttachUrl(page.page_name, chart, request) 130 131 # Images are displayed using the HTML "img" element (or equivalent) 132 # and may provide an imagemap. 133 134 if format in self.IMAGE_FORMATS: 135 if cmapx: 136 request.write('\n' + self.graphviz(filter, self.raw, digest, "cmapx") + '\n') 137 request.write(formatter.image(src="%s" % url, usemap="#%s" % cmapx, **self.get_format_attrs(attrs))) 138 else: 139 request.write(formatter.image(src="%s" % url, alt="graphviz image", **self.get_format_attrs(attrs))) 140 141 # Other objects are embedded using the HTML "object" element (or 142 # equivalent). 143 144 else: 145 request.write(formatter.transclusion(1, data=url, **self.get_format_attrs(attrs))) 146 request.write(formatter.text(_("graphviz image"))) 147 request.write(formatter.transclusion(0)) 148 149 def find_graph(self, digest, format): 150 151 "Find an existing graph using 'digest' and 'format'." 152 153 attach_files = AttachFile._get_files(self.request, self.request.page.page_name) 154 155 for chart in attach_files: 156 match = self.attach_regexp.match(chart) 157 158 if match and \ 159 match.group("digest") == digest and \ 160 match.group("format") == format: 161 162 return match.groupdict() 163 164 return None 165 166 def get_chartname(self, digest, format, attrs): 167 168 "Return the chart name for the 'digest', 'format' and 'attrs'." 169 170 wh = self.get_dimensions(attrs) 171 if wh: 172 dimensions = "_%s_%s" % wh 173 else: 174 dimensions = "" 175 return "graphviz_%s%s.%s" % (digest, dimensions, format) 176 177 def delete_old_graphs(self, formatter): 178 179 "Using the 'formatter' for page information, delete old graphs." 180 181 page_info = formatter.page.lastEditInfo() 182 try: 183 page_date = page_info['time'] 184 except KeyError, ex: 185 return 186 187 attach_files = AttachFile._get_files(self.request, self.request.page.page_name) 188 189 for chart in attach_files: 190 match = self.attach_regexp.match(chart) 191 192 if match and match.group("format") in self.OUTPUT_FORMATS: 193 fullpath = join(self.attach_dir, chart).encode(config.charset) 194 st = os.stat(fullpath) 195 chart_date = self.request.user.getFormattedDateTime(st.st_mtime) 196 if chart_date < page_date: 197 os.remove(fullpath) 198 199 def graphviz(self, filter, graph_def, digest, format): 200 201 """ 202 Using the 'filter' with the given 'graph_def' (and 'digest'), generate 203 output in the given 'format'. 204 """ 205 206 p = subprocess.Popen([join(BINARY_PATH, filter), '-T%s' % format], shell=False, \ 207 stdin=subprocess.PIPE, \ 208 stdout=subprocess.PIPE, \ 209 stderr=subprocess.PIPE) 210 211 p.stdin.write(graph_def) 212 p.stdin.flush() 213 p.stdin.close() 214 215 p.wait() 216 217 # Graph data always goes via standard output so that we can extract the 218 # width and height if possible. 219 220 output, attrs = self.process_output(p.stdout, format) 221 errors = p.stderr.read() 222 223 if len(errors) > 0: 224 raise GraphVizError, errors 225 226 # Copy to a file, returning the width and height if possible. 227 228 if format != "cmapx": 229 chart = self.get_chartname(digest, format, attrs) 230 filename = join(self.attach_dir, chart).encode(config.charset) 231 232 f = open(filename, "wb") 233 try: 234 f.write(output) 235 finally: 236 f.close() 237 238 return attrs 239 240 # Otherwise, return the output. 241 242 else: 243 return output 244 245 def process_output(self, output, format): 246 247 "Process graph 'output' in the given 'format'." 248 249 # Return the raw output if SVG is not being produced. 250 251 if format != "svg": 252 return output.read(), {} 253 254 # Otherwise, return the processed SVG output. 255 256 processed = [] 257 found = False 258 attrs = {} 259 260 for line in output.xreadlines(): 261 if not found and line.startswith("<svg "): 262 for match in self.attr_regexp.finditer(line): 263 attrs[match.group("attr")] = match.group("value") 264 found = True 265 processed.append(line) 266 267 return "".join(processed), attrs 268 269 def get_dimensions(self, attrs): 270 271 "Return a (width, height) tuple using the 'attrs' dictionary." 272 273 if attrs.has_key("width") and attrs.has_key("height"): 274 return attrs["width"], attrs["height"] 275 else: 276 return None 277 278 def get_format_attrs(self, attrs): 279 280 "Return a dictionary based on 'attrs' with only formatting attributes." 281 282 dattrs = {} 283 for key in ("width", "height"): 284 if attrs.has_key(key): 285 dattrs[key] = attrs[key] 286 return dattrs