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