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