Binary files genshi.orig/dist/Genshi-0.4dev_r466-py2.4.egg and genshi/dist/Genshi-0.4dev_r466-py2.4.egg differ
diff -rNu genshi.orig/genshi/pipes/djangohtml.py genshi/genshi/pipes/djangohtml.py
--- genshi.orig/genshi/pipes/djangohtml.py	1970-01-01 02:00:00.000000000 +0200
+++ genshi/genshi/pipes/djangohtml.py	2006-12-06 08:46:31.000000000 +0200
@@ -0,0 +1,115 @@
+"HTML utilities suitable for global use."
+
+import re, string
+
+# Configuration for urlize() function
+LEADING_PUNCTUATION  = ['(', '<', '&lt;']
+TRAILING_PUNCTUATION = ['.', ',', ')', '>', '\n', '&gt;']
+
+# list of possible strings used for bullets in bulleted lists
+DOTS = ['&middot;', '*', '\xe2\x80\xa2', '&#149;', '&bull;', '&#8226;']
+
+unencoded_ampersands_re = re.compile(r'&(?!(\w+|#\d+);)')
+word_split_re = re.compile(r'(\s+)')
+punctuation_re = re.compile('^(?P<lead>(?:%s)*)(?P<middle>.*?)(?P<trail>(?:%s)*)$' % \
+    ('|'.join([re.escape(x) for x in LEADING_PUNCTUATION]),
+    '|'.join([re.escape(x) for x in TRAILING_PUNCTUATION])))
+simple_email_re = re.compile(r'^\S+@[a-zA-Z0-9._-]+\.[a-zA-Z0-9._-]+$')
+link_target_attribute_re = re.compile(r'(<a [^>]*?)target=[^\s>]+')
+html_gunk_re = re.compile(r'(?:<br clear="all">|<i><\/i>|<b><\/b>|<em><\/em>|<strong><\/strong>|<\/?smallcaps>|<\/?uppercase>)', re.IGNORECASE)
+hard_coded_bullets_re = re.compile(r'((?:<p>(?:%s).*?[a-zA-Z].*?</p>\s*)+)' % '|'.join([re.escape(x) for x in DOTS]), re.DOTALL)
+trailing_empty_content_re = re.compile(r'(?:<p>(?:&nbsp;|\s|<br \/>)*?</p>\s*)+\Z')
+del x # Temporary variable
+
+def escape(html):
+    "Returns the given HTML with ampersands, quotes and carets encoded"
+    if not isinstance(html, basestring):
+        html = str(html)
+    return html.replace('&', '&amp;').replace('<', '&lt;').replace('>', '&gt;').replace('"', '&quot;').replace("'", '&#39;')
+
+def linebreaks(value):
+    "Converts newlines into <p> and <br />s"
+    value = re.sub(r'\r\n|\r|\n', '\n', value) # normalize newlines
+    paras = re.split('\n{2,}', value)
+    paras = ['<p>%s</p>' % p.strip().replace('\n', '<br />') for p in paras]
+    return '\n\n'.join(paras)
+
+def strip_tags(value):
+    "Returns the given HTML with all tags stripped"
+    return re.sub(r'<[^>]*?>', '', value)
+
+def strip_spaces_between_tags(value):
+    "Returns the given HTML with spaces between tags normalized to a single space"
+    return re.sub(r'>\s+<', '> <', value)
+
+def strip_entities(value):
+    "Returns the given HTML with all entities (&something;) stripped"
+    return re.sub(r'&(?:\w+|#\d);', '', value)
+
+def fix_ampersands(value):
+    "Returns the given HTML with all unencoded ampersands encoded correctly"
+    return unencoded_ampersands_re.sub('&amp;', value)
+
+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 = '<a href="http://%s"%s>%s</a>' % (middle, nofollow_attr, trim_url(anchor))
+            if middle.startswith('http://') or middle.startswith('https://'):
+                middle = '<a href="%s"%s>%s</a>' % (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 = '<a href="mailto:%s">%s</a>' % (middle, middle)
+            if lead + middle + trail != word:
+                words[i] = lead + middle + trail
+    return ''.join(words)
+
+def clean_html(text):
+    """
+    Cleans the given HTML. Specifically, it does the following:
+        * Converts <b> and <i> to <strong> and <em>.
+        * Encodes all ampersands correctly.
+        * Removes all "target" attributes from <a> tags.
+        * Removes extraneous HTML, such as presentational tags that open and
+          immediately close and <br clear="all">.
+        * Converts hard-coded bullets into HTML unordered lists.
+        * Removes stuff like "<p>&nbsp;&nbsp;</p>", but only if it's at the
+          bottom of the text.
+    """
+    from djangotext import normalize_newlines
+    text = normalize_newlines(text)
+    text = re.sub(r'<(/?)\s*b\s*>', '<\\1strong>', text)
+    text = re.sub(r'<(/?)\s*i\s*>', '<\\1em>', text)
+    text = fix_ampersands(text)
+    # Remove all target="" attributes from <a> tags.
+    text = link_target_attribute_re.sub('\\1', text)
+    # Trim stupid HTML such as <br clear="all">.
+    text = html_gunk_re.sub('', text)
+    # Convert hard-coded bullets into HTML unordered lists.
+    def replace_p_tags(match):
+        s = match.group().replace('</p>', '</li>')
+        for d in DOTS:
+            s = s.replace('<p>%s' % d, '<li>')
+        return '<ul>\n%s\n</ul>' % s
+    text = hard_coded_bullets_re.sub(replace_p_tags, text)
+    # Remove stuff like "<p>&nbsp;&nbsp;</p>", but only if it's at the bottom of the text.
+    text = trailing_empty_content_re.sub('', text)
+    return text
+
diff -rNu genshi.orig/genshi/pipes/djangotext.py genshi/genshi/pipes/djangotext.py
--- genshi.orig/genshi/pipes/djangotext.py	1970-01-01 02:00:00.000000000 +0200
+++ genshi/genshi/pipes/djangotext.py	2006-12-06 08:46:31.000000000 +0200
@@ -0,0 +1,111 @@
+import re
+
+# Capitalizes the first letter of a string.
+capfirst = lambda x: x and x[0].upper() + x[1:]
+
+def wrap(text, width):
+    """
+    A word-wrap function that preserves existing line breaks and most spaces in
+    the text. Expects that existing line breaks are posix newlines (\n).
+    See http://aspn.activestate.com/ASPN/Cookbook/Python/Recipe/148061
+    """
+    return reduce(lambda line, word, width=width: '%s%s%s' %
+                  (line,
+                   ' \n'[(len(line[line.rfind('\n')+1:])
+                         + len(word.split('\n',1)[0]
+                              ) >= width)],
+                   word),
+                  text.split(' ')
+                 )
+
+def truncate_words(s, num):
+    "Truncates a string after a certain number of words."
+    length = int(num)
+    words = s.split()
+    if len(words) > length:
+        words = words[:length]
+        if not words[-1].endswith('...'):
+            words.append('...')
+    return ' '.join(words)
+
+def get_valid_filename(s):
+    """
+    Returns the given string converted to a string that can be used for a clean
+    filename. Specifically, leading and trailing spaces are removed; other
+    spaces are converted to underscores; and all non-filename-safe characters
+    are removed.
+    >>> get_valid_filename("john's portrait in 2004.jpg")
+    'johns_portrait_in_2004.jpg'
+    """
+    s = s.strip().replace(' ', '_')
+    return re.sub(r'[^-A-Za-z0-9_.]', '', s)
+
+def get_text_list(list_, last_word='or'):
+    """
+    >>> get_text_list(['a', 'b', 'c', 'd'])
+    'a, b, c or d'
+    >>> get_text_list(['a', 'b', 'c'], 'and')
+    'a, b and c'
+    >>> get_text_list(['a', 'b'], 'and')
+    'a and b'
+    >>> get_text_list(['a'])
+    'a'
+    >>> get_text_list([])
+    ''
+    """
+    if len(list_) == 0: return ''
+    if len(list_) == 1: return list_[0]
+    return '%s %s %s' % (', '.join([str(i) for i in list_][:-1]), last_word, list_[-1])
+
+def normalize_newlines(text):
+    return re.sub(r'\r\n|\r|\n', '\n', text)
+
+def recapitalize(text):
+    "Recapitalizes text, placing caps after end-of-sentence punctuation."
+#     capwords = ()
+    text = text.lower()
+    capsRE = re.compile(r'(?:^|(?<=[\.\?\!] ))([a-z])')
+    text = capsRE.sub(lambda x: x.group(1).upper(), text)
+#     for capword in capwords:
+#         capwordRE = re.compile(r'\b%s\b' % capword, re.I)
+#         text = capwordRE.sub(capword, text)
+    return text
+
+def phone2numeric(phone):
+    "Converts a phone number with letters into its numeric equivalent."
+    letters = re.compile(r'[A-PR-Y]', re.I)
+    char2number = lambda m: {'a': '2', 'c': '2', 'b': '2', 'e': '3',
+         'd': '3', 'g': '4', 'f': '3', 'i': '4', 'h': '4', 'k': '5',
+         'j': '5', 'm': '6', 'l': '5', 'o': '6', 'n': '6', 'p': '7',
+         's': '7', 'r': '7', 'u': '8', 't': '8', 'w': '9', 'v': '8',
+         'y': '9', 'x': '9'}.get(m.group(0).lower())
+    return letters.sub(char2number, phone)
+
+# From http://www.xhaus.com/alan/python/httpcomp.html#gzip
+# Used with permission.
+def compress_string(s):
+    import cStringIO, gzip
+    zbuf = cStringIO.StringIO()
+    zfile = gzip.GzipFile(mode='wb', compresslevel=6, fileobj=zbuf)
+    zfile.write(s)
+    zfile.close()
+    return zbuf.getvalue()
+
+smart_split_re = re.compile('("(?:[^"\\\\]*(?:\\\\.[^"\\\\]*)*)"|\'(?:[^\'\\\\]*(?:\\\\.[^\'\\\\]*)*)\'|[^\\s]+)')
+def smart_split(text):
+    """
+    Generator that splits a string by spaces, leaving quoted phrases together.
+    Supports both single and double quotes, and supports escaping quotes with
+    backslashes. In the output, strings will keep their initial and trailing
+    quote marks.
+    >>> list(smart_split('This is "a person\'s" test.'))
+    ['This', 'is', '"a person\'s"', 'test.']
+    """
+    for bit in smart_split_re.finditer(text):
+        bit = bit.group(0)
+        if bit[0] == '"':
+            yield '"' + bit[1:-1].replace('\\"', '"').replace('\\\\', '\\') + '"'
+        elif bit[0] == "'":
+            yield "'" + bit[1:-1].replace("\\'", "'").replace("\\\\", "\\") + "'"
+        else:
+            yield bit
diff -rNu genshi.orig/genshi/pipes/filterslib.py genshi/genshi/pipes/filterslib.py
--- genshi.orig/genshi/pipes/filterslib.py	1970-01-01 02:00:00.000000000 +0200
+++ genshi/genshi/pipes/filterslib.py	2006-12-06 08:46:38.000000000 +0200
@@ -0,0 +1,454 @@
+import re
+import random as random_module
+from kid.pull import XML
+
+def register_filter(filter_name, filter_func):
+    globals()[filter_name]=filter_func
+
+###################
+# STRINGS         #
+###################
+
+def addslashes(value):
+    "Adds slashes - useful for passing strings to JavaScript, for example."
+    return value.replace('"', '\\"').replace("'", "\\'")
+
+def capfirst(value):
+    "Capitalizes the first character of the value"
+    value = str(value)
+    return value and value[0].upper() + value[1:]
+
+def addslashes(value):
+    "Adds slashes - useful for passing strings to JavaScript, for example."
+    return value.replace('"', '\\"').replace("'", "\\'")
+
+def capfirst(value):
+    "Capitalizes the first character of the value"
+    value = str(value)
+    return value and value[0].upper() + value[1:]
+
+def fix_ampersands(value):
+    "Replaces ampersands with ``&amp;`` entities"
+    from djangohtml import fix_ampersands
+    return fix_ampersands(value)
+
+def floatformat(text):
+    """
+    Displays a floating point number as 34.2 (with one decimal place) -- but
+    only if there's a point to be displayed
+    """
+    try:
+        f = float(text)
+    except ValueError:
+        return ''
+    m = f - int(f)
+    if m:
+        return '%.1f' % f
+    else:
+        return '%d' % int(f)
+
+def linenumbers(value):
+    "Displays text with line numbers"
+    from djangohtml import escape
+    lines = value.split('\n')
+    # Find the maximum width of the line count, for use with zero padding string format command
+    width = str(len(str(len(lines))))
+    for i, line in enumerate(lines):
+        lines[i] = ("%0" + width  + "d. %s") % (i + 1, escape(line))
+    return '\n'.join(lines)
+
+def lower(value):
+    "Converts a string into all lowercase"
+    return value.lower()
+
+def make_list(value):
+    """
+    Returns the value turned into a list. For an integer, it's a list of
+    digits. For a string, it's a list of characters.
+    """
+    return list(str(value))
+
+def slugify(value):
+    "Converts to lowercase, removes non-alpha chars and converts spaces to hyphens"
+    value = re.sub('[^\w\s-]', '', value).strip().lower()
+    return re.sub('[-\s]+', '-', value)
+
+def stringformat(value, arg):
+    """
+    Formats the variable according to the argument, a string formatting specifier.
+    This specifier uses Python string formating syntax, with the exception that
+    the leading "%" is dropped.
+
+    See http://docs.python.org/lib/typesseq-strings.html for documentation
+    of Python string formatting
+    """
+    try:
+        return ("%" + arg) % value
+    except (ValueError, TypeError):
+        return ""
+
+def title(value):
+    "Converts a string into titlecase"
+    return re.sub("([a-z])'([A-Z])", lambda m: m.group(0).lower(), value.title())
+
+def truncatewords(value, arg):
+    """
+    Truncates a string after a certain number of words
+
+    Argument: Number of words to truncate after
+    """
+    from djangotext import truncate_words
+    try:
+        length = int(arg)
+    except ValueError: # invalid literal for int()
+        return value # Fail silently.
+    if not isinstance(value, basestring):
+        value = str(value)
+    return truncate_words(value, length)
+
+def upper(value):
+    "Converts a string into all uppercase"
+    return value.upper()
+
+def urlencode(value):
+    "Escapes a value for use in a URL"
+    import urllib
+    return urllib.quote(value)
+
+def urlize(value):
+    "Converts URLs in plain text into clickable links"
+    from djangohtml import urlize, fix_ampersands
+    return XML(fix_ampersands(urlize(value, nofollow=True)))
+
+def urlizetrunc(value, limit):
+    """
+    Converts URLs into clickable links, truncating URLs to the given character limit,
+    and adding 'rel=nofollow' attribute to discourage spamming.
+
+    Argument: Length to truncate URLs to.
+    """
+    from djangohtml import urlize, fix_ampersands
+    return XML(fix_ampersands(urlize(value, trim_url_limit=int(limit), nofollow=True)))
+
+def wordcount(value):
+    "Returns the number of words"
+    return len(value.split())
+
+def wordwrap(value, arg):
+    """
+    Wraps words at specified line length
+
+    Argument: number of characters to wrap the text at.
+    """
+    from djangotext import wrap
+    return wrap(str(value), int(arg))
+
+def ljust(value, arg):
+    """
+    Left-aligns the value in a field of a given width
+
+    Argument: field size
+    """
+    return str(value).ljust(int(arg))
+
+def rjust(value, arg):
+    """
+    Right-aligns the value in a field of a given width
+
+    Argument: field size
+    """
+    return str(value).rjust(int(arg))
+
+def center(value, arg):
+    "Centers the value in a field of a given width"
+    return str(value).center(int(arg))
+
+def cut(value, arg):
+    "Removes all values of arg from the given string"
+    return value.replace(arg, '')
+
+###################
+# HTML STRINGS    #
+###################
+
+def escape(value):
+    "Escapes a string's HTML"
+    from djangohtml import escape
+    return escape(value)
+
+def linebreaks(value):
+    "Converts newlines into <p> and <br />s"
+    from djangohtml import linebreaks
+    return linebreaks(value)
+
+def linebreaksbr(value):
+    "Converts newlines into <br />s"
+    return value.replace('\n', '<br />')
+
+def removetags(value, tags):
+    "Removes a space separated list of [X]HTML tags from the output"
+    tags = [re.escape(tag) for tag in tags.split()]
+    tags_re = '(%s)' % '|'.join(tags)
+    starttag_re = re.compile(r'<%s(/?>|(\s+[^>]*>))' % tags_re)
+    endtag_re = re.compile('</%s>' % tags_re)
+    value = starttag_re.sub('', value)
+    value = endtag_re.sub('', value)
+    return value
+
+def striptags(value):
+    "Strips all [X]HTML tags"
+    from djangohtml import strip_tags
+    if not isinstance(value, basestring):
+        value = str(value)
+    return strip_tags(value)
+
+###################
+# LISTS           #
+###################
+
+def dictsort(value, arg):
+    """
+    Takes a list of dicts, returns that list sorted by the property given in
+    the argument.
+    """
+    raise NotImplementedError
+
+def dictsortreversed(value, arg):
+    """
+    Takes a list of dicts, returns that list sorted in reverse order by the
+    property given in the argument.
+    """
+    decorated = [(resolve_variable('var.' + arg, {'var' : item}), item) for item in value]
+    decorated.sort()
+    decorated.reverse()
+    return [item[1] for item in decorated]
+
+def first(value):
+    "Returns the first item in a list"
+    try:
+        return value[0]
+    except IndexError:
+        return ''
+
+def join(value, arg):
+    "Joins a list with a string, like Python's ``str.join(list)``"
+    try:
+        return arg.join(map(str, value))
+    except AttributeError: # fail silently but nicely
+        return value
+
+def length(value):
+    "Returns the length of the value - useful for lists"
+    return len(value)
+
+def length_is(value, arg):
+    "Returns a boolean of whether the value's length is the argument"
+    return len(value) == int(arg)
+
+def random(value):
+    "Returns a random item from the list"
+    return random_module.choice(value)
+
+def slice_(value, arg):
+    """
+    Returns a slice of the list.
+
+    Uses the same syntax as Python's list slicing; see
+    http://diveintopython.org/native_data_types/lists.html#odbchelper.list.slice
+    for an introduction.
+    """
+    try:
+        bits = []
+        for x in arg.split(':'):
+            if len(x) == 0:
+                bits.append(None)
+            else:
+                bits.append(int(x))
+        return value[slice(*bits)]
+
+    except (ValueError, TypeError):
+        return value # Fail silently.
+
+def unordered_list(value):
+    """
+    Recursively takes a self-nested list and returns an HTML unordered list --
+    WITHOUT opening and closing <ul> tags.
+
+    The list is assumed to be in the proper format. For example, if ``var`` contains
+    ``['States', [['Kansas', [['Lawrence', []], ['Topeka', []]]], ['Illinois', []]]]``,
+    then ``{{ var|unordered_list }}`` would return::
+
+        <li>States
+        <ul>
+                <li>Kansas
+                <ul>
+                        <li>Lawrence</li>
+                        <li>Topeka</li>
+                </ul>
+                </li>
+                <li>Illinois</li>
+        </ul>
+        </li>
+    """
+    def _helper(value, tabs):
+        indent = '\t' * tabs
+        if value[1]:
+            return '%s<li>%s\n%s<ul>\n%s\n%s</ul>\n%s</li>' % (indent, value[0], indent,
+                '\n'.join([_helper(v, tabs+1) for v in value[1]]), indent, indent)
+        else:
+            return '%s<li>%s</li>' % (indent, value[0])
+    return _helper(value, 1)
+
+###################
+# INTEGERS        #
+###################
+
+def add(value, arg):
+    "Adds the arg to the value"
+    return int(value) + int(arg)
+
+def get_digit(value, arg):
+    """
+    Given a whole number, returns the requested digit of it, where 1 is the
+    right-most digit, 2 is the second-right-most digit, etc. Returns the
+    original value for invalid input (if input or argument is not an integer,
+    or if argument is less than 1). Otherwise, output is always an integer.
+    """
+    try:
+        arg = int(arg)
+        value = int(value)
+    except ValueError:
+        return value # Fail silently for an invalid argument
+    if arg < 1:
+        return value
+    try:
+        return int(str(value)[-arg])
+    except IndexError:
+        return 0
+
+###################
+# DATES           #
+###################
+
+def date(value, arg=None):
+    "Formats a date according to the given format"
+    raise NotImplementedError
+
+def time(value, arg=None):
+    "Formats a time according to the given format"
+    raise NotImplementedError
+
+def timesince(value, arg=None):
+    'Formats a date as the time since that date (i.e. "4 days, 6 hours")'
+    raise NotImplementedError
+
+def timeuntil(value, arg=None):
+    'Formats a date as the time until that date (i.e. "4 days, 6 hours")'
+    raise NotImplementedError
+
+###################
+# LOGIC           #
+###################
+
+def default(value, arg):
+    "If value is unavailable, use given default"
+    return value or arg
+
+def default_if_none(value, arg):
+    "If value is None, use given default"
+    if value is None:
+        return arg
+    return value
+
+def divisibleby(value, arg):
+    "Returns true if the value is devisible by the argument"
+    return int(value) % int(arg) == 0
+
+def yesno(value, arg=None):
+    """
+    Given a string mapping values for true, false and (optionally) None,
+    returns one of those strings accoding to the value:
+
+    ==========  ======================  ==================================
+    Value       Argument                Outputs
+    ==========  ======================  ==================================
+    ``True``    ``"yeah,no,maybe"``     ``yeah``
+    ``False``   ``"yeah,no,maybe"``     ``no``
+    ``None``    ``"yeah,no,maybe"``     ``maybe``
+    ``None``    ``"yeah,no"``           ``"no"`` (converts None to False
+                                        if no mapping for None is given.
+    ==========  ======================  ==================================
+    """
+    if arg is None:
+        arg = 'yes,no,maybe'
+    bits = arg.split(',')
+    if len(bits) < 2:
+        return value # Invalid arg.
+    try:
+        yes, no, maybe = bits
+    except ValueError: # unpack list of wrong size (no "maybe" value provided)
+        yes, no, maybe = bits[0], bits[1], bits[1]
+    if value is None:
+        return maybe
+    if value:
+        return yes
+    return no
+
+###################
+# MISC            #
+###################
+
+def filesizeformat(bytes):
+    """
+    Format the value like a 'human-readable' file size (i.e. 13 KB, 4.1 MB, 102
+    bytes, etc).
+    """
+    bytes = float(bytes)
+    if bytes < 1024:
+        return "%d byte%s" % (bytes, bytes != 1 and 's' or '')
+    if bytes < 1024 * 1024:
+        return "%.1f KB" % (bytes / 1024)
+    if bytes < 1024 * 1024 * 1024:
+        return "%.1f MB" % (bytes / (1024 * 1024))
+    return "%.1f GB" % (bytes / (1024 * 1024 * 1024))
+
+def pluralize(value, arg='s'):
+    """
+    Returns a plural suffix if the value is not 1, for '1 vote' vs. '2 votes'
+    By default, 's' is used as a suffix; if an argument is provided, that string
+    is used instead. If the provided argument contains a comma, the text before
+    the comma is used for the singular case.
+    """
+    if not ',' in arg: 
+        arg = ',' + arg
+    bits = arg.split(',')
+    if len(bits) > 2:
+        return ''
+    singular_suffix, plural_suffix = bits[:2]
+
+    try:
+        if int(value) != 1:
+            return plural_suffix
+    except ValueError: # invalid string that's not a number
+        pass
+    except TypeError: # value isn't a string or a number; maybe it's a list?
+        try:
+            if len(value) != 1:
+                return plural_suffix
+        except TypeError: # len() of unsized object
+            pass
+    return singular_suffix
+
+def phone2numeric(value):
+    "Takes a phone number and converts it in to its numerical equivalent"
+    from djangotext import phone2numeric
+    return phone2numeric(value)
+
+def pprint(value):
+    "A wrapper around pprint.pprint -- for debugging, really"
+    from pprint import pformat
+    try:
+        return pformat(value)
+    except Exception, e:
+        return "Error in formatting:%s" % e
+
diff -rNu genshi.orig/genshi/template/eval.py genshi/genshi/template/eval.py
--- genshi.orig/genshi/template/eval.py	2006-12-06 08:25:16.000000000 +0200
+++ genshi/genshi/template/eval.py	2006-12-06 09:29:48.000000000 +0200
@@ -26,6 +26,8 @@
 
 __all__ = ['Expression', 'Undefined']
 
+import re
+_filter_expr = re.compile(r"\s*(\w+)(\s.*)?")
 
 class Expression(object):
     """Evaluates Python expressions used in templates.
@@ -74,14 +76,26 @@
         """Create the expression, either from a string, or from an AST node.
         
         @param source: either a string containing the source code of the
-            expression, or an AST node
+            expression (possibly with pipes syntax), or an AST node
         @param filename: the (preferably absolute) name of the file containing
             the expression
         @param lineno: the number of the line on which the expression was found
         """
         if isinstance(source, basestring):
-            self.source = source
-            self.code = _compile(_parse(source), self.source, filename=filename,
+            filters = source.split('||')
+            if len(filters)>1: 
+                filters = ([filters[0]] + 
+                    [_filter_expr.match(filter).groups() for
+                        filter in filters[1:]])
+
+                source = reduce(lambda input, filter:
+                    '_filterslib.%s(%s%s)' % (filter[0], input,
+                        (filter[1] and ', '+filter[1]) or ''),
+                    filters)
+
+            # unescape
+            self.source = source.replace(r'\|\|', '||')
+            self.code = _compile(_parse(self.source), self.source, filename=filename,
                                  lineno=lineno)
         else:
             assert isinstance(source, ast.Node)
@@ -199,8 +213,11 @@
                     '<Expression %s>' % (repr(source or '?').replace("'", '"')),
                     lineno, code.co_lnotab, (), ())
 
+import genshi.pipes.filterslib as _filterslib
+
 BUILTINS = __builtin__.__dict__.copy()
 BUILTINS['Undefined'] = Undefined
+BUILTINS['_filterslib'] = _filterslib
 _UNDEF = Undefined(None)
 
 def _lookup_name(data, name):
diff -rNu genshi.orig/genshi/template/tests/eval.py genshi/genshi/template/tests/eval.py
--- genshi.orig/genshi/template/tests/eval.py	2006-12-06 08:25:16.000000000 +0200
+++ genshi/genshi/template/tests/eval.py	2006-12-06 09:20:00.000000000 +0200
@@ -381,6 +381,25 @@
         expr = Expression("nothing[0]", filename='index.html', lineno=50)
         self.assertRaises(TypeError, expr.evaluate, {'nothing': object()})
 
+    def test_pipes_escaping(self):
+        expr = Expression("x+'\|\|'")
+        self.assertEquals('hello||', expr.evaluate({'x': 'hello'}))
+
+    def test_pipes_upper(self):
+        expr = Expression("x || upper'")
+        self.assertEquals('HELLO', expr.evaluate({'x': 'hello'}))
+
+    def test_pipes_arguments(self):
+        expr = Expression("x || add 4")
+        self.assertEquals(7, expr.evaluate({'x': 3}))
+
+    def test_two_pipes(self):
+        expr = Expression("x || add 4 || add 2")
+        self.assertEquals(9, expr.evaluate({'x': 3}))
+
+    def test_yes_no(self):
+        expr = Expression("x || yesno'")
+        self.assertEquals('yes', expr.evaluate({'x': True}))
 
 def suite():
     suite = unittest.TestSuite()
diff -rNu genshi.orig/genshi/template/tests/markup.py genshi/genshi/template/tests/markup.py
--- genshi.orig/genshi/template/tests/markup.py	2006-12-06 08:25:16.000000000 +0200
+++ genshi/genshi/template/tests/markup.py	2006-12-06 09:25:37.000000000 +0200
@@ -61,6 +61,14 @@
         tmpl = MarkupTemplate('<root>$foo</root>')
         self.assertEqual('<root>buzz</root>', str(tmpl.generate(foo=('buzz',))))
 
+    def test_interpolate_with_pipes(self):
+        tmpl = MarkupTemplate('<root>${ foo || lower } and ${ t || yesno }</root>')
+        self.assertEqual('<root>buzz and yes</root>', str(tmpl.generate(foo='BUzZ', t=True)))
+
+    def test_interpolate_with_pipe_escape(self):
+        tmpl = MarkupTemplate('<root>${ foo + "\|\|" }</root>')
+        self.assertEqual('<root>buzz||</root>', str(tmpl.generate(foo='buzz')))
+
     def test_empty_attr(self):
         tmpl = MarkupTemplate('<root attr=""/>')
         self.assertEqual('<root attr=""/>', str(tmpl.generate()))
diff -rNu genshi.orig/setup.py genshi/setup.py
--- genshi.orig/setup.py	2006-12-06 08:25:17.000000000 +0200
+++ genshi/setup.py	2006-12-06 12:54:32.000000000 +0200
@@ -90,7 +90,7 @@
         'Topic :: Text Processing :: Markup :: XML'
     ],
     keywords = ['python.templating.engines'],
-    packages = ['genshi', 'genshi.template'],
+    packages = ['genshi', 'genshi.template', 'genshi.pipes'],
     test_suite = 'genshi.tests.suite',
 
     extras_require = {'plugin': ['setuptools>=0.6a2']},

