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
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
95 p1_re = "(?P<p1>.*?)"
96 p2_re = "(?P<p2>.*?)"
97 end_re = "( *//.*)?"
98
99
100 url_re = re.compile(
101 r'\[ *URL=(?P<quote>[\'"])(?P<url>.+?)(?P=quote) *]',
102 re.IGNORECASE)
103
104
105 notwiki_re = re.compile(
106 r'[a-z0-9_]*:.*', re.IGNORECASE)
107
108
109 inc_re = re.compile(
110 r'\[\[ *Include *\( *%s( *, *%s)? *\) *\]\]%s' %
111 (p1_re, p2_re, end_re) )
112
113 set_re = re.compile(
114 r'\[\[ *Set *\( *%s *, *(?P<quote>[\'"])%s(?P=quote) *\) *\]\]%s' %
115 (p1_re, p2_re, end_re) )
116
117
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
148 str_out = cStringIO.StringIO()
149 request.redirect(str_out)
150
151
152 wiki.Parser(src_text, request).format(formatter)
153 request.redirect()
154
155
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
165 return url
166 elif url.startswith("/"):
167
168 return "%s/%s%s" % (request.getScriptname(), this_page, url)
169 else:
170
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
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
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
198 page = inc_match.group('p1')
199 ident = inc_match.group('p2')
200
201 other_line = _get_include(page, ident, this_page)
202 newlines.extend(other_line)
203 elif set_match:
204
205 var = set_match.group('p1')
206 val = set_match.group('p2')
207 substs[var] = val
208 elif get_match:
209
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
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
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
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
308 if not opt_show: return
309
310
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
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
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
348 if not os.path.isfile(dm2ri):
349
350 open(dm2ri,'w').close()
351
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>'))