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=None): 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 need_output = format in ("cmapx", "svg") 207 208 # Either write the output straight to a file. 209 210 if not need_output: 211 chart = self.get_chartname(digest, format) 212 filename = join(self.attach_dir, chart).encode(config.charset) 213 214 p = subprocess.Popen([ 215 join(BINARY_PATH, filter), '-T%s' % format, '-o%s' % filename 216 ], 217 shell=False, 218 stdin=subprocess.PIPE, 219 stdout=subprocess.PIPE, 220 stderr=subprocess.PIPE) 221 222 # Or intercept the output. 223 224 else: 225 p = subprocess.Popen([ 226 join(BINARY_PATH, filter), '-T%s' % format 227 ], 228 shell=False, 229 stdin=subprocess.PIPE, 230 stdout=subprocess.PIPE, 231 stderr=subprocess.PIPE) 232 233 p.stdin.write(graph_def) 234 p.stdin.flush() 235 p.stdin.close() 236 237 p.wait() 238 239 # Graph data always goes via standard output so that we can extract the 240 # width and height if possible. 241 242 if need_output: 243 output, attrs = self.process_output(p.stdout, format) 244 else: 245 output, attrs = None, {} 246 247 # Test for errors. 248 249 errors = p.stderr.read() 250 251 if len(errors) > 0: 252 raise GraphVizError, errors 253 254 # Return the output for imagemaps. 255 256 if format == "cmapx": 257 return output 258 259 # Copy to a file, if necessary. 260 261 elif need_output: 262 chart = self.get_chartname(digest, format, attrs) 263 filename = join(self.attach_dir, chart).encode(config.charset) 264 265 f = open(filename, "wb") 266 try: 267 f.write(output) 268 finally: 269 f.close() 270 271 # Return the dimensions, if defined. 272 273 return attrs 274 275 def process_output(self, output, format): 276 277 "Process graph 'output' in the given 'format'." 278 279 # Return the raw output if SVG is not being produced. 280 281 if format != "svg": 282 return output.read(), {} 283 284 # Otherwise, return the processed SVG output. 285 286 processed = [] 287 found = False 288 attrs = {} 289 290 for line in output.xreadlines(): 291 if not found and line.startswith("<svg "): 292 for match in self.attr_regexp.finditer(line): 293 attrs[match.group("attr")] = match.group("value") 294 found = True 295 processed.append(line) 296 297 return "".join(processed), attrs 298 299 def get_dimensions(self, attrs): 300 301 "Return a (width, height) tuple using the 'attrs' dictionary." 302 303 if attrs and attrs.has_key("width") and attrs.has_key("height"): 304 return attrs["width"], attrs["height"] 305 else: 306 return None 307 308 def get_format_attrs(self, attrs): 309 310 "Return a dictionary based on 'attrs' with only formatting attributes." 311 312 dattrs = {} 313 for key in ("width", "height"): 314 if attrs.has_key(key): 315 dattrs[key] = attrs[key] 316 return dattrs 317 318 # vim: tabstop=4 expandtab shiftwidth=4