paul@0 | 1 | # -*- coding: iso-8859-1 -*- |
paul@0 | 2 | """ |
paul@0 | 3 | MoinMoin - Graphviz Parser |
paul@0 | 4 | Based loosely on GNUPLOT parser by MoinMoin:KwonChanYoung |
paul@0 | 5 | |
paul@0 | 6 | @copyright: 2008 Wayne Tucker |
paul@0 | 7 | @license: GNU GPL, see COPYING for details. |
paul@0 | 8 | """ |
paul@0 | 9 | |
paul@0 | 10 | """ |
paul@0 | 11 | BASIC USAGE: |
paul@0 | 12 | |
paul@0 | 13 | embed a visualization of a graph in a wiki page: |
paul@0 | 14 | |
paul@0 | 15 | {{{#!graphviz |
paul@0 | 16 | digraph G { |
paul@0 | 17 | A -> B; |
paul@0 | 18 | }; |
paul@0 | 19 | }}} |
paul@0 | 20 | |
paul@0 | 21 | ADVANCED USAGE: |
paul@0 | 22 | |
paul@0 | 23 | This parser will check the first lines of the Graphviz data for C++ style |
paul@0 | 24 | comments instructing it to use a different filter (dot, neato, twopi, |
paul@0 | 25 | circo, or fdp - see http://graphviz.org/ for more info), use a different |
paul@0 | 26 | format for the output (see the FORMATS list in the Parser class below), |
paul@0 | 27 | or to generate and pass a client-side image map. |
paul@0 | 28 | |
paul@0 | 29 | Options: |
paul@0 | 30 | filter - the filter to use (see Parser.FILTERS) |
paul@0 | 31 | format - the output format (see Parser.FORMATS) |
paul@0 | 32 | cmapx - the map name to use for the client-side image map. Must match |
paul@0 | 33 | the graph name in the graph definition and shouldn't conflict |
paul@0 | 34 | with any other graphs that are used on the same page. |
paul@0 | 35 | |
paul@0 | 36 | embed a visualization of a graph in a wiki page, using the dot filter and |
paul@0 | 37 | providing a client-side image map (the filter=dot and format=png options are |
paul@0 | 38 | redundant since those are the defaults for this parser): |
paul@0 | 39 | |
paul@0 | 40 | {{{#!graphviz |
paul@0 | 41 | //filter=dot |
paul@0 | 42 | //format=png |
paul@0 | 43 | //cmapx=DocumentationMap |
paul@0 | 44 | digraph DocumentationMap { |
paul@0 | 45 | FrontPage [href="FrontPage", root=true]; |
paul@0 | 46 | HelpOnEditing [href="HelpOnEditing"]; |
paul@0 | 47 | SyntaxReference [href="SyntaxReference"]; |
paul@0 | 48 | WikiSandBox [href="WikiSandBox", color="grey"]; |
paul@0 | 49 | MoinMoin [href="http://moinmo.in"]; |
paul@0 | 50 | FrontPage -> WikiSandBox; |
paul@0 | 51 | FrontPage -> MoinMoin; |
paul@0 | 52 | WikiSandBox -> HelpOnEditing; |
paul@0 | 53 | WikiSandBox -> SyntaxReference; |
paul@0 | 54 | SyntaxReference -> FrontPage; |
paul@0 | 55 | }; |
paul@0 | 56 | }}} |
paul@0 | 57 | |
paul@0 | 58 | |
paul@0 | 59 | KNOWN BUGS: |
paul@0 | 60 | - Hasn't been thoroughly checked for potential methods of injecting |
paul@0 | 61 | arbitrary HTML into the output. |
paul@0 | 62 | - Only compatible with HTML rendering |
paul@0 | 63 | - May not use all of the MoinMoin interfaces properly - this is a |
paul@0 | 64 | quick hack based on looking at an example and digging through the |
paul@0 | 65 | MoinMoin source. The MoinMoin development docs haven't been |
paul@0 | 66 | consulted (yet). |
paul@0 | 67 | - Only image formats (png and gif) are currently implemented |
paul@0 | 68 | - Comments must start at the beginning of the graphviz block, and at the |
paul@0 | 69 | beginning of their respective lines. They must also not contain |
paul@0 | 70 | any extra whitespace surrounding the = sign. |
paul@0 | 71 | |
paul@0 | 72 | """ |
paul@0 | 73 | |
paul@0 | 74 | # Change this to the directory that the Graphviz binaries (dot, neato, etc.) |
paul@0 | 75 | # are installed in. |
paul@0 | 76 | |
paul@0 | 77 | BINARY_PATH = '/usr/bin' |
paul@0 | 78 | |
paul@0 | 79 | import os |
paul@0 | 80 | import sys |
paul@0 | 81 | import base64 |
paul@0 | 82 | import string |
paul@0 | 83 | import exceptions |
paul@0 | 84 | import codecs |
paul@0 | 85 | import subprocess |
paul@0 | 86 | import time |
paul@0 | 87 | import sha |
paul@0 | 88 | |
paul@0 | 89 | from MoinMoin import config |
paul@0 | 90 | from MoinMoin.action import AttachFile |
paul@0 | 91 | from MoinMoin import log |
paul@0 | 92 | from MoinMoin import wikiutil |
paul@0 | 93 | |
paul@0 | 94 | logging = log.getLogger(__name__) |
paul@0 | 95 | |
paul@0 | 96 | class GraphVizError(exceptions.RuntimeError): |
paul@0 | 97 | pass |
paul@0 | 98 | |
paul@0 | 99 | |
paul@0 | 100 | Dependencies = [] |
paul@0 | 101 | |
paul@0 | 102 | class Parser: |
paul@0 | 103 | """Uses the Graphviz programs to create a visualization of a graph.""" |
paul@0 | 104 | |
paul@0 | 105 | FILTERS = ['dot', 'neato', 'twopi', 'circo', 'fdp'] |
paul@0 | 106 | IMAGE_FORMATS = ['png', 'gif'] |
paul@0 | 107 | FORMATS = IMAGE_FORMATS + ['ps', 'svg', 'svgz', 'fig', 'mif', \ |
paul@0 | 108 | 'hpgl', 'pcl', 'dia', 'imap', 'cmapx'] |
paul@0 | 109 | extensions = [] |
paul@0 | 110 | Dependencies = Dependencies |
paul@0 | 111 | |
paul@0 | 112 | def __init__(self, raw, request, **kw): |
paul@0 | 113 | self.raw = raw |
paul@0 | 114 | self.request = request |
paul@0 | 115 | |
paul@0 | 116 | def format(self, formatter): |
paul@0 | 117 | """ Send the text. """ |
paul@0 | 118 | self.request.flush() # to identify error text |
paul@0 | 119 | |
paul@0 | 120 | self.filter = Parser.FILTERS[0] |
paul@0 | 121 | self.format = 'png' |
paul@0 | 122 | self.cmapx = None |
paul@0 | 123 | |
paul@0 | 124 | raw_lines = self.raw.splitlines() |
paul@0 | 125 | for l in raw_lines: |
paul@0 | 126 | if not l[0:2] == '//': |
paul@0 | 127 | break |
paul@0 | 128 | if l.lower().startswith('//filter='): |
paul@0 | 129 | tmp = l.split('=', 1)[1].lower() |
paul@0 | 130 | if tmp in Parser.FILTERS: |
paul@0 | 131 | self.filter = tmp |
paul@0 | 132 | else: |
paul@0 | 133 | logging.warn('unknown filter %s' % tmp) |
paul@0 | 134 | elif l.lower().startswith('//format='): |
paul@0 | 135 | tmp = l.split('=', 1)[1] |
paul@0 | 136 | if tmp in Parser.FORMATS: |
paul@0 | 137 | self.format = tmp |
paul@0 | 138 | elif l.lower().startswith('//cmapx='): |
paul@0 | 139 | self.cmapx = wikiutil.escape(l.split('=', 1)[1]) |
paul@0 | 140 | |
paul@0 | 141 | if not self.format in Parser.IMAGE_FORMATS: |
paul@0 | 142 | raise NotImplementedError, "only formats %s are currently supported" % Parser.IMAGE_FORMATS |
paul@0 | 143 | |
paul@0 | 144 | if self.cmapx: |
paul@0 | 145 | if not self.format in Parser.IMAGE_FORMATS: |
paul@0 | 146 | logging.warn('format %s is incompatible with cmapx option' % self.format) |
paul@0 | 147 | self.cmapx = None |
paul@0 | 148 | |
paul@0 | 149 | img_name = 'graphviz_%s.%s' % (sha.new(self.raw).hexdigest(), self.format) |
paul@0 | 150 | |
paul@0 | 151 | self.pagename = formatter.page.page_name |
paul@0 | 152 | url = AttachFile.getAttachUrl(self.pagename, img_name, self.request) |
paul@0 | 153 | self.attach_dir=AttachFile.getAttachDir(self.request,self.pagename,create=1) |
paul@0 | 154 | |
paul@0 | 155 | self.delete_old_graphs(formatter) |
paul@0 | 156 | |
paul@0 | 157 | if not os.path.isfile(self.attach_dir + '/' + img_name): |
paul@0 | 158 | self.graphviz(self.raw, fn='%s/%s' % (self.attach_dir, img_name)) |
paul@0 | 159 | |
paul@0 | 160 | if self.format in Parser.IMAGE_FORMATS: |
paul@0 | 161 | if self.cmapx: |
paul@0 | 162 | self.request.write('\n' + self.graphviz(self.raw, format='cmapx') + '\n') |
paul@0 | 163 | self.request.write(formatter.image(src="%s" % url, usemap="#%s" % self.cmapx)) |
paul@0 | 164 | else: |
paul@0 | 165 | self.request.write(formatter.image(src="%s" % url, alt="graphviz image")) |
paul@0 | 166 | else: |
paul@0 | 167 | # TODO: read the docs and figure out how to do this correctly |
paul@0 | 168 | self.request.write(formatter.attachment_link(True, url=url)) |
paul@0 | 169 | |
paul@0 | 170 | def delete_old_graphs(self, formatter): |
paul@0 | 171 | page_info = formatter.page.lastEditInfo() |
paul@0 | 172 | try: |
paul@0 | 173 | page_date = page_info['time'] |
paul@0 | 174 | except exceptions.KeyError, ex: |
paul@0 | 175 | return |
paul@0 | 176 | attach_files = AttachFile._get_files(self.request, self.pagename) |
paul@0 | 177 | for chart in attach_files: |
paul@0 | 178 | if chart.find('graphviz_') == 0 and chart[chart.rfind('.')+1:] in Parser.FORMATS: |
paul@0 | 179 | fullpath = os.path.join(self.attach_dir, chart).encode(config.charset) |
paul@0 | 180 | st = os.stat(fullpath) |
paul@0 | 181 | chart_date = self.request.user.getFormattedDateTime(st.st_mtime) |
paul@0 | 182 | if chart_date < page_date : |
paul@0 | 183 | os.remove(fullpath) |
paul@0 | 184 | else : |
paul@0 | 185 | continue |
paul@0 | 186 | |
paul@0 | 187 | def graphviz(self, graph_def, fn=None, format=None): |
paul@0 | 188 | if not format: |
paul@0 | 189 | format = self.format |
paul@0 | 190 | if fn: |
paul@0 | 191 | p = subprocess.Popen(['%s/%s' % (BINARY_PATH, self.filter), '-T%s' % format, '-o', fn], shell=False, \ |
paul@0 | 192 | stdin=subprocess.PIPE, \ |
paul@0 | 193 | stderr=subprocess.PIPE) |
paul@0 | 194 | else: |
paul@0 | 195 | p = subprocess.Popen(['%s/%s' % (BINARY_PATH, self.filter), '-T%s' % format], shell=False, \ |
paul@0 | 196 | stdin=subprocess.PIPE, \ |
paul@0 | 197 | stdout=subprocess.PIPE, \ |
paul@0 | 198 | stderr=subprocess.PIPE) |
paul@0 | 199 | |
paul@0 | 200 | p.stdin.write(graph_def) |
paul@0 | 201 | p.stdin.flush() |
paul@0 | 202 | p.stdin.close() |
paul@0 | 203 | |
paul@0 | 204 | p.wait() |
paul@0 | 205 | |
paul@0 | 206 | if not fn: |
paul@0 | 207 | output = p.stdout.read() |
paul@0 | 208 | |
paul@0 | 209 | errors = p.stderr.read() |
paul@0 | 210 | if len(errors) > 0: |
paul@0 | 211 | raise GraphVizError, errors |
paul@0 | 212 | |
paul@0 | 213 | p = None |
paul@0 | 214 | |
paul@0 | 215 | if fn: |
paul@0 | 216 | return None |
paul@0 | 217 | else: |
paul@0 | 218 | return output |
paul@0 | 219 | |