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@1 | 107 | OBJECT_FORMATS = ['svg', 'svgz'] |
paul@1 | 108 | OUTPUT_FORMATS = IMAGE_FORMATS + OBJECT_FORMATS |
paul@1 | 109 | FORMATS = OUTPUT_FORMATS + \ |
paul@1 | 110 | ['ps', 'fig', 'mif', 'hpgl', 'pcl', 'dia', 'imap', 'cmapx'] |
paul@0 | 111 | extensions = [] |
paul@0 | 112 | Dependencies = Dependencies |
paul@0 | 113 | |
paul@0 | 114 | def __init__(self, raw, request, **kw): |
paul@0 | 115 | self.raw = raw |
paul@0 | 116 | self.request = request |
paul@0 | 117 | |
paul@0 | 118 | def format(self, formatter): |
paul@0 | 119 | """ Send the text. """ |
paul@1 | 120 | |
paul@1 | 121 | request = self.request |
paul@1 | 122 | page = request.page |
paul@1 | 123 | _ = request.getText |
paul@1 | 124 | |
paul@1 | 125 | request.flush() # to identify error text |
paul@0 | 126 | |
paul@0 | 127 | self.filter = Parser.FILTERS[0] |
paul@0 | 128 | self.format = 'png' |
paul@0 | 129 | self.cmapx = None |
paul@0 | 130 | |
paul@0 | 131 | raw_lines = self.raw.splitlines() |
paul@0 | 132 | for l in raw_lines: |
paul@0 | 133 | if not l[0:2] == '//': |
paul@0 | 134 | break |
paul@0 | 135 | if l.lower().startswith('//filter='): |
paul@0 | 136 | tmp = l.split('=', 1)[1].lower() |
paul@0 | 137 | if tmp in Parser.FILTERS: |
paul@0 | 138 | self.filter = tmp |
paul@0 | 139 | else: |
paul@0 | 140 | logging.warn('unknown filter %s' % tmp) |
paul@0 | 141 | elif l.lower().startswith('//format='): |
paul@0 | 142 | tmp = l.split('=', 1)[1] |
paul@0 | 143 | if tmp in Parser.FORMATS: |
paul@0 | 144 | self.format = tmp |
paul@0 | 145 | elif l.lower().startswith('//cmapx='): |
paul@0 | 146 | self.cmapx = wikiutil.escape(l.split('=', 1)[1]) |
paul@0 | 147 | |
paul@1 | 148 | if not self.format in Parser.OUTPUT_FORMATS: |
paul@1 | 149 | raise NotImplementedError, "only formats %s are currently supported" % \ |
paul@1 | 150 | Parser.OUTPUT_FORMATS |
paul@0 | 151 | |
paul@0 | 152 | if self.cmapx: |
paul@0 | 153 | if not self.format in Parser.IMAGE_FORMATS: |
paul@0 | 154 | logging.warn('format %s is incompatible with cmapx option' % self.format) |
paul@0 | 155 | self.cmapx = None |
paul@0 | 156 | |
paul@0 | 157 | img_name = 'graphviz_%s.%s' % (sha.new(self.raw).hexdigest(), self.format) |
paul@0 | 158 | |
paul@0 | 159 | self.pagename = formatter.page.page_name |
paul@1 | 160 | url = AttachFile.getAttachUrl(self.pagename, img_name, request) |
paul@1 | 161 | self.attach_dir=AttachFile.getAttachDir(request,self.pagename,create=1) |
paul@0 | 162 | |
paul@0 | 163 | self.delete_old_graphs(formatter) |
paul@0 | 164 | |
paul@0 | 165 | if not os.path.isfile(self.attach_dir + '/' + img_name): |
paul@0 | 166 | self.graphviz(self.raw, fn='%s/%s' % (self.attach_dir, img_name)) |
paul@0 | 167 | |
paul@0 | 168 | if self.format in Parser.IMAGE_FORMATS: |
paul@0 | 169 | if self.cmapx: |
paul@1 | 170 | request.write('\n' + self.graphviz(self.raw, format='cmapx') + '\n') |
paul@1 | 171 | request.write(formatter.image(src="%s" % url, usemap="#%s" % self.cmapx)) |
paul@0 | 172 | else: |
paul@1 | 173 | request.write(formatter.image(src="%s" % url, alt="graphviz image")) |
paul@0 | 174 | else: |
paul@1 | 175 | request.write(formatter.transclusion(1, data=url)) |
paul@1 | 176 | request.write(formatter.text(_("graphviz image"))) |
paul@1 | 177 | request.write(formatter.transclusion(0)) |
paul@0 | 178 | |
paul@0 | 179 | def delete_old_graphs(self, formatter): |
paul@0 | 180 | page_info = formatter.page.lastEditInfo() |
paul@0 | 181 | try: |
paul@0 | 182 | page_date = page_info['time'] |
paul@0 | 183 | except exceptions.KeyError, ex: |
paul@0 | 184 | return |
paul@0 | 185 | attach_files = AttachFile._get_files(self.request, self.pagename) |
paul@0 | 186 | for chart in attach_files: |
paul@0 | 187 | if chart.find('graphviz_') == 0 and chart[chart.rfind('.')+1:] in Parser.FORMATS: |
paul@0 | 188 | fullpath = os.path.join(self.attach_dir, chart).encode(config.charset) |
paul@0 | 189 | st = os.stat(fullpath) |
paul@0 | 190 | chart_date = self.request.user.getFormattedDateTime(st.st_mtime) |
paul@0 | 191 | if chart_date < page_date : |
paul@0 | 192 | os.remove(fullpath) |
paul@0 | 193 | else : |
paul@0 | 194 | continue |
paul@0 | 195 | |
paul@0 | 196 | def graphviz(self, graph_def, fn=None, format=None): |
paul@0 | 197 | if not format: |
paul@0 | 198 | format = self.format |
paul@0 | 199 | if fn: |
paul@0 | 200 | p = subprocess.Popen(['%s/%s' % (BINARY_PATH, self.filter), '-T%s' % format, '-o', fn], shell=False, \ |
paul@0 | 201 | stdin=subprocess.PIPE, \ |
paul@0 | 202 | stderr=subprocess.PIPE) |
paul@0 | 203 | else: |
paul@0 | 204 | p = subprocess.Popen(['%s/%s' % (BINARY_PATH, self.filter), '-T%s' % format], shell=False, \ |
paul@0 | 205 | stdin=subprocess.PIPE, \ |
paul@0 | 206 | stdout=subprocess.PIPE, \ |
paul@0 | 207 | stderr=subprocess.PIPE) |
paul@0 | 208 | |
paul@0 | 209 | p.stdin.write(graph_def) |
paul@0 | 210 | p.stdin.flush() |
paul@0 | 211 | p.stdin.close() |
paul@0 | 212 | |
paul@0 | 213 | p.wait() |
paul@0 | 214 | |
paul@0 | 215 | if not fn: |
paul@0 | 216 | output = p.stdout.read() |
paul@0 | 217 | |
paul@0 | 218 | errors = p.stderr.read() |
paul@0 | 219 | if len(errors) > 0: |
paul@0 | 220 | raise GraphVizError, errors |
paul@0 | 221 | |
paul@0 | 222 | p = None |
paul@0 | 223 | |
paul@0 | 224 | if fn: |
paul@0 | 225 | return None |
paul@0 | 226 | else: |
paul@0 | 227 | return output |
paul@0 | 228 | |