# HG changeset patch # User Paul Boddie # Date 1316387792 -7200 # Node ID bb3f785cfacd8f588bdb259bb74440fc77bb8407 Moin Graphviz parser. diff -r 000000000000 -r bb3f785cfacd graphviz.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/graphviz.py Mon Sep 19 01:16:32 2011 +0200 @@ -0,0 +1,219 @@ +# -*- coding: iso-8859-1 -*- +""" + MoinMoin - Graphviz Parser + Based loosely on GNUPLOT parser by MoinMoin:KwonChanYoung + + @copyright: 2008 Wayne Tucker + @license: GNU GPL, see COPYING for details. +""" + +""" + BASIC USAGE: + + embed a visualization of a graph in a wiki page: + +{{{#!graphviz +digraph G { + A -> B; +}; +}}} + + ADVANCED USAGE: + + This parser will check the first lines of the Graphviz data for C++ style + comments instructing it to use a different filter (dot, neato, twopi, + circo, or fdp - see http://graphviz.org/ for more info), use a different + format for the output (see the FORMATS list in the Parser class below), + or to generate and pass a client-side image map. + + Options: + filter - the filter to use (see Parser.FILTERS) + format - the output format (see Parser.FORMATS) + cmapx - the map name to use for the client-side image map. Must match + the graph name in the graph definition and shouldn't conflict + with any other graphs that are used on the same page. + + embed a visualization of a graph in a wiki page, using the dot filter and + providing a client-side image map (the filter=dot and format=png options are + redundant since those are the defaults for this parser): + +{{{#!graphviz +//filter=dot +//format=png +//cmapx=DocumentationMap +digraph DocumentationMap { + FrontPage [href="FrontPage", root=true]; + HelpOnEditing [href="HelpOnEditing"]; + SyntaxReference [href="SyntaxReference"]; + WikiSandBox [href="WikiSandBox", color="grey"]; + MoinMoin [href="http://moinmo.in"]; + FrontPage -> WikiSandBox; + FrontPage -> MoinMoin; + WikiSandBox -> HelpOnEditing; + WikiSandBox -> SyntaxReference; + SyntaxReference -> FrontPage; +}; +}}} + + + KNOWN BUGS: + - Hasn't been thoroughly checked for potential methods of injecting + arbitrary HTML into the output. + - Only compatible with HTML rendering + - May not use all of the MoinMoin interfaces properly - this is a + quick hack based on looking at an example and digging through the + MoinMoin source. The MoinMoin development docs haven't been + consulted (yet). + - Only image formats (png and gif) are currently implemented + - Comments must start at the beginning of the graphviz block, and at the + beginning of their respective lines. They must also not contain + any extra whitespace surrounding the = sign. + +""" + +# Change this to the directory that the Graphviz binaries (dot, neato, etc.) +# are installed in. + +BINARY_PATH = '/usr/bin' + +import os +import sys +import base64 +import string +import exceptions +import codecs +import subprocess +import time +import sha + +from MoinMoin import config +from MoinMoin.action import AttachFile +from MoinMoin import log +from MoinMoin import wikiutil + +logging = log.getLogger(__name__) + +class GraphVizError(exceptions.RuntimeError): + pass + + +Dependencies = [] + +class Parser: + """Uses the Graphviz programs to create a visualization of a graph.""" + + FILTERS = ['dot', 'neato', 'twopi', 'circo', 'fdp'] + IMAGE_FORMATS = ['png', 'gif'] + FORMATS = IMAGE_FORMATS + ['ps', 'svg', 'svgz', 'fig', 'mif', \ + 'hpgl', 'pcl', 'dia', 'imap', 'cmapx'] + extensions = [] + Dependencies = Dependencies + + def __init__(self, raw, request, **kw): + self.raw = raw + self.request = request + + def format(self, formatter): + """ Send the text. """ + self.request.flush() # to identify error text + + self.filter = Parser.FILTERS[0] + self.format = 'png' + self.cmapx = None + + raw_lines = self.raw.splitlines() + for l in raw_lines: + if not l[0:2] == '//': + break + if l.lower().startswith('//filter='): + tmp = l.split('=', 1)[1].lower() + if tmp in Parser.FILTERS: + self.filter = tmp + else: + logging.warn('unknown filter %s' % tmp) + elif l.lower().startswith('//format='): + tmp = l.split('=', 1)[1] + if tmp in Parser.FORMATS: + self.format = tmp + elif l.lower().startswith('//cmapx='): + self.cmapx = wikiutil.escape(l.split('=', 1)[1]) + + if not self.format in Parser.IMAGE_FORMATS: + raise NotImplementedError, "only formats %s are currently supported" % Parser.IMAGE_FORMATS + + if self.cmapx: + if not self.format in Parser.IMAGE_FORMATS: + logging.warn('format %s is incompatible with cmapx option' % self.format) + self.cmapx = None + + img_name = 'graphviz_%s.%s' % (sha.new(self.raw).hexdigest(), self.format) + + self.pagename = formatter.page.page_name + url = AttachFile.getAttachUrl(self.pagename, img_name, self.request) + self.attach_dir=AttachFile.getAttachDir(self.request,self.pagename,create=1) + + self.delete_old_graphs(formatter) + + if not os.path.isfile(self.attach_dir + '/' + img_name): + self.graphviz(self.raw, fn='%s/%s' % (self.attach_dir, img_name)) + + if self.format in Parser.IMAGE_FORMATS: + if self.cmapx: + self.request.write('\n' + self.graphviz(self.raw, format='cmapx') + '\n') + self.request.write(formatter.image(src="%s" % url, usemap="#%s" % self.cmapx)) + else: + self.request.write(formatter.image(src="%s" % url, alt="graphviz image")) + else: + # TODO: read the docs and figure out how to do this correctly + self.request.write(formatter.attachment_link(True, url=url)) + + def delete_old_graphs(self, formatter): + page_info = formatter.page.lastEditInfo() + try: + page_date = page_info['time'] + except exceptions.KeyError, ex: + return + attach_files = AttachFile._get_files(self.request, self.pagename) + for chart in attach_files: + if chart.find('graphviz_') == 0 and chart[chart.rfind('.')+1:] in Parser.FORMATS: + fullpath = os.path.join(self.attach_dir, chart).encode(config.charset) + st = os.stat(fullpath) + chart_date = self.request.user.getFormattedDateTime(st.st_mtime) + if chart_date < page_date : + os.remove(fullpath) + else : + continue + + def graphviz(self, graph_def, fn=None, format=None): + if not format: + format = self.format + if fn: + p = subprocess.Popen(['%s/%s' % (BINARY_PATH, self.filter), '-T%s' % format, '-o', fn], shell=False, \ + stdin=subprocess.PIPE, \ + stderr=subprocess.PIPE) + else: + p = subprocess.Popen(['%s/%s' % (BINARY_PATH, self.filter), '-T%s' % format], shell=False, \ + stdin=subprocess.PIPE, \ + stdout=subprocess.PIPE, \ + stderr=subprocess.PIPE) + + p.stdin.write(graph_def) + p.stdin.flush() + p.stdin.close() + + p.wait() + + if not fn: + output = p.stdout.read() + + errors = p.stderr.read() + if len(errors) > 0: + raise GraphVizError, errors + + p = None + + if fn: + return None + else: + return output +