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
99 dotpath = 'dot'
100 dotimg = '"%s" -Tpng -Gbgcolor=transparent -o "%%s"' % dotpath
101 dotmap = '"%s" -Tcmap -o "%%s"' % dotpath
102
103
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
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
148 p1_re = "(?P<p1>.*?)"
149 p2_re = "(?P<p2>.*?)"
150 end_re = "( *//.*)?"
151
152
153 self.url_re = re.compile(
154 r'\[ *URL=(?P<quote>[\'"])(?P<url>.+?)(?P=quote) *]',
155 re.IGNORECASE)
156
157
158 self.notwiki_re = re.compile(
159 r'[a-z0-9_]*:.*', re.IGNORECASE)
160
161
162 self.inc_re = re.compile(
163 r'\[\[ *Include *\( *%s( *, *%s)? *\) *\]\]%s' %
164 (p1_re, p2_re, end_re))
165
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
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
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
202 str_out = cStringIO.StringIO()
203 self.request.redirect(str_out)
204
205
206 wiki.Parser(src_text, self.request).format(formatter)
207 self.request.redirect()
208
209
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
219 return url
220 elif url.startswith("/"):
221
222 return "%s/%s%s" % (self.request.getScriptname(), this_page, url)
223 else:
224
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
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
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
252 page = inc_match.group('p1')
253 ident = inc_match.group('p2')
254
255 other_line = self._get_include(page, ident, this_page)
256 newlines.extend(other_line)
257 elif set_match:
258
259 var = set_match.group('p1')
260 val = set_match.group('p2')
261 substs[var] = val
262 elif get_match:
263
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
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
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
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
361 if not opt_show: return
362
363
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
377 if opt_dbg:
378 self.request.write(formatter.rawHTML(
379 "<pre>\n%s\n</pre>" % '\n'.join(lines)))
380
381
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
398 if not os.path.isfile(dm2ri):
399
400 open(dm2ri,'w').close()
401
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
437
438
439
440