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.1" 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 request.flush() # to identify error text 81 82 filter = self.FILTERS[0] 83 format = 'png' 84 cmapx = None 85 width = None 86 height = None 87 88 raw_lines = self.raw.splitlines() 89 for l in raw_lines: 90 if not l[0:2] == '//': 91 break 92 93 parts = l[2:].split("=") 94 directive = parts[0] 95 value = "=".join(parts[1:]) 96 97 if directive == 'filter': 98 filter = value.lower() 99 if filter not in self.FILTERS: 100 logging.warn('unknown filter %s' % filter) 101 102 elif directive == 'format': 103 value = value.lower() 104 if value in self.OUTPUT_FORMATS: 105 format = value 106 107 elif directive == 'cmapx': 108 cmapx = wikiutil.escape(value) 109 110 if not format in self.OUTPUT_FORMATS: 111 raise NotImplementedError, "only formats %s are currently supported" % \ 112 self.OUTPUT_FORMATS 113 114 if cmapx and not format in self.IMAGE_FORMATS: 115 logging.warn('format %s is incompatible with cmapx option' % format) 116 cmapx = None 117 118 digest = sha.new(self.raw.encode('utf-8')).hexdigest() 119 120 # Make sure that an attachments directory exists and that old graphs are 121 # deleted. 122 123 self.attach_dir = AttachFile.getAttachDir(request, page.page_name, create=1) 124 self.delete_old_graphs(formatter) 125 126 # Find the details of the graph, rendering a new graph if necessary. 127 128 attrs = self.find_graph(digest, format) 129 if not attrs: 130 attrs = self.graphviz(filter, self.raw, digest, format) 131 132 chart = self.get_chartname(digest, format, attrs) 133 url = AttachFile.getAttachUrl(page.page_name, chart, request) 134 135 # Images are displayed using the HTML "img" element (or equivalent) 136 # and may provide an imagemap. 137 138 if format in self.IMAGE_FORMATS: 139 if cmapx: 140 request.write('\n' + self.graphviz(filter, self.raw, digest, "cmapx") + '\n') 141 request.write(formatter.image(src="%s" % url, usemap="#%s" % cmapx, **self.get_format_attrs(attrs))) 142 else: 143 request.write(formatter.image(src="%s" % url, alt="graphviz image", **self.get_format_attrs(attrs))) 144 145 # Other objects are embedded using the HTML "object" element (or 146 # equivalent). 147 148 else: 149 request.write(formatter.transclusion(1, data=url, **self.get_format_attrs(attrs))) 150 request.write(formatter.text(_("graphviz image"))) 151 request.write(formatter.transclusion(0)) 152 153 def find_graph(self, digest, format): 154 155 "Find an existing graph using 'digest' and 'format'." 156 157 attach_files = AttachFile._get_files(self.request, self.request.page.page_name) 158 159 for chart in attach_files: 160 match = self.attach_regexp.match(chart) 161 162 if match and \ 163 match.group("digest") == digest and \ 164 match.group("format") == format: 165 166 return match.groupdict() 167 168 return None 169 170 def get_chartname(self, digest, format, attrs=None): 171 172 "Return the chart name for the 'digest', 'format' and 'attrs'." 173 174 wh = self.get_dimensions(attrs) 175 if wh: 176 dimensions = "_%s_%s" % wh 177 else: 178 dimensions = "" 179 return "graphviz_%s%s.%s" % (digest, dimensions, format) 180 181 def delete_old_graphs(self, formatter): 182 183 "Using the 'formatter' for page information, delete old graphs." 184 185 page_info = formatter.page.lastEditInfo() 186 try: 187 page_date = page_info['time'] 188 except KeyError, ex: 189 return 190 191 attach_files = AttachFile._get_files(self.request, self.request.page.page_name) 192 193 for chart in attach_files: 194 match = self.attach_regexp.match(chart) 195 196 if match and match.group("format") in self.OUTPUT_FORMATS: 197 fullpath = join(self.attach_dir, chart).encode(config.charset) 198 st = os.stat(fullpath) 199 chart_date = self.request.user.getFormattedDateTime(st.st_mtime) 200 if chart_date < page_date: 201 os.remove(fullpath) 202 203 def graphviz(self, filter, graph_def, digest, format): 204 205 """ 206 Using the 'filter' with the given 'graph_def' (and 'digest'), generate 207 output in the given 'format'. 208 """ 209 210 need_output = format in ("cmapx", "svg") 211 212 # Either write the output straight to a file. 213 214 if not need_output: 215 chart = self.get_chartname(digest, format) 216 filename = join(self.attach_dir, chart).encode(config.charset) 217 218 p = subprocess.Popen([ 219 join(BINARY_PATH, filter), '-T%s' % format, '-o%s' % filename 220 ], 221 shell=False, 222 stdin=subprocess.PIPE, 223 stdout=subprocess.PIPE, 224 stderr=subprocess.PIPE) 225 226 # Or intercept the output. 227 228 else: 229 p = subprocess.Popen([ 230 join(BINARY_PATH, filter), '-T%s' % format 231 ], 232 shell=False, 233 stdin=subprocess.PIPE, 234 stdout=subprocess.PIPE, 235 stderr=subprocess.PIPE) 236 237 (stdoutdata, stderrdata) = p.communicate(input=graph_def.encode('utf-8')) 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(StringIO(stdoutdata), format) 244 else: 245 output, attrs = None, {} 246 247 # Test for errors. 248 249 errors = stderrdata 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.readlines(): 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