Latest Version: 0.9.6.2
/Users/bbangert/Programming/Python/FormEncode/formencode/htmlfill.py
0001"""
0002Parser for HTML forms, that fills in defaults and errors.  See
0003``render``.
0004"""
0005
0006import HTMLParser
0007import cgi
0008import re
0009from htmlentitydefs import name2codepoint
0010
0011__all__ = ['render', 'htmlliteral', 'default_formatter',
0012           'none_formatter', 'escape_formatter',
0013           'FillingParser']
0014
0015def render(form, defaults=None, errors=None, use_all_keys=False,
0016           error_formatters=None, add_attributes=None,
0017           auto_insert_errors=True, auto_error_formatter=None,
0018           text_as_default=False, listener=None, encoding=None):
0019    """
0020    Render the ``form`` (which should be a string) given the defaults
0021    and errors.  Defaults are the values that go in the input fields
0022    (overwriting any values that are there) and errors are displayed
0023    inline in the form (and also effect input classes).  Returns the
0024    rendered string.
0025
0026    If ``auto_insert_errors`` is true (the default) then any errors
0027    for which ``<form:error>`` tags can't be found will be put just
0028    above the associated input field, or at the top of the form if no
0029    field can be found.
0030
0031    If ``use_all_keys`` is true, if there are any extra fields from
0032    defaults or errors that couldn't be used in the form it will be an
0033    error.
0034
0035    ``error_formatters`` is a dictionary of formatter names to
0036    one-argument functions that format an error into HTML.  Some
0037    default formatters are provided if you don't provide this.
0038
0039    ``error_class`` is the class added to input fields when there is
0040    an error for that field.
0041
0042    ``add_attributes`` is a dictionary of field names to a dictionary
0043    of attribute name/values.  If the name starts with ``+`` then the
0044    value will be appended to any existing attribute (e.g.,
0045    ``{'+class': ' important'}``).
0046
0047    ``auto_error_formatter`` is used to create the HTML that goes
0048    above the fields.  By default it wraps the error message in a span
0049    and adds a ``<br>``.
0050
0051    If ``text_as_default`` is true (default false) then ``<input
0052    type=unknown>`` will be treated as text inputs.
0053
0054    ``listener`` can be an object that watches fields pass; the only
0055    one currently is in ``htmlfill_schemabuilder.SchemaBuilder``
0056    
0057    ``encoding`` specifies an encoding to assume when mixing str and 
0058    unicode text in the template.
0059    """
0060    if defaults is None:
0061        defaults = {}
0062    if auto_insert_errors and auto_error_formatter is None:
0063        auto_error_formatter = default_formatter
0064    p = FillingParser(
0065        defaults=defaults, errors=errors,
0066        use_all_keys=use_all_keys,
0067        error_formatters=error_formatters,
0068        add_attributes=add_attributes,
0069        auto_error_formatter=auto_error_formatter,
0070        text_as_default=text_as_default,
0071        listener=listener, encoding=encoding,
0072        )
0073    p.feed(form)
0074    p.close()
0075    return p.text()
0076
0077
0078class htmlliteral(object):
0079
0080    def __init__(self, html, text=None):
0081        if text is None:
0082            text = re.sub(r'<.*?>', '', html)
0083            text = html.replace('&gt;', '>')
0084            text = html.replace('&lt;', '<')
0085            text = html.replace('&quot;', '"')
0086            # @@: Not very complete
0087        self.html = html
0088        self.text = text
0089
0090    def __str__(self):
0091        return self.text
0092
0093    def __repr__(self):
0094        return '<%s html=%r text=%r>' % (self.html, self.text)
0095
0096    def __html__(self):
0097        return self.html
0098
0099def html_quote(v):
0100    if v is None:
0101        return ''
0102    elif hasattr(v, '__html__'):
0103        return v.__html__()
0104    elif isinstance(v, basestring):
0105        return cgi.escape(v, 1)
0106    else:
0107        # @@: Should this be unicode(v) or str(v)?
0108        return cgi.escape(str(v), 1)
0109
0110def default_formatter(error):
0111    """
0112    Formatter that escapes the error, wraps the error in a span with
0113    class ``error-message``, and adds a ``<br>``
0114    """
0115    return '<span class="error-message">%s</span><br />\n' % html_quote(error)
0116
0117def none_formatter(error):
0118    """
0119    Formatter that does nothing, no escaping HTML, nothin'
0120    """
0121    return error
0122
0123def escape_formatter(error):
0124    """
0125    Formatter that escapes HTML, no more.
0126    """
0127    return html_quote(error)
0128
0129def escapenl_formatter(error):
0130    """
0131    Formatter that escapes HTML, and translates newlines to ``<br>``
0132    """
0133    error = html_quote(error)
0134    error = error.replace('\n', '<br>\n')
0135    return error
0136
0137class FillingParser(HTMLParser.HTMLParser):
0138    r"""
0139    Fills HTML with default values, as in a form.
0140
0141    Examples::
0142
0143        >>> defaults = {'name': 'Bob Jones',
0144        ...             'occupation': 'Crazy Cultist',
0145        ...             'address': '14 W. Canal\nNew Guinea',
0146        ...             'living': 'no',
0147        ...             'nice_guy': 0}
0148        >>> parser = FillingParser(defaults)
0149        >>> parser.feed('<input type="text" name="name" value="fill">\
0150        ... <select name="occupation"><option value="">Default</option>\
0151        ... <option value="Crazy Cultist">Crazy cultist</option>\
0152        ... </select> <textarea cols=20 style="width: 100%" name="address">An address\
0153        ... </textarea> <input type="radio" name="living" value="yes">\
0154        ... <input type="radio" name="living" value="no">\
0155        ... <input type="checkbox" name="nice_guy" checked="checked">')
0156        >>> print parser.text()
0157        <input type="text" name="name" value="Bob Jones">
0158        <select name="occupation">
0159        <option value="">Default</option>
0160        <option value="Crazy Cultist" selected="selected">Crazy cultist</option>
0161        </select>
0162        <textarea cols=20 style="width: 100%" name="address">14 W. Canal
0163        New Guinea</textarea>
0164        <input type="radio" name="living" value="yes">
0165        <input type="radio" name="living" value="no" selected="selected">
0166        <input type="checkbox" name="nice_guy">
0167    """
0168
0169    def __init__(self, defaults, errors=None, use_all_keys=False,
0170                 error_formatters=None, error_class='error',
0171                 add_attributes=None, listener=None,
0172                 auto_error_formatter=None,
0173                 text_as_default=False, encoding=None):
0174        HTMLParser.HTMLParser.__init__(self)
0175        self._content = []
0176        self.source = None
0177        self.lines = None
0178        self.source_pos = None
0179        self.defaults = defaults
0180        self.in_textarea = None
0181        self.in_select = None
0182        self.skip_next = False
0183        self.errors = errors or {}
0184        if isinstance(self.errors, (str, unicode)):
0185            self.errors = {None: self.errors}
0186        self.in_error = None
0187        self.skip_error = False
0188        self.use_all_keys = use_all_keys
0189        self.used_keys = {}
0190        self.used_errors = {}
0191        if error_formatters is None:
0192            self.error_formatters = default_formatter_dict
0193        else:
0194            self.error_formatters = error_formatters
0195        self.error_class = error_class
0196        self.add_attributes = add_attributes or {}
0197        self.listener = listener
0198        self.auto_error_formatter = auto_error_formatter
0199        self.text_as_default = text_as_default
0200        self.encoding = encoding
0201
0202    def feed(self, data):
0203        self.data_is_str = isinstance(data, str)
0204        self.source = data
0205        self.lines = data.split('\n')
0206        self.source_pos = 1, 0
0207        if self.listener:
0208            self.listener.reset()
0209        HTMLParser.HTMLParser.feed(self, data)
0210
0211    def close(self):
0212        self.handle_misc(None)
0213        HTMLParser.HTMLParser.close(self)
0214        unused_errors = self.errors.copy()
0215        for key in self.used_errors.keys():
0216            if unused_errors.has_key(key):
0217                del unused_errors[key]
0218        if self.auto_error_formatter:
0219            for key, value in unused_errors.items():
0220                error_message = self.auto_error_formatter(value)
0221                error_message = '<!-- for: %s -->\n%s' % (key, error_message)
0222                self.insert_at_marker(
0223                    key, error_message)
0224            unused_errors = {}
0225        if self.use_all_keys:
0226            unused = self.defaults.copy()
0227            for key in self.used_keys.keys():
0228                if unused.has_key(key):
0229                    del unused[key]
0230            assert not unused, (
0231                "These keys from defaults were not used in the form: %s"
0232                % unused.keys())
0233            if unused_errors:
0234                error_text = []
0235                for key in unused_errors.keys():
0236                    error_text.append("%s: %s" % (key, self.errors[key]))
0237                assert False, (
0238                    "These errors were not used in the form: %s" %
0239                    ', '.join(error_text))
0240        if self.encoding is not None:
0241            new_content = []
0242            for item in self._content:
0243                if isinstance(item, str):
0244                    item = item.decode(self.encoding)
0245                new_content.append(item)
0246            self._content = new_content
0247        try:
0248            self._text = ''.join([
0249                t for t in self._content if not isinstance(t, tuple)])
0250        except UnicodeDecodeError, e:
0251            if self.data_is_str:
0252                e.reason += (
0253                    " the form was passed in as an encoded string, but "
0254                    "some data or error messages were unicode strings; "
0255                    "the form should be passed in as a unicode string")
0256            else:
0257                e.reason += (
0258                    " the form was passed in as an unicode string, but "
0259                    "some data or error message was an encoded string; "
0260                    "the data and error messages should be passed in as "
0261                    "unicode strings")
0262            raise
0263
0264    def add_key(self, key):
0265        self.used_keys[key] = 1
0266
0267    _entityref_re = re.compile('&([a-zA-Z][-.a-zA-Z\d]*);')
0268    _charref_re = re.compile('&#(\d+|[xX][a-fA-F\d]+);')
0269
0270    def unescape(self, s):
0271        s = self._entityref_re.sub(self._sub_entityref, s)
0272        s = self._charref_re.sub(self._sub_charref, s)
0273        return s
0274
0275    def _sub_entityref(self, match):
0276        name = match.group(1)
0277        if name not in name2codepoint:
0278            # If we don't recognize it, pass it through as though it
0279            # wasn't an entity ref at all
0280            return match.group(0)
0281        return unichr(name2codepoint[name])
0282
0283    def _sub_charref(self, match):
0284        num = match.group(1)
0285        if num.lower().startswith('x'):
0286            num = int(num[1:], 16)
0287        else:
0288            num = int(num)
0289        return unichr(num)
0290
0291    def handle_starttag(self, tag, attrs, startend=False):
0292        self.write_pos()
0293        if tag == 'input':
0294            self.handle_input(attrs, startend)
0295        elif tag == 'textarea':
0296            self.handle_textarea(attrs)
0297        elif tag == 'select':
0298            self.handle_select(attrs)
0299        elif tag == 'option':
0300            self.handle_option(attrs)
0301            return
0302        elif tag == 'form:error':
0303            self.handle_error(attrs)
0304            return
0305        elif tag == 'form:iferror':
0306            self.handle_iferror(attrs)
0307            return
0308        else:
0309            return
0310        if self.listener:
0311            self.listener.listen_input(self, tag, attrs)
0312
0313    def handle_misc(self, whatever):
0314        self.write_pos()
0315    handle_charref = handle_misc
0316    handle_entityref = handle_misc
0317    handle_data = handle_misc
0318    handle_comment = handle_misc
0319    handle_decl = handle_misc
0320    handle_pi = handle_misc
0321    unknown_decl = handle_misc
0322
0323    def handle_endtag(self, tag):
0324        self.write_pos()
0325        if tag == 'textarea':
0326            self.handle_end_textarea()
0327        elif tag == 'select':
0328            self.handle_end_select()
0329        elif tag == 'form:iferror':
0330            self.handle_end_iferror()
0331
0332    def handle_startendtag(self, tag, attrs):
0333        return self.handle_starttag(tag, attrs, True)
0334
0335    def handle_iferror(self, attrs):
0336        name = self.get_attr(attrs, 'name')
0337        notted = False
0338        if name.startswith('not '):
0339            notted = True
0340            name = name.split(None, 1)[1]
0341        assert name, "Name attribute in <iferror> required (%s)" % self.getpos()
0342        self.in_error = name
0343        ok = self.errors.get(name)
0344        if notted:
0345            ok = not ok
0346        if not ok:
0347            self.skip_error = True
0348        self.skip_next = True
0349
0350    def handle_end_iferror(self):
0351        self.in_error = None
0352        self.skip_error = False
0353        self.skip_next = True
0354
0355    def handle_error(self, attrs):
0356        name = self.get_attr(attrs, 'name')
0357        formatter = self.get_attr(attrs, 'format') or 'default'
0358        if name is None:
0359            name = self.in_error
0360        assert name is not None, (
0361            "Name attribute in <form:error> required if not contained in "
0362            "<form:iferror> (%i:%i)" % self.getpos())
0363        error = self.errors.get(name, '')
0364        if error:
0365            error = self.error_formatters[formatter](error)
0366            self.write_text(error)
0367        self.skip_next = True
0368        self.used_errors[name] = 1
0369
0370    def handle_input(self, attrs, startend):
0371        t = (self.get_attr(attrs, 'type') or 'text').lower()
0372        name = self.get_attr(attrs, 'name')
0373        self.write_marker(name)
0374        value = self.defaults.get(name)
0375        if self.add_attributes.has_key(name):
0376            for attr_name, attr_value in self.add_attributes[name].items():
0377                if attr_name.startswith('+'):
0378                    attr_name = attr_name[1:]
0379                    self.set_attr(attrs, attr_name,
0380                                  self.get_attr(attrs, attr_name, '')
0381                                  + attr_value)
0382                else:
0383                    self.set_attr(attrs, attr_name, attr_value)
0384        if (self.error_class
0385            and self.errors.get(self.get_attr(attrs, 'name'))):
0386            self.add_class(attrs, self.error_class)
0387        if t in ('text', 'hidden'):
0388            if value is None:
0389                value = self.get_attr(attrs, 'value', '')
0390            self.set_attr(attrs, 'value', value)
0391            self.write_tag('input', attrs, startend)
0392            self.skip_next = True
0393            self.add_key(name)
0394        elif t == 'checkbox':
0395            selected = False
0396            if not self.get_attr(attrs, 'value'):
0397                selected = value
0398            elif self.selected_multiple(value, self.get_attr(attrs, 'value')):
0399                selected = True
0400            if selected:
0401                self.set_attr(attrs, 'checked', 'checked')
0402            else:
0403                self.del_attr(attrs, 'checked')
0404            self.write_tag('input', attrs, startend)
0405            self.skip_next = True
0406            self.add_key(name)
0407        elif t == 'radio':
0408            if str(value) == self.get_attr(attrs, 'value'):
0409                self.set_attr(attrs, 'checked', 'checked')
0410            else:
0411                self.del_attr(attrs, 'checked')
0412            self.write_tag('input', attrs, startend)
0413            self.skip_next = True
0414            self.add_key(name)
0415        elif t == 'file':
0416            pass # don't skip next
0417        elif t == 'password':
0418            self.set_attr(attrs, 'value', value or
0419                          self.get_attr(attrs, 'value', ''))
0420            self.write_tag('input', attrs, startend)
0421            self.skip_next = True
0422            self.add_key(name)
0423        elif t == 'image':
0424            self.set_attr(attrs, 'src', value or
0425                          self.get_attr(attrs, 'src', ''))
0426            self.write_tag('input', attrs, startend)
0427            self.skip_next = True
0428            self.add_key(name)
0429        elif t == 'submit' or t == 'reset' or t == 'button':
0430            self.set_attr(attrs, 'value', value or
0431                          self.get_attr(attrs, 'value', ''))
0432            self.write_tag('input', attrs, startend)
0433            self.skip_next = True
0434            self.add_key(name)
0435        elif self.text_as_default:
0436            if value is None:
0437                value = self.get_attr(attrs, 'value', '')
0438            self.set_attr(attrs, 'value', value)
0439            self.write_tag('input', attrs, startend)
0440            self.skip_next = True
0441            self.add_key(name)
0442        else:
0443            assert 0, "I don't know about this kind of <input>: %s (pos: %s)"                      % (t, self.getpos())
0445
0446    def handle_textarea(self, attrs):