1.1 --- a/graphviz.py Tue Sep 20 00:02:48 2011 +0200
1.2 +++ b/graphviz.py Sat Jan 14 16:35:38 2012 +0100
1.3 @@ -4,6 +4,7 @@
1.4 Based loosely on GNUPLOT parser by MoinMoin:KwonChanYoung
1.5
1.6 @copyright: 2008 Wayne Tucker
1.7 + @copyright: 2011, 2012 Paul Boddie <paul@boddie.org.uk>
1.8 @license: GNU GPL, see COPYING for details.
1.9 """
1.10
1.11 @@ -64,7 +65,6 @@
1.12 quick hack based on looking at an example and digging through the
1.13 MoinMoin source. The MoinMoin development docs haven't been
1.14 consulted (yet).
1.15 - - Only image formats (png and gif) are currently implemented
1.16 - Comments must start at the beginning of the graphviz block, and at the
1.17 beginning of their respective lines. They must also not contain
1.18 any extra whitespace surrounding the = sign.
1.19 @@ -77,14 +77,9 @@
1.20 BINARY_PATH = '/usr/bin'
1.21
1.22 import os
1.23 -import sys
1.24 -import base64
1.25 -import string
1.26 -import exceptions
1.27 -import codecs
1.28 import subprocess
1.29 -import time
1.30 import sha
1.31 +import re
1.32
1.33 from MoinMoin import config
1.34 from MoinMoin.action import AttachFile
1.35 @@ -93,10 +88,9 @@
1.36
1.37 logging = log.getLogger(__name__)
1.38
1.39 -class GraphVizError(exceptions.RuntimeError):
1.40 +class GraphVizError(RuntimeError):
1.41 pass
1.42
1.43 -
1.44 Dependencies = []
1.45
1.46 class Parser:
1.47 @@ -111,12 +105,30 @@
1.48 extensions = []
1.49 Dependencies = Dependencies
1.50
1.51 + attach_regexp = re.compile(
1.52 + r"graphviz_"
1.53 + r"(?P<digest>.*?)"
1.54 + r"(?:" # begin optional section
1.55 + r"_(?P<width>.*?)_(?P<height>.*?)" # dimensions
1.56 + r")?" # end optional section
1.57 + r"\.(?P<format>.*)"
1.58 + r"$")
1.59 +
1.60 + attr_regexp = re.compile(
1.61 + r"(?P<attr>width|height)"
1.62 + r"\s*=\s*"
1.63 + r"""(?P<quote>['"])""" # start quote
1.64 + r"(?P<value>.*?)"
1.65 + r"""(?P=quote)""", # matching quote
1.66 + re.UNICODE)
1.67 +
1.68 def __init__(self, raw, request, **kw):
1.69 self.raw = raw
1.70 self.request = request
1.71
1.72 def format(self, formatter):
1.73 - """ Send the text. """
1.74 +
1.75 + "Using the 'formatter', return the formatted page output."
1.76
1.77 request = self.request
1.78 page = request.page
1.79 @@ -125,86 +137,126 @@
1.80 request.flush() # to identify error text
1.81
1.82 self.filter = Parser.FILTERS[0]
1.83 - self.format = 'png'
1.84 - self.cmapx = None
1.85 + format = 'png'
1.86 + cmapx = None
1.87 + width = None
1.88 + height = None
1.89
1.90 raw_lines = self.raw.splitlines()
1.91 for l in raw_lines:
1.92 if not l[0:2] == '//':
1.93 break
1.94 - if l.lower().startswith('//filter='):
1.95 - tmp = l.split('=', 1)[1].lower()
1.96 - if tmp in Parser.FILTERS:
1.97 - self.filter = tmp
1.98 +
1.99 + parts = l[2:].split("=")
1.100 + directive = parts[0]
1.101 + value = "=".join(parts[1:])
1.102 +
1.103 + if directive == 'filter':
1.104 + filter = value.lower()
1.105 + if filter in Parser.FILTERS:
1.106 + self.filter = filter
1.107 else:
1.108 - logging.warn('unknown filter %s' % tmp)
1.109 - elif l.lower().startswith('//format='):
1.110 - tmp = l.split('=', 1)[1]
1.111 - if tmp in Parser.FORMATS:
1.112 - self.format = tmp
1.113 - elif l.lower().startswith('//cmapx='):
1.114 - self.cmapx = wikiutil.escape(l.split('=', 1)[1])
1.115 + logging.warn('unknown filter %s' % filter)
1.116
1.117 - if not self.format in Parser.OUTPUT_FORMATS:
1.118 + elif directive == 'format':
1.119 + value = value.lower()
1.120 + if value in Parser.FORMATS:
1.121 + format = value
1.122 +
1.123 + elif directive == 'cmapx':
1.124 + cmapx = wikiutil.escape(value)
1.125 +
1.126 + if not format in Parser.OUTPUT_FORMATS:
1.127 raise NotImplementedError, "only formats %s are currently supported" % \
1.128 Parser.OUTPUT_FORMATS
1.129
1.130 - if self.cmapx:
1.131 - if not self.format in Parser.IMAGE_FORMATS:
1.132 - logging.warn('format %s is incompatible with cmapx option' % self.format)
1.133 - self.cmapx = None
1.134 + if cmapx:
1.135 + if not format in Parser.IMAGE_FORMATS:
1.136 + logging.warn('format %s is incompatible with cmapx option' % format)
1.137 + cmapx = None
1.138
1.139 - img_name = 'graphviz_%s.%s' % (sha.new(self.raw).hexdigest(), self.format)
1.140 + digest = sha.new(self.raw).hexdigest()
1.141
1.142 self.pagename = formatter.page.page_name
1.143 - url = AttachFile.getAttachUrl(self.pagename, img_name, request)
1.144 - self.attach_dir=AttachFile.getAttachDir(request,self.pagename,create=1)
1.145 -
1.146 + self.attach_dir = AttachFile.getAttachDir(request, self.pagename, create=1)
1.147 self.delete_old_graphs(formatter)
1.148
1.149 - if not os.path.isfile(self.attach_dir + '/' + img_name):
1.150 - self.graphviz(self.raw, fn='%s/%s' % (self.attach_dir, img_name))
1.151 + attrs = self.find_graph(digest, format)
1.152 + if not attrs:
1.153 + attrs = self.graphviz(self.raw, digest, format)
1.154 +
1.155 + chart = self.get_chartname(digest, format, attrs)
1.156 + url = AttachFile.getAttachUrl(self.pagename, chart, request)
1.157
1.158 - if self.format in Parser.IMAGE_FORMATS:
1.159 - if self.cmapx:
1.160 - request.write('\n' + self.graphviz(self.raw, format='cmapx') + '\n')
1.161 - request.write(formatter.image(src="%s" % url, usemap="#%s" % self.cmapx))
1.162 + if format in Parser.IMAGE_FORMATS:
1.163 + if cmapx:
1.164 + request.write('\n' + self.graphviz(self.raw, digest, "cmapx") + '\n')
1.165 + request.write(formatter.image(src="%s" % url, usemap="#%s" % cmapx, **self.get_format_attrs(attrs)))
1.166 else:
1.167 - request.write(formatter.image(src="%s" % url, alt="graphviz image"))
1.168 + request.write(formatter.image(src="%s" % url, alt="graphviz image", **self.get_format_attrs(attrs)))
1.169 else:
1.170 - request.write(formatter.transclusion(1, data=url))
1.171 + request.write(formatter.transclusion(1, data=url, **self.get_format_attrs(attrs)))
1.172 request.write(formatter.text(_("graphviz image")))
1.173 request.write(formatter.transclusion(0))
1.174
1.175 + def find_graph(self, digest, format):
1.176 +
1.177 + "Find an existing graph using 'digest' and 'format'."
1.178 +
1.179 + attach_files = AttachFile._get_files(self.request, self.pagename)
1.180 +
1.181 + for chart in attach_files:
1.182 + match = self.attach_regexp.match(chart)
1.183 +
1.184 + if match and \
1.185 + match.group("digest") == digest and \
1.186 + match.group("format") == format:
1.187 +
1.188 + return match.groupdict()
1.189 +
1.190 + return None
1.191 +
1.192 + def get_chartname(self, digest, format, attrs):
1.193 +
1.194 + "Return the chart name for the 'digest', 'format' and 'attrs'."
1.195 +
1.196 + wh = self.get_dimensions(attrs)
1.197 + if wh:
1.198 + dimensions = "_%s_%s" % wh
1.199 + else:
1.200 + dimensions = ""
1.201 + return "graphviz_%s%s.%s" % (digest, dimensions, format)
1.202 +
1.203 def delete_old_graphs(self, formatter):
1.204 +
1.205 + "Using the 'formatter' for page information, delete old graphs."
1.206 +
1.207 page_info = formatter.page.lastEditInfo()
1.208 try:
1.209 page_date = page_info['time']
1.210 - except exceptions.KeyError, ex:
1.211 + except KeyError, ex:
1.212 return
1.213 +
1.214 attach_files = AttachFile._get_files(self.request, self.pagename)
1.215 +
1.216 for chart in attach_files:
1.217 - if chart.find('graphviz_') == 0 and chart[chart.rfind('.')+1:] in Parser.FORMATS:
1.218 + match = self.attach_regexp.match(chart)
1.219 +
1.220 + if match and match.group("format") in Parser.FORMATS:
1.221 fullpath = os.path.join(self.attach_dir, chart).encode(config.charset)
1.222 st = os.stat(fullpath)
1.223 - chart_date = self.request.user.getFormattedDateTime(st.st_mtime)
1.224 - if chart_date < page_date :
1.225 + chart_date = self.request.user.getFormattedDateTime(st.st_mtime)
1.226 + if chart_date < page_date:
1.227 os.remove(fullpath)
1.228 - else :
1.229 - continue
1.230 +
1.231 + def graphviz(self, graph_def, digest, format):
1.232
1.233 - def graphviz(self, graph_def, fn=None, format=None):
1.234 - if not format:
1.235 - format = self.format
1.236 - if fn:
1.237 - p = subprocess.Popen(['%s/%s' % (BINARY_PATH, self.filter), '-T%s' % format, '-o', fn], shell=False, \
1.238 - stdin=subprocess.PIPE, \
1.239 - stderr=subprocess.PIPE)
1.240 - else:
1.241 - p = subprocess.Popen(['%s/%s' % (BINARY_PATH, self.filter), '-T%s' % format], shell=False, \
1.242 - stdin=subprocess.PIPE, \
1.243 - stdout=subprocess.PIPE, \
1.244 - stderr=subprocess.PIPE)
1.245 + "Using the 'graph_def' and 'digest', generate output in the given 'format'."
1.246 +
1.247 + p = subprocess.Popen(['%s/%s' % (BINARY_PATH, self.filter), '-T%s' % format], shell=False, \
1.248 + stdin=subprocess.PIPE, \
1.249 + stdout=subprocess.PIPE, \
1.250 + stderr=subprocess.PIPE)
1.251
1.252 p.stdin.write(graph_def)
1.253 p.stdin.flush()
1.254 @@ -212,17 +264,73 @@
1.255
1.256 p.wait()
1.257
1.258 - if not fn:
1.259 - output = p.stdout.read()
1.260 + # Graph data always goes via standard output so that we can extract the
1.261 + # width and height if possible.
1.262
1.263 + output, attrs = self.process_output(p.stdout, format)
1.264 errors = p.stderr.read()
1.265 +
1.266 if len(errors) > 0:
1.267 raise GraphVizError, errors
1.268
1.269 - p = None
1.270 + # Copy to a file, returning the width and height if possible.
1.271 +
1.272 + if format != "cmapx":
1.273 + chart = self.get_chartname(digest, format, attrs)
1.274 + filename = os.path.join(self.attach_dir, chart).encode(config.charset)
1.275
1.276 - if fn:
1.277 - return None
1.278 + f = open(filename, "wb")
1.279 + try:
1.280 + f.write(output)
1.281 + finally:
1.282 + f.close()
1.283 +
1.284 + return attrs
1.285 +
1.286 + # Otherwise, return the output.
1.287 +
1.288 else:
1.289 return output
1.290
1.291 + def process_output(self, output, format):
1.292 +
1.293 + "Process graph 'output' in the given 'format'."
1.294 +
1.295 + # Return the raw output if SVG is not being produced.
1.296 +
1.297 + if format != "svg":
1.298 + return output.read(), {}
1.299 +
1.300 + # Otherwise, return the processed SVG output.
1.301 +
1.302 + processed = []
1.303 + found = False
1.304 + attrs = {}
1.305 +
1.306 + for line in output.xreadlines():
1.307 + if not found and line.startswith("<svg "):
1.308 + for match in self.attr_regexp.finditer(line):
1.309 + attrs[match.group("attr")] = match.group("value")
1.310 + found = True
1.311 + processed.append(line)
1.312 +
1.313 + return "".join(processed), attrs
1.314 +
1.315 + def get_dimensions(self, attrs):
1.316 +
1.317 + "Return a (width, height) tuple using the 'attrs' dictionary."
1.318 +
1.319 + if attrs.has_key("width") and attrs.has_key("height"):
1.320 + return attrs["width"], attrs["height"]
1.321 + else:
1.322 + return None
1.323 +
1.324 + def get_format_attrs(self, attrs):
1.325 +
1.326 + "Return a dictionary based on 'attrs' with only formatting attributes."
1.327 +
1.328 + dattrs = {}
1.329 + for key in ("width", "height"):
1.330 + if attrs.has_key(key):
1.331 + dattrs[key] = attrs[key]
1.332 + return dattrs