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 = ['(', '<', '<'] +TRAILING_PUNCTUATION = ['.', ',', ')', '>', '\n', '>'] + +# list of possible strings used for bullets in bulleted lists +DOTS = ['·', '*', '\xe2\x80\xa2', '•', '•', '•'] + +unencoded_ampersands_re = re.compile(r'&(?!(\w+|#\d+);)') +word_split_re = re.compile(r'(\s+)') +punctuation_re = re.compile('^(?P(?:%s)*)(?P.*?)(?P(?:%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'(]*?)target=[^\s>]+') +html_gunk_re = re.compile(r'(?:
|<\/i>|<\/b>|<\/em>|<\/strong>|<\/?smallcaps>|<\/?uppercase>)', re.IGNORECASE) +hard_coded_bullets_re = re.compile(r'((?:

(?:%s).*?[a-zA-Z].*?

\s*)+)' % '|'.join([re.escape(x) for x in DOTS]), re.DOTALL) +trailing_empty_content_re = re.compile(r'(?:

(?: |\s|
)*?

\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('&', '&').replace('<', '<').replace('>', '>').replace('"', '"').replace("'", ''') + +def linebreaks(value): + "Converts newlines into

and
s" + value = re.sub(r'\r\n|\r|\n', '\n', value) # normalize newlines + paras = re.split('\n{2,}', value) + paras = ['

%s

' % p.strip().replace('\n', '
') 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('&', 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 = '
%s' % (middle, nofollow_attr, trim_url(anchor)) + 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 ''.join(words) + +def clean_html(text): + """ + Cleans the given HTML. Specifically, it does the following: + * Converts and to and . + * Encodes all ampersands correctly. + * Removes all "target" attributes from tags. + * Removes extraneous HTML, such as presentational tags that open and + immediately close and
. + * Converts hard-coded bullets into HTML unordered lists. + * Removes stuff like "

  

", 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
tags. + text = link_target_attribute_re.sub('\\1', text) + # Trim stupid HTML such as
. + text = html_gunk_re.sub('', text) + # Convert hard-coded bullets into HTML unordered lists. + def replace_p_tags(match): + s = match.group().replace('

', '') + for d in DOTS: + s = s.replace('

%s' % d, '

  • ') + return '
      \n%s\n
    ' % s + text = hard_coded_bullets_re.sub(replace_p_tags, text) + # Remove stuff like "

      

    ", 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 ``&`` 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

    and
    s" + from djangohtml import linebreaks + return linebreaks(value) + +def linebreaksbr(value): + "Converts newlines into
    s" + return value.replace('\n', '
    ') + +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('' % 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

      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:: + +
    • States +
        +
      • Kansas +
          +
        • Lawrence
        • +
        • Topeka
        • +
        +
      • +
      • Illinois
      • +
      +
    • + """ + def _helper(value, tabs): + indent = '\t' * tabs + if value[1]: + return '%s
    • %s\n%s
        \n%s\n%s
      \n%s
    • ' % (indent, value[0], indent, + '\n'.join([_helper(v, tabs+1) for v in value[1]]), indent, indent) + else: + return '%s
    • %s
    • ' % (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 @@ '' % (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('$foo') self.assertEqual('buzz', str(tmpl.generate(foo=('buzz',)))) + def test_interpolate_with_pipes(self): + tmpl = MarkupTemplate('${ foo || lower } and ${ t || yesno }') + self.assertEqual('buzz and yes', str(tmpl.generate(foo='BUzZ', t=True))) + + def test_interpolate_with_pipe_escape(self): + tmpl = MarkupTemplate('${ foo + "\|\|" }') + self.assertEqual('buzz||', str(tmpl.generate(foo='buzz'))) + def test_empty_attr(self): tmpl = MarkupTemplate('') self.assertEqual('', 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']},