1 """
   2 MoinMoin processor for dot.
   3 
   4 Copyright (C) 2004, 2005  Alexandre Duret-Lutz <adl@gnu.org>
   5 
   6 This module is free software; you can redistribute it and/or modify
   7 it under the terms of the GNU General Public License as published by
   8 the Free Software Foundation; either version 2, or (at your option)
   9 any later version.
  10 
  11 This processor passes its input to dot (from AT&T's GraphViz package)
  12 to render graph diagrams.
  13 
  14 -- #---------------------------------------------------------------------------
  15 
  16 Usage:
  17 
  18   Plain Dot features:
  19 
  20   {{{#!dot
  21     digraph G {
  22       node [style=filled, fillcolor=white]
  23       a -> b -> c -> d -> e -> a
  24 
  25       // a comment
  26 
  27       a [URL='http://some.where/a']   // link to an external URL
  28       b [URL='MoinMoinLink']          // link to a wiki absolute page
  29       c [URL='/Subpage']              // link to a wiki subpage
  30       d [URL='#anchor']               // link to an anchor in current page
  31       e [fillcolor=blue]
  32     }
  33   }}}
  34 
  35   Extra MoinMoin-ish features:
  36 
  37   {{{#!dot OPTIONS
  38     digraph G {
  39       node [style=filled, fillcolor=white]
  40       a -> b -> c -> d -> e -> a
  41 
  42       [[Include(MoinMoinPage)]]       // include a whole wiki page content
  43       [[Include(MoinMoinPage,name)]]  // same, but just a named dot section
  44       [[Include(,name)]]              // include named dot sect of current page
  45       [[Set(varname,'value')]]        // assign a value to a variable
  46       [[Get(varname)]]                // expand a variable
  47     }
  48   }}}
  49 
  50   Options:
  51     * name=IDENTIFIER  name this dot section; used in conjunction with Include.
  52     * show[=0|1]       allow to hide a dot section; useful to define hidden
  53                         named section used as 'libraries' to be included.
  54     * debug[=0|1]      when not 0,preceed the image by the expanded dot source.
  55     * help[=0|1|2]     when not 0, display 1:short or 2:full help in the page.
  56 
  57 and the result will be an attached PNG, displayed at this point
  58 in the document.  The AttachFile action must therefore be enabled.
  59 
  60 If some node in the input contains a URL label, the processor will
  61 generate a user-side image map.
  62 
  63 GraphViz: http://www.research.att.com/sw/tools/graphviz/
  64 
  65 Examples of use of this processor:
  66     * http://spot.lip6.fr/wiki/LtlTranslationAlgorithms (with image map)
  67     * http://spot.lip6.fr/wiki/HowToParseLtlFormulae (without image map)
  68 
  69 -- #---------------------------------------------------------------------------
  70 
  71 ChangeLog:
  72 
  73 Henning von Bargen <henning on arcor de> 2007-10-23
  74  * Changed to allow running on MS Windows:
  75  * replaced '/' in former line 377 with os.sep,
  76  * in execFilterIO, replaced os.popen3 with equivalent subprocess code.
  77  
  78 Oleg Kobchenko <olegyk AT spam-remove yahoo DOT come> 2007-01-18
  79    http://moinmoin.wikiwikiweb.de/OlegKobchenko
  80  * add: config for dot path
  81  * add: execFilterIO to capture stderr
  82  * add: exception handling of dot errors, including when dot not found
  83 
  84 Alexandre Duret-Lutz  <adl@gnu.org>  2005-03-23:
  85   * Rewrite as a parser for Moin 1.3.4.
  86     (I haven't tested any of the features Pascal added.  I hope they
  87     didn't broke in the transition.)
  88 
  89 Pascal Bauermeister <pascal DOT bauermeister AT gmail DOT com> 2004-11-03:
  90   * Macros: Include/Set/Get
  91   * MoinMoin URLs
  92   * Can force image rebuild thanks to special attachment:
  93     delete.me.to.regenerate.
  94 
  95 -- #---------------------------------------------------------------------------
  96 """
  97 
  98 # Graphviz dot path and cmds -- configuration settings
  99 dotpath = 'dot' # Mac: '/Applications/Graphviz.app/Contents/MacOS/dot'
 100 dotimg  = '"%s" -Tpng -Gbgcolor=transparent -o "%%s"' % dotpath
 101 dotmap  = '"%s" -Tcmap -o "%%s"'                      % dotpath
 102 
 103 # Parser's name
 104 NAME = __name__.split(".")[-1]
 105 
 106 Dependencies = []
 107 
 108 import os, re, sha
 109 import cStringIO, string
 110 from MoinMoin.action import AttachFile
 111 from MoinMoin.Page import Page
 112 from MoinMoin import wikiutil, config
 113 
 114 if os.name == "nt":
 115     import subprocess
 116 
 117 def execFilterIO(cmd, filename, input):
 118     if os.name == "nt":
 119         p = subprocess.Popen(cmd % filename, shell=False,
 120                   stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
 121         (child_in, child_out, child_err) = (p.stdin, p.stdout, p.stderr)
 122     else:
 123         child_in, child_out, child_err = os.popen3(cmd % filename, "b")
 124     
 125     child_in.write(input)
 126     child_in.close()
 127     output = child_out.read()
 128     status = child_out.close()
 129     err = child_err.read()
 130     child_err.close()
 131     if status or len(err):
 132         raise RuntimeError("Parser failed: %s" % err)
 133     return output
 134 
 135 class Parser:
 136 
 137     extensions = ['.dot']
 138 
 139     def __init__(self, raw, request, **kw):
 140         # save call arguments for later use in format()
 141         self.raw = raw.encode('utf-8')
 142         self.request = request
 143 
 144         self.attrs, msg = wikiutil.parseAttributes(request,
 145                                                    kw.get('format_args', ''))
 146 
 147         # Some regexes that we will need
 148         p1_re = "(?P<p1>.*?)"
 149         p2_re = "(?P<p2>.*?)"
 150         end_re = "( *//.*)?"
 151 
 152         #   an URL
 153         self.url_re = re.compile(
 154             r'\[ *URL=(?P<quote>[\'"])(?P<url>.+?)(?P=quote) *]',
 155             re.IGNORECASE)
 156 
 157         #   non-wiki URLs
 158         self.notwiki_re = re.compile(
 159             r'[a-z0-9_]*:.*', re.IGNORECASE)
 160 
 161         #   include pseudo-macro
 162         self.inc_re = re.compile(
 163             r'\[\[ *Include *\( *%s( *, *%s)? *\) *\]\]%s' %
 164                          (p1_re, p2_re, end_re))
 165         #   set pseudo-macro
 166         self.set_re = re.compile(
 167             r'\[\[ *Set *\( *%s *, *(?P<quote>[\'"])%s(?P=quote) *\) *\]\]%s' %
 168             (p1_re, p2_re, end_re))
 169 
 170         #   get pseudo-macro
 171         self.get_re = re.compile(
 172             r'\[\[ *Get *\( *%s *\) *\]\]' % (p1_re))
 173 
 174 
 175     def _usage(self, full=False):
 176 
 177         """Return the interesting part of the module's doc"""
 178 
 179         if full: return __doc__
 180 
 181         lines = __doc__.splitlines()
 182         start = 0
 183         end = len(lines)
 184         for i in range(end):
 185             if lines[i].strip().lower() == "usage:":
 186                 start = i
 187                 break
 188         for i in range(start, end):
 189             if lines[i].startswith ('--'):
 190                 end = i
 191                 break
 192         return '\n'.join(lines[start:end])
 193 
 194 
 195     # FIXME: Unused?
 196     def _format(self, src_text, formatter):
 197 
 198         """Parse the source text (in wiki source format) and make HTML,
 199          after diverting sys.stdout to a string"""
 200 
 201         # create str to collect output and divert output to that string
 202         str_out = cStringIO.StringIO()
 203         self.request.redirect(str_out)
 204 
 205         # parse this line and restore output
 206         wiki.Parser(src_text, self.request).format(formatter)
 207         self.request.redirect()
 208 
 209          # return what was generated
 210         return str_out.getvalue().strip()
 211 
 212 
 213     def _resolve_link(self, url, this_page):
 214 
 215         """Return external URL, anchor, or wiki link"""
 216 
 217         if self.notwiki_re.match(url) or url.startswith("#"):
 218             # return url as-is
 219             return url
 220         elif url.startswith("/"):
 221             # a wiki subpage
 222             return "%s/%s%s" % (self.request.getScriptname(), this_page, url)
 223         else:
 224             # a wiki page
 225             return "%s/%s" % (self.request.getScriptname(), url)
 226 
 227 
 228     def _preprocess(self, formatter, lines, newlines, substs, recursions):
 229 
 230         """Resolve URLs and pseudo-macros (incl. includes) """
 231 
 232         for line in lines:
 233             # Handle URLs to resolve Wiki links
 234             sline = line.strip()
 235             url_match = self.url_re.search(line)
 236             inc_match = self.inc_re.match(sline)
 237             set_match = self.set_re.match(sline)
 238             get_match = self.get_re.search(line)
 239 
 240             this_page = formatter.page.page_name
 241 
 242             if url_match:
 243                 # Process URL; handle both normal URLs and wiki names
 244                 url = url_match.group('url')
 245                 newurl = self._resolve_link(url, this_page)
 246                 line = line[:url_match.start()] \
 247                        + '[URL="%s"]' % newurl \
 248                        + line[url_match.end():]
 249                 newlines.append(line)
 250             elif inc_match:
 251                 # Process [[Include(page[,ident])]]
 252                 page = inc_match.group('p1')
 253                 ident = inc_match.group('p2')
 254                 # load page, search for named dot section, add it
 255                 other_line = self._get_include(page, ident, this_page)
 256                 newlines.extend(other_line)
 257             elif set_match:
 258                 # Process [[Set(var,'value')]]
 259                 var = set_match.group('p1')
 260                 val = set_match.group('p2')
 261                 substs[var] = val
 262             elif get_match:
 263                 # Process [[Get(var)]]
 264                 var = get_match.group('p1')
 265                 val = substs.get(var, None)
 266                 if val is None:
 267                     raise RuntimeError("Cannot resolve Variable '%s'" % var)
 268                 line = line[:get_match.start()] + val + line[get_match.end():]
 269                 newlines.append(line)
 270             else:
 271                 # Process other lines
 272                 newlines.append(line)
 273         return newlines
 274 
 275 
 276     def _get_include(self, page, ident, this_page):
 277 
 278         """Return the content of the given page; if ident is not empty,
 279         extract the content of an enclosed section:
 280         {{{#!dot ... name=ident ...
 281           ...content...
 282         }}}
 283         """
 284 
 285         lines = self._get_page_body(page, this_page)
 286 
 287         if not ident: return lines
 288 
 289         start_re = re.compile(r'{{{#!%s.* name=' % NAME)
 290 
 291         inside = False
 292         found =[]
 293 
 294         for line in lines:
 295             if not inside:
 296                 f = start_re.search(line)
 297                 if f:
 298                     name = line[f.end():].split()[0]
 299                     inside = name == ident
 300             else:
 301                 pos = line.find('}}}')
 302                 if pos >=0:
 303                     found.append(line[:pos])
 304                     inside = False
 305                 else: found.append(line)
 306 
 307         if len(found)==0:
 308             raise RuntimeError("Identifier '%s' not found in page '%s'" %
 309                                (ident, page))
 310 
 311         return found
 312 
 313 
 314     def _get_page_body(self, page, this_page):
 315 
 316         """Return the content of a named page; accepts relative pages"""
 317 
 318         if page.startswith("/") or len(page)==0:
 319             page = this_page + page
 320 
 321         p = Page(page)
 322         if not p.exists ():
 323             raise RuntimeError("Page '%s' not found" % page)
 324         else:
 325             return p.get_raw_body().split('\n')
 326 
 327 
 328     def format(self, formatter):
 329         """The parser's entry point"""
 330 
 331         lines = self.raw.split('\n')
 332 
 333         # parse bangpath for arguments
 334         opt_show = 1
 335         opt_dbg  = False
 336         opt_name = None
 337         opt_help = None
 338         for (key, val) in self.attrs.items():
 339             if   key == 'show':  opt_show = bool(val)
 340             elif key == 'debug': opt_dbg  = bool(val)
 341             elif key == 'name':  opt_name = val
 342             elif key == 'help':  opt_help = val
 343             else:
 344                 self.request.write(formatter.rawHTML("""
 345                 <p><strong class="error">
 346                 Error: processor %s: invalid argument: %s
 347                 <pre>%s</pre></strong> </p>
 348                 """ % (NAME, str(attrs), self._usage())))
 349                 return
 350 
 351         # help ?
 352         if opt_help is not None and opt_help != '0':
 353             self.request.write(formatter.rawHTML("""
 354             <p>
 355             Processor %s usage:
 356             <pre>%s</pre></p>
 357             """ % (NAME, self._usage(opt_help == '2'))))
 358             return
 359 
 360         # don't show ?
 361         if not opt_show: return
 362 
 363         # preprocess lines
 364         newlines = []
 365         substs = {}
 366         try:
 367             lines = self._preprocess(formatter, lines, newlines, substs, 0)
 368         except RuntimeError, str:
 369             self.request.write(formatter.rawHTML("""
 370             <p><strong class="error">
 371             Error: macro %s: %s
 372             </strong> </p>
 373             """ % (NAME, str) ))
 374             opt_dbg = True
 375 
 376         # debug ?  pre-print and exit
 377         if opt_dbg:
 378             self.request.write(formatter.rawHTML(
 379                 "<pre>\n%s\n</pre>" % '\n'.join(lines)))
 380 
 381         # go !
 382 
 383         all = '\n'.join(lines).strip()
 384         name = 'autogenerated-' + sha.new(all).hexdigest()
 385         pngname = name + '.png'
 386         dotname = name + '.map'
 387 
 388         need_map = 0 <= all.find('URL')
 389 
 390         pagename = formatter.page.page_name
 391         attdir = AttachFile.getAttachDir(self.request, pagename, create=1) + os.sep
 392         pngpath = attdir + pngname
 393         mappath = attdir + dotname
 394 
 395         dm2ri = attdir + "delete.me.to.regenerate.images"
 396 
 397         # delete autogenerated attachments if dm2ri attachment does not exist
 398         if not os.path.isfile(dm2ri):
 399             # create dm2ri attachment
 400             open(dm2ri,'w').close()
 401             # delete autogenerated attachments
 402             for root, dirs, files in os.walk(attdir, topdown=False):
 403                 for name in files:
 404                     if name.startswith("autogenerated-"):
 405                         os.remove(os.path.join(root, name))
 406 
 407         try:
 408             if not os.path.exists(pngpath):
 409                 p = execFilterIO(dotimg, pngpath, all)
 410             if need_map and not os.path.exists(mappath):
 411                 p = execFilterIO(dotmap, mappath, all)
 412 
 413             url = AttachFile.getAttachUrl(pagename, pngname, self.request)
 414             if not need_map:
 415                 self.request.write(formatter.image(src = url))
 416             else:
 417                 self.request.write(formatter.image(src = url,
 418                                               usemap = '#' + name,
 419                                               border = 0))
 420                 self.request.write(formatter.rawHTML('<MAP name="' + name
 421                                                      + '\">\n'))
 422                 import codecs
 423                 p = codecs.open(mappath, "r", "utf-8")
 424                 m = p.read()
 425                 p.close()
 426                 self.request.write(formatter.rawHTML(m + '</MAP>'))
 427 
 428         except RuntimeError, str:
 429             self.request.write(formatter.rawHTML("""
 430             <p><strong class="error">
 431             Error: macro %s: %s
 432             </strong> </p>
 433             """ % (NAME, formatter.escapedText(str)) ))
 434 
 435 
 436 # One possible improvement is to not assume that 'dot' is in the system wide path. 
 437 # You could include a 'dotpath' variable at the beginning of the script that the 
 438 # Wiki administrator could set when installing dot.py into the parser directory.
 439 #
 440 #   Done. --OlegKobchenko

MoinMoin: ParserMarket/OldParsers/dot.py (last edited 2008-01-27 21:40:25 by FedericoLorenzi)