1 """
   2 MoinMoin processor for dot.
   3 
   4 Copyright (C) 2004  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 Contributions
  72 
  73 Pascal Bauermeister <pascal DOT bauermeister AT gmail DOT com> 2004-11-03:
  74   * Macros: Include/Set/Get
  75   * MoinMoin URLs
  76   * Can force image rebuild thanks to special attachment:
  77     delete.me.to.regenerate.
  78 
  79 -- #---------------------------------------------------------------------------
  80 """
  81 
  82 # Processor name
  83 NAME = __name__.split(".")[-1]
  84 
  85 Dependencies = []
  86 
  87 import os, re, sha
  88 import cStringIO, string
  89 from MoinMoin.action import AttachFile
  90 from MoinMoin.Page import Page
  91 from MoinMoin import wikiutil, config
  92 
  93 
  94 # Some regexes that we will need
  95 p1_re = "(?P<p1>.*?)"
  96 p2_re = "(?P<p2>.*?)"
  97 end_re = "( *//.*)?"
  98 
  99 #   an URL
 100 url_re = re.compile(
 101     r'\[ *URL=(?P<quote>[\'"])(?P<url>.+?)(?P=quote) *]',
 102     re.IGNORECASE)
 103 
 104 #   non-wiki URLs
 105 notwiki_re = re.compile(
 106     r'[a-z0-9_]*:.*', re.IGNORECASE)
 107 
 108 #   include pseudo-macro
 109 inc_re = re.compile(
 110     r'\[\[ *Include *\( *%s( *, *%s)? *\) *\]\]%s' %
 111                      (p1_re, p2_re, end_re) )
 112 #   set pseudo-macro
 113 set_re = re.compile(
 114     r'\[\[ *Set *\( *%s *, *(?P<quote>[\'"])%s(?P=quote) *\) *\]\]%s' %
 115     (p1_re, p2_re, end_re) )
 116 
 117 #   get pseudo-macro
 118 get_re = re.compile(
 119     r'\[\[ *Get *\( *%s *\) *\]\]' % (p1_re) )
 120 
 121 
 122 def _usage (full=False):
 123 
 124     """Return the interesting part of the module's doc"""
 125 
 126     if full: return __doc__
 127 
 128     lines = __doc__.splitlines ()
 129     start = 0
 130     end = len (lines)
 131     for i in range (end):
 132         if lines [i].strip ().lower () == "usage:":
 133             start = i
 134             break
 135     for i in range (start, end):
 136         if lines [i].startswith ('--'):
 137             end = i
 138             break
 139     return '\n'.join (lines [start:end])            
 140 
 141 
 142 def _format(src_text, request, formatter):
 143 
 144     """Parse the source text (in wiki source format) and make HTML,
 145      after diverting sys.stdout to a string"""
 146 
 147     # create str to collect output and divert output to that string
 148     str_out = cStringIO.StringIO()
 149     request.redirect(str_out)
 150 
 151     # parse this line and restore output
 152     wiki.Parser(src_text, request).format(formatter)
 153     request.redirect()
 154 
 155      # return what was generated
 156     return str_out.getvalue().strip()
 157 
 158 
 159 def _resolve_link(request, url, this_page):
 160 
 161     """Return external URL, anchor, or wiki link"""
 162     
 163     if notwiki_re.match(url) or url.startswith("#"):
 164         # return url as-is
 165         return url
 166     elif url.startswith("/"):
 167         # a wiki subpage
 168         return "%s/%s%s" % (request.getScriptname(), this_page, url)
 169     else:
 170         # a wiki page
 171         return "%s/%s" % (request.getScriptname(), url)
 172 
 173 
 174 def _preprocess(request, formatter, lines, newlines, substs, recursions):
 175 
 176     """Resolve URLs and pseudo-macros (incl. includes) """
 177     
 178     for line in lines:
 179         # Handle URLs to resolve Wiki links
 180         sline = line.strip()
 181         url_match = url_re.search(line)
 182         inc_match = inc_re.match(sline)
 183         set_match = set_re.match(sline)
 184         get_match = get_re.search(line)
 185 
 186         this_page = formatter.page.page_name
 187         
 188         if url_match:
 189             # Process URL; handle both normal URLs and wiki names
 190             url = url_match.group('url')
 191             newurl = _resolve_link(request, url, this_page)
 192             line = line[:url_match.start()] \
 193                    + '[URL="%s"]' % newurl \
 194                    + line[url_match.end():]
 195             newlines.append(line)
 196         elif inc_match:
 197             # Process [[Include(page[,ident])]]
 198             page = inc_match.group('p1')
 199             ident = inc_match.group('p2')
 200             # load page, search for named dot section, add it
 201             other_line = _get_include(page, ident, this_page)
 202             newlines.extend(other_line)
 203         elif set_match:
 204             # Process [[Set(var,'value')]]
 205             var = set_match.group('p1')
 206             val = set_match.group('p2')
 207             substs[var] = val
 208         elif get_match:
 209             # Process [[Get(var)]]
 210             var = get_match.group('p1')
 211             val = substs.get(var, None)
 212             if val is None:
 213                 raise RuntimeError("Cannot resolve Variable '%s'" % var)
 214             line = line[:get_match.start()] + val + line[get_match.end():]
 215             newlines.append(line)
 216         else:
 217             # Process other lines
 218             newlines.append(line)
 219     return newlines
 220 
 221 
 222 def _get_include(page, ident, this_page):
 223 
 224     """Return the content of the given page; if ident is not empty,
 225     extract the content of an enclosed section:
 226     {{{#!dot ... name=ident ...
 227       ...content...
 228     }}}
 229     """
 230     
 231     lines = _get_page_body(page, this_page)
 232 
 233     if not ident: return lines
 234 
 235     start_re = re.compile(r'{{{#!%s.* name=' % NAME)
 236 
 237     inside = False
 238     found =[]
 239 
 240     for line in lines:
 241         if not inside:
 242             f = start_re.search(line)
 243             if f:
 244                 name = line[f.end():].split()[0]
 245                 inside = name == ident
 246         else:
 247             pos = line.find('}}}')
 248             if pos >=0:
 249                 found.append(line[:pos])
 250                 inside = False
 251             else: found.append(line)
 252 
 253     if len(found)==0:
 254         raise RuntimeError("Identifier '%s' not found in page '%s'" %
 255                            (ident, page))
 256 
 257     return found
 258 
 259 
 260 def _get_page_body(page, this_page):
 261 
 262     """Return the content of a named page; accepts relative pages"""
 263     
 264     if page.startswith("/") or len(page)==0:
 265         page = this_page + page
 266 
 267     p = Page(page)
 268     if not p.exists ():
 269         raise RuntimeError("Page '%s' not found" % page)
 270     else:
 271         return p.get_raw_body().split('\n')
 272 
 273 
 274 def process(request, formatter, lines):
 275 
 276     """The processor's entry point"""
 277 
 278     # parse bangpath for arguments
 279     opt_show = 1
 280     opt_dbg  = False
 281     opt_name = None
 282     opt_help = None
 283     bang = lines[0]
 284     for arg in bang.split()[1:]:
 285         if   arg.startswith("show"):  opt_show  = arg[4:] != "=0"
 286         elif arg.startswith("debug"): opt_dbg   = arg[5:] != "=0"
 287         elif arg.startswith("name="): opt_name  = arg[5:]
 288         elif arg.startswith("help"):  opt_help  = arg[4:]
 289         else:
 290             request.write(formatter.rawHTML("""
 291             <p><strong class="error">
 292             Error: processor %s: invalid argument: %s
 293             <pre>%s</pre></strong> </p>
 294             """ % (NAME, arg, _usage () )))
 295             return
 296     del lines[0]
 297 
 298     # help ?
 299     if opt_help is not None and opt_help != "=0":
 300         request.write(formatter.rawHTML("""
 301         <p>
 302         Processor %s usage:
 303         <pre>%s</pre></p>
 304         """ % (NAME, _usage (opt_help=="=2"))))
 305         return
 306 
 307     # don't show ?
 308     if not opt_show: return
 309     
 310     # preprocess lines
 311     newlines = []
 312     substs = {}
 313     try:
 314         lines = _preprocess(request, formatter, lines, newlines, substs, 0)
 315     except RuntimeError, str:
 316         request.write(formatter.rawHTML("""
 317         <p><strong class="error">
 318         Error: macro %s: %s
 319         </strong> </p>
 320         """ % (NAME, str) ))
 321         opt_dbg = True
 322 
 323     # debug ?  pre-print and exit
 324     if opt_dbg:
 325         l = ["{{{%s" % bang]
 326         l.extend(lines)
 327         l.append("}}}")
 328         request.write(formatter.rawHTML(
 329             "<pre>\n%s\n</pre>" % '\n'.join(l)))
 330 
 331     # go !
 332 
 333     all = '\n'.join(lines).strip()
 334     name = 'autogenerated-' + sha.new(all).hexdigest()
 335     pngname = name + '.png'
 336     dotname = name + '.map'
 337 
 338     need_map = 0 <= all.find('URL')
 339 
 340     pagename = formatter.page.page_name
 341     attdir = AttachFile.getAttachDir(request, pagename, create=1) + '/'
 342     pngpath = attdir + pngname
 343     mappath = attdir + dotname
 344 
 345     dm2ri = attdir + "delete.me.to.regenerate.images"
 346 
 347     # delete autogenerated attachments if dm2ri attachment does not exist
 348     if not os.path.isfile(dm2ri):
 349         # create dm2ri attachment
 350         open(dm2ri,'w').close()
 351         # delete autogenerated attachments
 352         for root, dirs, files in os.walk(attdir, topdown=False):
 353             for name in files:
 354                 if name.startswith("autogenerated-"):
 355                     os.remove(os.path.join(root, name))
 356 
 357     if not os.path.exists(pngpath):
 358         p = os.popen('dot -Tpng -Gbgcolor=transparent -o ' + pngpath, 'w')
 359         p.write(all)
 360         p.close()
 361     if need_map and not os.path.exists(mappath):
 362         p = os.popen('dot -Tcmap -o ' + mappath, 'w')
 363         p.write(all)
 364         p.close()
 365 
 366     url = AttachFile.getAttachUrl(pagename, pngname, request)
 367     if not need_map:
 368         request.write(formatter.image(src = url))
 369     else:
 370         request.write(formatter.image(src = url,
 371                                       usemap = '#' + name,
 372                                       border = 0))
 373         request.write(formatter.rawHTML('<MAP name="' + name + '\">\n'))
 374         p = open(mappath, 'r')
 375         m = p.read()
 376         p.close()
 377         request.write(formatter.rawHTML(m + '</MAP>'))

MoinMoin: ProcessorMarket/dot.py (last edited 2007-10-29 19:19:22 by localhost)