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