1.1 --- /dev/null Thu Jan 01 00:00:00 1970 +0000
1.2 +++ b/parsers/graphviz.py Sat Jan 14 20:40:47 2012 +0100
1.3 @@ -0,0 +1,318 @@
1.4 +# -*- coding: iso-8859-1 -*-
1.5 +"""
1.6 + MoinMoin - Graphviz Parser
1.7 + Based loosely on GNUPLOT parser by MoinMoin:KwonChanYoung
1.8 +
1.9 + @copyright: 2008 Wayne Tucker
1.10 + @copyright: 2011, 2012 Paul Boddie <paul@boddie.org.uk>
1.11 + @license: GNU GPL, see COPYING for details.
1.12 +"""
1.13 +
1.14 +# Change this to the directory that the Graphviz binaries (dot, neato, etc.)
1.15 +# are installed in.
1.16 +
1.17 +BINARY_PATH = '/usr/bin'
1.18 +
1.19 +from os.path import join
1.20 +import os
1.21 +import subprocess
1.22 +import sha
1.23 +import re
1.24 +
1.25 +from MoinMoin import config
1.26 +from MoinMoin.action import AttachFile
1.27 +from MoinMoin import log
1.28 +from MoinMoin import wikiutil
1.29 +
1.30 +logging = log.getLogger(__name__)
1.31 +
1.32 +class GraphVizError(RuntimeError):
1.33 + pass
1.34 +
1.35 +Dependencies = ["pages"]
1.36 +
1.37 +class Parser:
1.38 +
1.39 + "Uses the Graphviz programs to create a visualization of a graph."
1.40 +
1.41 + extensions = []
1.42 + Dependencies = Dependencies
1.43 +
1.44 + FILTERS = ['dot', 'neato', 'twopi', 'circo', 'fdp']
1.45 + IMAGE_FORMATS = ['png', 'gif']
1.46 + SVG_FORMATS = ['svg', 'svgz']
1.47 + OUTPUT_FORMATS = IMAGE_FORMATS + SVG_FORMATS + \
1.48 + ['ps', 'fig', 'mif', 'hpgl', 'pcl', 'dia', 'imap']
1.49 +
1.50 + attach_regexp = re.compile(
1.51 + r"graphviz_"
1.52 + r"(?P<digest>.*?)"
1.53 + r"(?:" # begin optional section
1.54 + r"_(?P<width>.*?)_(?P<height>.*?)" # dimensions
1.55 + r")?" # end optional section
1.56 + r"\.(?P<format>.*)"
1.57 + r"$")
1.58 +
1.59 + attr_regexp = re.compile(
1.60 + r"(?P<attr>width|height)"
1.61 + r"\s*=\s*"
1.62 + r"""(?P<quote>['"])""" # start quote
1.63 + r"(?P<value>.*?)"
1.64 + r"""(?P=quote)""", # matching quote
1.65 + re.UNICODE)
1.66 +
1.67 + def __init__(self, raw, request, **kw):
1.68 + self.raw = raw
1.69 + self.request = request
1.70 +
1.71 + def format(self, formatter):
1.72 +
1.73 + "Using the 'formatter', return the formatted page output."
1.74 +
1.75 + request = self.request
1.76 + page = request.page
1.77 + _ = request.getText
1.78 +
1.79 + request.flush() # to identify error text
1.80 +
1.81 + filter = self.FILTERS[0]
1.82 + format = 'png'
1.83 + cmapx = None
1.84 + width = None
1.85 + height = None
1.86 +
1.87 + raw_lines = self.raw.splitlines()
1.88 + for l in raw_lines:
1.89 + if not l[0:2] == '//':
1.90 + break
1.91 +
1.92 + parts = l[2:].split("=")
1.93 + directive = parts[0]
1.94 + value = "=".join(parts[1:])
1.95 +
1.96 + if directive == 'filter':
1.97 + filter = value.lower()
1.98 + if filter not in self.FILTERS:
1.99 + logging.warn('unknown filter %s' % filter)
1.100 +
1.101 + elif directive == 'format':
1.102 + value = value.lower()
1.103 + if value in self.OUTPUT_FORMATS:
1.104 + format = value
1.105 +
1.106 + elif directive == 'cmapx':
1.107 + cmapx = wikiutil.escape(value)
1.108 +
1.109 + if not format in self.OUTPUT_FORMATS:
1.110 + raise NotImplementedError, "only formats %s are currently supported" % \
1.111 + self.OUTPUT_FORMATS
1.112 +
1.113 + if cmapx and not format in self.IMAGE_FORMATS:
1.114 + logging.warn('format %s is incompatible with cmapx option' % format)
1.115 + cmapx = None
1.116 +
1.117 + digest = sha.new(self.raw).hexdigest()
1.118 +
1.119 + # Make sure that an attachments directory exists and that old graphs are
1.120 + # deleted.
1.121 +
1.122 + self.attach_dir = AttachFile.getAttachDir(request, page.page_name, create=1)
1.123 + self.delete_old_graphs(formatter)
1.124 +
1.125 + # Find the details of the graph, rendering a new graph if necessary.
1.126 +
1.127 + attrs = self.find_graph(digest, format)
1.128 + if not attrs:
1.129 + attrs = self.graphviz(filter, self.raw, digest, format)
1.130 +
1.131 + chart = self.get_chartname(digest, format, attrs)
1.132 + url = AttachFile.getAttachUrl(page.page_name, chart, request)
1.133 +
1.134 + # Images are displayed using the HTML "img" element (or equivalent)
1.135 + # and may provide an imagemap.
1.136 +
1.137 + if format in self.IMAGE_FORMATS:
1.138 + if cmapx:
1.139 + request.write('\n' + self.graphviz(filter, self.raw, digest, "cmapx") + '\n')
1.140 + request.write(formatter.image(src="%s" % url, usemap="#%s" % cmapx, **self.get_format_attrs(attrs)))
1.141 + else:
1.142 + request.write(formatter.image(src="%s" % url, alt="graphviz image", **self.get_format_attrs(attrs)))
1.143 +
1.144 + # Other objects are embedded using the HTML "object" element (or
1.145 + # equivalent).
1.146 +
1.147 + else:
1.148 + request.write(formatter.transclusion(1, data=url, **self.get_format_attrs(attrs)))
1.149 + request.write(formatter.text(_("graphviz image")))
1.150 + request.write(formatter.transclusion(0))
1.151 +
1.152 + def find_graph(self, digest, format):
1.153 +
1.154 + "Find an existing graph using 'digest' and 'format'."
1.155 +
1.156 + attach_files = AttachFile._get_files(self.request, self.request.page.page_name)
1.157 +
1.158 + for chart in attach_files:
1.159 + match = self.attach_regexp.match(chart)
1.160 +
1.161 + if match and \
1.162 + match.group("digest") == digest and \
1.163 + match.group("format") == format:
1.164 +
1.165 + return match.groupdict()
1.166 +
1.167 + return None
1.168 +
1.169 + def get_chartname(self, digest, format, attrs=None):
1.170 +
1.171 + "Return the chart name for the 'digest', 'format' and 'attrs'."
1.172 +
1.173 + wh = self.get_dimensions(attrs)
1.174 + if wh:
1.175 + dimensions = "_%s_%s" % wh
1.176 + else:
1.177 + dimensions = ""
1.178 + return "graphviz_%s%s.%s" % (digest, dimensions, format)
1.179 +
1.180 + def delete_old_graphs(self, formatter):
1.181 +
1.182 + "Using the 'formatter' for page information, delete old graphs."
1.183 +
1.184 + page_info = formatter.page.lastEditInfo()
1.185 + try:
1.186 + page_date = page_info['time']
1.187 + except KeyError, ex:
1.188 + return
1.189 +
1.190 + attach_files = AttachFile._get_files(self.request, self.request.page.page_name)
1.191 +
1.192 + for chart in attach_files:
1.193 + match = self.attach_regexp.match(chart)
1.194 +
1.195 + if match and match.group("format") in self.OUTPUT_FORMATS:
1.196 + fullpath = join(self.attach_dir, chart).encode(config.charset)
1.197 + st = os.stat(fullpath)
1.198 + chart_date = self.request.user.getFormattedDateTime(st.st_mtime)
1.199 + if chart_date < page_date:
1.200 + os.remove(fullpath)
1.201 +
1.202 + def graphviz(self, filter, graph_def, digest, format):
1.203 +
1.204 + """
1.205 + Using the 'filter' with the given 'graph_def' (and 'digest'), generate
1.206 + output in the given 'format'.
1.207 + """
1.208 +
1.209 + need_output = format in ("cmapx", "svg")
1.210 +
1.211 + # Either write the output straight to a file.
1.212 +
1.213 + if not need_output:
1.214 + chart = self.get_chartname(digest, format)
1.215 + filename = join(self.attach_dir, chart).encode(config.charset)
1.216 +
1.217 + p = subprocess.Popen([
1.218 + join(BINARY_PATH, filter), '-T%s' % format, '-o%s' % filename
1.219 + ],
1.220 + shell=False,
1.221 + stdin=subprocess.PIPE,
1.222 + stdout=subprocess.PIPE,
1.223 + stderr=subprocess.PIPE)
1.224 +
1.225 + # Or intercept the output.
1.226 +
1.227 + else:
1.228 + p = subprocess.Popen([
1.229 + join(BINARY_PATH, filter), '-T%s' % format
1.230 + ],
1.231 + shell=False,
1.232 + stdin=subprocess.PIPE,
1.233 + stdout=subprocess.PIPE,
1.234 + stderr=subprocess.PIPE)
1.235 +
1.236 + p.stdin.write(graph_def)
1.237 + p.stdin.flush()
1.238 + p.stdin.close()
1.239 +
1.240 + p.wait()
1.241 +
1.242 + # Graph data always goes via standard output so that we can extract the
1.243 + # width and height if possible.
1.244 +
1.245 + if need_output:
1.246 + output, attrs = self.process_output(p.stdout, format)
1.247 + else:
1.248 + output, attrs = None, {}
1.249 +
1.250 + # Test for errors.
1.251 +
1.252 + errors = p.stderr.read()
1.253 +
1.254 + if len(errors) > 0:
1.255 + raise GraphVizError, errors
1.256 +
1.257 + # Return the output for imagemaps.
1.258 +
1.259 + if format == "cmapx":
1.260 + return output
1.261 +
1.262 + # Copy to a file, if necessary.
1.263 +
1.264 + elif need_output:
1.265 + chart = self.get_chartname(digest, format, attrs)
1.266 + filename = join(self.attach_dir, chart).encode(config.charset)
1.267 +
1.268 + f = open(filename, "wb")
1.269 + try:
1.270 + f.write(output)
1.271 + finally:
1.272 + f.close()
1.273 +
1.274 + # Return the dimensions, if defined.
1.275 +
1.276 + return attrs
1.277 +
1.278 + def process_output(self, output, format):
1.279 +
1.280 + "Process graph 'output' in the given 'format'."
1.281 +
1.282 + # Return the raw output if SVG is not being produced.
1.283 +
1.284 + if format != "svg":
1.285 + return output.read(), {}
1.286 +
1.287 + # Otherwise, return the processed SVG output.
1.288 +
1.289 + processed = []
1.290 + found = False
1.291 + attrs = {}
1.292 +
1.293 + for line in output.xreadlines():
1.294 + if not found and line.startswith("<svg "):
1.295 + for match in self.attr_regexp.finditer(line):
1.296 + attrs[match.group("attr")] = match.group("value")
1.297 + found = True
1.298 + processed.append(line)
1.299 +
1.300 + return "".join(processed), attrs
1.301 +
1.302 + def get_dimensions(self, attrs):
1.303 +
1.304 + "Return a (width, height) tuple using the 'attrs' dictionary."
1.305 +
1.306 + if attrs and attrs.has_key("width") and attrs.has_key("height"):
1.307 + return attrs["width"], attrs["height"]
1.308 + else:
1.309 + return None
1.310 +
1.311 + def get_format_attrs(self, attrs):
1.312 +
1.313 + "Return a dictionary based on 'attrs' with only formatting attributes."
1.314 +
1.315 + dattrs = {}
1.316 + for key in ("width", "height"):
1.317 + if attrs.has_key(key):
1.318 + dattrs[key] = attrs[key]
1.319 + return dattrs
1.320 +
1.321 +# vim: tabstop=4 expandtab shiftwidth=4