1# -*- coding: iso-8859-1 -*-
2"""
3    MoinMoin - "text/html+css" Formatter
4
5    @copyright: 2000-2004 by Juergen Hermann <jh@web.de>
6    @license: GNU GPL, see COPYING for details.
7"""
8import os.path, re
9
10from MoinMoin import log
11logging = log.getLogger(__name__)
12
13from MoinMoin.formatter import FormatterBase
14from MoinMoin import wikiutil, i18n
15from MoinMoin.Page import Page
16from MoinMoin.action import AttachFile
17
18# insert IDs into output wherever they occur
19# warning: breaks toggle line numbers javascript
20_id_debug = False
21
22line_anchors = True
23prettyprint = False
24
25# These are the HTML elements that we treat as block elements.
26_blocks = set(['dd', 'div', 'dl', 'dt', 'form', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6',
27               'hr', 'li', 'ol', 'p', 'pre', 'table', 'tbody', 'td', 'tfoot', 'th',
28               'thead', 'tr', 'ul', 'blockquote', ])
29
30# These are the HTML elements which are typically only used with
31# an opening tag without a separate closing tag.  We do not
32# include 'script' or 'style' because sometimes they do have
33# content, and also IE has a parsing bug with those two elements (only)
34# when they don't have a closing tag even if valid XHTML.
35
36_self_closing_tags = set(['area', 'base', 'br', 'col', 'frame', 'hr', 'img',
37                          'input', 'isindex', 'link', 'meta', 'param'])
38
39# We only open those tags and let the browser auto-close them:
40_auto_closing_tags = set(['p'])
41
42# These are the elements which generally should cause an increase in the
43# indention level in the html souce code.
44_indenting_tags = set(['ol', 'ul', 'dl', 'li', 'dt', 'dd', 'tr', 'td'])
45
46# These are the elements that discard any whitespace they contain as
47# immediate child nodes.
48_space_eating_tags = set(['colgroup', 'dl', 'frameset', 'head', 'map' 'menu',
49                          'ol', 'optgroup', 'select', 'table', 'tbody', 'tfoot',
50                          'thead', 'tr', 'ul'])
51
52# These are standard HTML attributes which are typically used without any
53# value; e.g., as boolean flags indicated by their presence.
54_html_attribute_boolflags = set(['compact', 'disabled', 'ismap', 'nohref',
55                                 'noresize', 'noshade', 'nowrap', 'readonly',
56                                 'selected', 'wrap'])
57
58# These are all the standard HTML attributes that are allowed on any element.
59_common_attributes = set(['accesskey', 'class', 'dir', 'disabled', 'id', 'lang',
60                          'style', 'tabindex', 'title'])
61
62
63def rewrite_attribute_name(name, default_namespace='html'):
64    """
65    Takes an attribute name and tries to make it HTML correct.
66
67    This function takes an attribute name as a string, as it may be
68    passed in to a formatting method using a keyword-argument syntax,
69    and tries to convert it into a real attribute name.  This is
70    necessary because some attributes may conflict with Python
71    reserved words or variable syntax (such as 'for', 'class', or
72    'z-index'); and also to help with backwards compatibility with
73    older versions of MoinMoin where different names may have been
74    used (such as 'content_id' or 'css').
75
76    Returns a tuple of strings: (namespace, attribute).
77
78    Namespaces: The default namespace is always assumed to be 'html',
79    unless the input string contains a colon or a double-underscore;
80    in which case the first such occurance is assumed to separate the
81    namespace prefix from name.  So, for example, to get the HTML
82    attribute 'for' (as on a <label> element), you can pass in the
83    string 'html__for' or even '__for'.
84
85    Hyphens:  To better support hyphens (which are not allowed in Python
86    variable names), all occurances of two underscores will be replaced
87    with a hyphen.  If you use this, then you must also provide a
88    namespace since the first occurance of '__' separates a namespace
89    from the name.
90
91    Special cases: Within the 'html' namespace, mainly to preserve
92    backwards compatibility, these exceptions ars recognized:
93    'content_type', 'content_id', 'css_class', and 'css'.
94    Additionally all html attributes starting with 'on' are forced to
95    lower-case.  Also the string 'xmlns' is recognized as having
96    no namespace.
97
98    Examples:
99        'id' -> ('html', 'id')
100        'css_class' -> ('html', 'class')
101        'content_id' -> ('html', 'id')
102        'content_type' -> ('html', 'type')
103        'html__for' -> ('html', 'for)
104        'xml__space' -> ('xml', 'space')
105        '__z__index' -> ('html', 'z-index')
106        '__http__equiv' -> ('html', 'http-equiv')
107        'onChange' -> ('html', 'onchange')
108        'xmlns' -> ('', 'xmlns')
109        'xmlns__abc' -> ('xmlns', 'abc')
110
111    (In actuality we only deal with namespace prefixes, not any real
112    namespace URI...we only care about the syntax not the meanings.)
113    """
114
115    # Handle any namespaces (just in case someday we support XHTML)
116    if ':' in name:
117        ns, name = name.split(':', 1)
118    elif '__' in name:
119        ns, name = name.split('__', 1)
120    elif name == 'xmlns':
121        ns = ''
122    else:
123        ns = default_namespace
124
125    name.replace('__', '-')
126    if ns == 'html':
127        # We have an HTML attribute, fix according to DTD
128        if name == 'content_type': # MIME type such as in <a> and <link> elements
129            name = 'type'
130        elif name == 'content_id': # moin historical convention
131            name = 'id'
132        elif name in ('css_class', 'css'): # to avoid python word 'class'
133            name = 'class'
134        elif name.startswith('on'): # event handler hook
135            name = name.lower()
136    return ns, name
137
138
139def extend_attribute_dictionary(attributedict, ns, name, value):
140    """Add a new attribute to an attribute dictionary, merging values where possible.
141
142    The attributedict must be a dictionary with tuple-keys of the form:
143    (namespace, attrname).
144
145    The given ns, name, and value will be added to the dictionary.  It
146    will replace the old value if it existed, except for a few special
147    cases where the values are logically merged instead (CSS class
148    names and style rules).
149
150    As a special case, if value is None (not just ''), then the
151    attribute is actually deleted from the dictionary.
152    """
153
154    key = ns, name
155    if value is None:
156        if key in attributedict:
157            del attributedict[key]
158    else:
159        if ns == 'html' and key in attributedict:
160            if name == 'class':
161                # CSS classes are appended by space-separated list
162                value = attributedict[key] + ' ' + value
163            elif name == 'style':
164                # CSS styles are appended by semicolon-separated rules list
165                value = attributedict[key] + '; ' + value
166            elif name in _html_attribute_boolflags:
167                # All attributes must have a value. According to XHTML those
168                # traditionally used as flags should have their value set to
169                # the same as the attribute name.
170                value = name
171        attributedict[key] = value
172
173
174class Formatter(FormatterBase):
175    """
176        Send HTML data.
177    """
178
179    hardspace = '&nbsp;'
180    indentspace = ' '
181
182    def __init__(self, request, **kw):
183        FormatterBase.__init__(self, request, **kw)
184        self._indent_level = 0
185
186        self._in_code = 0 # used by text_gedit
187        self._in_code_area = 0
188        self._in_code_line = 0
189        self._code_area_js = 0
190        self._code_area_state = ['', 0, -1, -1, 0]
191
192        # code format string. id - code block id, num - line number.
193        # Caution: upon changing, also check line numbers hide/show js.
194        self._code_id_format = "%(id)s_%(num)d"
195
196        self._show_section_numbers = None
197        self.pagelink_preclosed = False
198        self._is_included = kw.get('is_included', False)
199        self.request = request
200        self.cfg = request.cfg
201        self.no_magic = kw.get('no_magic', False) # disabled tag auto closing
202
203        if not hasattr(request, '_fmt_hd_counters'):
204            request._fmt_hd_counters = []
205
206    # Primitive formatter functions #####################################
207
208    # all other methods should use these to format tags. This keeps the
209    # code clean and handle pathological cases like unclosed p and
210    # inline tags.
211
212    def _langAttr(self, lang=None):
213        """ Return lang and dir attribute
214        (INTERNAL USE BY HTML FORMATTER ONLY!)
215
216        Must be used on all block elements - div, p, table, etc.
217        @param lang: if defined, will return attributes for lang. if not
218            defined, will return attributes only if the current lang is
219            different from the content lang.
220        @rtype: dict
221        @return: language attributes
222        """
223        if not lang:
224            lang = self.request.current_lang
225            # Actions that generate content in user language should change
226            # the content lang from the default defined in cfg.
227            if lang == self.request.content_lang:
228                # lang is inherited from content div
229                return {}
230
231        #attr = {'xml:lang': lang, 'lang': lang, 'dir': i18n.getDirection(lang),}
232        attr = {'lang': lang, 'dir': i18n.getDirection(lang), }
233        return attr
234
235    def _formatAttributes(self, attr=None, allowed_attrs=None, **kw):
236        """ Return HTML attributes formatted as a single string. (INTERNAL USE BY HTML FORMATTER ONLY!)
237
238        @param attr: dict containing keys and values
239        @param allowed_attrs: A list of allowable attribute names
240        @param kw: other arbitrary attributes expressed as keyword arguments.
241        @rtype: string
242        @return: formated attributes or empty string
243
244        The attributes and their values can either be given in the
245        'attr' dictionary, or as extra keyword arguments.  They are
246        both merged together.  See the function
247        rewrite_attribute_name() for special notes on how to name
248        attributes.
249
250        Setting a value to None rather than a string (or string
251        coercible) will remove that attribute from the list.
252
253        If the list of allowed_attrs is provided, then an error is
254        raised if an HTML attribute is encountered that is not in that
255        list (or is not a common attribute which is always allowed or
256        is not in another XML namespace using the double-underscore
257        syntax).
258        """
259
260        # Merge the attr dict and kw dict into a single attributes
261        # dictionary (rewriting any attribute names, extracting
262        # namespaces, and merging some values like css classes).
263        attributes = {} # dict of key=(namespace,name): value=attribute_value
264        if attr:
265            for a, v in attr.items():
266                a_ns, a_name = rewrite_attribute_name(a)
267                extend_attribute_dictionary(attributes, a_ns, a_name, v)
268        if kw:
269            for a, v in kw.items():
270                a_ns, a_name = rewrite_attribute_name(a)
271                extend_attribute_dictionary(attributes, a_ns, a_name, v)
272
273        # Add title attribute if missing, but it has an alt.
274        if ('html', 'alt') in attributes and ('html', 'title') not in attributes:
275            attributes[('html', 'title')] = attributes[('html', 'alt')]
276
277        # Force both lang and xml:lang to be present and identical if
278        # either exists.  The lang takes precedence over xml:lang if
279        # both exist.
280        #if ('html', 'lang') in attributes:
281        #    attributes[('xml', 'lang')] = attributes[('html', 'lang')]
282        #elif ('xml', 'lang') in attributes:
283        #    attributes[('html', 'lang')] = attributes[('xml', 'lang')]
284
285        # Check all the HTML attributes to see if they are known and
286        # allowed.  Ignore attributes if in non-HTML namespaces.
287        if allowed_attrs:
288            for name in [key[1] for key in attributes if key[0] == 'html']:
289                if name in _common_attributes or name in allowed_attrs:
290                    pass
291                elif name.startswith('on'):
292                    pass  # Too many event handlers to enumerate, just let them all pass.
293                else:
294                    # Unknown or unallowed attribute.
295                    err = 'Illegal HTML attribute "%s" passed to formatter' % name
296                    raise ValueError(err)
297
298        # Finally, format them all as a single string.
299        if attributes:
300            # Construct a formatted string containing all attributes
301            # with their values escaped.  Any html:* namespace
302            # attributes drop the namespace prefix.  We build this by
303            # separating the attributes into three categories:
304            #
305            #  * Those without any namespace (should only be xmlns attributes)
306            #  * Those in the HTML namespace (we drop the html: prefix for these)
307            #  * Those in any other non-HTML namespace, including xml:
308
309            xmlnslist = ['%s="%s"' % (k[1], wikiutil.escape(v, 1))
310                         for k, v in attributes.items() if not k[0]]
311            htmllist = ['%s="%s"' % (k[1], wikiutil.escape(v, 1))
312                        for k, v in attributes.items() if k[0] == 'html']
313            otherlist = ['%s:%s="%s"' % (k[0], k[1], wikiutil.escape(v, 1))
314                         for k, v in attributes.items() if k[0] and k[0] != 'html']
315
316            # Join all these lists together in a space-separated string.  Also
317            # prefix the whole thing with a space too.
318            htmllist.sort()
319            otherlist.sort()
320            all = [''] + xmlnslist + htmllist + otherlist
321            return ' '.join(all)
322        return ''
323
324    def _open(self, tag, newline=False, attr=None, allowed_attrs=None,
325              is_unique=False, **kw):
326        """ Open a tag with optional attributes (INTERNAL USE BY HTML FORMATTER ONLY!)
327
328        @param tag: html tag, string
329        @param newline: render tag so following data is on a separate line
330        @param attr: dict with tag attributes
331        @param allowed_attrs: list of allowed attributes for this element
332        @param is_unique: ID is already unique
333        @param kw: arbitrary attributes and values
334        @rtype: string ?
335        @return: open tag with attributes as a string
336        """
337        # If it is self-closing, then don't expect a closing tag later on.
338        is_self_closing = (tag in _self_closing_tags) and ' /' or ''
339
340        # make ID unique
341        id = None
342        if not is_unique:
343            if attr and 'id' in attr:
344                id = self.make_id_unique(attr['id'])
345                id = self.qualify_id(id)
346                attr['id'] = id
347            if 'id' in kw:
348                id = self.make_id_unique(kw['id'])
349                id = self.qualify_id(id)
350                kw['id'] = id
351        else:
352            if attr and 'id' in attr:
353                id = attr['id']
354            if 'id' in kw:
355                id = kw['id']
356
357        if tag in _blocks:
358            # Block elements
359            result = []
360
361            # Add language attributes, but let caller overide the default
362            attributes = self._langAttr()
363            if attr:
364                attributes.update(attr)
365
366            # Format
367            attributes = self._formatAttributes(attributes, allowed_attrs=allowed_attrs, **kw)
368            result.append('<%s%s%s>' % (tag, attributes, is_self_closing))
369            if newline:
370                result.append(self._newline())
371            if _id_debug and id:
372                result.append('(%s) ' % id)
373            tagstr = ''.join(result)
374        else:
375            # Inline elements
376            tagstr = '<%s%s%s>' % (tag,
377                                      self._formatAttributes(attr, allowed_attrs, **kw),
378                                      is_self_closing)
379        return tagstr
380
381    def _close(self, tag, newline=False):
382        """ Close tag (INTERNAL USE BY HTML FORMATTER ONLY!)
383
384        @param tag: html tag, string
385        @param newline: render tag so following data is on a separate line
386        @rtype: string
387        @return: closing tag as a string
388        """
389        if tag in _self_closing_tags or (tag in _auto_closing_tags and not self.no_magic):
390            # This tag was already closed
391            tagstr = ''
392        elif tag in _blocks:
393            # Block elements
394            result = []
395            if newline:
396                result.append(self._newline())
397            result.append('</%s>' % (tag))
398            tagstr = ''.join(result)
399        else:
400            # Inline elements
401            tagstr = '</%s>' % tag
402
403        if newline:
404            tagstr += self._newline()
405        return tagstr
406
407    # Public methods ###################################################
408
409    def startContent(self, content_id='content', newline=True, **kw):
410        """ Start page content div.
411
412        A link anchor is provided at the beginning of the div, with
413        an id of 'top' (modified by the request ID cache).
414        """
415
416        if hasattr(self, 'page'):
417            self.request.uid_generator.begin(self.page.page_name)
418
419        result = []
420        # Use the content language
421        attr = self._langAttr(self.request.content_lang)
422        attr['id'] = content_id
423        result.append(self._open('div', newline=False, attr=attr,
424                                 allowed_attrs=['align'], **kw))
425        result.append(self.anchordef('top'))
426        if newline:
427            result.append('\n')
428        return ''.join(result)
429
430    def endContent(self, newline=True):
431        """ Close page content div.
432
433        A link anchor is provided at the end of the div, with
434        an id of 'bottom' (modified by the request ID cache).
435        """
436
437        result = []
438        result.append(self.anchordef('bottom'))
439        result.append(self._close('div', newline=newline))
440        if hasattr(self, 'page'):
441            self.request.uid_generator.end()
442        return ''.join(result)
443
444    def lang(self, on, lang_name):
445        """ Insert text with specific lang and direction.
446
447            Enclose within span tag if lang_name is different from
448            the current lang
449        """
450        tag = 'span'
451        if lang_name != self.request.current_lang:
452            # Enclose text in span using lang attributes
453            if on:
454                attr = self._langAttr(lang=lang_name)
455                return self._open(tag, attr=attr)
456            return self._close(tag)
457
458        # Direction did not change, no need for span
459        return ''
460
461    # Links ##############################################################
462
463    def pagelink(self, on, pagename='', page=None, **kw):
464        """ Link to a page.
465
466            formatter.text_python will use an optimized call with a page!=None
467            parameter. DO NOT USE THIS YOURSELF OR IT WILL BREAK.
468
469            See wikiutil.link_tag() for possible keyword parameters.
470        """
471        FormatterBase.pagelink(self, on, pagename, page, **kw)
472        if 'generated' in kw:
473            del kw['generated']
474        if page is None:
475            page = Page(self.request, pagename, formatter=self)
476        if self.request.user.show_nonexist_qm and on and not page.exists():
477            self.pagelink_preclosed = True
478            return (page.link_to(self.request, on=1, **kw) +
479                    self.text("?") +
480                    page.link_to(self.request, on=0, **kw))
481        elif not on and self.pagelink_preclosed:
482            self.pagelink_preclosed = False
483            return ""
484        else:
485            return page.link_to(self.request, on=on, **kw)
486
487    def interwikilink(self, on, interwiki='', pagename='', **kw):
488        """
489        @keyword title: override using the interwiki wikiname as title
490        """
491        querystr = kw.get('querystr', {})
492        wikitag, wikiurl, wikitail, wikitag_bad = wikiutil.resolve_interwiki(self.request, interwiki, pagename)
493        wikiurl = wikiutil.mapURL(self.request, wikiurl)
494        if wikitag == 'Self': # for own wiki, do simple links
495            wikitail = wikiutil.url_unquote(wikitail)
496            try: # XXX this is the only place where we access self.page - do we need it? Crashes silently on actions!
497                pagename = wikiutil.AbsPageName(self.page.page_name, wikitail)
498            except:
499                pagename = wikitail
500            return self.pagelink(on, pagename, **kw)
501        else: # return InterWiki hyperlink
502            if on:
503                href = wikiutil.join_wiki(wikiurl, wikitail)
504                if querystr:
505                    separator = ('?', '&')['?' in href]
506                    href = '%s%s%s' % (href, separator, wikiutil.makeQueryString(querystr))
507                anchor = kw.get('anchor')
508                if anchor:
509                    href = '%s#%s' % (href, self.sanitize_to_id(anchor))
510                if wikitag_bad:
511                    html_class = 'badinterwiki'
512                else:
513                    html_class = 'interwiki'
514                title = kw.get('title', wikitag)
515                return self.url(1, href, title=title, css=html_class) # interwiki links with umlauts
516            else:
517                return self.url(0)
518
519    def url(self, on, url=None, css=None, do_escape=None, **kw):
520        """
521        Inserts an <a> element (you can give any A tag attributes as kw args).
522
523        @param on: 1 to start the link, 0 to end the link (no other arguments are needed when on==0).
524        @param url: the URL to link to; will go through Wiki URL mapping.
525        @param css: a space-separated list of CSS classes
526        @param do_escape: DEPRECATED and not used any more, please remove it from your code!
527                          We will remove this parameter in moin 1.8 (it used to filter url
528                          param through wikiutil.escape, but text_html formatter's _open
529                          will do it again, so this just leads to double escaping now).
530        """
531        if do_escape is not None:
532            if do_escape:
533                logging.warning("Deprecation warning: MoinMoin.formatter.text_html.url being called with do_escape=1/True parameter, please review caller.")
534            else:
535                logging.warning("Deprecation warning: MoinMoin.formatter.text_html.url being called with do_escape=0/False parameter, please remove it from the caller.")
536        if on:
537            attrs = self._langAttr()
538
539            # Handle the URL mapping
540            if url is None and 'href' in kw:
541                url = kw['href']
542                del kw['href']
543            if url is not None:
544                url = wikiutil.mapURL(self.request, url)
545                attrs['href'] = url
546
547            if css:
548                attrs['class'] = css
549
550            markup = self._open('a', attr=attrs, **kw)
551        else:
552            markup = self._close('a')
553        return markup
554
555    def anchordef(self, id):
556        """Inserts an invisible element used as a link target.
557
558        Inserts an empty <span> element with an id attribute, used as an anchor
559        for link references.  We use <span></span> rather than <span/>
560        for browser portability.
561        """
562        # Don't add newlines, \n, as it will break pre and
563        # line-numbered code sections (from line_achordef() method).
564        #return '<a id="%s"></a>' % (id, ) # do not use - this breaks PRE sections for IE
565        id = self.make_id_unique(id)
566        id = self.qualify_id(id)
567        return '<span class="anchor" id="%s"></span>' % id
568
569    def line_anchordef(self, lineno):
570        if line_anchors:
571            return self.anchordef("line-%d" % lineno)
572        else:
573            return ''
574
575    def anchorlink(self, on, name='', **kw):
576        """Insert an <a> link pointing to an anchor on the same page.
577
578        Call once with on=1 to start the link, and a second time with
579        on=0 to end it.  No other arguments are needed on the second
580        call.
581
582        The name argument should be the same as the id provided to the
583        anchordef() method, or some other elment.  It should NOT start
584        with '#' as that will be added automatically.
585
586        The id argument, if provided, is instead the id of this link
587        itself and not of the target element the link references.
588        """
589        attrs = self._langAttr()
590        if name:
591            name = self.sanitize_to_id(name)
592            attrs['href'] = '#' + self.qualify_id(name)
593        if 'href' in kw:
594            del kw['href']
595        if on:
596            str = self._open('a', attr=attrs, **kw)
597        else:
598            str = self._close('a')
599        return str
600
601    def line_anchorlink(self, on, lineno=0):
602        if line_anchors:
603            return self.anchorlink(on, name="line-%d" % lineno)
604        else:
605            return ''
606
607    # Attachments ######################################################
608
609    def attachment_link(self, on, url=None, querystr=None, **kw):
610        """ Link to an attachment.
611
612            @param on: 1/True=start link, 0/False=end link
613            @param url: filename.ext or PageName/filename.ext
614        """
615        assert on in (0, 1, False, True) # make sure we get called the new way, not like the 1.5 api was
616        _ = self.request.getText
617        if querystr is None:
618            querystr = {}
619        assert isinstance(querystr, dict) # new in 1.6, only support dicts
620        if 'do' not in querystr:
621            querystr['do'] = 'view'
622        if on:
623            pagename, filename = AttachFile.absoluteName(url, self.page.page_name)
624            #logging.debug("attachment_link: url %s pagename %s filename %s" % (url, pagename, filename))
625            fname = wikiutil.taintfilename(filename)
626            if AttachFile.exists(self.request, pagename, fname):
627                target = AttachFile.getAttachUrl(pagename, fname, self.request, do=querystr['do'])
628                if not 'title' in kw:
629                    kw['title'] = "attachment:%s" % url
630                kw['css'] = 'attachment'
631            else:
632                target = AttachFile.getAttachUrl(pagename, fname, self.request, do='upload_form')
633                kw['title'] = _('Upload new attachment "%(filename)s"') % {'filename': fname}
634                kw['css'] = 'attachment nonexistent'
635            return self.url(on, target, **kw)
636        else:
637            return self.url(on)
638
639    def attachment_image(self, url, **kw):
640        _ = self.request.getText
641        pagename, filename = AttachFile.absoluteName(url, self.page.page_name)
642        fname = wikiutil.taintfilename(filename)
643        exists = AttachFile.exists(self.request, pagename, fname)
644        if exists:
645            kw['css'] = 'attachment'
646            kw['src'] = AttachFile.getAttachUrl(pagename, fname, self.request, addts=1)
647            title = _('Inlined image: %(url)s') % {'url': self.text(url)}
648            if not 'title' in kw:
649                kw['title'] = title
650            # alt is required for images:
651            if not 'alt' in kw:
652                kw['alt'] = kw['title']
653            return self.image(**kw)
654        else:
655            title = _('Upload new attachment "%(filename)s"') % {'filename': fname}
656            img = self.icon('attachimg')
657            css = 'nonexistent'
658            target = AttachFile.getAttachUrl(pagename, fname, self.request, do='upload_form')
659            return self.url(1, target, css=css, title=title) + img + self.url(0)
660
661    def attachment_drawing(self, url, text, **kw):
662        # ToDo try to move this to a better place e.g. __init__
663        try:
664            drawing_action = AttachFile.get_action(self.request, url, do='modify')
665            assert drawing_action is not None
666            attachment_drawing = wikiutil.importPlugin(self.request.cfg, 'action',
667                                              drawing_action, 'attachment_drawing')
668            return attachment_drawing(self, url, text, **kw)
669        except (wikiutil.PluginMissingError, wikiutil.PluginAttributeError, AssertionError):
670            return url
671
672    # Text ##############################################################
673
674    def _text(self, text):
675        text = wikiutil.escape(text)
676        if self._in_code:
677            text = text.replace(' ', self.hardspace)
678        return text
679
680    # Inline ###########################################################
681
682    def strong(self, on, **kw):
683        """Creates an HTML <strong> element.
684
685        Call once with on=1 to start the region, and a second time
686        with on=0 to end it.
687        """
688        tag = 'strong'
689        if on:
690            return self._open(tag, allowed_attrs=[], **kw)
691        return self._close(tag)
692
693    def emphasis(self, on, **kw):
694        """Creates an HTML <em> element.
695
696        Call once with on=1 to start the region, and a second time
697        with on=0 to end it.
698        """
699        tag = 'em'
700        if on:
701            return self._open(tag, allowed_attrs=[], **kw)
702        return self._close(tag)
703
704    def underline(self, on, **kw):
705        """Creates a text span for underlining (css class "u").
706
707        Call once with on=1 to start the region, and a second time
708        with on=0 to end it.
709        """
710        tag = 'span'
711        if on:
712            return self._open(tag, attr={'class': 'u'}, allowed_attrs=[], **kw)
713        return self._close(tag)
714
715    def highlight(self, on, **kw):
716        """Creates a text span for highlighting (css class "highlight").
717
718        Call once with on=1 to start the region, and a second time
719        with on=0 to end it.
720        """
721        tag = 'strong'
722        if on:
723            return self._open(tag, attr={'class': 'highlight'}, allowed_attrs=[], **kw)
724        return self._close(tag)
725
726    def sup(self, on, **kw):
727        """Creates a <sup> element for superscript text.
728
729        Call once with on=1 to start the region, and a second time
730        with on=0 to end it.
731        """
732        tag = 'sup'
733        if on:
734            return self._open(tag, allowed_attrs=[], **kw)
735        return self._close(tag)
736
737    def sub(self, on, **kw):
738        """Creates a <sub> element for subscript text.
739
740        Call once with on=1 to start the region, and a second time
741        with on=0 to end it.
742        """
743        tag = 'sub'
744        if on:
745            return self._open(tag, allowed_attrs=[], **kw)
746        return self._close(tag)
747
748    def strike(self, on, **kw):
749        """Creates a text span for line-through (strikeout) text (css class 'strike').
750
751        Call once with on=1 to start the region, and a second time
752        with on=0 to end it.
753        """
754        # This does not use <strike> because has been deprecated in standard HTML.
755        tag = 'span'
756        if on:
757            return self._open(tag, attr={'class': 'strike'},
758                              allowed_attrs=[], **kw)
759        return self._close(tag)
760
761    def code(self, on, **kw):
762        """Creates a <tt> element for inline code or monospaced text.
763
764        Call once with on=1 to start the region, and a second time
765        with on=0 to end it.
766
767        Any text within this section will have spaces converted to
768        non-break spaces.
769        """
770        tag = 'tt'
771        self._in_code = on
772        if on:
773            return self._open(tag, allowed_attrs=[], **kw)
774        return self._close(tag)
775
776    def small(self, on, **kw):
777        """Creates a <small> element for smaller font.
778
779        Call once with on=1 to start the region, and a second time
780        with on=0 to end it.
781        """
782        tag = 'small'
783        if on:
784            return self._open(tag, allowed_attrs=[], **kw)
785        return self._close(tag)
786
787    def big(self, on, **kw):
788        """Creates a <big> element for larger font.
789
790        Call once with on=1 to start the region, and a second time
791        with on=0 to end it.
792        """
793        tag = 'big'
794        if on:
795            return self._open(tag, allowed_attrs=[], **kw)
796        return self._close(tag)
797
798
799    # Block elements ####################################################
800
801    def preformatted(self, on, **kw):
802        """Creates a preformatted text region, with a <pre> element.
803
804        Call once with on=1 to start the region, and a second time
805        with on=0 to end it.
806        """
807        FormatterBase.preformatted(self, on)
808        tag = 'pre'
809        if on:
810            return self._open(tag, newline=1, **kw)
811        return self._close(tag)
812
813    # Use by code area
814    _toggleLineNumbersScript = """
815<script type="text/javascript">
816function isnumbered(obj) {
817  return obj.childNodes.length && obj.firstChild.childNodes.length && obj.firstChild.firstChild.className == 'LineNumber';
818}
819function nformat(num,chrs,add) {
820  var nlen = Math.max(0,chrs-(''+num).length), res = '';
821  while (nlen>0) { res += ' '; nlen-- }
822  return res+num+add;
823}
824function addnumber(did, nstart, nstep) {
825  var c = document.getElementById(did), l = c.firstChild, n = 1;
826  if (!isnumbered(c)) {
827    if (typeof nstart == 'undefined') nstart = 1;
828    if (typeof nstep  == 'undefined') nstep = 1;
829    var n = nstart;
830    while (l != null) {
831      if (l.tagName == 'SPAN') {
832        var s = document.createElement('SPAN');
833        var a = document.createElement('A');
834        s.className = 'LineNumber';
835        a.appendChild(document.createTextNode(nformat(n,4,'')));
836        a.href = '#' + did + '_' + n;
837        s.appendChild(a);
838        s.appendChild(document.createTextNode(' '));
839        n += nstep;
840        if (l.childNodes.length) {
841          l.insertBefore(s, l.firstChild);
842        }
843        else {
844          l.appendChild(s);
845        }
846      }
847      l = l.nextSibling;
848    }
849  }
850  return false;
851}
852function remnumber(did) {
853  var c = document.getElementById(did), l = c.firstChild;
854  if (isnumbered(c)) {
855    while (l != null) {
856      if (l.tagName == 'SPAN' && l.firstChild.className == 'LineNumber') l.removeChild(l.firstChild);
857      l = l.nextSibling;
858    }
859  }
860  return false;
861}
862function togglenumber(did, nstart, nstep) {
863  var c = document.getElementById(did);
864  if (isnumbered(c)) {
865    remnumber(did);
866  } else {
867    addnumber(did,nstart,nstep);
868  }
869  return false;
870}
871</script>
872"""
873
874    def code_area(self, on, code_id, code_type='code', show=0, start=-1, step=-1, msg=None):
875        """Creates a formatted code region, with line numbering.
876
877        This region is formatted as a <div> with a <pre> inside it.  The
878        code_id argument is assigned to the 'id' of the div element, and
879        must be unique within the document.  The show, start, and step are
880        used for line numbering.
881
882        Note this is not like most formatter methods, it can not take any
883        extra keyword arguments.
884
885        Call once with on=1 to start the region, and a second time
886        with on=0 to end it.
887
888        the msg string is not escaped
889        """
890        _ = self.request.getText
891        res = []
892        if on:
893            code_id = self.sanitize_to_id('CA-%s' % code_id)
894            ci = self.qualify_id(self.make_id_unique(code_id))
895
896            # Open a code area
897            self._in_code_area = 1
898            self._in_code_line = 0
899            # id in here no longer used
900            self._code_area_state = [None, show, start, step, start, ci]
901
902            if msg:
903                attr = {'class': 'codemsg'}
904                res.append(self._open('div', attr={'class': 'codemsg'}))
905                res.append(msg)
906                res.append(self._close('div'))
907
908            # Open the code div - using left to right always!
909            attr = {'class': 'codearea', 'lang': 'en', 'dir': 'ltr'}
910            res.append(self._open('div', attr=attr))
911
912            # Add the script only in the first code area on the page
913            if self._code_area_js == 0 and self._code_area_state[1] >= 0:
914                res.append(self._toggleLineNumbersScript)
915                self._code_area_js = 1
916
917            # Add line number link, but only for JavaScript enabled browsers.
918            if self._code_area_state[1] >= 0:
919                toggleLineNumbersLink = r'''
920<script type="text/javascript">
921document.write('<a href="#" onclick="return togglenumber(\'%s\', %d, %d);" \
922                class="codenumbers">%s<\/a>');
923</script>
924''' % (ci, self._code_area_state[2], self._code_area_state[3],
925       _("Toggle line numbers"))
926                res.append(toggleLineNumbersLink)
927
928            # Open pre - using left to right always!
929            attr = {'id': ci, 'lang': 'en', 'dir': 'ltr'}
930            res.append(self._open('pre', newline=True, attr=attr, is_unique=True))
931        else:
932            # Close code area
933            res = []
934            if self._in_code_line:
935                res.append(self.code_line(0))
936            res.append(self._close('pre'))
937            res.append(self._close('div'))
938
939            # Update state
940            self._in_code_area = 0
941
942        return ''.join(res)
943
944    def code_line(self, on):
945        res = ''
946        if not on or (on and self._in_code_line):
947            res += '</span>\n'
948        if on:
949            res += '<span class="line">'
950            if self._code_area_state[1] > 0:
951                res += ('<span class="LineNumber"><a href="#%(fmt)s">%%(num)4d</a> </span><span class="LineAnchor" id="%(fmt)s"></span>' % {'fmt': self._code_id_format, }) % {
952                    'id': self._code_area_state[5],
953                    'num': self._code_area_state[4],
954                    }
955                self._code_area_state[4] += self._code_area_state[3]
956        self._in_code_line = on != 0
957        return res
958
959    def code_token(self, on, tok_type):
960        return ['<span class="%s">' % tok_type, '</span>'][not on]
961
962    # Paragraphs, Lines, Rules ###########################################
963
964    def _indent_spaces(self):
965        """Returns space(s) for indenting the html source so list nesting is easy to read.
966
967        Note that this mostly works, but because of caching may not always be accurate."""
968        if prettyprint:
969            return self.indentspace * self._indent_level
970        else:
971            return ''
972
973    def _newline(self):
974        """Returns the whitespace for starting a new html source line, properly indented."""
975        if prettyprint:
976            return '\n' + self._indent_spaces()
977        else:
978            return ''
979
980    def linebreak(self, preformatted=1):
981        """Creates a line break in the HTML output.
982
983        If preformatted is true a <br> element is inserted, otherwise
984        the linebreak will only be visible in the HTML source.
985        """
986        if self._in_code_area:
987            preformatted = 1
988        return ['\n', '<br>\n'][not preformatted] + self._indent_spaces()
989
990    def paragraph(self, on, **kw):
991        """Creates a paragraph with a <p> element.
992
993        Call once with on=1 to start the region, and a second time
994        with on=0 to end it.
995        """
996        if self._terse:
997            return ''
998        FormatterBase.paragraph(self, on)
999        tag = 'p'
1000        if on:
1001            tagstr = self._open(tag, **kw)
1002        else:
1003            tagstr = self._close(tag)
1004        return tagstr
1005
1006    def rule(self, size=None, **kw):
1007        """Creates a horizontal rule with an <hr> element.
1008
1009        If size is a number in the range [1..6], the CSS class of the rule
1010        is set to 'hr1' through 'hr6'.  The intent is that the larger the
1011        size number the thicker or bolder the rule will be.
1012        """
1013        if size and 1 <= size <= 6:
1014            # Add hr class: hr1 - hr6
1015            return self._open('hr', newline=1, attr={'class': 'hr%d' % size}, **kw)
1016        return self._open('hr', newline=1, **kw)
1017
1018    # Images / Transclusion ##############################################
1019
1020    def icon(self, type):
1021        return self.request.theme.make_icon(type)
1022
1023    smiley = icon
1024
1025    def image(self, src=None, **kw):
1026        """Creates an inline image with an <img> element.
1027
1028        The src argument must be the URL to the image file.
1029        """
1030        if src:
1031            kw['src'] = src
1032        return self._open('img', **kw)
1033
1034    def transclusion(self, on, **kw):
1035        """Transcludes (includes/embeds) another object."""
1036        if on:
1037            return self._open('object',
1038                              allowed_attrs=['archive', 'classid', 'codebase',
1039                                             'codetype', 'data', 'declare',
1040                                             'height', 'name', 'standby',
1041                                             'type', 'width', ],
1042                              **kw)
1043        else:
1044            return self._close('object')
1045
1046    def transclusion_param(self, **kw):
1047        """Give a parameter to a transcluded object."""
1048        return self._open('param',
1049                          allowed_attrs=['name', 'type', 'value', 'valuetype', ],
1050                          **kw)
1051
1052    # Lists ##############################################################
1053
1054    def number_list(self, on, type=None, start=None, **kw):
1055        """Creates an HTML ordered list, <ol> element.
1056
1057        The 'type' if specified can be any legal numbered
1058        list-style-type, such as 'decimal','lower-roman', etc.
1059
1060        The 'start' argument if specified gives the numeric value of
1061        the first list item (default is 1).
1062
1063        Call once with on=1 to start the list, and a second time
1064        with on=0 to end it.
1065        """
1066        tag = 'ol'
1067        if on:
1068            attr = {}
1069            if type is not None:
1070                attr['type'] = type
1071            if start is not None:
1072                attr['start'] = start
1073            tagstr = self._open(tag, newline=1, attr=attr, **kw)
1074        else:
1075            tagstr = self._close(tag, newline=1)
1076        return tagstr
1077
1078    def bullet_list(self, on, **kw):
1079        """Creates an HTML ordered list, <ul> element.
1080
1081        The 'type' if specified can be any legal unnumbered
1082        list-style-type, such as 'disc','square', etc.
1083
1084        Call once with on=1 to start the list, and a second time
1085        with on=0 to end it.
1086        """
1087        tag = 'ul'
1088        if on:
1089            tagstr = self._open(tag, newline=1, **kw)
1090        else:
1091            tagstr = self._close(tag, newline=1)
1092        return tagstr
1093
1094    def listitem(self, on, **kw):
1095        """Adds a list item, <li> element, to a previously opened
1096        bullet or number list.
1097
1098        Call once with on=1 to start the region, and a second time
1099        with on=0 to end it.
1100        """
1101        tag = 'li'
1102        if on:
1103            tagstr = self._open(tag, newline=1, **kw)
1104        else:
1105            tagstr = self._close(tag, newline=1)
1106        return tagstr
1107
1108    def definition_list(self, on, **kw):
1109        """Creates an HTML definition list, <dl> element.
1110
1111        Call once with on=1 to start the list, and a second time
1112        with on=0 to end it.
1113        """
1114        tag = 'dl'
1115        if on:
1116            tagstr = self._open(tag, newline=1, **kw)
1117        else:
1118            tagstr = self._close(tag, newline=1)
1119        return tagstr
1120
1121    def definition_term(self, on, **kw):
1122        """Adds a new term to a definition list, HTML element <dt>.
1123
1124        Call once with on=1 to start the term, and a second time
1125        with on=0 to end it.
1126        """
1127        tag = 'dt'
1128        if on:
1129            tagstr = self._open(tag, newline=1, **kw)
1130        else:
1131            tagstr = self._close(tag, newline=0)
1132        return tagstr
1133
1134    def definition_desc(self, on, **kw):
1135        """Gives the definition to a definition item, HTML element <dd>.
1136
1137        Call once with on=1 to start the definition, and a second time
1138        with on=0 to end it.
1139        """
1140        tag = 'dd'
1141        if on:
1142            tagstr = self._open(tag, newline=1, **kw)
1143        else:
1144            tagstr = self._close(tag, newline=0)
1145        return tagstr
1146
1147    def heading(self, on, depth, **kw):
1148        # remember depth of first heading, and adapt counting depth accordingly
1149        if not self._base_depth:
1150            self._base_depth = depth
1151
1152        count_depth = max(depth - (self._base_depth - 1), 1)
1153
1154        # check numbering, possibly changing the default
1155        if self._show_section_numbers is None:
1156            self._show_section_numbers = self.cfg.show_section_numbers
1157            numbering = self.request.getPragma('section-numbers', '').lower()
1158            if numbering in ['0', 'off']:
1159                self._show_section_numbers = 0
1160            elif numbering in ['1', 'on']:
1161                self._show_section_numbers = 1
1162            elif numbering in ['2', '3', '4', '5', '6']:
1163                # explicit base level for section number display
1164                self._show_section_numbers = int(numbering)
1165
1166        heading_depth = depth
1167
1168        # closing tag, with empty line after, to make source more readable
1169        if not on:
1170            return self._close('h%d' % heading_depth) + '\n'
1171
1172        # create section number
1173        number = ''
1174        if self._show_section_numbers:
1175            # count headings on all levels
1176            self.request._fmt_hd_counters = self.request._fmt_hd_counters[:count_depth]
1177            while len(self.request._fmt_hd_counters) < count_depth:
1178                self.request._fmt_hd_counters.append(0)
1179            self.request._fmt_hd_counters[-1] = self.request._fmt_hd_counters[-1] + 1
1180            number = '.'.join([str(x) for x in self.request._fmt_hd_counters[self._show_section_numbers-1:]])
1181            if number: number += ". "
1182
1183        # Add space before heading, easier to check source code
1184        result = '\n' + self._open('h%d' % heading_depth, **kw)
1185
1186        if self.request.user.show_topbottom:
1187            result += "%s%s%s%s%s%s" % (
1188                       self.anchorlink(1, "bottom"), self.icon('bottom'), self.anchorlink(0),
1189                       self.anchorlink(1, "top"), self.icon('top'), self.anchorlink(0))
1190
1191        return "%s%s" % (result, number)
1192
1193
1194    # Tables #############################################################
1195
1196    _allowed_table_attrs = {
1197        'table': ['class', 'id', 'style'],
1198        'row': ['class', 'id', 'style'],
1199        '': ['colspan', 'rowspan', 'class', 'id', 'style', 'abbr'],
1200    }
1201
1202    def _checkTableAttr(self, attrs, prefix):
1203        """ Check table attributes
1204
1205        Convert from wikitable attributes to html 4 attributes.
1206
1207        @param attrs: attribute dict
1208        @param prefix: used in wiki table attributes
1209        @rtype: dict
1210        @return: valid table attributes
1211        """
1212        if not attrs:
1213            return {}
1214
1215        result = {}
1216        s = [] # we collect synthesized style in s
1217        for key, val in attrs.items():
1218            # Ignore keys that don't start with prefix
1219            if prefix and key[:len(prefix)] != prefix:
1220                continue
1221            key = key[len(prefix):]
1222            val = val.strip('"')
1223            # remove invalid attrs from dict and synthesize style
1224            if key == 'width':
1225                s.append("width: %s" % val)
1226            elif key == 'height':
1227                s.append("height: %s" % val)
1228            elif key == 'bgcolor':
1229                s.append("background-color: %s" % val)
1230            elif key == 'align':
1231                s.append("text-align: %s" % val)
1232            elif key == 'valign':
1233                s.append("vertical-align: %s" % val)
1234            # Ignore unknown keys
1235            if key not in self._allowed_table_attrs[prefix]:
1236                continue
1237            result[key] = val
1238        st = result.get('style', '').split(';')
1239        st = '; '.join(st + s)
1240        st = st.strip(';')
1241        st = st.strip()
1242        if not st:
1243            try:
1244                del result['style'] # avoid empty style attr
1245            except:
1246                pass
1247        else:
1248            result['style'] = st
1249        #logging.debug("_checkTableAttr returns %r" % result)
1250        return result
1251
1252
1253    def table(self, on, attrs=None, **kw):
1254        """ Create table
1255
1256        @param on: start table
1257        @param attrs: table attributes
1258        @rtype: string
1259        @return start or end tag of a table
1260        """
1261        result = []
1262        if on:
1263            # Open div to get correct alignment with table width smaller
1264            # than 100%
1265            result.append(self._open('div', newline=1))
1266
1267            # Open table
1268            if not attrs:
1269                attrs = {}
1270            else:
1271                attrs = self._checkTableAttr(attrs, 'table')
1272            result.append(self._open('table', newline=1, attr=attrs,
1273                                     allowed_attrs=self._allowed_table_attrs['table'],
1274                                     **kw))
1275            result.append(self._open('tbody', newline=1))
1276        else:
1277            # Close tbody, table, and then div
1278            result.append(self._close('tbody'))
1279            result.append(self._close('table'))
1280            result.append(self._close('div'))
1281
1282        return ''.join(result)
1283
1284    def table_row(self, on, attrs=None, **kw):
1285        tag = 'tr'
1286        if on:
1287            if not attrs:
1288                attrs = {}
1289            else:
1290                attrs = self._checkTableAttr(attrs, 'row')
1291            return self._open(tag, newline=1, attr=attrs,
1292                             allowed_attrs=self._allowed_table_attrs['row'],
1293                             **kw)
1294        return self._close(tag) + '\n'
1295
1296    def table_cell(self, on, attrs=None, **kw):
1297        tag = 'td'
1298        if on:
1299            if not attrs:
1300                attrs = {}
1301            else:
1302                attrs = self._checkTableAttr(attrs, '')
1303            return '  ' + self._open(tag, attr=attrs,
1304                             allowed_attrs=self._allowed_table_attrs[''],
1305                             **kw)
1306        return self._close(tag) + '\n'
1307
1308    def text(self, text, **kw):
1309        txt = FormatterBase.text(self, text, **kw)
1310        if kw:
1311            return self._open('span', **kw) + txt + self._close('span')
1312        return txt
1313
1314    def escapedText(self, text, **kw):
1315        txt = wikiutil.escape(text)
1316        if kw:
1317            return self._open('span', **kw) + txt + self._close('span')
1318        return txt
1319
1320    def rawHTML(self, markup):
1321        return markup
1322
1323    def sysmsg(self, on, **kw):
1324        tag = 'div'
1325        if on:
1326            return self._open(tag, attr={'class': 'message'}, **kw)
1327        return self._close(tag)
1328
1329    def div(self, on, **kw):
1330        css_class = kw.get('css_class')
1331        # the display of comment class divs depends on a user setting:
1332        if css_class and 'comment' in css_class.split():
1333            style = kw.get('style')
1334            display = self.request.user.show_comments and "display:''" or "display:none"
1335            if not style:
1336                style = display
1337            else:
1338                style += "; %s" % display
1339            kw['style'] = style
1340        tag = 'div'
1341        if on:
1342            return self._open(tag, **kw)
1343        return self._close(tag)
1344
1345    def span(self, on, **kw):
1346        css_class = kw.get('css_class')
1347        # the display of comment class spans depends on a user setting:
1348        if css_class and 'comment' in css_class.split():
1349            style = kw.get('style')
1350            display = self.request.user.show_comments and "display:''" or "display:none"
1351            if not style:
1352                style = display
1353            else:
1354                style += "; %s" % display
1355            kw['style'] = style
1356        tag = 'span'
1357        if on:
1358            return self._open(tag, **kw)
1359        return self._close(tag)
1360
1361    def sanitize_to_id(self, text):
1362        return wikiutil.anchor_name_from_text(text)
1363
1364