From: Armin Ronacher Date: Tue, 13 Mar 2007 15:48:10 +0000 (+0100) Subject: [svn] FILTERS!!111!!!111oneoneone X-Git-Tag: 2.0rc1~443 X-Git-Url: http://git.tremily.us/?a=commitdiff_plain;h=2b765132258cd23b5227c63e8bb6d20cc211dbdb;p=jinja2.git [svn] FILTERS!!111!!!111oneoneone --HG-- branch : trunk --- diff --git a/jinja/exceptions.py b/jinja/exceptions.py index 8b3d667..c13725e 100644 --- a/jinja/exceptions.py +++ b/jinja/exceptions.py @@ -29,6 +29,15 @@ class FilterNotFound(KeyError, TemplateError): KeyError.__init__(self, message) +class FilterArgumentError(TypeError, TemplateError): + """ + An argument passed to the filter was invalid. + """ + + def __init__(self, message): + TypeError.__init__(self, message) + + class TestNotFound(KeyError, TemplateError): """ Raised if a test does not exist. diff --git a/jinja/filters.py b/jinja/filters.py index 8d758b2..5b71570 100644 --- a/jinja/filters.py +++ b/jinja/filters.py @@ -10,8 +10,9 @@ """ from random import choice from urllib import urlencode, quote -from jinja.utils import escape +from jinja.utils import escape, urlize from jinja.datastructure import Undefined +from jinja.exceptions import FilterArgumentError try: @@ -65,6 +66,14 @@ def do_replace(s, old, new, count=None): {{ "aaaaargh"|replace("a", "d'oh, ", 2) }} -> d'oh, d'oh, aaargh """ + if not isinstance(old, basestring) or \ + not isinstance(new, basestring): + raise FilterArgumentException('the replace filter requires ' + 'string replacement arguments') + elif not isinstance(count, (int, long)): + raise FilterArgumentException('the count parameter of the ' + 'replace filter requires ' + 'an integer') if count is None: return s.replace(old, new) return s.replace(old, new, count) @@ -128,6 +137,46 @@ def do_title(s): do_title = stringfilter(do_title) +def do_dictsort(case_sensitive=False, by='key'): + """ + Sort a dict and yield (key, value) pairs. Because python dicts are + unsorted you may want to use this function to order them by either + key or value: + + .. sourcecode:: jinja + + {% for item in mydict|dictsort %} + sort the dict by key, case insensitive + + {% for item in mydict|dicsort(true) %} + sort the dict by key, case sensitive + + {% for item in mydict|dictsort(false, 'value') %} + sort the dict by key, case insensitive, sorted + normally and ordered by value. + """ + if by == 'key': + pos = 0 + elif by == 'value': + pos = 1 + else: + raise FilterArgumentError('You can only sort by either ' + '"key" or "value"') + def sort_func(value, env): + if isinstance(value, basestring): + value = env.to_unicode(value) + if not case_sensitive: + value = value.lower() + return value + + def wrapped(env, context, value): + items = value.items() + items.sort(lambda a, b: cmp(sort_func(a[pos], env), + sort_func(b[pos], env))) + return items + return wrapped + + def do_default(default_value=u'', boolean=False): """ If the value is undefined it will return the passed default value, @@ -294,6 +343,209 @@ def do_jsonencode(): return lambda e, c, v: simplejson.dumps(v) +def do_filesizeformat(): + """ + Format the value like a 'human-readable' file size (i.e. 13 KB, 4.1 MB, 102 + bytes, etc). + """ + def wrapped(env, context, value): + # fail silently + try: + bytes = float(value) + except TypeError: + bytes = 0 + + if bytes < 1024: + return "%d Byte%s" % (bytes, bytes != 1 and 's' or '') + elif bytes < 1024 * 1024: + return "%.1f KB" % (bytes / 1024) + elif bytes < 1024 * 1024 * 1024: + return "%.1f MB" % (bytes / (1024 * 1024)) + return "%.1f GB" % (bytes / (1024 * 1024 * 1024)) + return wrapped + + +def do_pprint(): + """ + Pretty print a variable. Useful for debugging. + """ + def wrapped(env, context, value): + from pprint import pformat + return pformat(value) + return wrapped + + +def do_urlize(value, trim_url_limit=None, nofollow=False): + """ + Converts URLs in plain text into clickable links. + + If you pass the filter an additional integer it will shorten the urls + to that number. Also a third argument exists that makes the urls + "nofollow": + + .. sourcecode:: jinja + + {{ mytext|urlize(40, True) }} + links are shortened to 40 chars and defined with rel="nofollow" + """ + return urlize(value, trim_url_limit, nofollow) +do_urlize = stringfilter(do_urlize) + + +def do_indent(s, width=4, indentfirst=False): + """ + {{ s|indent[ width[ indentfirst[ usetab]]] }} + + Return a copy of the passed string, each line indented by + 4 spaces. The first line is not indented. If you want to + change the number of spaces or indent the first line too + you can pass additional parameters to the filter: + + .. sourcecode:: jinja + + {{ mytext|indent(2, True) }} + indent by two spaces and indent the first line too. + """ + indention = ' ' * width + if indentfirst: + return u'\n'.join([indention + line for line in s.splitlines()]) + return s.replace('\n', '\n' + indention) +do_indent = stringfilter(do_indent) + + +def do_truncate(s, length=255, killwords=False, end='...'): + """ + {{ s|truncate[ length[ killwords[ end]]] }} + + Return a truncated copy of the string. The length is specified + with the first parameter which defaults to ``255``. If the second + parameter is ``true`` the filter will cut the text at length. Otherwise + it will try to save the last word. If the text was in fact + truncated it will append an ellipsis sign (``"..."``). If you want a + different ellipsis sign than ``"..."`` you can specify it using the + third parameter. + + .. sourcecode jinja:: + + {{ mytext|truncate(300, false, '»') }} + truncate mytext to 300 chars, don't split up words, use a + right pointing double arrow as ellipsis sign. + """ + if len(s) <= length: + return s + elif killwords: + return s[:length] + end + words = s.split(' ') + result = [] + m = 0 + for word in words: + m += len(word) + 1 + if m > length: + break + result.append(word) + result.append(end) + return u' '.join(result) +do_truncate = stringfilter(do_truncate) + + +def do_wordwrap(s, pos=79, hard=False): + """ + Return a copy of the string passed to the filter wrapped after + ``79`` characters. You can override this default using the first + parameter. If you set the second parameter to `true` Jinja will + also split words apart (usually a bad idea because it makes + reading hard). + """ + if len(s) < pos: + return s + if hard: + return u'\n'.join([s[idx:idx + pos] for idx in + xrange(0, len(s), pos)]) + # code from http://aspn.activestate.com/ASPN/Cookbook/Python/Recipe/148061 + return reduce(lambda line, word, pos=pos: u'%s%s%s' % + (line, u' \n'[(len(line)-line.rfind('\n') - 1 + + len(word.split('\n', 1)[0]) >= pos)], + word), s.split(' ')) +do_wordwrap = stringfilter(do_wordwrap) + + +def do_wordcount(s): + """ + Count the words in that string. + """ + return len([x for x in s.split() if x]) +do_wordcount = stringfilter(do_wordcount) + + +def do_textile(s): + """ + Prase the string using textile. + + requires the `PyTextile`_ library. + + .. _PyTextile: http://dealmeida.net/projects/textile/ + """ + from textile import textile + return textile(s) +do_textile = stringfilter(do_textile) + + +def do_markdown(s): + """ + Parse the string using markdown. + + requires the `Python-markdown`_ library. + + .. _Python-markdown: http://www.freewisdom.org/projects/python-markdown/ + """ + from markdown import markdown + return markdown(s) +do_markdown = stringfilter(do_markdown) + + +def do_rst(s): + """ + Parse the string using the reStructuredText parser from the + docutils package. + + requires `docutils`_. + + .. _docutils: from http://docutils.sourceforge.net/ + """ + try: + from docutils.core import publish_parts + parts = publish_parts(source=s, writer_name='html4css1') + return parts['fragment'] + except: + return s +do_rst = stringfilter(do_rst) + + +def do_int(): + """ + Convert the value into an integer. + """ + def wrapped(env, context, value): + return int(value) + return wrapped + + +def do_float(): + """ + Convert the value into a floating point number. + """ + def wrapped(env, context, value): + return float(value) + return wrapped + + +def do_string(): + """ + Convert the value into an string. + """ + return lambda e, c, v: e.to_unicode(v) + + FILTERS = { 'replace': do_replace, 'upper': do_upper, @@ -306,6 +558,7 @@ FILTERS = { 'default': do_default, 'join': do_join, 'count': do_count, + 'dictsort': do_dictsort, 'length': do_count, 'reverse': do_reverse, 'center': do_center, @@ -315,5 +568,17 @@ FILTERS = { 'last': do_last, 'random': do_random, 'urlencode': do_urlencode, - 'jsonencode': do_jsonencode + 'jsonencode': do_jsonencode, + 'filesizeformat': do_filesizeformat, + 'pprint': do_pprint, + 'indent': do_indent, + 'truncate': do_truncate, + 'wordwrap': do_wordwrap, + 'wordcount': do_wordcount, + 'textile': do_textile, + 'markdown': do_markdown, + 'rst': do_rst, + 'int': do_int, + 'float': do_float, + 'string': do_string } diff --git a/jinja/utils.py b/jinja/utils.py index 311d905..262db19 100644 --- a/jinja/utils.py +++ b/jinja/utils.py @@ -5,11 +5,15 @@ Utility functions. - :copyright: 2007 by Armin Ronacher. + **license information**: some of the regular expressions and + the ``urlize`` function were taken from the django framework. + + :copyright: 2007 by Armin Ronacher, Lawrence Journal-World. :license: BSD, see LICENSE for more details. """ import re import sys +import string from types import MethodType, FunctionType from jinja.nodes import Trans from jinja.datastructure import Markup @@ -33,6 +37,19 @@ _escape_res = ( re.compile('(&|<|>)') ) +_integer_re = re.compile('^(\d+)$') + +_word_split_re = re.compile(r'(\s+)') + +_punctuation_re = re.compile( + '^(?P(?:%s)*)(?P.*?)(?P(?:%s)*)$' % ( + '|'.join([re.escape(p) for p in ('(', '<', '<')]), + '|'.join([re.escape(p) for p in ('.', ',', ')', '>', '\n', '>')]) + ) +) + +_simple_email_re = re.compile(r'^\S+@[a-zA-Z0-9._-]+\.[a-zA-Z0-9._-]+$') + def escape(x, attribute=False): """ @@ -42,6 +59,51 @@ def escape(x, attribute=False): _escape_pairs[m.group()], x)) +def urlize(text, trim_url_limit=None, nofollow=False): + """ + Converts any URLs in text into clickable links. Works on http://, + https:// and www. links. Links can have trailing punctuation (periods, + commas, close-parens) and leading punctuation (opening parens) and + it'll still do the right thing. + + If trim_url_limit is not None, the URLs in link text will be limited + to trim_url_limit characters. + + If nofollow is True, the URLs in link text will get a rel="nofollow" + attribute. + """ + trim_url = lambda x, limit=trim_url_limit: limit is not None \ + and (x[:limit] + (len(x) >=limit and '...' + or '')) or x + words = _word_split_re.split(text) + nofollow_attr = nofollow and ' rel="nofollow"' or '' + for i, word in enumerate(words): + match = _punctuation_re.match(word) + if match: + lead, middle, trail = match.groups() + if middle.startswith('www.') or ( + '@' not in middle and + not middle.startswith('http://') and + len(middle) > 0 and + middle[0] in string.letters + string.digits and ( + middle.endswith('.org') or + middle.endswith('.net') or + middle.endswith('.com') + )): + middle = '%s' % (middle, + nofollow_attr, trim_url(middle)) + if middle.startswith('http://') or \ + middle.startswith('https://'): + middle = '%s' % (middle, + nofollow_attr, trim_url(middle)) + if '@' in middle and not middle.startswith('www.') and \ + not ':' in middle and _simple_email_re.match(middle): + middle = '%s' % (middle, middle) + if lead + middle + trail != word: + words[i] = lead + middle + trail + return u''.join(words) + + def find_translations(environment, source): """ Find all translatable strings in a template and yield @@ -98,7 +160,7 @@ def raise_template_exception(template, exception, filename, lineno, context): Raise an exception "in a template". Return a traceback object. """ - # some traceback systems allow to skip blocks + # some traceback systems allow to skip frames __traceback_hide__ = True offset = '\n' * (lineno - 1) diff --git a/setup.py b/setup.py index 48e1a5e..697fb4b 100644 --- a/setup.py +++ b/setup.py @@ -14,7 +14,8 @@ setup( license = 'BSD', author = 'Armin Ronacher', author_email = 'armin.ronacher@active-4.com', - description = 'A small but fast and easy to use stand-alone template engine written in pure python.', + description = 'A small but fast and easy to use stand-alone template ' + 'engine written in pure python.', zip_safe = True, classifiers = [ 'Development Status :: 5 - Production/Stable', @@ -28,7 +29,7 @@ setup( 'Topic :: Text Processing :: Markup :: HTML' ], keywords = ['python.templating.engines'], - packages = ['jinja'], + packages = ['jinja', 'jinja.translators'], extras_require = {'plugin': ['setuptools>=0.6a2']}, entry_points=''' [python.templating.engines]