--- MoinMoin/formatter/text_html.py.orig	2005-12-04 07:37:54.000000000 -0500
+++ MoinMoin/formatter/text_html.py	2006-01-12 14:47:02.000000000 -0500
@@ -67,35 +67,144 @@
                 # lang is inherited from content div
                 return {}
 
-        attr = {'lang': lang, 'dir': i18n.getDirection(lang),}
+        attr = {'xml:lang': lang, 'lang': lang, 'dir': i18n.getDirection(lang),}
         return attr
 
-    def formatAttributes(self, attr=None):
-        """ Return formatted attributes string
+    # These are all the (X)HTML attributes that can be on any element,
+    # excluding lang, xml:lang, and class -- because they are handled
+    # as special cases.
+    _common_attrs = ['accesskey','dir','disabled','id','style','tabindex','title','xmlns']
+
+    def formatAttributes(self, attr=None, allowed_attrs=None, **kw):
+        """ Return HTML attributes formatted as a single string.
 
         @param attr: dict containing keys and values
-        @rtype: string ?
+        @param allowed_attrs: A list of allowable attribute names
+        @param **kw: other arbitrary attributes expressed as keyword arguments.
+        @rtype: string
         @return: formated attributes or empty string
+
+        If a list of keyword arguments is provided, you should use
+        '__' (double underscore) in place of a ':' (such as
+        "xml__lang" in place of "xml:lang".  Also use "content_type"
+        instead of "type".
+
+        You can optionally use "html__" as a prefix to any standard
+        HTML attribute which you want ignored with other non-HTML
+        formatters, or to avoid Python keyword (like "for").
+        
+        As special cases use "content_type" for "type",
+        "http_equiv" for "http-equiv", and "z_index" for "z-index".
+
+        Setting a keyword to None rather than a string (or string
+        coercible) value will remove that attribute from the list.
+        
+        If the list of allowed_attrs is provided, then an error is
+        raised if an attribute is encountered that is not in that list
+        (or is not a common attribute which is always allowed or is
+        not in another XML namespace using the double-underscore
+        syntax).
         """
-        if attr:
-            attr = [' %s="%s"' % (k, v) for k, v in attr.items()]           
+        if attr is None:
+            attr = {}
+        # Handle special cases in passed-in attribute dict
+        for old, new in [('content_type','type'), ('http_equiv','http-equiv'),
+                         ('z_index','z-index')]:
+            if attr.has_key(old):
+                attr[new] = attr[old]
+                del attr[old]
+
+        if attr.has_key('css_class'):
+            attr['class'] = attr['css_class']
+            del attr['css_class']
+
+        # Parse the keyword argument list
+        for kwname, val in kw.items():
+            kwname.replace('__',':')
+            if kwname[:5]=='html:':
+                kwname = kwname[5:] # Strip off "html:" prefix
+
+            if kwname[:2]=='on' and len(kwname)>2:
+                # Event handlers such as onclick, allow all of them,
+                # and convert to lowercase as the standard says.
+                kwname = kwname.lower()
+            elif kwname=='content_type':
+                # Special case because 'type' is used for other purposes
+                kwname = 'type'
+            elif kwname=='http_equiv': # Special case because of hyphen
+                kwname = 'http-equiv'
+            elif kwname=='z_index': # Special case because of hyphen
+                kwname = 'z-index'
+            elif kwname in ('class','css','css_class'):
+                # Append all css classes together rather than replace.
+                # Accept "css" and "css_class" for backwards compatibility in this wiki.
+                if attr.has_key('class'):
+                    attr['class'] = '%s %s' % (attr['class'], val)
+                else:
+                    attr['class'] = val
+                continue
+            elif kwname=='style':
+                # Append styles together rather than replace
+                if attr.has_key('style'):
+                    attr['style'] = '%s; %s' % (attr['style'],val)
+                else:
+                    attr['style'] = val
+                continue
+            elif kwname=='lang' or kwname=='xml:lang':
+                # Make lang and xml:lang always the same and both present
+                attr['lang'] = val
+                attr['xml:lang'] = val
+                continue
+            elif kwname in self._common_attrs:
+                pass
+            elif ':' in kwname:
+                # Allow all xml namespace-qualified attributes
+                pass
+            elif not allowed_attrs or kwname in allowed_attrs:
+                pass
+            else:
+                # Unknown attribute
+                e='Illegal HTML attribute "%s" passed to formatter' % kwname
+                raise ValueError(e)
+
+            if val is None:
+                del attr[kwname]
+            else:
+                attr[kwname] = val
+
+        if attr.has_key('alt') and not attr.has_key('title'):
+            # Force title attribute if also has an alt
+            attr['title'] = attr['alt']
+
+        if attr.has_key('lang') and not attr.has_key('xml:lang'):
+            attr['xml:lang'] = attr['lang']
+
+        if len(attr) > 0:
+            attr = [' %s="%s"' % (k, wikiutil.escape(v,1))
+                    for k, v in attr.items() if v]
             return ''.join(attr)
         return ''
 
     # TODO: use set when we require Python 2.3
     # TODO: The list is not complete, add missing from dtd
-    _blocks = 'p div pre table tr td ol ul dl li dt dd h1 h2 h3 h4 h5 h6 hr form'
+    _blocks = 'p div pre table tbody thead tfoot tr th td ol ul dl li dt dd h1 h2 h3 h4 h5 h6 hr form'
     _blocks = dict(zip(_blocks.split(), [1] * len(_blocks)))
+    _self_closing_tags = ['base','br','frame','hr','img','input','isindex','link','meta','param']
 
-    def open(self, tag, newline=False, attr=None):
+    def open(self, tag, newline=False, attr=None, allowed_attrs=None, **kw):
         """ Open a tag with optional attributes
         
         @param tag: html tag, string
-        @param newline: render tag on a separate line
-        @parm attr: dict with tag attributes
+        @param newline: render tag so following data is on a separate line
+        @param attr: dict with tag attributes
+        @param allowed_attrs: list of allowed attributes for this element
+        @param kw: arbitrary attributes and values
         @rtype: string ?
-        @return: open tag with attributes
+        @return: open tag with attributes as a string
         """
+        self_close=''
+        if tag in self._self_closing_tags:
+            self_close=' /'
         if tag in self._blocks:
             # Block elements
             result = []
@@ -106,25 +215,33 @@
                 attributes.update(attr)
             
             # Format
-            attributes = self.formatAttributes(attributes)
-            result.append('<%s%s>' % (tag, attributes))
+            attributes = self.formatAttributes(attributes, allowed_attrs=allowed_attrs, **kw)
+            result.append('<%s%s%s>' % (tag, attributes, self_close))
             if newline:
                 result.append('\n')
             return ''.join(result)
         else:
             # Inline elements
             # Add to inlineStack
-            self._inlineStack.append(tag)
+            if not self_close:
+                # Only push on stack if we expect a close-tag later
+                self._inlineStack.append(tag)
             # Format
-            return '<%s%s>' % (tag, self.formatAttributes(attr))
-       
+            return '<%s%s%s>' % (tag,
+                                 self.formatAttributes(attr, allowed_attrs, **kw),
+                                 self_close)
+
     def close(self, tag, newline=False):
         """ Close tag
 
         @param tag: html tag, string
-        @rtype: string ?
-        @return: closing tag
+        @param newline: render tag so following data is on a separate line
+        @rtype: string
+        @return: closing tag as a string
         """
+        if tag in self._self_closing_tags:
+            # This tag was already closed
+            return ''
         if tag in self._blocks:
             # Block elements
             # Close all tags in inline stack
@@ -137,7 +254,9 @@
             # Format with newline
             if newline:
                 result.append('\n')
-            result.append('</%s>\n' % (tag))
+            result.append('</%s>' % (tag))
+            if newline:
+                result.append('\n')
             return ''.join(result)            
         else:
             # Inline elements 
@@ -150,7 +269,7 @@
 
     # Public methods ###################################################
 
-    def startContent(self, content_id='content', **kwargs):
+    def startContent(self, content_id='content', newline=True, **kw):
         """ Start page content div """
 
         # Setup id
@@ -163,11 +282,12 @@
         # Use the content language
         attr = self.langAttr(self.request.content_lang)
         attr['id'] = content_id
-        result.append(self.open('div', newline=1, attr=attr))
+        result.append(self.open('div', newline=newline, attr=attr,
+                                allowed_attrs=['align'], **kw))
         result.append(self.anchordef(aid))
         return ''.join(result)
         
-    def endContent(self):
+    def endContent(self,newline=True):
         """ Close page content div """
 
         # Setup id
@@ -182,7 +302,7 @@
 
         result = []
         result.append(self.anchordef(aid))
-        result.append(self.close('div', newline=1))
+        result.append(self.close('div', newline=newline))
         return ''.join(result) 
 
     def lang(self, on, lang_name):
@@ -205,7 +325,7 @@
     def sysmsg(self, on, **kw):
         tag = 'div'
         if on:
-            return self.open(tag, attr={'class': 'message'})
+            return self.open(tag, attr={'class': 'message'}, **kw)
         return self.close(tag)
     
     # Links ##############################################################
@@ -259,41 +379,95 @@
             # unescaped=1 was changed to 0 to make interwiki links with pages with umlauts (or other non-ascii) work
 
     def url(self, on, url=None, css=None, **kw):
-        """
+        """ Inserts an <a> element.
+
+            Call once with on=1 to start the link, and again with on=0
+            to end it (no other arguments are needed when on==0).
+
             Keyword params:
-                title - <a> title attribute
-                attrs -  just include those <a> attrs "as is"
+                url - the URL to link to; will go through Wiki URL mapping.
+                type - icon type: one of "www" or "mailto" to use that icon
+                css - a space-separated list of CSS classes
+                attrs -  just include this string verbatim inside
+                         the <a> element; can be used for arbitrary attrs
         """
+        if not on:
+            return self.close('a')
+        attrs = self.langAttr()
+
+        # Handle the URL mapping
+        if url is None and kw.has_key('href'):
+            url = kw['href']
+            del kw['href']
         if url is not None:
             url = wikiutil.mapURL(self.request, url)
-        title = kw.get('title', None)
-        attrs = kw.get('attrs', None)
+            attrs['href'] = url
+
+        if css:
+            attrs['class'] = css
+        
+        if kw.has_key('type'): # Icon type
+            icon_type = kw['type']
+            del kw['type']
+        else:
+            icon_type = None
+
+        if kw.has_key('attrs'):
+            # for backwards compatibility, raw pre-formated attribute string
+            extra_attrs = kw['attrs']
+            del kw['attrs']
+        else:
+            extra_attrs = None
+
+        # create link
         if on:
-            str = '<a'
-            if css: 
-                str = '%s class="%s"' % (str, css)
-            if title:
-                str = '%s title="%s"' % (str, title)
-            if attrs:
-                str = '%s %s' % (str, attrs)
-            str = '%s href="%s">' % (str, wikiutil.escape(url, 1))
+            str = self.open('a', attr=attrs, **kw)
+            if extra_attrs:
+                # insert this into the tag (messy)
+                if str[-2:]=='/>':
+                    str = '%s %s />' % (str[:-2], extra_attrs)
+                else:
+                    str = '%s %s>' % (str[:-1], extra_attrs)
         else:
-            str = '</a>'
+            str = self.close('a')
+
+        if icon_type=='www':
+             str = '%s%s ' % (str, self.icon("www"))
+        elif icon_type=='mailto':
+             str = '%s%s ' % (str, self.icon('mailto'))
         return str
 
     def anchordef(self, id):
-        #return '<a id="%s"></a>' % (id, ) # this breaks PRE sections for IE
-        # do not add a \n here, it breaks pre sections with line_anchordef
-        return '<span id="%s" class="anchor"></span>' % (id, )
+        """Inserts an element with an id attribute, used as an anchor
+        for link references.
+        """
+        return '<span id="%s" class="anchor"></span>' % wikiutil.escape(id, 1)
 
     def line_anchordef(self, lineno):
         return self.anchordef("line-%d" % lineno)
 
-    def anchorlink(self, on, name='', id=None):
-        extra = ''
-        if id:
-            extra = ' id="%s"' % id
-        return ['<a href="#%s"%s>' % (name, extra), '</a>'][not on]
+    def anchorlink(self, on, name='', **kw):
+        """Inserts an <a> link pointing to an anchor within the same
+        document.  Call once with on=1 to start the link, and a
+        second time with on=0 to end it.  No other arguments are
+        needed on the second call.
+
+        The name argument should be the same as the id provided to the
+        anchordef() method, or some other elment.  The id argument, if
+        provided, is instead the id of this link itself and not of the
+        element the link references.
+        """
+
+        attrs = self.langAttr()
+        if name:
+            attrs['href'] = '#%s' % name
+        if kw.has_key('href'):
+            del kw['href']
+        if on:
+            str = self.open('a', attr=attrs, **kw)
+        else:
+            str = self.close('a')
+        return str
 
     def line_anchorlink(self, on, lineno=0):
         return self.anchorlink(on, name="line-%d" % lineno)
@@ -415,46 +589,55 @@
 
     # Inline ###########################################################
         
-    def strong(self, on):
+    def strong(self, on, **kw):
+        """Creates an HTML <strong> element.
+
+           Call once with on=1 to start the region, and a second time
+           with on=0 to end it.
+
+           Any other keyword argument represents other standard HTML
+           attributes of the <strong> element.  Use an "xml_" prefix
+           for the keyword in place of "xml:" (such as xml_lang="en").
+        """
         tag = 'strong'
         if on:
-            return self.open(tag)
+            return self.open(tag, allowed_attrs=[], **kw)
         return self.close(tag)
 
-    def emphasis(self, on):
+    def emphasis(self, on, **kw):
         tag = 'em'
         if on:
-            return self.open(tag)
+            return self.open(tag, allowed_attrs=[], **kw)
         return self.close(tag)
 
-    def underline(self, on):
+    def underline(self, on, **kw):
         tag = 'span'
         if on:
-            return self.open(tag, attr={'class': 'u'})
+            return self.open(tag, attr={'class': 'u'}, allowed_attrs=[], **kw)
         return self.close(tag)
 
-    def highlight(self, on):
+    def highlight(self, on, **kw):
         tag = 'strong'
         if on:
-            return self.open(tag, attr={'class': 'highlight'})
+            return self.open(tag, attr={'class': 'highlight'}, allowed_attrs=[], **kw)
         return self.close(tag)
 
-    def sup(self, on):
+    def sup(self, on, **kw):
         tag = 'sup'
         if on:
-            return self.open(tag)
+            return self.open(tag, allowed_attrs=[], **kw)
         return self.close(tag)
 
-    def sub(self, on):
+    def sub(self, on, **kw):
         tag = 'sub'
         if on:
-            return self.open(tag)
+            return self.open(tag, allowed_attrs=[], **kw)
         return self.close(tag)
 
-    def strike(self, on):
+    def strike(self, on, **kw):
         tag = 'strike'
         if on:
-            return self.open(tag)
+            return self.open(tag, allowed_attrs=[], **kw)
         return self.close(tag)
 
     def code(self, on, **kw):
@@ -462,34 +645,34 @@
         # Maybe we don't need this, because we have tt will be in inlineStack.
         self._in_code = on        
         if on:
-            return self.open(tag)
+            return self.open(tag, allowed_attrs=[], **kw)
         return self.close(tag)
         
-    def small(self, on):
+    def small(self, on, **kw):
         tag = 'small'
         if on:
-            return self.open(tag)
+            return self.open(tag, allowed_attrs=[], **kw)
         return self.close(tag)
 
-    def big(self, on):
+    def big(self, on, **kw):
         tag = 'big'
         if on:
-            return self.open(tag)
+            return self.open(tag, allowed_attrs=[], **kw)
         return self.close(tag)
 
 
     # Block elements ####################################################
 
-    def preformatted(self, on, attr=None):
+    def preformatted(self, on, **kw):
         FormatterBase.preformatted(self, on)
         tag = 'pre'
         if on:
-            return self.open(tag, newline=1, attr=attr)
+            return self.open(tag, newline=1, **kw)
         return self.close(tag)
                 
     # Use by code area
     _toggleLineNumbersScript = """
-<script type="text/JavaScript">
+<script type="text/javascript">
 function isnumbered(obj) {
   return obj.childNodes.length && obj.firstChild.childNodes.length && obj.firstChild.firstChild.className == 'LineNumber';
 }
@@ -562,7 +745,7 @@
             if self._code_area_state[1] >= 0:
                 toggleLineNumbersLink = r'''
 <script type="text/javascript">
-document.write('<a href="#" onClick="return togglenumber(\'%s\', %d, %d);" \
+document.write('<a href="#" onclick="return togglenumber(\'%s\', %d, %d);" \
                 class="codenumbers">Toggle line numbers<\/a>');
 </script>
 ''' % (self._code_area_state[0], self._code_area_state[2], self._code_area_state[3])
@@ -605,9 +788,9 @@
     def linebreak(self, preformatted=1):
         if self._in_code_area:
             preformatted = 1
-        return ['\n', '<br>\n'][not preformatted]
+        return ['\n', '<br />\n'][not preformatted]
         
-    def paragraph(self, on):
+    def paragraph(self, on, **kw):
         if self._terse:
             return ''
         FormatterBase.paragraph(self, on)
@@ -615,14 +798,14 @@
             self._in_li = self._in_li + 1
         tag = 'p'
         if on:
-            return self.open(tag)
-        return self.close(tag)
+            return self.open(tag,**kw)
+        return self.close(tag) + '\n'
             
-    def rule(self, size=None):
+    def rule(self, size=None, **kw):
         if size:
             # Add hr class: hr1 - hr6
-            return self.open('hr', newline=1, attr={'class': 'hr%d' % size})
-        return self.open('hr', newline=1)
+            return self.open('hr', newline=1, attr={'class': 'hr%d' % size}, **kw)
+        return self.open('hr', newline=1, **kw)
                 
     def icon(self, type):
         return self.request.theme.make_icon(type)
@@ -634,9 +817,20 @@
             href = self.request.theme.img_url(img)
         return self.image(src=href, alt=text, width=str(w), height=str(h))
 
+    def image(self, src=None, **kw):
+        if src:
+            kw['src']=src
+        return self.open('img',**kw)
+
     # Lists ##############################################################
 
-    def number_list(self, on, type=None, start=None):
+    _list_stack=[]
+    def _indent_list(self):
+        if not self._list_stack:
+            return ''
+        return '\n' + '  '*len(self._list_stack)
+
+    def number_list(self, on, type=None, start=None, **kw):
         tag = 'ol'
         if on:
             attr = {}
@@ -644,49 +838,52 @@
                 attr['type'] = type
             if start is not None:
                 attr['start'] = start
-            return self.open(tag, newline=1, attr=attr)
-        return self.close(tag)
+            tagstr = self.open(tag, newline=0, attr=attr, **kw)
+            self._list_stack.append(tag)
+            return self._indent_list() + tagstr + '\n'
+        else:
+            self._list_stack.pop()
+            tagstr = self.close(tag)
+            return self._indent_list() + tagstr
     
-    def bullet_list(self, on):
+    def bullet_list(self, on, **kw):
         tag = 'ul'
         if on:
-            return self.open(tag, newline=1)
-        return self.close(tag)
+            tagstr = self.open(tag, newline=0, **kw)
+            self._list_stack.append(tag)
+            return self._indent_list() + tagstr + '\n'
+        else:
+            self._list_stack.pop()
+            tagstr = self.close(tag)
+            return self._indent_list() + tagstr
            
     def listitem(self, on, **kw):
         """ List item inherit its lang from the list. """
         tag = 'li'
         self._in_li = on != 0
         if on:
-            attr = {}
-            css_class = kw.get('css_class', None)
-            if css_class:
-                attr['class'] = css_class
-            style = kw.get('style', None)
-            if style:
-                attr['style'] = style
-            return self.open(tag, attr=attr)
-        return self.close(tag)
+            return '  ' + self.open(tag, **kw)
+        return self.close(tag) + '\n'
 
-    def definition_list(self, on):
+    def definition_list(self, on, **kw):
         tag = 'dl'
         if on:
-            return self.open(tag, newline=1)
+            return self.open(tag, newline=1, **kw)
         return self.close(tag)
 
-    def definition_term(self, on):
+    def definition_term(self, on, **kw):
         tag = 'dt'
         if on:
-            return self.open(tag)
-        return self.close(tag)
+            return self.open(tag, **kw)
+        return self.close(tag) + '\n'
         
-    def definition_desc(self, on):
+    def definition_desc(self, on, **kw):
         tag = 'dd'
         if on:
-            return self.open(tag)
-        return self.close(tag)
+            return '  ' + self.open(tag, **kw)
+        return self.close(tag) + '\n'
 
-    def heading(self, on, depth, id = None, **kw):
+    def heading(self, on, depth, **kw):
         # remember depth of first heading, and adapt counting depth accordingly
         if not self._base_depth:
             self._base_depth = depth
@@ -722,11 +919,8 @@
             number = '.'.join(map(str, self.request._fmt_hd_counters[self._show_section_numbers-1:]))
             if number: number += ". "
 
-        attr = {}
-        if id:
-            attr['id'] = id
         # Add space before heading, easier to check source code
-        result = '\n' + self.open('h%d' % heading_depth, attr=attr)
+        result = '\n' + self.open('h%d' % heading_depth, **kw)
 
         # TODO: convert this to readable code
         if self.request.user.show_topbottom:
@@ -795,7 +989,7 @@
         return result
 
 
-    def table(self, on, attrs=None):
+    def table(self, on, attrs=None, **kw):
         """ Create table
 
         @param on: start table
@@ -814,36 +1008,53 @@
                 attrs = {}
             else:
                 attrs = self._checkTableAttr(attrs, 'table')
-            result.append(self.open('table', newline=1, attr=attrs))
+            result.append(self.open('table', newline=1, attr=attrs,
+                                    allowed_attrs=self._allowed_table_attrs['table'],
+                                    **kw))
+            result.append(self.open('tbody', newline=1))
         else:
-            # Close table then div
+            # Close tbody, table, and then div
+            result.append(self.close('tbody'))
             result.append(self.close('table'))
             result.append(self.close('div'))
 
         return ''.join(result)    
     
-    def table_row(self, on, attrs=None):
+    def table_row(self, on, attrs=None, **kw):
         tag = 'tr'
         if on:
             if not attrs:
                 attrs = {}
             else:
                 attrs = self._checkTableAttr(attrs, 'row')
-            return self.open(tag, newline=1, attr=attrs)
-        return self.close(tag)
+            return self.open(tag, newline=1, attr=attrs,
+                             allowed_attrs=self._allowed_table_attrs['row'],
+                             **kw)
+        return self.close(tag) + '\n'
     
-    def table_cell(self, on, attrs=None):
+    def table_cell(self, on, attrs=None, **kw):
         tag = 'td'
         if on:
             if not attrs:
                 attrs = {}
             else:
                 attrs = self._checkTableAttr(attrs, '')
-            return self.open(tag, newline=1, attr=attrs)
-        return self.close(tag)
-
-    def escapedText(self, text):
-        return wikiutil.escape(text)
+            return '  ' + self.open(tag, attr=attrs,
+                             allowed_attrs=self._allowed_table_attrs[''],
+                             **kw)
+        return self.close(tag) + '\n'
+
+    def text(self, text, **kw):
+        txt = FormatterBase.text( self, text, **kw )
+        if len(kw)>0:
+            return self.open('span',**kw) + txt + self.close('span')
+        return txt
+
+    def escapedText(self, text, **kw):
+        txt = wikiutil.escape(text)
+        if len(kw)>0:
+            return self.open('span',**kw) + txt + self.close('span')
+        return txt
 
     def rawHTML(self, markup):
         return markup
