# -*- coding: iso-8859-1 -*- ''' main.py -- actual implementation of all macros in the package. ---- Copyright (C) 2007-2008 Zoran Isailovski Licensed under the Apache License, Version 2.0 (see http://www.apache.org/licenses/LICENSE-2.0) or, at your option, under the terms described in the file LICENSE.txt found in the root of the distribution of this package (as far as it denotes a different license then the above). ---- ''' __todo__ = '''\ ''' import sys, os DEBUG = False # Graphviz executables - names (dot, neato, twopi, fdp) and location GRAPHVIZ_TOOLS_DIR = r'' # leave empty if the executables are on your PATH GRAPHVIZ_TOOLS = 'dot twopi neato circo fdp'.split() DEFAULT_TOOL = 'DOT' # keep this upper case to distinguish from explicit tool name ##EXE_SUFFIX = (sys.platform == 'win32' and '.exe' or '') # suffix for executables on your platform EXE_SUFFIX = '' # actually, omitting the suffix should work on win32, too, if PATHEXT is properly configured # Absolute location of the graphviz executables (dot, neato, twopi, fdp). # Calculated from the above options - don't modify! GRAPHVIZ = dict( [ (k, os.path.join(GRAPHVIZ_TOOLS_DIR, '%s%s' % (k, EXE_SUFFIX))) for k in GRAPHVIZ_TOOLS ]) del k ###################################################################### ## ## Moin Interface ## ###################################################################### import re from cStringIO import StringIO from MoinMoin import config ##import MoinLegacy # re-inforce backward compatibility #from umoin.MoinLegacy import wiki from MoinMoin.parser import text_moin_wiki as wiki from MoinMoin.version import release as moinVersion moinVersion = tuple(map(int, moinVersion.split('.'))) #UID = 'BB962F5E-DB8E-424C-8E4D-D2B53286D6F3' class GraphvizRenderError(Exception): pass class GraphvizSyntaxError(GraphvizRenderError): pass class Parser: """ MoinMoin GraphViz parser. """ extensions = [] Dependencies = [] def __init__(self, raw, request, **kw): self.args = kw.get('format_args', 'dot') (tool, ) = parseArguments(self.args) self.raw = raw self.request = request p = request.formatter.page self.renderer = Renderer(tool, targetdir=p.getPagePath('attachments'), encoding=config.charset) def format(self, formatter): append_text = '' try: s = self.renderer.render(self.raw) except GraphvizSyntaxError, e: s = e.imagefilename #eliminate source path in the error message, which annoys users append_text = "{{{%s}}}" % str(e).replace(e.dotfilename, "") except GraphvizRenderError, e: self.request.write("GraphViz:
%s
" % e) return fmt = '{{attachment:%s}}' if moinVersion >= (1,6) else 'attachment:%s' fmt += append_text s = wiki2html(self.request, fmt % os.path.basename(s)) if DEBUG: print '[TRACE] attachment html:', s self.request.write(s) def parseArguments(s): progs = '|'.join(GRAPHVIZ_TOOLS) pattern = r'^\s*(?P%s)?\s*$' % progs m = re.match(pattern, s, re.IGNORECASE) ##assert m, 'Invalid parser arguments' program = m and m.group('tool') or DEFAULT_TOOL return (program, ) def wiki2html(request, text): stream = StringIO() request.redirect(stream) parser = wiki.Parser(text, request) parser.format(request.formatter) html = stream.getvalue() request.redirect() stream.close(); del stream return html if __name__ == '__main__': print parseArguments(' fdp ') print parseArguments('dot') print parseArguments('') print parseArguments('xxx') ###################################################################### ## ## Rendering Stuff ## ###################################################################### import sys import sha import string from subprocess import Popen, PIPE try: __file = __file__ except NameError: __file = sys.argv[0] ##__filedir = os.path.join(os.path.dirname(__file)) ##__moindir = os.path.abspath(os.path.join(__filedir, '../../../../..')) ##BASE_DIR = __moindir ##del __filedir, __moindir ##if DEBUG: print '[TRACE] BASE_DIR =', BASE_DIR class Snippets: ''' Snippets are graph script fragments that can be included in the graph. To include snippet FOO, use $FOO in the graph. ''' STD_GRAPH_HEADER = '''overlap=false graph [ charset="utf-8" ] // hint from an anonimous user node [fontname=Verdana fontsize=10 style=filled fillcolor=azure color=steelblue fontcolor=steelblue] edge [fontname=Verdana fontsize=8 color=steelblue fontcolor=steelblue]''' ATTEMPT_TO_JOIN_EDGES = 'concentrate=true' LEFT_TO_RIGHT = 'rankdir=LR' SAMPLE_SCRIPT = r''' digraph XXX { $STD_GRAPH_HEADER A -> {B C}; B -> C } ''' class Renderer: def __init__(self, tool='dot', targetdir='', format='png', encoding='utf-8'): tool = GRAPHVIZ.get(tool) or GRAPHVIZ['dot'] # do not longer assume relative paths are relative to the moin installation dir ##tool = os.path.join(BASE_DIR, tool) tool = os.path.normpath(tool) # executables may be reachable via PATH, so os.path.isfile is not sufficient # instead of checking for file existence up front, # check exit code after command execution (see oscmd) ##assert os.path.isfile(tool), 'File not found: %r' % tool self.toolpath = tool self.toolname = os.path.splitext(os.path.basename(tool))[0] self.targetdir = targetdir self.format = format self.encoding = encoding # see method normalizedScript below def render(self, script): script = self.normalizedScript(script) uid = self.hashFor(script) gname = graphName(script) imagefilename = 'graphviz-%s-%s.%s' % (gname, uid, self.format) imagefilename = os.path.join(self.targetdir, imagefilename) ##if DEBUG: print '[TRACE] imagefilename:', imagefilename if not os.path.isfile(imagefilename): if DEBUG: print '[TRACE] creating graph image:', imagefilename dotfilename = '%s-%s.%s' % ('graph', uid, 'graphviz.txt') dotfilename = os.path.join(self.targetdir, dotfilename) fwrite(dotfilename, script) try: renderGraphImage(self.toolpath, self.format, imagefilename, dotfilename) except GraphvizRenderError, e: if os.path.exists(imagefilename): #imagefilename exists means nothing terriblely happeded, #just little cases about dot syntax. if DEBUG: print "[TRACE] Syntax Error" e = GraphvizSyntaxError(e) e.imagefilename = imagefilename e.dotfilename = dotfilename raise e finally: os.remove(dotfilename) return imagefilename def normalizedScript(self, script=SAMPLE_SCRIPT): ##return script.strip() % vars(Snippets) # for syntax %(name)s ##return string.Template(script.strip()).safe_substitute(vars(Snippets)) # for syntax $name v = vars(Snippets) s = string.Template(script.strip() % v).safe_substitute(v) # for both syntaxes # the following is a hint from an anonimous user, the purpose of which I do not quite # understand yet. it should fix a "unicode problem", but it is unclear what the problem # actually is and how it occurs. I'll take the patch in just in case, in the hope that # it really fixes something and someone can explain what that something is. -- ZI, 2008-04-20 return unicode(s).encode(self.encoding) #return s def hashFor(self, content): return sha.new(content).hexdigest() def graphName(script): ##m = re.match(r'^(?:\n|\s)*(?:di)?graph\s*(\w*)', script) # allows spaces but no comments at beginning of script m = re.search(r'^(?:di)?graph\s+(\w*)', script, re.M) if m is None: raise GraphvizRenderError( \ "Could not derive graph name from graph script. Check the syntax, please!") return m.group(1) def renderGraphImage(tool, format, imagefilename, dotfilename): cmd = '%(tool)s -T%(format)s -o"%(imagefilename)s" "%(dotfilename)s"' % locals() if DEBUG: print '[TRACE] executing:', cmd oscmd(cmd) ###################################################################### ## ## File IO and OS Auxiliaries ## ###################################################################### def fread(fname): f = open(fname, 'rt') try: return f.read() finally: f.close() def fwrite(fname, content): f = open(fname, 'wt') try: return f.write(str(content)) finally: f.close() def oscmd(cmd): '''instead of simply calling os.system(cmd) capture stderr and raise GraphvizRenderError if exit code != 0 ''' p = Popen(cmd, stdout=PIPE, stderr=PIPE, shell=True, bufsize=1024, close_fds=True) stdo, stde = p.communicate() if p.returncode != 0: raise GraphvizRenderError("%s\n%s" % (stdo, stde)) ###################################################################### ## ## TEST ## ###################################################################### TEST_SCRIPT = '''\ digraph X { $STD_GRAPH_HEADER subgraph cluster1 { label="global space" node [fillcolor=cornsilk] P Q } B -> C C -> A Q -> A } ''' ##if __name__ == '__main__': ## print ## r = Renderer('fdp') ## s = r.render(TEST_SCRIPT) ## print s ## os.startfile(s)