| 1 |
"""Default variable filters.""" |
|---|
| 2 |
|
|---|
| 3 |
import re |
|---|
| 4 |
|
|---|
| 5 |
try: |
|---|
| 6 |
from decimal import Decimal, InvalidOperation, ROUND_HALF_UP |
|---|
| 7 |
except ImportError: |
|---|
| 8 |
from django.utils._decimal import Decimal, InvalidOperation, ROUND_HALF_UP |
|---|
| 9 |
|
|---|
| 10 |
import random as random_module |
|---|
| 11 |
try: |
|---|
| 12 |
from functools import wraps |
|---|
| 13 |
except ImportError: |
|---|
| 14 |
from django.utils.functional import wraps # Python 2.3, 2.4 fallback. |
|---|
| 15 |
|
|---|
| 16 |
from django.template import Variable, Library |
|---|
| 17 |
from django.conf import settings |
|---|
| 18 |
from django.utils.translation import ugettext, ungettext |
|---|
| 19 |
from django.utils.encoding import force_unicode, iri_to_uri |
|---|
| 20 |
from django.utils.safestring import mark_safe, SafeData |
|---|
| 21 |
|
|---|
| 22 |
register = Library() |
|---|
| 23 |
|
|---|
| 24 |
####################### |
|---|
| 25 |
# STRING DECORATOR # |
|---|
| 26 |
####################### |
|---|
| 27 |
|
|---|
| 28 |
def stringfilter(func): |
|---|
| 29 |
""" |
|---|
| 30 |
Decorator for filters which should only receive unicode objects. The object |
|---|
| 31 |
passed as the first positional argument will be converted to a unicode |
|---|
| 32 |
object. |
|---|
| 33 |
""" |
|---|
| 34 |
def _dec(*args, **kwargs): |
|---|
| 35 |
if args: |
|---|
| 36 |
args = list(args) |
|---|
| 37 |
args[0] = force_unicode(args[0]) |
|---|
| 38 |
if isinstance(args[0], SafeData) and getattr(func, 'is_safe', False): |
|---|
| 39 |
return mark_safe(func(*args, **kwargs)) |
|---|
| 40 |
return func(*args, **kwargs) |
|---|
| 41 |
|
|---|
| 42 |
# Include a reference to the real function (used to check original |
|---|
| 43 |
# arguments by the template parser). |
|---|
| 44 |
_dec._decorated_function = getattr(func, '_decorated_function', func) |
|---|
| 45 |
for attr in ('is_safe', 'needs_autoescape'): |
|---|
| 46 |
if hasattr(func, attr): |
|---|
| 47 |
setattr(_dec, attr, getattr(func, attr)) |
|---|
| 48 |
return wraps(func)(_dec) |
|---|
| 49 |
|
|---|
| 50 |
################### |
|---|
| 51 |
# STRINGS # |
|---|
| 52 |
################### |
|---|
| 53 |
|
|---|
| 54 |
def addslashes(value): |
|---|
| 55 |
""" |
|---|
| 56 |
Adds slashes before quotes. Useful for escaping strings in CSV, for |
|---|
| 57 |
example. Less useful for escaping JavaScript; use the ``escapejs`` |
|---|
| 58 |
filter instead. |
|---|
| 59 |
""" |
|---|
| 60 |
return value.replace('\\', '\\\\').replace('"', '\\"').replace("'", "\\'") |
|---|
| 61 |
addslashes.is_safe = True |
|---|
| 62 |
addslashes = stringfilter(addslashes) |
|---|
| 63 |
|
|---|
| 64 |
def capfirst(value): |
|---|
| 65 |
"""Capitalizes the first character of the value.""" |
|---|
| 66 |
return value and value[0].upper() + value[1:] |
|---|
| 67 |
capfirst.is_safe=True |
|---|
| 68 |
capfirst = stringfilter(capfirst) |
|---|
| 69 |
|
|---|
| 70 |
_base_js_escapes = ( |
|---|
| 71 |
('\\', r'\x5C'), |
|---|
| 72 |
('\'', r'\x27'), |
|---|
| 73 |
('"', r'\x22'), |
|---|
| 74 |
('>', r'\x3E'), |
|---|
| 75 |
('<', r'\x3C'), |
|---|
| 76 |
('&', r'\x26'), |
|---|
| 77 |
('=', r'\x3D'), |
|---|
| 78 |
('-', r'\x2D'), |
|---|
| 79 |
(';', r'\x3B'), |
|---|
| 80 |
(u'\u2028', r'\u2028'), |
|---|
| 81 |
(u'\u2029', r'\u2029') |
|---|
| 82 |
) |
|---|
| 83 |
|
|---|
| 84 |
# Escape every ASCII character with a value less than 32. |
|---|
| 85 |
_js_escapes = (_base_js_escapes + |
|---|
| 86 |
tuple([('%c' % z, '\\x%02X' % z) for z in range(32)])) |
|---|
| 87 |
|
|---|
| 88 |
def escapejs(value): |
|---|
| 89 |
"""Hex encodes characters for use in JavaScript strings.""" |
|---|
| 90 |
for bad, good in _js_escapes: |
|---|
| 91 |
value = value.replace(bad, good) |
|---|
| 92 |
return value |
|---|
| 93 |
escapejs = stringfilter(escapejs) |
|---|
| 94 |
|
|---|
| 95 |
def fix_ampersands(value): |
|---|
| 96 |
"""Replaces ampersands with ``&`` entities.""" |
|---|
| 97 |
from django.utils.html import fix_ampersands |
|---|
| 98 |
return fix_ampersands(value) |
|---|
| 99 |
fix_ampersands.is_safe=True |
|---|
| 100 |
fix_ampersands = stringfilter(fix_ampersands) |
|---|
| 101 |
|
|---|
| 102 |
# Values for testing floatformat input against infinity and NaN representations, |
|---|
| 103 |
# which differ across platforms and Python versions. Some (i.e. old Windows |
|---|
| 104 |
# ones) are not recognized by Decimal but we want to return them unchanged vs. |
|---|
| 105 |
# returning an empty string as we do for completley invalid input. Note these |
|---|
| 106 |
# need to be built up from values that are not inf/nan, since inf/nan values do |
|---|
| 107 |
# not reload properly from .pyc files on Windows prior to some level of Python 2.5 |
|---|
| 108 |
# (see Python Issue757815 and Issue1080440). |
|---|
| 109 |
pos_inf = 1e200 * 1e200 |
|---|
| 110 |
neg_inf = -1e200 * 1e200 |
|---|
| 111 |
nan = (1e200 * 1e200) / (1e200 * 1e200) |
|---|
| 112 |
special_floats = [str(pos_inf), str(neg_inf), str(nan)] |
|---|
| 113 |
|
|---|
| 114 |
def floatformat(text, arg=-1): |
|---|
| 115 |
""" |
|---|
| 116 |
Displays a float to a specified number of decimal places. |
|---|
| 117 |
|
|---|
| 118 |
If called without an argument, it displays the floating point number with |
|---|
| 119 |
one decimal place -- but only if there's a decimal place to be displayed: |
|---|
| 120 |
|
|---|
| 121 |
* num1 = 34.23234 |
|---|
| 122 |
* num2 = 34.00000 |
|---|
| 123 |
* num3 = 34.26000 |
|---|
| 124 |
* {{ num1|floatformat }} displays "34.2" |
|---|
| 125 |
* {{ num2|floatformat }} displays "34" |
|---|
| 126 |
* {{ num3|floatformat }} displays "34.3" |
|---|
| 127 |
|
|---|
| 128 |
If arg is positive, it will always display exactly arg number of decimal |
|---|
| 129 |
places: |
|---|
| 130 |
|
|---|
| 131 |
* {{ num1|floatformat:3 }} displays "34.232" |
|---|
| 132 |
* {{ num2|floatformat:3 }} displays "34.000" |
|---|
| 133 |
* {{ num3|floatformat:3 }} displays "34.260" |
|---|
| 134 |
|
|---|
| 135 |
If arg is negative, it will display arg number of decimal places -- but |
|---|
| 136 |
only if there are places to be displayed: |
|---|
| 137 |
|
|---|
| 138 |
* {{ num1|floatformat:"-3" }} displays "34.232" |
|---|
| 139 |
* {{ num2|floatformat:"-3" }} displays "34" |
|---|
| 140 |
* {{ num3|floatformat:"-3" }} displays "34.260" |
|---|
| 141 |
|
|---|
| 142 |
If the input float is infinity or NaN, the (platform-dependent) string |
|---|
| 143 |
representation of that value will be displayed. |
|---|
| 144 |
""" |
|---|
| 145 |
|
|---|
| 146 |
try: |
|---|
| 147 |
input_val = force_unicode(text) |
|---|
| 148 |
d = Decimal(input_val) |
|---|
| 149 |
except UnicodeEncodeError: |
|---|
| 150 |
return u'' |
|---|
| 151 |
except InvalidOperation: |
|---|
| 152 |
if input_val in special_floats: |
|---|
| 153 |
return input_val |
|---|
| 154 |
try: |
|---|
| 155 |
d = Decimal(force_unicode(float(text))) |
|---|
| 156 |
except (ValueError, InvalidOperation, TypeError, UnicodeEncodeError): |
|---|
| 157 |
return u'' |
|---|
| 158 |
try: |
|---|
| 159 |
p = int(arg) |
|---|
| 160 |
except ValueError: |
|---|
| 161 |
return input_val |
|---|
| 162 |
|
|---|
| 163 |
try: |
|---|
| 164 |
m = int(d) - d |
|---|
| 165 |
except (OverflowError, InvalidOperation): |
|---|
| 166 |
return input_val |
|---|
| 167 |
|
|---|
| 168 |
if not m and p < 0: |
|---|
| 169 |
return mark_safe(u'%d' % (int(d))) |
|---|
| 170 |
|
|---|
| 171 |
if p == 0: |
|---|
| 172 |
exp = Decimal(1) |
|---|
| 173 |
else: |
|---|
| 174 |
exp = Decimal('1.0') / (Decimal(10) ** abs(p)) |
|---|
| 175 |
try: |
|---|
| 176 |
return mark_safe(u'%s' % str(d.quantize(exp, ROUND_HALF_UP))) |
|---|
| 177 |
except InvalidOperation: |
|---|
| 178 |
return input_val |
|---|
| 179 |
floatformat.is_safe = True |
|---|
| 180 |
|
|---|
| 181 |
def iriencode(value): |
|---|
| 182 |
"""Escapes an IRI value for use in a URL.""" |
|---|
| 183 |
return force_unicode(iri_to_uri(value)) |
|---|
| 184 |
iriencode.is_safe = True |
|---|
| 185 |
iriencode = stringfilter(iriencode) |
|---|
| 186 |
|
|---|
| 187 |
def linenumbers(value, autoescape=None): |
|---|
| 188 |
"""Displays text with line numbers.""" |
|---|
| 189 |
from django.utils.html import escape |
|---|
| 190 |
lines = value.split(u'\n') |
|---|
| 191 |
# Find the maximum width of the line count, for use with zero padding |
|---|
| 192 |
# string format command |
|---|
| 193 |
width = unicode(len(unicode(len(lines)))) |
|---|
| 194 |
if not autoescape or isinstance(value, SafeData): |
|---|
| 195 |
for i, line in enumerate(lines): |
|---|
| 196 |
lines[i] = (u"%0" + width + u"d. %s") % (i + 1, line) |
|---|
| 197 |
else: |
|---|
| 198 |
for i, line in enumerate(lines): |
|---|
| 199 |
lines[i] = (u"%0" + width + u"d. %s") % (i + 1, escape(line)) |
|---|
| 200 |
return mark_safe(u'\n'.join(lines)) |
|---|
| 201 |
linenumbers.is_safe = True |
|---|
| 202 |
linenumbers.needs_autoescape = True |
|---|
| 203 |
linenumbers = stringfilter(linenumbers) |
|---|
| 204 |
|
|---|
| 205 |
def lower(value): |
|---|
| 206 |
"""Converts a string into all lowercase.""" |
|---|
| 207 |
return value.lower() |
|---|
| 208 |
lower.is_safe = True |
|---|
| 209 |
lower = stringfilter(lower) |
|---|
| 210 |
|
|---|
| 211 |
def make_list(value): |
|---|
| 212 |
""" |
|---|
| 213 |
Returns the value turned into a list. |
|---|
| 214 |
|
|---|
| 215 |
For an integer, it's a list of digits. |
|---|
| 216 |
For a string, it's a list of characters. |
|---|
| 217 |
""" |
|---|
| 218 |
return list(value) |
|---|
| 219 |
make_list.is_safe = False |
|---|
| 220 |
make_list = stringfilter(make_list) |
|---|
| 221 |
|
|---|
| 222 |
def slugify(value): |
|---|
| 223 |
""" |
|---|
| 224 |
Normalizes string, converts to lowercase, removes non-alpha characters, |
|---|
| 225 |
and converts spaces to hyphens. |
|---|
| 226 |
""" |
|---|
| 227 |
import unicodedata |
|---|
| 228 |
value = unicodedata.normalize('NFKD', value).encode('ascii', 'ignore') |
|---|
| 229 |
value = unicode(re.sub('[^\w\s-]', '', value).strip().lower()) |
|---|
| 230 |
return mark_safe(re.sub('[-\s]+', '-', value)) |
|---|
| 231 |
slugify.is_safe = True |
|---|
| 232 |
slugify = stringfilter(slugify) |
|---|
| 233 |
|
|---|
| 234 |
def stringformat(value, arg): |
|---|
| 235 |
""" |
|---|
| 236 |
Formats the variable according to the arg, a string formatting specifier. |
|---|
| 237 |
|
|---|
| 238 |
This specifier uses Python string formating syntax, with the exception that |
|---|
| 239 |
the leading "%" is dropped. |
|---|
| 240 |
|
|---|
| 241 |
See http://docs.python.org/lib/typesseq-strings.html for documentation |
|---|
| 242 |
of Python string formatting |
|---|
| 243 |
""" |
|---|
| 244 |
try: |
|---|
| 245 |
return (u"%" + unicode(arg)) % value |
|---|
| 246 |
except (ValueError, TypeError): |
|---|
| 247 |
return u"" |
|---|
| 248 |
stringformat.is_safe = True |
|---|
| 249 |
|
|---|
| 250 |
def title(value): |
|---|
| 251 |
"""Converts a string into titlecase.""" |
|---|
| 252 |
return re.sub("([a-z])'([A-Z])", lambda m: m.group(0).lower(), value.title()) |
|---|
| 253 |
title.is_safe = True |
|---|
| 254 |
title = stringfilter(title) |
|---|
| 255 |
|
|---|
| 256 |
def truncatewords(value, arg): |
|---|
| 257 |
""" |
|---|
| 258 |
Truncates a string after a certain number of words. |
|---|
| 259 |
|
|---|
| 260 |
Argument: Number of words to truncate after. |
|---|
| 261 |
""" |
|---|
| 262 |
from django.utils.text import truncate_words |
|---|
| 263 |
try: |
|---|
| 264 |
length = int(arg) |
|---|
| 265 |
except ValueError: # Invalid literal for int(). |
|---|
| 266 |
return value # Fail silently. |
|---|
| 267 |
return truncate_words(value, length) |
|---|
| 268 |
truncatewords.is_safe = True |
|---|
| 269 |
truncatewords = stringfilter(truncatewords) |
|---|
| 270 |
|
|---|
| 271 |
def truncatewords_html(value, arg): |
|---|
| 272 |
""" |
|---|
| 273 |
Truncates HTML after a certain number of words. |
|---|
| 274 |
|
|---|
| 275 |
Argument: Number of words to truncate after. |
|---|
| 276 |
""" |
|---|
| 277 |
from django.utils.text import truncate_html_words |
|---|
| 278 |
try: |
|---|
| 279 |
length = int(arg) |
|---|
| 280 |
except ValueError: # invalid literal for int() |
|---|
| 281 |
return value # Fail silently. |
|---|
| 282 |
return truncate_html_words(value, length) |
|---|
| 283 |
truncatewords_html.is_safe = True |
|---|
| 284 |
truncatewords_html = stringfilter(truncatewords_html) |
|---|
| 285 |
|
|---|
| 286 |
def upper(value): |
|---|
| 287 |
"""Converts a string into all uppercase.""" |
|---|
| 288 |
return value.upper() |
|---|
| 289 |
upper.is_safe = False |
|---|
| 290 |
upper = stringfilter(upper) |
|---|
| 291 |
|
|---|
| 292 |
def urlencode(value): |
|---|
| 293 |
"""Escapes a value for use in a URL.""" |
|---|
| 294 |
from django.utils.http import urlquote |
|---|
| 295 |
return urlquote(value) |
|---|
| 296 |
urlencode.is_safe = False |
|---|
| 297 |
urlencode = stringfilter(urlencode) |
|---|
| 298 |
|
|---|
| 299 |
def urlize(value, autoescape=None): |
|---|
| 300 |
"""Converts URLs in plain text into clickable links.""" |
|---|
| 301 |
from django.utils.html import urlize |
|---|
| 302 |
return mark_safe(urlize(value, nofollow=True, autoescape=autoescape)) |
|---|
| 303 |
urlize.is_safe=True |
|---|
| 304 |
urlize.needs_autoescape = True |
|---|
| 305 |
urlize = stringfilter(urlize) |
|---|
| 306 |
|
|---|
| 307 |
def urlizetrunc(value, limit, autoescape=None): |
|---|
| 308 |
""" |
|---|
| 309 |
Converts URLs into clickable links, truncating URLs to the given character |
|---|
| 310 |
limit, and adding 'rel=nofollow' attribute to discourage spamming. |
|---|
| 311 |
|
|---|
| 312 |
Argument: Length to truncate URLs to. |
|---|
| 313 |
""" |
|---|
| 314 |
from django.utils.html import urlize |
|---|
| 315 |
return mark_safe(urlize(value, trim_url_limit=int(limit), nofollow=True, |
|---|
| 316 |
autoescape=autoescape)) |
|---|
| 317 |
urlizetrunc.is_safe = True |
|---|
| 318 |
urlizetrunc.needs_autoescape = True |
|---|
| 319 |
urlizetrunc = stringfilter(urlizetrunc) |
|---|
| 320 |
|
|---|
| 321 |
def wordcount(value): |
|---|
| 322 |
"""Returns the number of words.""" |
|---|
| 323 |
return len(value.split()) |
|---|
| 324 |
wordcount.is_safe = False |
|---|
| 325 |
wordcount = stringfilter(wordcount) |
|---|
| 326 |
|
|---|
| 327 |
def wordwrap(value, arg): |
|---|
| 328 |
""" |
|---|
| 329 |
Wraps words at specified line length. |
|---|
| 330 |
|
|---|
| 331 |
Argument: number of characters to wrap the text at. |
|---|
| 332 |
""" |
|---|
| 333 |
from django.utils.text import wrap |
|---|
| 334 |
return wrap(value, int(arg)) |
|---|
| 335 |
wordwrap.is_safe = True |
|---|
| 336 |
wordwrap = stringfilter(wordwrap) |
|---|
| 337 |
|
|---|
| 338 |
def ljust(value, arg): |
|---|
| 339 |
""" |
|---|
| 340 |
Left-aligns the value in a field of a given width. |
|---|
| 341 |
|
|---|
| 342 |
Argument: field size. |
|---|
| 343 |
""" |
|---|
| 344 |
return value.ljust(int(arg)) |
|---|
| 345 |
ljust.is_safe = True |
|---|
| 346 |
ljust = stringfilter(ljust) |
|---|
| 347 |
|
|---|
| 348 |
def rjust(value, arg): |
|---|
| 349 |
""" |
|---|
| 350 |
Right-aligns the value in a field of a given width. |
|---|
| 351 |
|
|---|
| 352 |
Argument: field size. |
|---|
| 353 |
""" |
|---|
| 354 |
return value.rjust(int(arg)) |
|---|
| 355 |
rjust.is_safe = True |
|---|
| 356 |
rjust = stringfilter(rjust) |
|---|
| 357 |
|
|---|
| 358 |
def center(value, arg): |
|---|
| 359 |
"""Centers the value in a field of a given width.""" |
|---|
| 360 |
return value.center(int(arg)) |
|---|
| 361 |
center.is_safe = True |
|---|
| 362 |
center = stringfilter(center) |
|---|
| 363 |
|
|---|
| 364 |
def cut(value, arg): |
|---|
| 365 |
""" |
|---|
| 366 |
Removes all values of arg from the given string. |
|---|
| 367 |
""" |
|---|
| 368 |
safe = isinstance(value, SafeData) |
|---|
| 369 |
value = value.replace(arg, u'') |
|---|
| 370 |
if safe and arg != ';': |
|---|
| 371 |
return mark_safe(value) |
|---|
| 372 |
return value |
|---|
| 373 |
cut = stringfilter(cut) |
|---|
| 374 |
|
|---|
| 375 |
################### |
|---|
| 376 |
# HTML STRINGS # |
|---|
| 377 |
################### |
|---|
| 378 |
|
|---|
| 379 |
def escape(value): |
|---|
| 380 |
""" |
|---|
| 381 |
Marks the value as a string that should not be auto-escaped. |
|---|
| 382 |
""" |
|---|
| 383 |
from django.utils.safestring import mark_for_escaping |
|---|
| 384 |
return mark_for_escaping(value) |
|---|
| 385 |
escape.is_safe = True |
|---|
| 386 |
escape = stringfilter(escape) |
|---|
| 387 |
|
|---|
| 388 |
def force_escape(value): |
|---|
| 389 |
""" |
|---|
| 390 |
Escapes a string's HTML. This returns a new string containing the escaped |
|---|
| 391 |
characters (as opposed to "escape", which marks the content for later |
|---|
| 392 |
possible escaping). |
|---|
| 393 |
""" |
|---|
| 394 |
from django.utils.html import escape |
|---|
| 395 |
return mark_safe(escape(value)) |
|---|
| 396 |
force_escape = stringfilter(force_escape) |
|---|
| 397 |
force_escape.is_safe = True |
|---|
| 398 |
|
|---|
| 399 |
def linebreaks(value, autoescape=None): |
|---|
| 400 |
""" |
|---|
| 401 |
Replaces line breaks in plain text with appropriate HTML; a single |
|---|
| 402 |
newline becomes an HTML line break (``<br />``) and a new line |
|---|
| 403 |
followed by a blank line becomes a paragraph break (``</p>``). |
|---|
| 404 |
""" |
|---|
| 405 |
from django.utils.html import linebreaks |
|---|
| 406 |
autoescape = autoescape and not isinstance(value, SafeData) |
|---|
| 407 |
return mark_safe(linebreaks(value, autoescape)) |
|---|
| 408 |
linebreaks.is_safe = True |
|---|
| 409 |
linebreaks.needs_autoescape = True |
|---|
| 410 |
linebreaks = stringfilter(linebreaks) |
|---|
| 411 |
|
|---|
| 412 |
def linebreaksbr(value, autoescape=None): |
|---|
| 413 |
""" |
|---|
| 414 |
Converts all newlines in a piece of plain text to HTML line breaks |
|---|
| 415 |
(``<br />``). |
|---|
| 416 |
""" |
|---|
| 417 |
if autoescape and not isinstance(value, SafeData): |
|---|
| 418 |
from django.utils.html import escape |
|---|
| 419 |
value = escape(value) |
|---|
| 420 |
return mark_safe(value.replace('\n', '<br />')) |
|---|
| 421 |
linebreaksbr.is_safe = True |
|---|
| 422 |
linebreaksbr.needs_autoescape = True |
|---|
| 423 |
linebreaksbr = stringfilter(linebreaksbr) |
|---|
| 424 |
|
|---|
| 425 |
def safe(value): |
|---|
| 426 |
""" |
|---|
| 427 |
Marks the value as a string that should not be auto-escaped. |
|---|
| 428 |
""" |
|---|
| 429 |
return mark_safe(value) |
|---|
| 430 |
safe.is_safe = True |
|---|
| 431 |
safe = stringfilter(safe) |
|---|
| 432 |
|
|---|
| 433 |
def safeseq(value): |
|---|
| 434 |
""" |
|---|
| 435 |
A "safe" filter for sequences. Marks each element in the sequence, |
|---|
| 436 |
individually, as safe, after converting them to unicode. Returns a list |
|---|
| 437 |
with the results. |
|---|
| 438 |
""" |
|---|
| 439 |
return [mark_safe(force_unicode(obj)) for obj in value] |
|---|
| 440 |
safeseq.is_safe = True |
|---|
| 441 |
|
|---|
| 442 |
def removetags(value, tags): |
|---|
| 443 |
"""Removes a space separated list of [X]HTML tags from the output.""" |
|---|
| 444 |
tags = [re.escape(tag) for tag in tags.split()] |
|---|
| 445 |
tags_re = u'(%s)' % u'|'.join(tags) |
|---|
| 446 |
starttag_re = re.compile(ur'<%s(/?>|(\s+[^>]*>))' % tags_re, re.U) |
|---|
| 447 |
endtag_re = re.compile(u'</%s>' % tags_re) |
|---|
| 448 |
value = starttag_re.sub(u'', value) |
|---|
| 449 |
value = endtag_re.sub(u'', value) |
|---|
| 450 |
return value |
|---|
| 451 |
removetags.is_safe = True |
|---|
| 452 |
removetags = stringfilter(removetags) |
|---|
| 453 |
|
|---|
| 454 |
def striptags(value): |
|---|
| 455 |
"""Strips all [X]HTML tags.""" |
|---|
| 456 |
from django.utils.html import strip_tags |
|---|
| 457 |
return strip_tags(value) |
|---|
| 458 |
striptags.is_safe = True |
|---|
| 459 |
striptags = stringfilter(striptags) |
|---|
| 460 |
|
|---|
| 461 |
################### |
|---|
| 462 |
# LISTS # |
|---|
| 463 |
################### |
|---|
| 464 |
|
|---|
| 465 |
def dictsort(value, arg): |
|---|
| 466 |
""" |
|---|
| 467 |
Takes a list of dicts, returns that list sorted by the property given in |
|---|
| 468 |
the argument. |
|---|
| 469 |
""" |
|---|
| 470 |
var_resolve = Variable(arg).resolve |
|---|
| 471 |
decorated = [(var_resolve(item), item) for item in value] |
|---|
| 472 |
decorated.sort() |
|---|
| 473 |
return [item[1] for item in decorated] |
|---|
| 474 |
dictsort.is_safe = False |
|---|
| 475 |
|
|---|
| 476 |
def dictsortreversed(value, arg): |
|---|
| 477 |
""" |
|---|
| 478 |
Takes a list of dicts, returns that list sorted in reverse order by the |
|---|
| 479 |
property given in the argument. |
|---|
| 480 |
""" |
|---|
| 481 |
var_resolve = Variable(arg).resolve |
|---|
| 482 |
decorated = [(var_resolve(item), item) for item in value] |
|---|
| 483 |
decorated.sort() |
|---|
| 484 |
decorated.reverse() |
|---|
| 485 |
return [item[1] for item in decorated] |
|---|
| 486 |
dictsortreversed.is_safe = False |
|---|
| 487 |
|
|---|
| 488 |
def first(value): |
|---|
| 489 |
"""Returns the first item in a list.""" |
|---|
| 490 |
try: |
|---|
| 491 |
return value[0] |
|---|
| 492 |
except IndexError: |
|---|
| 493 |
return u'' |
|---|
| 494 |
first.is_safe = False |
|---|
| 495 |
|
|---|
| 496 |
def join(value, arg, autoescape=None): |
|---|
| 497 |
""" |
|---|
| 498 |
Joins a list with a string, like Python's ``str.join(list)``. |
|---|
| 499 |
""" |
|---|
| 500 |
value = map(force_unicode, value) |
|---|
| 501 |
if autoescape: |
|---|
| 502 |
from django.utils.html import conditional_escape |
|---|
| 503 |
value = [conditional_escape(v) for v in value] |
|---|
| 504 |
try: |
|---|
| 505 |
data = arg.join(value) |
|---|
| 506 |
except AttributeError: # fail silently but nicely |
|---|
| 507 |
return value |
|---|
| 508 |
return mark_safe(data) |
|---|
| 509 |
join.is_safe = True |
|---|
| 510 |
join.needs_autoescape = True |
|---|
| 511 |
|
|---|
| 512 |
def last(value): |
|---|
| 513 |
"Returns the last item in a list" |
|---|
| 514 |
try: |
|---|
| 515 |
return value[-1] |
|---|
| 516 |
except IndexError: |
|---|
| 517 |
return u'' |
|---|
| 518 |
last.is_safe = True |
|---|
| 519 |
|
|---|
| 520 |
def length(value): |
|---|
| 521 |
"""Returns the length of the value - useful for lists.""" |
|---|
| 522 |
try: |
|---|
| 523 |
return len(value) |
|---|
| 524 |
except (ValueError, TypeError): |
|---|
| 525 |
return '' |
|---|
| 526 |
length.is_safe = True |
|---|
| 527 |
|
|---|
| 528 |
def length_is(value, arg): |
|---|
| 529 |
"""Returns a boolean of whether the value's length is the argument.""" |
|---|
| 530 |
try: |
|---|
| 531 |
return len(value) == int(arg) |
|---|
| 532 |
except (ValueError, TypeError): |
|---|
| 533 |
return '' |
|---|
| 534 |
length_is.is_safe = False |
|---|
| 535 |
|
|---|
| 536 |
def random(value): |
|---|
| 537 |
"""Returns a random item from the list.""" |
|---|
| 538 |
return random_module.choice(value) |
|---|
| 539 |
random.is_safe = True |
|---|
| 540 |
|
|---|
| 541 |
def slice_(value, arg): |
|---|
| 542 |
""" |
|---|
| 543 |
Returns a slice of the list. |
|---|
| 544 |
|
|---|
| 545 |
Uses the same syntax as Python's list slicing; see |
|---|
| 546 |
http://diveintopython.org/native_data_types/lists.html#odbchelper.list.slice |
|---|
| 547 |
for an introduction. |
|---|
| 548 |
""" |
|---|
| 549 |
try: |
|---|
| 550 |
bits = [] |
|---|
| 551 |
for x in arg.split(u':'): |
|---|
| 552 |
if len(x) == 0: |
|---|
| 553 |
bits.append(None) |
|---|
| 554 |
else: |
|---|
| 555 |
bits.append(int(x)) |
|---|
| 556 |
return value[slice(*bits)] |
|---|
| 557 |
|
|---|
| 558 |
except (ValueError, TypeError): |
|---|
| 559 |
return value # Fail silently. |
|---|
| 560 |
slice_.is_safe = True |
|---|
| 561 |
|
|---|
| 562 |
def unordered_list(value, autoescape=None): |
|---|
| 563 |
""" |
|---|
| 564 |
Recursively takes a self-nested list and returns an HTML unordered list -- |
|---|
| 565 |
WITHOUT opening and closing <ul> tags. |
|---|
| 566 |
|
|---|
| 567 |
The list is assumed to be in the proper format. For example, if ``var`` |
|---|
| 568 |
contains: ``['States', ['Kansas', ['Lawrence', 'Topeka'], 'Illinois']]``, |
|---|
| 569 |
then ``{{ var|unordered_list }}`` would return:: |
|---|
| 570 |
|
|---|
| 571 |
<li>States |
|---|
| 572 |
<ul> |
|---|
| 573 |
<li>Kansas |
|---|
| 574 |
<ul> |
|---|
| 575 |
<li>Lawrence</li> |
|---|
| 576 |
<li>Topeka</li> |
|---|
| 577 |
</ul> |
|---|
| 578 |
</li> |
|---|
| 579 |
<li>Illinois</li> |
|---|
| 580 |
</ul> |
|---|
| 581 |
</li> |
|---|
| 582 |
""" |
|---|
| 583 |
if autoescape: |
|---|
| 584 |
from django.utils.html import conditional_escape |
|---|
| 585 |
escaper = conditional_escape |
|---|
| 586 |
else: |
|---|
| 587 |
escaper = lambda x: x |
|---|
| 588 |
def convert_old_style_list(list_): |
|---|
| 589 |
""" |
|---|
| 590 |
Converts old style lists to the new easier to understand format. |
|---|
| 591 |
|
|---|
| 592 |
The old list format looked like: |
|---|
| 593 |
['Item 1', [['Item 1.1', []], ['Item 1.2', []]] |
|---|
| 594 |
|
|---|
| 595 |
And it is converted to: |
|---|
| 596 |
['Item 1', ['Item 1.1', 'Item 1.2]] |
|---|
| 597 |
""" |
|---|
| 598 |
if not isinstance(list_, (tuple, list)) or len(list_) != 2: |
|---|
| 599 |
return list_, False |
|---|
| 600 |
first_item, second_item = list_ |
|---|
| 601 |
if second_item == []: |
|---|
| 602 |
return [first_item], True |
|---|
| 603 |
old_style_list = True |
|---|
| 604 |
new_second_item = [] |
|---|
| 605 |
for sublist in second_item: |
|---|
| 606 |
item, old_style_list = convert_old_style_list(sublist) |
|---|
| 607 |
if not old_style_list: |
|---|
| 608 |
break |
|---|
| 609 |
new_second_item.extend(item) |
|---|
| 610 |
if old_style_list: |
|---|
| 611 |
second_item = new_second_item |
|---|
| 612 |
return [first_item, second_item], old_style_list |
|---|
| 613 |
def _helper(list_, tabs=1): |
|---|
| 614 |
indent = u'\t' * tabs |
|---|
| 615 |
output = [] |
|---|
| 616 |
|
|---|
| 617 |
list_length = len(list_) |
|---|
| 618 |
i = 0 |
|---|
| 619 |
while i < list_length: |
|---|
| 620 |
title = list_[i] |
|---|
| 621 |
sublist = '' |
|---|
| 622 |
sublist_item = None |
|---|
| 623 |
if isinstance(title, (list, tuple)): |
|---|
| 624 |
sublist_item = title |
|---|
| 625 |
title = '' |
|---|
| 626 |
elif i < list_length - 1: |
|---|
| 627 |
next_item = list_[i+1] |
|---|
| 628 |
if next_item and isinstance(next_item, (list, tuple)): |
|---|
| 629 |
# The next item is a sub-list. |
|---|
| 630 |
sublist_item = next_item |
|---|
| 631 |
# We've processed the next item now too. |
|---|
| 632 |
i += 1 |
|---|
| 633 |
if sublist_item: |
|---|
| 634 |
sublist = _helper(sublist_item, tabs+1) |
|---|
| 635 |
sublist = '\n%s<ul>\n%s\n%s</ul>\n%s' % (indent, sublist, |
|---|
| 636 |
indent, indent) |
|---|
| 637 |
output.append('%s<li>%s%s</li>' % (indent, |
|---|
| 638 |
escaper(force_unicode(title)), sublist)) |
|---|
| 639 |
i += 1 |
|---|
| 640 |
return '\n'.join(output) |
|---|
| 641 |
value, converted = convert_old_style_list(value) |
|---|
| 642 |
return mark_safe(_helper(value)) |
|---|
| 643 |
unordered_list.is_safe = True |
|---|
| 644 |
unordered_list.needs_autoescape = True |
|---|
| 645 |
|
|---|
| 646 |
################### |
|---|
| 647 |
# INTEGERS # |
|---|
| 648 |
################### |
|---|
| 649 |
|
|---|
| 650 |
def add(value, arg): |
|---|
| 651 |
"""Adds the arg to the value.""" |
|---|
| 652 |
return int(value) + int(arg) |
|---|
| 653 |
add.is_safe = False |
|---|
| 654 |
|
|---|
| 655 |
def get_digit(value, arg): |
|---|
| 656 |
""" |
|---|
| 657 |
Given a whole number, returns the requested digit of it, where 1 is the |
|---|
| 658 |
right-most digit, 2 is the second-right-most digit, etc. Returns the |
|---|
| 659 |
original value for invalid input (if input or argument is not an integer, |
|---|
| 660 |
or if argument is less than 1). Otherwise, output is always an integer. |
|---|
| 661 |
""" |
|---|
| 662 |
try: |
|---|
| 663 |
arg = int(arg) |
|---|
| 664 |
value = int(value) |
|---|
| 665 |
except ValueError: |
|---|
| 666 |
return value # Fail silently for an invalid argument |
|---|
| 667 |
if arg < 1: |
|---|
| 668 |
return value |
|---|
| 669 |
try: |
|---|
| 670 |
return int(str(value)[-arg]) |
|---|
| 671 |
except IndexError: |
|---|
| 672 |
return 0 |
|---|
| 673 |
get_digit.is_safe = False |
|---|
| 674 |
|
|---|
| 675 |
################### |
|---|
| 676 |
# DATES # |
|---|
| 677 |
################### |
|---|
| 678 |
|
|---|
| 679 |
def date(value, arg=None): |
|---|
| 680 |
"""Formats a date according to the given format.""" |
|---|
| 681 |
from django.utils.dateformat import format |
|---|
| 682 |
if not value: |
|---|
| 683 |
return u'' |
|---|
| 684 |
if arg is None: |
|---|
| 685 |
arg = settings.DATE_FORMAT |
|---|
| 686 |
try: |
|---|
| 687 |
return format(value, arg) |
|---|
| 688 |
except AttributeError: |
|---|
| 689 |
return '' |
|---|
| 690 |
date.is_safe = False |
|---|
| 691 |
|
|---|
| 692 |
def time(value, arg=None): |
|---|
| 693 |
"""Formats a time according to the given format.""" |
|---|
| 694 |
from django.utils.dateformat import time_format |
|---|
| 695 |
if value in (None, u''): |
|---|
| 696 |
return u'' |
|---|
| 697 |
if arg is None: |
|---|
| 698 |
arg = settings.TIME_FORMAT |
|---|
| 699 |
try: |
|---|
| 700 |
return time_format(value, arg) |
|---|
| 701 |
except AttributeError: |
|---|
| 702 |
return '' |
|---|
| 703 |
time.is_safe = False |
|---|
| 704 |
|
|---|
| 705 |
def timesince(value, arg=None): |
|---|
| 706 |
"""Formats a date as the time since that date (i.e. "4 days, 6 hours").""" |
|---|
| 707 |
from django.utils.timesince import timesince |
|---|
| 708 |
if not value: |
|---|
| 709 |
return u'' |
|---|
| 710 |
try: |
|---|
| 711 |
if arg: |
|---|
| 712 |
return timesince(value, arg) |
|---|
| 713 |
return timesince(value) |
|---|
| 714 |
except (ValueError, TypeError): |
|---|
| 715 |
return u'' |
|---|
| 716 |
timesince.is_safe = False |
|---|
| 717 |
|
|---|
| 718 |
def timeuntil(value, arg=None): |
|---|
| 719 |
"""Formats a date as the time until that date (i.e. "4 days, 6 hours").""" |
|---|
| 720 |
from django.utils.timesince import timeuntil |
|---|
| 721 |
from datetime import datetime |
|---|
| 722 |
if not value: |
|---|
| 723 |
return u'' |
|---|
| 724 |
try: |
|---|
| 725 |
return timeuntil(value, arg) |
|---|
| 726 |
except (ValueError, TypeError): |
|---|
| 727 |
return u'' |
|---|
| 728 |
timeuntil.is_safe = False |
|---|
| 729 |
|
|---|
| 730 |
################### |
|---|
| 731 |
# LOGIC # |
|---|
| 732 |
################### |
|---|
| 733 |
|
|---|
| 734 |
def default(value, arg): |
|---|
| 735 |
"""If value is unavailable, use given default.""" |
|---|
| 736 |
return value or arg |
|---|
| 737 |
default.is_safe = False |
|---|
| 738 |
|
|---|
| 739 |
def default_if_none(value, arg): |
|---|
| 740 |
"""If value is None, use given default.""" |
|---|
| 741 |
if value is None: |
|---|
| 742 |
return arg |
|---|
| 743 |
return value |
|---|
| 744 |
default_if_none.is_safe = False |
|---|
| 745 |
|
|---|
| 746 |
def divisibleby(value, arg): |
|---|
| 747 |
"""Returns True if the value is devisible by the argument.""" |
|---|
| 748 |
return int(value) % int(arg) == 0 |
|---|
| 749 |
divisibleby.is_safe = False |
|---|
| 750 |
|
|---|
| 751 |
def yesno(value, arg=None): |
|---|
| 752 |
""" |
|---|
| 753 |
Given a string mapping values for true, false and (optionally) None, |
|---|
| 754 |
returns one of those strings accoding to the value: |
|---|
| 755 |
|
|---|
| 756 |
========== ====================== ================================== |
|---|
| 757 |
Value Argument Outputs |
|---|
| 758 |
========== ====================== ================================== |
|---|
| 759 |
``True`` ``"yeah,no,maybe"`` ``yeah`` |
|---|
| 760 |
``False`` ``"yeah,no,maybe"`` ``no`` |
|---|
| 761 |
``None`` ``"yeah,no,maybe"`` ``maybe`` |
|---|
| 762 |
``None`` ``"yeah,no"`` ``"no"`` (converts None to False |
|---|
| 763 |
if no mapping for None is given. |
|---|
| 764 |
========== ====================== ================================== |
|---|
| 765 |
""" |
|---|
| 766 |
if arg is None: |
|---|
| 767 |
arg = ugettext('yes,no,maybe') |
|---|
| 768 |
bits = arg.split(u',') |
|---|
| 769 |
if len(bits) < 2: |
|---|
| 770 |
return value # Invalid arg. |
|---|
| 771 |
try: |
|---|
| 772 |
yes, no, maybe = bits |
|---|
| 773 |
except ValueError: |
|---|
| 774 |
# Unpack list of wrong size (no "maybe" value provided). |
|---|
| 775 |
yes, no, maybe = bits[0], bits[1], bits[1] |
|---|
| 776 |
if value is None: |
|---|
| 777 |
return maybe |
|---|
| 778 |
if value: |
|---|
| 779 |
return yes |
|---|
| 780 |
return no |
|---|
| 781 |
yesno.is_safe = False |
|---|
| 782 |
|
|---|
| 783 |
################### |
|---|
| 784 |
# MISC # |
|---|
| 785 |
################### |
|---|
| 786 |
|
|---|
| 787 |
def filesizeformat(bytes): |
|---|
| 788 |
""" |
|---|
| 789 |
Formats the value like a 'human-readable' file size (i.e. 13 KB, 4.1 MB, |
|---|
| 790 |
102 bytes, etc). |
|---|
| 791 |
""" |
|---|
| 792 |
try: |
|---|
| 793 |
bytes = float(bytes) |
|---|
| 794 |
except TypeError: |
|---|
| 795 |
return u"0 bytes" |
|---|
| 796 |
|
|---|
| 797 |
if bytes < 1024: |
|---|
| 798 |
return ungettext("%(size)d byte", "%(size)d bytes", bytes) % {'size': bytes} |
|---|
| 799 |
if bytes < 1024 * 1024: |
|---|
| 800 |
return ugettext("%.1f KB") % (bytes / 1024) |
|---|
| 801 |
if bytes < 1024 * 1024 * 1024: |
|---|
| 802 |
return ugettext("%.1f MB") % (bytes / (1024 * 1024)) |
|---|
| 803 |
return ugettext("%.1f GB") % (bytes / (1024 * 1024 * 1024)) |
|---|
| 804 |
filesizeformat.is_safe = True |
|---|
| 805 |
|
|---|
| 806 |
def pluralize(value, arg=u's'): |
|---|
| 807 |
""" |
|---|
| 808 |
Returns a plural suffix if the value is not 1. By default, 's' is used as |
|---|
| 809 |
the suffix: |
|---|
| 810 |
|
|---|
| 811 |
* If value is 0, vote{{ value|pluralize }} displays "0 votes". |
|---|
| 812 |
* If value is 1, vote{{ value|pluralize }} displays "1 vote". |
|---|
| 813 |
* If value is 2, vote{{ value|pluralize }} displays "2 votes". |
|---|
| 814 |
|
|---|
| 815 |
If an argument is provided, that string is used instead: |
|---|
| 816 |
|
|---|
| 817 |
* If value is 0, class{{ value|pluralize:"es" }} displays "0 classes". |
|---|
| 818 |
* If value is 1, class{{ value|pluralize:"es" }} displays "1 class". |
|---|
| 819 |
* If value is 2, class{{ value|pluralize:"es" }} displays "2 classes". |
|---|
| 820 |
|
|---|
| 821 |
If the provided argument contains a comma, the text before the comma is |
|---|
| 822 |
used for the singular case and the text after the comma is used for the |
|---|
| 823 |
plural case: |
|---|
| 824 |
|
|---|
| 825 |
* If value is 0, cand{{ value|pluralize:"y,ies" }} displays "0 candies". |
|---|
| 826 |
* If value is 1, cand{{ value|pluralize:"y,ies" }} displays "1 candy". |
|---|
| 827 |
* If value is 2, cand{{ value|pluralize:"y,ies" }} displays "2 candies". |
|---|
| 828 |
""" |
|---|
| 829 |
if not u',' in arg: |
|---|
| 830 |
arg = u',' + arg |
|---|
| 831 |
bits = arg.split(u',') |
|---|
| 832 |
if len(bits) > 2: |
|---|
| 833 |
return u'' |
|---|
| 834 |
singular_suffix, plural_suffix = bits[:2] |
|---|
| 835 |
|
|---|
| 836 |
try: |
|---|
| 837 |
if int(value) != 1: |
|---|
| 838 |
return plural_suffix |
|---|
| 839 |
except ValueError: # Invalid string that's not a number. |
|---|
| 840 |
pass |
|---|
| 841 |
except TypeError: # Value isn't a string or a number; maybe it's a list? |
|---|
| 842 |
try: |
|---|
| 843 |
if len(value) != 1: |
|---|
| 844 |
return plural_suffix |
|---|
| 845 |
except TypeError: # len() of unsized object. |
|---|
| 846 |
pass |
|---|
| 847 |
return singular_suffix |
|---|
| 848 |
pluralize.is_safe = False |
|---|
| 849 |
|
|---|
| 850 |
def phone2numeric(value): |
|---|
| 851 |
"""Takes a phone number and converts it in to its numerical equivalent.""" |
|---|
| 852 |
from django.utils.text import phone2numeric |
|---|
| 853 |
return phone2numeric(value) |
|---|
| 854 |
phone2numeric.is_safe = True |
|---|
| 855 |
|
|---|
| 856 |
def pprint(value): |
|---|
| 857 |
"""A wrapper around pprint.pprint -- for debugging, really.""" |
|---|
| 858 |
from pprint import pformat |
|---|
| 859 |
try: |
|---|
| 860 |
return pformat(value) |
|---|
| 861 |
except Exception, e: |
|---|
| 862 |
return u"Error in formatting: %s" % force_unicode(e, errors="replace") |
|---|
| 863 |
pprint.is_safe = True |
|---|
| 864 |
|
|---|
| 865 |
# Syntax: register.filter(name of filter, callback) |
|---|
| 866 |
register.filter(add) |
|---|
| 867 |
register.filter(addslashes) |
|---|
| 868 |
register.filter(capfirst) |
|---|
| 869 |
register.filter(center) |
|---|
| 870 |
register.filter(cut) |
|---|
| 871 |
register.filter(date) |
|---|
| 872 |
register.filter(default) |
|---|
| 873 |
register.filter(default_if_none) |
|---|
| 874 |
register.filter(dictsort) |
|---|
| 875 |
register.filter(dictsortreversed) |
|---|
| 876 |
register.filter(divisibleby) |
|---|
| 877 |
register.filter(escape) |
|---|
| 878 |
register.filter(escapejs) |
|---|
| 879 |
register.filter(filesizeformat) |
|---|
| 880 |
register.filter(first) |
|---|
| 881 |
register.filter(fix_ampersands) |
|---|
| 882 |
register.filter(floatformat) |
|---|
| 883 |
register.filter(force_escape) |
|---|
| 884 |
register.filter(get_digit) |
|---|
| 885 |
register.filter(iriencode) |
|---|
| 886 |
register.filter(join) |
|---|
| 887 |
register.filter(last) |
|---|
| 888 |
register.filter(length) |
|---|
| 889 |
register.filter(length_is) |
|---|
| 890 |
register.filter(linebreaks) |
|---|
| 891 |
register.filter(linebreaksbr) |
|---|
| 892 |
register.filter(linenumbers) |
|---|
| 893 |
register.filter(ljust) |
|---|
| 894 |
register.filter(lower) |
|---|
| 895 |
register.filter(make_list) |
|---|
| 896 |
register.filter(phone2numeric) |
|---|
| 897 |
register.filter(pluralize) |
|---|
| 898 |
register.filter(pprint) |
|---|
| 899 |
register.filter(removetags) |
|---|
| 900 |
register.filter(random) |
|---|
| 901 |
register.filter(rjust) |
|---|
| 902 |
register.filter(safe) |
|---|
| 903 |
register.filter(safeseq) |
|---|
| 904 |
register.filter('slice', slice_) |
|---|
| 905 |
register.filter(slugify) |
|---|
| 906 |
register.filter(stringformat) |
|---|
| 907 |
register.filter(striptags) |
|---|
| 908 |
register.filter(time) |
|---|
| 909 |
register.filter(timesince) |
|---|
| 910 |
register.filter(timeuntil) |
|---|
| 911 |
register.filter(title) |
|---|
| 912 |
register.filter(truncatewords) |
|---|
| 913 |
register.filter(truncatewords_html) |
|---|
| 914 |
register.filter(unordered_list) |
|---|
| 915 |
register.filter(upper) |
|---|
| 916 |
register.filter(urlencode) |
|---|
| 917 |
register.filter(urlize) |
|---|
| 918 |
register.filter(urlizetrunc) |
|---|
| 919 |
register.filter(wordcount) |
|---|
| 920 |
register.filter(wordwrap) |
|---|
| 921 |
register.filter(yesno) |
|---|