Changes to work with Lenny MoinMoin 1.7, not ideal solution
[deb/moinmoin.git] / theme / __init__.py
1 # -*- coding: iso-8859-1 -*-
2 """
3     MoinMoin - Theme Package
4
5     @copyright: 2003-2008 MoinMoin:ThomasWaldmann
6     @license: GNU GPL, see COPYING for details.
7 """
8
9 from MoinMoin import i18n, wikiutil, config, version, caching
10 from MoinMoin.Page import Page
11 from MoinMoin.util import pysupport
12
13 modules = pysupport.getPackageModules(__file__)
14
15 # Check whether we can emit a RSS feed.
16 # RSS is broken on plain Python 2.3.x/2.4.x, and works only when installing PyXML.
17 # News: A user reported that the RSS is valid when using Python 2.5.1 on Windows.
18 import sys, xml
19 rss_supported = sys.version_info[:3] >= (2, 5, 1) or '_xmlplus' in xml.__file__
20
21
22 class ThemeBase:
23     """ Base class for themes
24
25     This class supply all the standard template that sub classes can
26     use without rewriting the same code. If you want to change certain
27     elements, override them.
28     """
29
30     name = 'base'
31
32     # fake _ function to get gettext recognize those texts:
33     _ = lambda x: x
34
35     # TODO: remove icons that are not used any more.
36     icons = {
37         # key         alt                        icon filename      w   h
38         # ------------------------------------------------------------------
39         # navibar
40         'help':        ("%(page_help_contents)s", "moin-help.png",   12, 11),
41         'find':        ("%(page_find_page)s",     "moin-search.png", 12, 12),
42         'diff':        (_("Diffs"),               "moin-diff.png",   15, 11),
43         'info':        (_("Info"),                "moin-info.png",   12, 11),
44         'edit':        (_("Edit"),                "moin-edit.png",   12, 12),
45         'unsubscribe': (_("Unsubscribe"),         "moin-unsubscribe.png", 14, 10),
46         'subscribe':   (_("Subscribe"),           "moin-subscribe.png", 14, 10),
47         'raw':         (_("Raw"),                 "moin-raw.png",    12, 13),
48         'xml':         (_("XML"),                 "moin-xml.png",    20, 13),
49         'print':       (_("Print"),               "moin-print.png",  16, 14),
50         'view':        (_("View"),                "moin-show.png",   12, 13),
51         'home':        (_("Home"),                "moin-home.png",   13, 12),
52         'up':          (_("Up"),                  "moin-parent.png", 15, 13),
53         # FileAttach
54         'attach':     ("%(attach_count)s",       "moin-attach.png",  7, 15),
55         'attachimg':  ("",                       "attach.png",      32, 32),
56         # RecentChanges
57         'rss':        (_("[RSS]"),               "moin-rss.png",    24, 24),
58         'deleted':    (_("[DELETED]"),           "moin-deleted.png", 60, 12),
59         'updated':    (_("[UPDATED]"),           "moin-updated.png", 60, 12),
60         'renamed':    (_("[RENAMED]"),           "moin-renamed.png", 60, 12),
61         'conflict':   (_("[CONFLICT]"),          "moin-conflict.png", 60, 12),
62         'new':        (_("[NEW]"),               "moin-new.png",    31, 12),
63         'diffrc':     (_("[DIFF]"),              "moin-diff.png",   15, 11),
64         # General
65         'bottom':     (_("[BOTTOM]"),            "moin-bottom.png", 14, 10),
66         'top':        (_("[TOP]"),               "moin-top.png",    14, 10),
67         'www':        ("[WWW]",                  "moin-www.png",    11, 11),
68         'mailto':     ("[MAILTO]",               "moin-email.png",  14, 10),
69         'news':       ("[NEWS]",                 "moin-news.png",   10, 11),
70         'telnet':     ("[TELNET]",               "moin-telnet.png", 10, 11),
71         'ftp':        ("[FTP]",                  "moin-ftp.png",    11, 11),
72         'file':       ("[FILE]",                 "moin-ftp.png",    11, 11),
73         # search forms
74         'searchbutton': ("[?]",                  "moin-search.png", 12, 12),
75         'interwiki':  ("[%(wikitag)s]",          "moin-inter.png",  16, 16),
76
77         # smileys (this is CONTENT, but good looking smileys depend on looking
78         # adapted to the theme background color and theme style in general)
79         #vvv    ==      vvv  this must be the same for GUI editor converter
80         'X-(':        ("X-(",                    'angry.png',       15, 15),
81         ':D':         (":D",                     'biggrin.png',     15, 15),
82         '<:(':        ("<:(",                    'frown.png',       15, 15),
83         ':o':         (":o",                     'redface.png',     15, 15),
84         ':(':         (":(",                     'sad.png',         15, 15),
85         ':)':         (":)",                     'smile.png',       15, 15),
86         'B)':         ("B)",                     'smile2.png',      15, 15),
87         ':))':        (":))",                    'smile3.png',      15, 15),
88         ';)':         (";)",                     'smile4.png',      15, 15),
89         '/!\\':       ("/!\\",                   'alert.png',       15, 15),
90         '<!>':        ("<!>",                    'attention.png',   15, 15),
91         '(!)':        ("(!)",                    'idea.png',        15, 15),
92
93         # copied 2001-11-16 from http://pikie.darktech.org/cgi/pikie.py?EmotIcon
94         ':-?':        (":-?",                    'tongue.png',      15, 15),
95         ':\\':        (":\\",                    'ohwell.png',      15, 15),
96         '>:>':        (">:>",                    'devil.png',       15, 15),
97         '|)':         ("|)",                     'tired.png',       15, 15),
98
99         # some folks use noses in their emoticons
100         ':-(':        (":-(",                    'sad.png',         15, 15),
101         ':-)':        (":-)",                    'smile.png',       15, 15),
102         'B-)':        ("B-)",                    'smile2.png',      15, 15),
103         ':-))':       (":-))",                   'smile3.png',      15, 15),
104         ';-)':        (";-)",                    'smile4.png',      15, 15),
105         '|-)':        ("|-)",                    'tired.png',       15, 15),
106
107         # version 1.0
108         '(./)':       ("(./)",                   'checkmark.png',   20, 15),
109         '{OK}':       ("{OK}",                   'thumbs-up.png',   14, 12),
110         '{X}':        ("{X}",                    'icon-error.png',  16, 16),
111         '{i}':        ("{i}",                    'icon-info.png',   16, 16),
112         '{1}':        ("{1}",                    'prio1.png',       15, 13),
113         '{2}':        ("{2}",                    'prio2.png',       15, 13),
114         '{3}':        ("{3}",                    'prio3.png',       15, 13),
115
116         # version 1.3.4 (stars)
117         # try {*}{*}{o}
118         '{*}':        ("{*}",                    'star_on.png',     15, 15),
119         '{o}':        ("{o}",                    'star_off.png',    15, 15),
120     }
121     del _
122
123     # Style sheets - usually there is no need to override this in sub
124     # classes. Simply supply the css files in the css directory.
125
126     # Standard set of style sheets
127     stylesheets = (
128         # media         basename
129         ('all',         'common'),
130         ('screen',      'screen'),
131         ('print',       'print'),
132         ('projection',  'projection'),
133         )
134
135     # Used in print mode
136     stylesheets_print = (
137         # media         basename
138         ('all',         'common'),
139         ('all',         'print'),
140         )
141
142     # Used in slide show mode
143     stylesheets_projection = (
144         # media         basename
145         ('all',         'common'),
146         ('all',         'projection'),
147        )
148
149     stylesheetsCharset = 'utf-8'
150
151     def __init__(self, request):
152         """
153         Initialize the theme object.
154
155         @param request: the request object
156         """
157         self.request = request
158         self.cfg = request.cfg
159         self._cache = {} # Used to cache elements that may be used several times
160         self._status = []
161         self._send_title_called = False
162
163     def img_url(self, img):
164         """ Generate an image href
165
166         @param img: the image filename
167         @rtype: string
168         @return: the image href
169         """
170         return "%s/%s/img/%s" % (self.cfg.url_prefix_static, self.name, img)
171
172     def emit_custom_html(self, html):
173         """
174         generate custom HTML code in `html`
175
176         @param html: a string or a callable object, in which case
177                      it is called and its return value is used
178         @rtype: string
179         @return: string with html
180         """
181         if html:
182             if callable(html):
183                 html = html(self.request)
184         return html
185
186     def logo(self):
187         """ Assemble logo with link to front page
188
189         The logo contain an image and or text or any html markup the
190         admin inserted in the config file. Everything it enclosed inside
191         a div with id="logo".
192
193         @rtype: unicode
194         @return: logo html
195         """
196         html = u''
197         if self.cfg.logo_string:
198             page = wikiutil.getFrontPage(self.request)
199             logo = page.link_to_raw(self.request, self.cfg.logo_string)
200             html = u'''<div id="logo">%s</div>''' % logo
201         return html
202
203     def interwiki(self, d):
204         """ Assemble the interwiki name display, linking to page_front_page
205
206         @param d: parameter dictionary
207         @rtype: string
208         @return: interwiki html
209         """
210         if self.request.cfg.show_interwiki:
211             page = wikiutil.getFrontPage(self.request)
212             text = self.request.cfg.interwikiname or 'Self'
213             link = page.link_to(self.request, text=text, rel='nofollow')
214             html = u'<div id="interwiki"><span>%s</span></div>' % link
215         else:
216             html = u''
217         return html
218
219     def title(self, d):
220         """ Assemble the title (now using breadcrumbs)
221
222         @param d: parameter dictionary
223         @rtype: string
224         @return: title html
225         """
226         _ = self.request.getText
227         content = []
228         if d['title_text'] == d['page'].split_title(): # just showing a page, no action
229             curpage = ''
230             segments = d['page_name'].split('/') # was: title_text
231             for s in segments[:-1]:
232                 curpage += s
233                 content.append("<li>%s</li>" % Page(self.request, curpage).link_to(self.request, s))
234                 curpage += '/'
235             link_text = segments[-1]
236             link_title = _('Click to do a full-text search for this title')
237             link_query = {
238                 'action': 'fullsearch',
239                 'value': 'linkto:"%s"' % d['page_name'],
240                 'context': '180',
241             }
242             # we dont use d['title_link'] any more, but make it ourselves:
243             link = d['page'].link_to(self.request, link_text, querystr=link_query, title=link_title, css_class='backlink', rel='nofollow')
244             content.append(('<li>%s</li>') % link)
245         else:
246             content.append('<li>%s</li>' % wikiutil.escape(d['title_text']))
247
248         html = '''
249 <ul id="pagelocation">
250 %s
251 </ul>
252 ''' % "".join(content)
253         return html
254
255     def username(self, d):
256         """ Assemble the username / userprefs link
257
258         @param d: parameter dictionary
259         @rtype: unicode
260         @return: username html
261         """
262         request = self.request
263         _ = request.getText
264
265         userlinks = []
266         # Add username/homepage link for registered users. We don't care
267         # if it exists, the user can create it.
268         if request.user.valid and request.user.name:
269             interwiki = wikiutil.getInterwikiHomePage(request)
270             name = request.user.name
271             aliasname = request.user.aliasname
272             if not aliasname:
273                 aliasname = name
274             title = "%s @ %s" % (aliasname, interwiki[0])
275             # link to (interwiki) user homepage
276             homelink = (request.formatter.interwikilink(1, title=title, id="userhome", generated=True, *interwiki) +
277                         request.formatter.text(name) +
278                         request.formatter.interwikilink(0, title=title, id="userhome", *interwiki))
279             userlinks.append(homelink)
280             # link to userprefs action
281             if 'userprefs' not in self.request.cfg.actions_excluded:
282                 userlinks.append(d['page'].link_to(request, text=_('Settings'),
283                                                querystr={'action': 'userprefs'}, id='userprefs', rel='nofollow'))
284
285         if request.user.valid:
286             if request.user.auth_method in request.cfg.auth_can_logout:
287                 userlinks.append(d['page'].link_to(request, text=_('Logout'),
288                                                    querystr={'action': 'logout', 'logout': 'logout'}, id='logout', rel='nofollow'))
289         else:
290             query = {'action': 'login'}
291             # special direct-login link if the auth methods want no input
292             if request.cfg.auth_login_inputs == ['special_no_input']:
293                 query['login'] = '1'
294             if request.cfg.auth_have_login:
295                 userlinks.append(d['page'].link_to(request, text=_("Login"),
296                                                    querystr=query, id='login', rel='nofollow'))
297
298         userlinks = [u'<li>%s</li>' % link for link in userlinks]
299         html = u'<ul id="username">%s</ul>' % ''.join(userlinks)
300         return html
301
302     def splitNavilink(self, text, localize=1):
303         """ Split navibar links into pagename, link to page
304
305         Admin or user might want to use shorter navibar items by using
306         the [[page|title]] or [[url|title]] syntax. In this case, we don't
307         use localization, and the links goes to page or to the url, not
308         the localized version of page.
309
310         Supported syntax:
311             * PageName
312             * WikiName:PageName
313             * wiki:WikiName:PageName
314             * url
315             * all targets as seen above with title: [[target|title]]
316
317         @param text: the text used in config or user preferences
318         @rtype: tuple
319         @return: pagename or url, link to page or url
320         """
321         request = self.request
322         fmt = request.formatter
323         title = None
324
325         # Handle [[pagename|title]] or [[url|title]] formats
326         if text.startswith('[[') and text.endswith(']]'):
327             text = text[2:-2]
328             try:
329                 pagename, title = text.split('|', 1)
330                 pagename = pagename.strip()
331                 title = title.strip()
332                 localize = 0
333             except (ValueError, TypeError):
334                 # Just use the text as is.
335                 pagename = text.strip()
336         else:
337             pagename = text
338
339         if wikiutil.is_URL(pagename):
340             if not title:
341                 title = pagename
342             link = fmt.url(1, pagename) + fmt.text(title) + fmt.url(0)
343             return pagename, link
344
345         # remove wiki: url prefix
346         if pagename.startswith("wiki:"):
347             pagename = pagename[5:]
348
349         # try handling interwiki links
350         try:
351             interwiki, page = wikiutil.split_interwiki(pagename)
352             thiswiki = request.cfg.interwikiname
353             if interwiki == thiswiki or interwiki == 'Self':
354                 pagename = page
355             else:
356                 if not title:
357                     title = page
358                 link = fmt.interwikilink(True, interwiki, page) + fmt.text(title) + fmt.interwikilink(False, interwiki, page)
359                 return pagename, link
360         except ValueError:
361             pass
362
363         # Handle regular pagename like "FrontPage"
364         pagename = request.normalizePagename(pagename)
365
366         # Use localized pages for the current user
367         if localize:
368             page = wikiutil.getLocalizedPage(request, pagename)
369         else:
370             page = Page(request, pagename)
371
372         pagename = page.page_name # can be different, due to i18n
373
374         if not title:
375             title = page.split_title()
376             title = self.shortenPagename(title)
377
378         link = page.link_to(request, title)
379
380         return pagename, link
381
382     def shortenPagename(self, name):
383         """ Shorten page names
384
385         Shorten very long page names that tend to break the user
386         interface. The short name is usually fine, unless really stupid
387         long names are used (WYGIWYD).
388
389         If you don't like to do this in your theme, or want to use
390         different algorithm, override this method.
391
392         @param name: page name, unicode
393         @rtype: unicode
394         @return: shortened version.
395         """
396         maxLength = self.maxPagenameLength()
397         # First use only the sub page name, that might be enough
398         if len(name) > maxLength:
399             name = name.split('/')[-1]
400             # If it's not enough, replace the middle with '...'
401             if len(name) > maxLength:
402                 half, left = divmod(maxLength - 3, 2)
403                 name = u'%s...%s' % (name[:half + left], name[-half:])
404         return name
405
406     def maxPagenameLength(self):
407         """ Return maximum length for shortened page names """
408         return 25
409
410     def navibar(self, d):
411         """ Assemble the navibar
412
413         @param d: parameter dictionary
414         @rtype: unicode
415         @return: navibar html
416         """
417         request = self.request
418         found = {} # pages we found. prevent duplicates
419         items = [] # navibar items
420         item = u'<li class="%s">%s</li>'
421         current = d['page_name']
422
423         # Process config navi_bar
424         if request.cfg.navi_bar:
425             for text in request.cfg.navi_bar:
426                 pagename, link = self.splitNavilink(text)
427                 if pagename == current:
428                     cls = 'wikilink current'
429                 else:
430                     cls = 'wikilink'
431                 items.append(item % (cls, link))
432                 found[pagename] = 1
433
434         # Add user links to wiki links, eliminating duplicates.
435         userlinks = request.user.getQuickLinks()
436         for text in userlinks:
437             # Split text without localization, user knows what he wants
438             pagename, link = self.splitNavilink(text, localize=0)
439             if not pagename in found:
440                 if pagename == current:
441                     cls = 'userlink current'
442                 else:
443                     cls = 'userlink'
444                 items.append(item % (cls, link))
445                 found[pagename] = 1
446
447         # Add current page at end of local pages
448         if not current in found:
449             title = d['page'].split_title()
450             title = self.shortenPagename(title)
451             link = d['page'].link_to(request, title)
452             cls = 'current'
453             items.append(item % (cls, link))
454
455         # Add sister pages.
456         for sistername, sisterurl in request.cfg.sistersites:
457             if sistername == request.cfg.interwikiname: # it is THIS wiki
458                 cls = 'sisterwiki current'
459                 items.append(item % (cls, sistername))
460             else:
461                 # TODO optimize performance
462                 cache = caching.CacheEntry(request, 'sisters', sistername, 'farm', use_pickle=True)
463                 if cache.exists():
464                     data = cache.content()
465                     sisterpages = data['sisterpages']
466                     if current in sisterpages:
467                         cls = 'sisterwiki'
468                         url = sisterpages[current]
469                         link = request.formatter.url(1, url) + \
470                                request.formatter.text(sistername) +\
471                                request.formatter.url(0)
472                         items.append(item % (cls, link))
473
474         # Assemble html
475         items = u''.join(items)
476         html = u'''
477 <ul id="navibar">
478 %s
479 </ul>
480 ''' % items
481         return html
482
483     def get_icon(self, icon):
484         """ Return icon data from self.icons
485
486         If called from <<Icon(file)>> we have a filename, not a
487         key. Using filenames is deprecated, but for now, we simulate old
488         behavior.
489
490         @param icon: icon name or file name (string)
491         @rtype: tuple
492         @return: alt (unicode), href (string), width, height (int)
493         """
494         if icon in self.icons:
495             alt, icon, w, h = self.icons[icon]
496         else:
497             # Create filenames to icon data mapping on first call, then
498             # cache in class for next calls.
499             if not getattr(self.__class__, 'iconsByFile', None):
500                 d = {}
501                 for data in self.icons.values():
502                     d[data[1]] = data
503                 self.__class__.iconsByFile = d
504
505             # Try to get icon data by file name
506             if icon in self.iconsByFile:
507                 alt, icon, w, h = self.iconsByFile[icon]
508             else:
509                 alt, icon, w, h = '', icon, '', ''
510
511         return alt, self.img_url(icon), w, h
512
513     def make_icon(self, icon, vars=None, **kw):
514         """
515         This is the central routine for making <img> tags for icons!
516         All icons stuff except the top left logo and search field icons are
517         handled here.
518
519         @param icon: icon id (dict key)
520         @param vars: ...
521         @rtype: string
522         @return: icon html (img tag)
523         """
524         if vars is None:
525             vars = {}
526         alt, img, w, h = self.get_icon(icon)
527         try:
528             alt = vars['icon-alt-text'] # if it is possible we take the alt-text from 'page_icons_table'
529         except KeyError, err:
530             try:
531                 alt = alt % vars # if not we just leave the  alt-text from 'icons'
532             except KeyError, err:
533                 alt = 'KeyError: %s' % str(err)
534         alt = self.request.getText(alt)
535         tag = self.request.formatter.image(src=img, alt=alt, width=w, height=h, **kw)
536         return tag
537
538     def make_iconlink(self, which, d):
539         """
540         Make a link with an icon
541
542         @param which: icon id (dictionary key)
543         @param d: parameter dictionary
544         @rtype: string
545         @return: html link tag
546         """
547         qs = {}
548         pagekey, querystr, title, icon = self.cfg.page_icons_table[which]
549         qs.update(querystr) # do not modify the querystr dict in the cfg!
550         d['icon-alt-text'] = d['title'] = title % d
551         d['i18ntitle'] = self.request.getText(d['title'])
552         img_src = self.make_icon(icon, d)
553         rev = d['rev']
554         if rev and which in ['raw', 'print', ]:
555             qs['rev'] = str(rev)
556         attrs = {'rel': 'nofollow', 'title': d['i18ntitle'], }
557         page = d[pagekey]
558         if isinstance(page, unicode):
559             # e.g. d['page_parent_page'] is just the unicode pagename
560             # while d['page'] will give a page object
561             page = Page(self.request, page)
562         return page.link_to_raw(self.request, text=img_src, querystr=qs, **attrs)
563
564     def msg(self, d):
565         """ Assemble the msg display
566
567         Display a message with a widget or simple strings with a clear message link.
568
569         @param d: parameter dictionary
570         @rtype: unicode
571         @return: msg display html
572         """
573         _ = self.request.getText
574         msgs = d['msg']
575
576         result = u""
577         close = d['page'].link_to(self.request, text=_('Clear message'), css_class="clear-link")
578         for msg, msg_class in msgs:
579             try:
580                 result += u'<p>%s</p>' % msg.render()
581                 close = ''
582             except AttributeError:
583                 if msg and msg_class:
584                     result += u'<p><div class="%s">%s</div></p>' % (msg_class, msg)
585                 elif msg:
586                     result += u'<p>%s</p>\n' % msg
587         if result:
588             html = result + close
589             return u'<div id="message">\n%s\n</div>\n' % html
590         else:
591             return u''
592
593         return u'<div id="message">\n%s\n</div>\n' % html
594
595     def trail(self, d):
596         """ Assemble page trail
597
598         @param d: parameter dictionary
599         @rtype: unicode
600         @return: trail html
601         """
602         request = self.request
603         user = request.user
604         html = ''
605         if not user.valid or user.show_page_trail:
606             trail = user.getTrail()
607             if trail:
608                 items = []
609                 for pagename in trail:
610                     try:
611                         interwiki, page = wikiutil.split_interwiki(pagename)
612                         if interwiki != request.cfg.interwikiname and interwiki != 'Self':
613                             link = (self.request.formatter.interwikilink(True, interwiki, page) +
614                                     self.shortenPagename(page) +
615                                     self.request.formatter.interwikilink(False, interwiki, page))
616                             items.append('<li>%s</li>' % link)
617                             continue
618                         else:
619                             pagename = page
620
621                     except ValueError:
622                         pass
623                     page = Page(request, pagename)
624                     title = page.split_title()
625                     title = self.shortenPagename(title)
626                     link = page.link_to(request, title)
627                     items.append('<li>%s</li>' % link)
628                 html = '''
629 <ul id="pagetrail">
630 %s
631 </ul>''' % ''.join(items)
632         return html
633
634     def html_stylesheets(self, d):
635         """ Assemble html head stylesheet links
636
637         @param d: parameter dictionary
638         @rtype: string
639         @return: stylesheets links
640         """
641         link = '<link rel="stylesheet" type="text/css" charset="%s" media="%s" href="%s">'
642
643         # Check mode
644         if d.get('print_mode'):
645             media = d.get('media', 'print')
646             stylesheets = getattr(self, 'stylesheets_' + media)
647         else:
648             stylesheets = self.stylesheets
649         usercss = self.request.user.valid and self.request.user.css_url
650
651         # Create stylesheets links
652         html = []
653         prefix = self.cfg.url_prefix_static
654         csshref = '%s/%s/css' % (prefix, self.name)
655         for media, basename in stylesheets:
656             href = '%s/%s.css' % (csshref, basename)
657             html.append(link % (self.stylesheetsCharset, media, href))
658
659             # Don't add user css url if it matches one of ours
660             if usercss and usercss == href:
661                 usercss = None
662
663         # admin configurable additional css (farm or wiki level)
664         for media, csshref in self.request.cfg.stylesheets:
665             html.append(link % (self.stylesheetsCharset, media, csshref))
666
667         csshref = '%s/%s/css/msie.css' % (prefix, self.name)
668         html.append("""
669 <!-- css only for MSIE browsers -->
670 <!--[if IE]>
671    %s
672 <![endif]-->
673 """ % link % (self.stylesheetsCharset, 'all', csshref))
674
675         # Add user css url (assuming that user css uses same charset)
676         if usercss and usercss.lower() != "none":
677             html.append(link % (self.stylesheetsCharset, 'all', usercss))
678
679         return '\n'.join(html)
680
681     def shouldShowPageinfo(self, page):
682         """ Should we show page info?
683
684         Should be implemented by actions. For now, we check here by action
685         name and page.
686
687         @param page: current page
688         @rtype: bool
689         @return: true if should show page info
690         """
691         if page.exists() and self.request.user.may.read(page.page_name):
692             # These  actions show the  page content.
693             # TODO: on new action, page info will not show.
694             # A better solution will be if the action itself answer the question: showPageInfo().
695             contentActions = [u'', u'show', u'refresh', u'preview', u'diff',
696                               u'subscribe', u'RenamePage', u'CopyPage', u'DeletePage',
697                               u'SpellCheck', u'print']
698             return self.request.action in contentActions
699         return False
700
701     def pageinfo(self, page):
702         """ Return html fragment with page meta data
703
704         Since page information uses translated text, it uses the ui
705         language and direction. It looks strange sometimes, but
706         translated text using page direction looks worse.
707
708         @param page: current page
709         @rtype: unicode
710         @return: page last edit information
711         """
712         _ = self.request.getText
713         html = ''
714         if self.shouldShowPageinfo(page):
715             info = page.lastEditInfo()
716             if info:
717                 if info['editor']:
718                     info = _("last edited %(time)s by %(editor)s") % info
719                 else:
720                     info = _("last modified %(time)s") % info
721                 pagename = page.page_name
722                 if self.request.cfg.show_interwiki:
723                     pagename = "%s: %s" % (self.request.cfg.interwikiname, pagename)
724                 info = "%s  (%s)" % (wikiutil.escape(pagename), info)
725                 html = '<p id="pageinfo" class="info"%(lang)s>%(info)s</p>\n' % {
726                     'lang': self.ui_lang_attr(),
727                     'info': info
728                     }
729         return html
730
731     def searchform(self, d):
732         """
733         assemble HTML code for the search forms
734
735         @param d: parameter dictionary
736         @rtype: unicode
737         @return: search form html
738         """
739         _ = self.request.getText
740         form = self.request.form
741         updates = {
742             'search_label': _('Search:'),
743             'search_value': wikiutil.escape(form.get('value', [''])[0], 1),
744             'search_full_label': _('Text'),
745             'search_title_label': _('Titles'),
746             'baseurl': self.request.getScriptname(),
747             'pagename_quoted': wikiutil.quoteWikinameURL(d['page'].page_name),
748             }
749         d.update(updates)
750
751         html = u'''
752 <form id="searchform" method="get" action="%(baseurl)s/%(pagename_quoted)s">
753 <div>
754 <input type="hidden" name="action" value="fullsearch">
755 <input type="hidden" name="context" value="180">
756 <label for="searchinput">%(search_label)s</label>
757 <input id="searchinput" type="text" name="value" value="%(search_value)s" size="20"
758     onfocus="searchFocus(this)" onblur="searchBlur(this)"
759     onkeyup="searchChange(this)" onchange="searchChange(this)" alt="Search">
760 <input id="titlesearch" name="titlesearch" type="submit"
761     value="%(search_title_label)s" alt="Search Titles">
762 <input id="fullsearch" name="fullsearch" type="submit"
763     value="%(search_full_label)s" alt="Search Full Text">
764 </div>
765 </form>
766 <script type="text/javascript">
767 <!--// Initialize search form
768 var f = document.getElementById('searchform');
769 f.getElementsByTagName('label')[0].style.display = 'none';
770 var e = document.getElementById('searchinput');
771 searchChange(e);
772 searchBlur(e);
773 //-->
774 </script>
775 ''' % d
776         return html
777
778     def showversion(self, d, **keywords):
779         """
780         assemble HTML code for copyright and version display
781
782         @param d: parameter dictionary
783         @rtype: string
784         @return: copyright and version display html
785         """
786         html = ''
787         if self.cfg.show_version and not keywords.get('print_mode', 0):
788             html = (u'<div id="version">MoinMoin Release %s [Revision %s], '
789                      'Copyright by Juergen Hermann et al.</div>') % (version.release, version.revision, )
790         return html
791
792     def headscript(self, d):
793         """ Return html head script with common functions
794
795         @param d: parameter dictionary
796         @rtype: unicode
797         @return: script for html head
798         """
799         # Don't add script for print view
800         if self.request.action == 'print':
801             return u''
802
803         _ = self.request.getText
804         script = u"""
805 <script type="text/javascript">
806 <!--
807 var search_hint = "%(search_hint)s";
808 //-->
809 </script>
810 """ % {
811     'search_hint': _('Search Wiki'),
812     }
813         return script
814
815     def shouldUseRSS(self, page):
816         """ Return True if RSS feature is available and we are on the
817             RecentChanges page, or False.
818
819             Currently rss is broken on plain Python, and works only when
820             installing PyXML. Return true if PyXML is installed.
821         """
822         if not rss_supported:
823             return False
824         return page.page_name == u'RecentChanges' or \
825            page.page_name == self.request.getText(u'RecentChanges')
826
827     def rsshref(self, page):
828         """ Create rss href, used for rss button and head link
829
830         @rtype: unicode
831         @return: rss href
832         """
833         request = self.request
834         url = page.url(request, querystr={
835                 'action': 'rss_rc', 'ddiffs': '1', 'unique': '1', }, escape=0)
836         return url
837
838     def rsslink(self, d):
839         """ Create rss link in head, used by FireFox
840
841         RSS link for FireFox. This shows an rss link in the bottom of
842         the page and let you subscribe to the wiki rss feed.
843
844         @rtype: unicode
845         @return: html head
846         """
847         link = u''
848         page = d['page']
849         if self.shouldUseRSS(page):
850             link = (u'<link rel="alternate" title="%s Recent Changes" '
851                     u'href="%s" type="application/rss+xml">') % (
852                         wikiutil.escape(self.cfg.sitename, True),
853                         wikiutil.escape(self.rsshref(page), True) )
854         return link
855
856     def html_head(self, d):
857         """ Assemble html head
858
859         @param d: parameter dictionary
860         @rtype: unicode
861         @return: html head
862         """
863         html = [
864             u'<title>%(title)s - %(sitename)s</title>' % {
865                 'title': wikiutil.escape(d['title']),
866                 'sitename': wikiutil.escape(d['sitename']),
867             },
868             self.externalScript('common'),
869             self.headscript(d), # Should move to separate .js file
870             self.guiEditorScript(d),
871             self.html_stylesheets(d),
872             self.rsslink(d),
873             self.universal_edit_button(d),
874             ]
875         return '\n'.join(html)
876
877     def externalScript(self, name):
878         """ Format external script html """
879         src = '%s/common/js/%s.js' % (self.request.cfg.url_prefix_static, name)
880         return '<script type="text/javascript" src="%s"></script>' % src
881
882     def universal_edit_button(self, d, **keywords):
883         """ Generate HTML for an edit link in the header."""
884         page = d['page']
885         if 'edit' in self.request.cfg.actions_excluded:
886             return ""
887         if not (page.isWritable() and
888                 self.request.user.may.write(page.page_name)):
889             return ""
890         _ = self.request.getText
891         querystr = {'action': 'edit'}
892         text = _(u'Edit')
893         url = page.url(self.request, querystr=querystr, escape=0)
894         return (u'<link rel="alternate" type="application/wiki" '
895                 u'title="%s" href="%s" />' % (text, url))
896
897     def credits(self, d, **keywords):
898         """ Create credits html from credits list """
899         if isinstance(self.cfg.page_credits, (list, tuple)):
900             items = ['<li>%s</li>' % i for i in self.cfg.page_credits]
901             html = '<ul id="credits">\n%s\n</ul>\n' % ''.join(items)
902         else:
903             # Old config using string, output as is
904             html = self.cfg.page_credits
905         return html
906
907     def actionsMenu(self, page):
908         """ Create actions menu list and items data dict
909
910         The menu will contain the same items always, but items that are
911         not available will be disabled (some broken browsers will let
912         you select disabled options though).
913
914         The menu should give best user experience for javascript
915         enabled browsers, and acceptable behavior for those who prefer
916         not to use Javascript.
917
918         TODO: Move actionsMenuInit() into body onload - requires that the theme will render body,
919               it is currently done in wikiutil/page.
920
921         @param page: current page, Page object
922         @rtype: unicode
923         @return: actions menu html fragment
924         """
925         request = self.request
926         _ = request.getText
927         rev = request.rev
928
929         menu = [
930             'raw',
931             'print',
932             'RenderAsDocbook',
933             'refresh',
934             '__separator__',
935             'SpellCheck',
936             'LikePages',
937             'LocalSiteMap',
938             '__separator__',
939             'RenamePage',
940             'CopyPage',
941             'DeletePage',
942             '__separator__',
943             'MyPages',
944             'SubscribeUser',
945             '__separator__',
946             'Despam',
947             'revert',
948             'PackagePages',
949             'SyncPages',
950             ]
951
952         titles = {
953             # action: menu title
954             '__title__': _("More Actions:"),
955             # Translation may need longer or shorter separator
956             '__separator__': _('------------------------'),
957             'raw': _('Raw Text'),
958             'print': _('Print View'),
959             'refresh': _('Delete Cache'),
960             'SpellCheck': _('Check Spelling'), # rename action!
961             'RenamePage': _('Rename Page'),
962             'CopyPage': _('Copy Page'),
963             'DeletePage': _('Delete Page'),
964             'LikePages': _('Like Pages'),
965             'LocalSiteMap': _('Local Site Map'),
966             'MyPages': _('My Pages'),
967             'SubscribeUser': _('Subscribe User'),
968             'Despam': _('Remove Spam'),
969             'revert': _('Revert to this revision'),
970             'PackagePages': _('Package Pages'),
971             'RenderAsDocbook': _('Render as Docbook'),
972             'SyncPages': _('Sync Pages'),
973             }
974
975         options = []
976         option = '<option value="%(action)s"%(disabled)s>%(title)s</option>'
977         # class="disabled" is a workaround for browsers that ignore
978         # "disabled", e.g IE, Safari
979         # for XHTML: data['disabled'] = ' disabled="disabled"'
980         disabled = ' disabled class="disabled"'
981
982         # Format standard actions
983         available = request.getAvailableActions(page)
984         for action in menu:
985             data = {'action': action, 'disabled': '', 'title': titles[action]}
986             # removes excluded actions from the more actions menu
987             if action in request.cfg.actions_excluded:
988                 continue
989
990             # Enable delete cache only if page can use caching
991             if action == 'refresh':
992                 if not page.canUseCache():
993                     data['action'] = 'show'
994                     data['disabled'] = disabled
995
996             # revert action enabled only if user can revert
997             if action == 'revert' and not request.user.may.revert(page.page_name):
998                 data['action'] = 'show'
999                 data['disabled'] = disabled
1000
1001             # SubscribeUser action enabled only if user has admin rights
1002             if action == 'SubscribeUser' and not request.user.may.admin(page.page_name):
1003                 data['action'] = 'show'
1004                 data['disabled'] = disabled
1005
1006             # PackagePages action only if user has write rights
1007             if action == 'PackagePages' and not request.user.may.write(page.page_name):
1008                 data['action'] = 'show'
1009                 data['disabled'] = disabled
1010
1011             # Despam action enabled only for superusers
1012             if action == 'Despam' and not request.user.isSuperUser():
1013                 data['action'] = 'show'
1014                 data['disabled'] = disabled
1015
1016             # Special menu items. Without javascript, executing will
1017             # just return to the page.
1018             if action.startswith('__'):
1019                 data['action'] = 'show'
1020
1021             # Actions which are not available for this wiki, user or page
1022             if (action == '__separator__' or
1023                 (action[0].isupper() and not action in available)):
1024                 data['disabled'] = disabled
1025
1026             options.append(option % data)
1027
1028         # Add custom actions not in the standard menu, except for
1029         # some actions like AttachFile (we have them on top level)
1030         more = [item for item in available if not item in titles and not item in ('AttachFile', )]
1031         more.sort()
1032         if more:
1033             # Add separator
1034             separator = option % {'action': 'show', 'disabled': disabled,
1035                                   'title': titles['__separator__']}
1036             options.append(separator)
1037             # Add more actions (all enabled)
1038             for action in more:
1039                 data = {'action': action, 'disabled': ''}
1040                 # Always add spaces: AttachFile -> Attach File
1041                 # XXX do not create page just for using split_title -
1042                 # creating pages for non-existent does 2 storage lookups
1043                 #title = Page(request, action).split_title(force=1)
1044                 title = action
1045                 # Use translated version if available
1046                 data['title'] = _(title)
1047                 options.append(option % data)
1048
1049         data = {
1050             'label': titles['__title__'],
1051             'options': '\n'.join(options),
1052             'rev_field': rev and '<input type="hidden" name="rev" value="%d">' % rev or '',
1053             'do_button': _("Do"),
1054             'baseurl': self.request.getScriptname(),
1055             'pagename_quoted': wikiutil.quoteWikinameURL(page.page_name),
1056             }
1057         html = '''
1058 <form class="actionsmenu" method="GET" action="%(baseurl)s/%(pagename_quoted)s">
1059 <div>
1060     <label>%(label)s</label>
1061     <select name="action"
1062         onchange="if ((this.selectedIndex != 0) &&
1063                       (this.options[this.selectedIndex].disabled == false)) {
1064                 this.form.submit();
1065             }
1066             this.selectedIndex = 0;">
1067         %(options)s
1068     </select>
1069     <input type="submit" value="%(do_button)s">
1070     %(rev_field)s
1071 </div>
1072 <script type="text/javascript">
1073 <!--// Init menu
1074 actionsMenuInit('%(label)s');
1075 //-->
1076 </script>
1077 </form>
1078 ''' % data
1079
1080         return html
1081
1082     def editbar(self, d):
1083         """ Assemble the page edit bar.
1084
1085         Create html on first call, then return cached html.
1086
1087         @param d: parameter dictionary
1088         @rtype: unicode
1089         @return: iconbar html
1090         """
1091         page = d['page']
1092         if not self.shouldShowEditbar(page):
1093             return ''
1094
1095         html = self._cache.get('editbar')
1096         if html is None:
1097             # Remove empty items and format as list
1098             items = ''.join(['<li>%s</li>' % item
1099                              for item in self.editbarItems(page) if item])
1100             html = u'<ul class="editbar">%s</ul>\n' % items
1101             self._cache['editbar'] = html
1102
1103         return html
1104
1105     def shouldShowEditbar(self, page):
1106         """ Should we show the editbar?
1107
1108         Actions should implement this, because only the action knows if
1109         the edit bar makes sense. Until it goes into actions, we do the
1110         checking here.
1111
1112         @param page: current page
1113         @rtype: bool
1114         @return: true if editbar should show
1115         """
1116         # Show editbar only for existing pages, including deleted pages,
1117         # that the user may read. If you may not read, you can't edit,
1118         # so you don't need editbar.
1119         if (page.exists(includeDeleted=1) and
1120             self.request.user.may.read(page.page_name)):
1121             form = self.request.form
1122             action = self.request.action
1123             # Do not show editbar on edit but on save/cancel
1124             return not (action == 'edit' and
1125                         not form.has_key('button_save') and
1126                         not form.has_key('button_cancel'))
1127         return False
1128
1129     def editbarItems(self, page):
1130         """ Return list of items to show on the editbar
1131
1132         This is separate method to make it easy to customize the
1133         edtibar in sub classes.
1134         """
1135         _ = self.request.getText
1136         editbar_actions = []
1137         for editbar_item in self.request.cfg.edit_bar:
1138             if editbar_item == 'Discussion':
1139                 if not self.request.cfg.supplementation_page and self.request.getPragma('supplementation-page', 1) in ('on', '1'):
1140                     editbar_actions.append(self.supplementation_page_nameLink(page))
1141                 elif self.request.cfg.supplementation_page and not self.request.getPragma('supplementation-page', 1) in ('off', '0'):
1142                     editbar_actions.append(self.supplementation_page_nameLink(page))
1143             elif editbar_item == 'Comments':
1144                 # we just use <a> to get same style as other links, but we add some dummy
1145                 # link target to get correct mouseover pointer appearance. return false
1146                 # keeps the browser away from jumping to the link target::
1147                 editbar_actions.append('<a href="#" class="toggleCommentsButton" style="display:none;" onClick="toggleComments();return false;">%s</a>' % _('Comments'))
1148             elif editbar_item == 'Edit':
1149                 editbar_actions.append(self.editorLink(page))
1150             elif editbar_item == 'Info':
1151                 editbar_actions.append(self.infoLink(page))
1152             elif editbar_item == 'Subscribe':
1153                 editbar_actions.append(self.subscribeLink(page))
1154             elif editbar_item == 'Quicklink':
1155                 editbar_actions.append(self.quicklinkLink(page))
1156             elif editbar_item == 'Attachments':
1157                 editbar_actions.append(self.attachmentsLink(page))
1158             elif editbar_item == 'ActionsMenu':
1159                 editbar_actions.append(self.actionsMenu(page))
1160         return editbar_actions
1161
1162     def supplementation_page_nameLink(self, page):
1163         """Return a link to the discussion page
1164
1165            If the discussion page doesn't exist and the user
1166            has no right to create it, show a disabled link.
1167         """
1168         _ = self.request.getText
1169         suppl_name = self.request.cfg.supplementation_page_name
1170         suppl_name_full = "%s/%s" % (page.page_name, suppl_name)
1171
1172         test = Page(self.request, suppl_name_full)
1173         if not test.exists() and not self.request.user.may.write(suppl_name_full):
1174             return ('<span class="disabled">%s</span>' % _(suppl_name))
1175         else:
1176             return page.link_to(self.request, text=_(suppl_name),
1177                                 querystr={'action': 'supplementation'}, css_class='nbsupplementation', rel='nofollow')
1178
1179     def guiworks(self, page):
1180         """ Return whether the gui editor / converter can work for that page.
1181
1182             The GUI editor currently only works for wiki format.
1183             For simplicity, we also tell it does not work if the admin forces the text editor.
1184         """
1185         is_wiki = page.pi['format'] == 'wiki'
1186         gui_disallowed = self.cfg.editor_force and self.cfg.editor_default == 'text'
1187         return is_wiki and not gui_disallowed
1188
1189
1190     def editorLink(self, page):
1191         """ Return a link to the editor
1192
1193         If the user can't edit, return a disabled edit link.
1194
1195         If the user want to show both editors, it will display "Edit
1196         (Text)", otherwise as "Edit".
1197         """
1198         if 'edit' in self.request.cfg.actions_excluded:
1199             return ""
1200
1201         if not (page.isWritable() and
1202                 self.request.user.may.write(page.page_name)):
1203             return self.disabledEdit()
1204
1205         _ = self.request.getText
1206         querystr = {'action': 'edit'}
1207
1208         guiworks = self.guiworks(page)
1209         if self.showBothEditLinks() and guiworks:
1210             text = _('Edit (Text)')
1211             querystr['editor'] = 'text'
1212             attrs = {'name': 'texteditlink', 'rel': 'nofollow', }
1213         else:
1214             text = _('Edit')
1215             if guiworks:
1216                 # 'textonly' will be upgraded dynamically to 'guipossible' by JS
1217                 querystr['editor'] = 'textonly'
1218                 attrs = {'name': 'editlink', 'rel': 'nofollow', }
1219             else:
1220                 querystr['editor'] = 'text'
1221                 attrs = {'name': 'texteditlink', 'rel': 'nofollow', }
1222
1223         return page.link_to(self.request, text=text, querystr=querystr, **attrs)
1224
1225     def showBothEditLinks(self):
1226         """ Return True if both edit links should be displayed """
1227         editor = self.request.user.editor_ui
1228         if editor == '<default>':
1229             editor = self.request.cfg.editor_ui
1230         return editor == 'freechoice'
1231
1232     def guiEditorScript(self, d):
1233         """ Return a script that set the gui editor link variables
1234
1235         The link will be created only when javascript is enabled and
1236         the browser is compatible with the editor.
1237         """
1238         page = d['page']
1239         if not (page.isWritable() and
1240                 self.request.user.may.write(page.page_name) and
1241                 self.showBothEditLinks() and
1242                 self.guiworks(page)):
1243             return ''
1244
1245         _ = self.request.getText
1246         return """\
1247 <script type="text/javascript">
1248 <!-- // GUI edit link and i18n
1249 var gui_editor_link_href = "%(url)s";
1250 var gui_editor_link_text = "%(text)s";
1251 //-->
1252 </script>
1253 """ % {'url': page.url(self.request, querystr={'action': 'edit', 'editor': 'gui', }),
1254        'text': _('Edit (GUI)'),
1255       }
1256
1257     def disabledEdit(self):
1258         """ Return a disabled edit link """
1259         _ = self.request.getText
1260         return ('<span class="disabled">%s</span>'
1261                 % _('Immutable Page'))
1262
1263     def infoLink(self, page):
1264         """ Return link to page information """
1265         if 'info' in self.request.cfg.actions_excluded:
1266             return ""
1267
1268         _ = self.request.getText
1269         return page.link_to(self.request,
1270                             text=_('Info'),
1271                             querystr={'action': 'info'}, css_class='nbinfo', rel='nofollow')
1272
1273     def subscribeLink(self, page):
1274         """ Return subscribe/unsubscribe link to valid users
1275
1276         @rtype: unicode
1277         @return: subscribe or unsubscribe link
1278         """
1279         if not ((self.cfg.mail_enabled or self.cfg.jabber_enabled) and self.request.user.valid):
1280             return ''
1281
1282         _ = self.request.getText
1283         if self.request.user.isSubscribedTo([page.page_name]):
1284             action, text = 'unsubscribe', _("Unsubscribe")
1285         else:
1286             action, text = 'subscribe', _("Subscribe")
1287         if action in self.request.cfg.actions_excluded:
1288             return ""
1289         return page.link_to(self.request, text=text, querystr={'action': action}, css_class='nbsubscribe', rel='nofollow')
1290
1291     def quicklinkLink(self, page):
1292         """ Return add/remove quicklink link
1293
1294         @rtype: unicode
1295         @return: link to add or remove a quicklink
1296         """
1297         if not self.request.user.valid:
1298             return ''
1299
1300         _ = self.request.getText
1301         if self.request.user.isQuickLinkedTo([page.page_name]):
1302             action, text = 'quickunlink', _("Remove Link")
1303         else:
1304             action, text = 'quicklink', _("Add Link")
1305         if action in self.request.cfg.actions_excluded:
1306             return ""
1307         return page.link_to(self.request, text=text, querystr={'action': action}, css_class='nbquicklink', rel='nofollow')
1308
1309     def attachmentsLink(self, page):
1310         """ Return link to page attachments """
1311         if 'AttachFile' in self.request.cfg.actions_excluded:
1312             return ""
1313
1314         _ = self.request.getText
1315         return page.link_to(self.request,
1316                             text=_('Attachments'),
1317                             querystr={'action': 'AttachFile'}, css_class='nbattachments', rel='nofollow')
1318
1319     def startPage(self):
1320         """ Start page div with page language and direction
1321
1322         @rtype: unicode
1323         @return: page div with language and direction attribtues
1324         """
1325         return u'<div id="page"%s>\n' % self.content_lang_attr()
1326
1327     def endPage(self):
1328         """ End page div
1329
1330         Add an empty page bottom div to prevent floating elements to
1331         float out of the page bottom over the footer.
1332         """
1333         return '<div id="pagebottom"></div>\n</div>\n'
1334
1335     # Public functions #####################################################
1336
1337     def header(self, d, **kw):
1338         """ Assemble page header
1339
1340         Default behavior is to start a page div. Sub class and add
1341         footer items.
1342
1343         @param d: parameter dictionary
1344         @rtype: string
1345         @return: page header html
1346         """
1347         return self.startPage()
1348
1349     editorheader = header
1350
1351     def footer(self, d, **keywords):
1352         """ Assemble page footer
1353
1354         Default behavior is to end page div. Sub class and add
1355         footer items.
1356
1357         @param d: parameter dictionary
1358         @keyword ...:...
1359         @rtype: string
1360         @return: page footer html
1361         """
1362         return self.endPage()
1363
1364     # RecentChanges ######################################################
1365
1366     def recentchanges_entry(self, d):
1367         """
1368         Assemble a single recentchanges entry (table row)
1369
1370         @param d: parameter dictionary
1371         @rtype: string
1372         @return: recentchanges entry html
1373         """
1374         _ = self.request.getText
1375         html = []
1376         html.append('<tr>\n')
1377
1378         html.append('<td class="rcicon1">%(icon_html)s</td>\n' % d)
1379
1380         html.append('<td class="rcpagelink">%(pagelink_html)s</td>\n' % d)
1381
1382         html.append('<td class="rctime">')
1383         if d['time_html']:
1384             html.append("%(time_html)s" % d)
1385         html.append('</td>\n')
1386
1387         html.append('<td class="rcicon2">%(info_html)s</td>\n' % d)
1388
1389         html.append('<td class="rceditor">')
1390         if d['editors']:
1391             html.append('<br>'.join(d['editors']))
1392         html.append('</td>\n')
1393
1394         html.append('<td class="rccomment">')
1395         if d['comments']:
1396             if d['changecount'] > 1:
1397                 notfirst = 0
1398                 for comment in d['comments']:
1399                     html.append('%s<tt>#%02d</tt>&nbsp;%s' % (
1400                         notfirst and '<br>' or '', comment[0], comment[1]))
1401                     notfirst = 1
1402             else:
1403                 comment = d['comments'][0]
1404                 html.append('%s' % comment[1])
1405         html.append('</td>\n')
1406
1407         html.append('</tr>\n')
1408
1409         return ''.join(html)
1410
1411     def recentchanges_daybreak(self, d):
1412         """
1413         Assemble a rc daybreak indication (table row)
1414
1415         @param d: parameter dictionary
1416         @rtype: string
1417         @return: recentchanges daybreak html
1418         """
1419         if d['bookmark_link_html']:
1420             set_bm = '&nbsp; %(bookmark_link_html)s' % d
1421         else:
1422             set_bm = ''
1423         return ('<tr class="rcdaybreak"><td colspan="%d">'
1424                 '<strong>%s</strong>'
1425                 '%s'
1426                 '</td></tr>\n') % (6, d['date'], set_bm)
1427
1428     def recentchanges_header(self, d):
1429         """
1430         Assemble the recentchanges header (intro + open table)
1431
1432         @param d: parameter dictionary
1433         @rtype: string
1434         @return: recentchanges header html
1435         """
1436         _ = self.request.getText
1437
1438         # Should use user interface language and direction
1439         html = '<div class="recentchanges"%s>\n' % self.ui_lang_attr()
1440         html += '<div>\n'
1441         page = d['page']
1442         if self.shouldUseRSS(page):
1443             link = [
1444                 u'<div class="rcrss">',
1445                 self.request.formatter.url(1, self.rsshref(page)),
1446                 self.request.formatter.rawHTML(self.make_icon("rss")),
1447                 self.request.formatter.url(0),
1448                 u'</div>',
1449                 ]
1450             html += ''.join(link)
1451         html += '<p>'
1452         # Add day selector
1453         if d['rc_days']:
1454             days = []
1455             for day in d['rc_days']:
1456                 if day == d['rc_max_days']:
1457                     days.append('<strong>%d</strong>' % day)
1458                 else:
1459                     days.append(
1460                         wikiutil.link_tag(self.request,
1461                             '%s?max_days=%d' % (d['q_page_name'], day),
1462                             str(day),
1463                             self.request.formatter, rel='nofollow'))
1464             days = ' | '.join(days)
1465             html += (_("Show %s days.") % (days, ))
1466
1467         if d['rc_update_bookmark']:
1468             html += " %(rc_update_bookmark)s %(rc_curr_bookmark)s" % d
1469
1470         html += '</p>\n</div>\n'
1471
1472         html += '<table>\n'
1473         return html
1474
1475     def recentchanges_footer(self, d):
1476         """
1477         Assemble the recentchanges footer (close table)
1478
1479         @param d: parameter dictionary
1480         @rtype: string
1481         @return: recentchanges footer html
1482         """
1483         _ = self.request.getText
1484         html = ''
1485         html += '</table>\n'
1486         if d['rc_msg']:
1487             html += "<br>%(rc_msg)s\n" % d
1488         html += '</div>\n'
1489         return html
1490
1491     # Language stuff ####################################################
1492
1493     def ui_lang_attr(self):
1494         """Generate language attributes for user interface elements
1495
1496         User interface elements use the user language (if any), kept in
1497         request.lang.
1498
1499         @rtype: string
1500         @return: lang and dir html attributes
1501         """
1502         lang = self.request.lang
1503         return ' lang="%s" dir="%s"' % (lang, i18n.getDirection(lang))
1504
1505     def content_lang_attr(self):
1506         """Generate language attributes for wiki page content
1507
1508         Page content uses the page language or the wiki default language.
1509
1510         @rtype: string
1511         @return: lang and dir html attributes
1512         """
1513         lang = self.request.content_lang
1514         return ' lang="%s" dir="%s"' % (lang, i18n.getDirection(lang))
1515
1516     def add_msg(self, msg, msg_class=None):
1517         """ Adds a message to a list which will be used to generate status
1518         information.
1519
1520         @param msg: additional message
1521         @param msg_class: html class for the div of the additional message.
1522         """
1523         if not msg_class:
1524             msg_class = 'dialog'
1525         if self._send_title_called:
1526             raise Exception("You cannot call add_msg() after send_title()")
1527         self._status.append((msg, msg_class))
1528
1529     # stuff from wikiutil.py
1530     def send_title(self, text, **keywords):
1531         """
1532         Output the page header (and title).
1533
1534         @param text: the title text
1535         @keyword page: the page instance that called us - using this is more efficient than using pagename..
1536         @keyword pagename: 'PageName'
1537         @keyword print_mode: 1 (or 0)
1538         @keyword editor_mode: 1 (or 0)
1539         @keyword media: css media type, defaults to 'screen'
1540         @keyword allow_doubleclick: 1 (or 0)
1541         @keyword html_head: additional <head> code
1542         @keyword body_attr: additional <body> attributes
1543         @keyword body_onload: additional "onload" JavaScript code
1544         """
1545         request = self.request
1546         _ = request.getText
1547         rev = request.rev
1548
1549         if keywords.has_key('page'):
1550             page = keywords['page']
1551             pagename = page.page_name
1552         else:
1553             pagename = keywords.get('pagename', '')
1554             page = Page(request, pagename)
1555         if keywords.get('msg', ''):
1556             raise DeprecationWarning("Using send_page(msg=) is deprecated! Use theme.add_msg() instead!")
1557         scriptname = request.getScriptname()
1558         pagename_quoted = wikiutil.quoteWikinameURL(pagename)
1559
1560         # get name of system pages
1561         page_front_page = wikiutil.getFrontPage(request).page_name
1562         page_help_contents = wikiutil.getLocalizedPage(request, 'HelpContents').page_name
1563         page_title_index = wikiutil.getLocalizedPage(request, 'TitleIndex').page_name
1564         page_site_navigation = wikiutil.getLocalizedPage(request, 'SiteNavigation').page_name
1565         page_word_index = wikiutil.getLocalizedPage(request, 'WordIndex').page_name
1566         page_help_formatting = wikiutil.getLocalizedPage(request, 'HelpOnFormatting').page_name
1567         page_find_page = wikiutil.getLocalizedPage(request, 'FindPage').page_name
1568         home_page = wikiutil.getInterwikiHomePage(request) # sorry theme API change!!! Either None or tuple (wikiname,pagename) now.
1569         page_parent_page = getattr(page.getParentPage(), 'page_name', None)
1570
1571         # Prepare the HTML <head> element
1572         user_head = [request.cfg.html_head]
1573
1574         # include charset information - needed for moin_dump or any other case
1575         # when reading the html without a web server
1576         user_head.append('''<meta http-equiv="Content-Type" content="%s;charset=%s">\n''' % (page.output_mimetype, page.output_charset))
1577
1578         meta_keywords = request.getPragma('keywords')
1579         meta_desc = request.getPragma('description')
1580         if meta_keywords:
1581             user_head.append('<meta name="keywords" content="%s">\n' % wikiutil.escape(meta_keywords, 1))
1582         if meta_desc:
1583             user_head.append('<meta name="description" content="%s">\n' % wikiutil.escape(meta_desc, 1))
1584
1585         # search engine precautions / optimization:
1586         # if it is an action or edit/search, send query headers (noindex,nofollow):
1587         if request.query_string:
1588             user_head.append(request.cfg.html_head_queries)
1589         elif request.request_method == 'POST':
1590             user_head.append(request.cfg.html_head_posts)
1591         # we don't want to have BadContent stuff indexed:
1592         elif pagename in ['BadContent', 'LocalBadContent', ]:
1593             user_head.append(request.cfg.html_head_posts)
1594         # if it is a special page, index it and follow the links - we do it
1595         # for the original, English pages as well as for (the possibly
1596         # modified) frontpage:
1597         elif pagename in [page_front_page, request.cfg.page_front_page,
1598                           page_title_index, 'TitleIndex',
1599                           page_find_page, 'FindPage',
1600                           page_site_navigation, 'SiteNavigation',
1601                           'RecentChanges', ]:
1602             user_head.append(request.cfg.html_head_index)
1603         # if it is a normal page, index it, but do not follow the links, because
1604         # there are a lot of illegal links (like actions) or duplicates:
1605         else:
1606             user_head.append(request.cfg.html_head_normal)
1607
1608         if 'pi_refresh' in keywords and keywords['pi_refresh']:
1609             user_head.append('<meta http-equiv="refresh" content="%d;URL=%s">' % keywords['pi_refresh'])
1610
1611         # output buffering increases latency but increases throughput as well
1612         output = []
1613         # later: <html xmlns=\"http://www.w3.org/1999/xhtml\">
1614         output.append("""\
1615 <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN" "http://www.w3.org/TR/html4/strict.dtd">
1616 <html>
1617 <head>
1618 %s
1619 %s
1620 %s
1621 """ % (
1622             ''.join(user_head),
1623             self.html_head({
1624                 'page': page,
1625                 'title': text,
1626                 'sitename': request.cfg.html_pagetitle or request.cfg.sitename,
1627                 'print_mode': keywords.get('print_mode', False),
1628                 'media': keywords.get('media', 'screen'),
1629             }),
1630             keywords.get('html_head', ''),
1631         ))
1632
1633         # Links
1634         output.append('<link rel="Start" href="%s/%s">\n' % (scriptname, wikiutil.quoteWikinameURL(page_front_page)))
1635         if pagename:
1636             output.append('<link rel="Alternate" title="%s" href="%s/%s?action=raw">\n' % (
1637                 _('Wiki Markup'), scriptname, pagename_quoted, ))
1638             output.append('<link rel="Alternate" media="print" title="%s" href="%s/%s?action=print">\n' % (
1639                 _('Print View'), scriptname, pagename_quoted, ))
1640
1641             # !!! currently disabled due to Mozilla link prefetching, see
1642             # http://www.mozilla.org/projects/netlib/Link_Prefetching_FAQ.html
1643             #~ all_pages = request.getPageList()
1644             #~ if all_pages:
1645             #~     try:
1646             #~         pos = all_pages.index(pagename)
1647             #~     except ValueError:
1648             #~         # this shopuld never happend in theory, but let's be sure
1649             #~         pass
1650             #~     else:
1651             #~         request.write('<link rel="First" href="%s/%s">\n' % (request.getScriptname(), quoteWikinameURL(all_pages[0]))
1652             #~         if pos > 0:
1653             #~             request.write('<link rel="Previous" href="%s/%s">\n' % (request.getScriptname(), quoteWikinameURL(all_pages[pos-1])))
1654             #~         if pos+1 < len(all_pages):
1655             #~             request.write('<link rel="Next" href="%s/%s">\n' % (request.getScriptname(), quoteWikinameURL(all_pages[pos+1])))
1656             #~         request.write('<link rel="Last" href="%s/%s">\n' % (request.getScriptname(), quoteWikinameURL(all_pages[-1])))
1657
1658             if page_parent_page:
1659                 output.append('<link rel="Up" href="%s/%s">\n' % (scriptname, wikiutil.quoteWikinameURL(page_parent_page)))
1660
1661         # write buffer because we call AttachFile
1662         request.write(''.join(output))
1663         output = []
1664
1665         # XXX maybe this should be removed completely. moin emits all attachments as <link rel="Appendix" ...>
1666         # and it is at least questionable if this fits into the original intent of rel="Appendix".
1667         if pagename and request.user.may.read(pagename):
1668             from MoinMoin.action import AttachFile
1669             AttachFile.send_link_rel(request, pagename)
1670
1671         output.extend([
1672             '<link rel="Search" href="%s/%s">\n' % (scriptname, wikiutil.quoteWikinameURL(page_find_page)),
1673             '<link rel="Index" href="%s/%s">\n' % (scriptname, wikiutil.quoteWikinameURL(page_title_index)),
1674             '<link rel="Glossary" href="%s/%s">\n' % (scriptname, wikiutil.quoteWikinameURL(page_word_index)),
1675             '<link rel="Help" href="%s/%s">\n' % (scriptname, wikiutil.quoteWikinameURL(page_help_formatting)),
1676                       ])
1677
1678         output.append("</head>\n")
1679         request.write(''.join(output))
1680         output = []
1681         request.flush()
1682
1683         # start the <body>
1684         bodyattr = []
1685         if keywords.has_key('body_attr'):
1686             bodyattr.append(' ')
1687             bodyattr.append(keywords['body_attr'])
1688
1689         # Add doubleclick edit action
1690         if (pagename and keywords.get('allow_doubleclick', 0) and
1691             not keywords.get('print_mode', 0) and
1692             request.user.edit_on_doubleclick):
1693             if request.user.may.write(pagename): # separating this gains speed
1694                 url = page.url(request, {'action': 'edit'})
1695                 bodyattr.append(''' ondblclick="location.href='%s'" ''' % wikiutil.escape(url, True))
1696
1697         # Set body to the user interface language and direction
1698         bodyattr.append(' %s' % self.ui_lang_attr())
1699
1700         body_onload = keywords.get('body_onload', '')
1701         if body_onload:
1702             bodyattr.append(''' onload="%s"''' % body_onload)
1703         output.append('\n<body%s>\n' % ''.join(bodyattr))
1704
1705         # Output -----------------------------------------------------------
1706
1707         # If in print mode, start page div and emit the title
1708         if keywords.get('print_mode', 0):
1709             d = {
1710                 'title_text': text,
1711                 'page': page,
1712                 'page_name': pagename or '',
1713                 'rev': rev,
1714             }
1715             request.themedict = d
1716             output.append(self.startPage())
1717             output.append(self.interwiki(d))
1718             output.append(self.title(d))
1719
1720         # In standard mode, emit theme.header
1721         else:
1722             exists = pagename and page.exists(includeDeleted=True)
1723             # prepare dict for theme code:
1724             d = {
1725                 'theme': self.name,
1726                 'script_name': scriptname,
1727                 'title_text': text,
1728                 'logo_string': request.cfg.logo_string,
1729                 'site_name': request.cfg.sitename,
1730                 'page': page,
1731                 'rev': rev,
1732                 'pagesize': pagename and page.size() or 0,
1733                 # exists checked to avoid creation of empty edit-log for non-existing pages
1734                 'last_edit_info': exists and page.lastEditInfo() or '',
1735                 'page_name': pagename or '',
1736                 'page_find_page': page_find_page,
1737                 'page_front_page': page_front_page,
1738                 'home_page': home_page,
1739                 'page_help_contents': page_help_contents,
1740                 'page_help_formatting': page_help_formatting,
1741                 'page_parent_page': page_parent_page,
1742                 'page_title_index': page_title_index,
1743                 'page_word_index': page_word_index,
1744                 'user_name': request.user.name,
1745                 'user_valid': request.user.valid,
1746                 'msg': self._status,
1747                 'trail': keywords.get('trail', None),
1748                 # Discontinued keys, keep for a while for 3rd party theme developers
1749                 'titlesearch': 'use self.searchform(d)',
1750                 'textsearch': 'use self.searchform(d)',
1751                 'navibar': ['use self.navibar(d)'],
1752                 'available_actions': ['use self.request.availableActions(page)'],
1753             }
1754
1755             # add quoted versions of pagenames
1756             newdict = {}
1757             for key in d:
1758                 if key.startswith('page_'):
1759                     if not d[key] is None:
1760                         newdict['q_'+key] = wikiutil.quoteWikinameURL(d[key])
1761                     else:
1762                         newdict['q_'+key] = None
1763             d.update(newdict)
1764             request.themedict = d
1765
1766             # now call the theming code to do the rendering
1767             if keywords.get('editor_mode', 0):
1768                 output.append(self.editorheader(d))
1769             else:
1770                 output.append(self.header(d))
1771
1772         # emit it
1773         request.write(''.join(output))
1774         output = []
1775         request.flush()
1776         self._send_title_called = True
1777
1778     def send_footer(self, pagename, **keywords):
1779         """
1780         Output the page footer.
1781
1782         @param pagename: WikiName of the page
1783         @keyword print_mode: true, when page is displayed in Print mode
1784         """
1785         request = self.request
1786         d = request.themedict
1787
1788         # Emit end of page in print mode, or complete footer in standard mode
1789         if keywords.get('print_mode', 0):
1790             request.write(self.pageinfo(d['page']))
1791             request.write(self.endPage())
1792         else:
1793             request.write(self.footer(d, **keywords))
1794
1795     # stuff moved from request.py
1796     def send_closing_html(self):
1797         """ generate timing info html and closing html tag,
1798             everyone calling send_title must call this at the end to close
1799             the body and html tags.
1800         """
1801         request = self.request
1802
1803         # as this is the last chance to emit some html, we stop the clocks:
1804         request.clock.stop('run')
1805         request.clock.stop('total')
1806
1807         # Close html code
1808         if request.cfg.show_timings and request.action != 'print':
1809             request.write('<ul id="timings">\n')
1810             for t in request.clock.dump():
1811                 request.write('<li>%s</li>\n' % t)
1812             request.write('</ul>\n')
1813         #request.write('<!-- auth_method == %s -->' % repr(request.user.auth_method))
1814         request.write('</body>\n</html>\n\n')
1815
1816