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('>', '>')
0084 text = html.replace('<', '<')
0085 text = html.replace('"', '"')
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):