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('>', '>')
0086 text = html.replace('<', '<')
0087 text = html.replace('"', '"')
0088
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
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
0281
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
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