*new in Jinja 1.1*
+`watchchanges`
+
+ Jinja does not provide an django like ``{% ifchanged %}`` tag. As
+ replacement for this tag there is a special function in the namespace
+ called `watchchanges`.
+
+ You can use it like this:
+
+ .. sourcecode:: html+jinja
+
+ {% for changed, article in watchchanges(articles, 'date', 'day') %}
+ {% if changed %}<h3>{{ articles.date.day }}</h3>{% endif %}
+ <h4>{{ article.title|e }}</h4>
+ <p>{{ article.body|e }}</p>
+ {% endif %}
+
+ For each iteration `watchchanges` will check the given attribute. If it
+ changed to the former iteration the first yielded item (in this example
+ it's called `changed`) will be `true`, else `false`.
+
+ In this example `articles` is a list of articles for the template with
+ an attribute called `date.day` which represents the current day. To only
+ add a new day headline if the day has changed `watchchanges` is now told
+ to check `articles.date.days`.
+
+ If you want to observe more than one attribute you can provide pairs:
+
+ .. sourcecode:: html+jinja
+
+ {% for changed, item in watchchanges(foo, ('a', 'b'), ('a', 'c')) %}
+ ...
+ {% endfor %}
+
+ Note that if you want to watch two first level attributes you have to
+ either use the list syntax `[]` or add a colon:
+
+ .. sourcecode:: html+jinja
+
+ {% for changed, item in watchchanges(foo, ['a'], ('b',)) %}
+ ...
+ {% endfor %}
+
+ otherwise Jinja cannot differ between a pair of parentheses to group
+ expressions or the sequence syntax.
+
+ If you don't provide any arguments the value of the variable itself
+ is checked.
+
+ *new in Jinja 1.1*
+
Loops
=====
"""
from jinja.filters import FILTERS as DEFAULT_FILTERS
from jinja.tests import TESTS as DEFAULT_TESTS
-from jinja.utils import debug_context, safe_range, generate_lorem_ipsum
+from jinja.utils import debug_context, safe_range, generate_lorem_ipsum, \
+ watch_changes
DEFAULT_NAMESPACE = {
'range': safe_range,
'debug': debug_context,
- 'lipsum': generate_lorem_ipsum
+ 'lipsum': generate_lorem_ipsum,
+ 'watchchanges': watch_changes
}
pass
return Undefined
+ def get_attributes(self, obj, attributes):
+ """
+ Get some attributes from an object. If attributes is an
+ empty sequence the object is returned as it.
+ """
+ get = self.get_attribute
+ for name in attributes:
+ obj = get(obj, name)
+ return obj
+
def call_function(self, f, context, args, kwargs, dyn_args, dyn_kwargs):
"""
Function call helper. Called for all functions that are passed
],
# comments
'comment_begin': [
- (c(r'(.*?)(\-%s\s*|%s)' % (
+ (c(r'(.*?)((?:\-%s\s*|%s)%s)' % (
e(environment.comment_end_string),
- e(environment.comment_end_string)
+ e(environment.comment_end_string),
+ block_suffix_re
)), ('comment', 'comment_end'), '#pop'),
(c('(.)'), (Failure('Missing end of comment tag'),), None)
],
# directives
'block_begin': [
- (c('\-%s\s*|%s' % (
+ (c('(?:\-%s\s*|%s)%s' % (
e(environment.block_end_string),
- e(environment.block_end_string)
+ e(environment.block_end_string),
+ block_suffix_re
)), 'block_end', '#pop'),
] + tag_rules,
# variables
return self.extends is not None and [self.extends] or []
def __repr__(self):
- return 'Template(%r, %r, %r)' % (
+ return 'Template(%r, %r, %s)' % (
self.filename,
self.extends,
- NodeList.__repr__(self)
+ list.__repr__(self)
)
tokens = []
for t_lineno, t_token, t_data in gen:
if t_token == 'string':
- tokens.append('u' + t_data)
- else:
- tokens.append(t_data)
+ # because some libraries have problems with unicode
+ # objects we do some lazy unicode processing here.
+ # if a string is ASCII only we yield it as string
+ # in other cases as unicode. This works around
+ # problems with datetimeobj.strftime()
+ try:
+ str(t_data)
+ except UnicodeError:
+ tokens.append('u' + t_data)
+ continue
+ tokens.append(t_data)
source = '\xef\xbb\xbf' + (template % (u' '.join(tokens)).
encode('utf-8'))
try:
# handle real loop code
self.indention += 1
write(self.nodeinfo(node.body))
- buf.append(self.handle_node(node.body) or self.indent('pass'))
+ if node.body:
+ buf.append(self.handle_node(node.body))
+ else:
+ write('pass')
self.indention -= 1
# else part of loop
#: number of maximal range items
MAX_RANGE = 1000000
-_integer_re = re.compile('^(\d+)$')
-
_word_split_re = re.compile(r'(\s+)')
_punctuation_re = re.compile(
return u'\n'.join([u'<p>%s</p>' % escape(x) for x in result])
-# python2.4 and lower has a bug regarding joining of broken generators
+def watch_changes(env, context, iterable, *attributes):
+ """
+ Wise replacement for ``{% ifchanged %}``.
+ """
+ # find the attributes to watch
+ if attributes:
+ tests = []
+ tmp = []
+ for attribute in attributes:
+ if isinstance(attribute, (str, unicode, int, long, bool)):
+ tmp.append(attribute)
+ else:
+ tests.append(tuple(attribute))
+ if tmp:
+ tests.append(tuple(attribute))
+ last = tuple([object() for x in tests])
+ # or no attributes if we watch the object itself
+ else:
+ tests = None
+ last = object()
+
+ # iterate trough it and keep check the attributes or values
+ for item in iterable:
+ if tests is None:
+ cur = item
+ else:
+ cur = tuple([env.get_attributes(item, x) for x in tests])
+ if cur != last:
+ changed = True
+ last = cur
+ else:
+ changed = False
+ yield changed, item
+watch_changes.jinja_context_callable = True
+
+
+# python2.4 and lower has a bug regarding joining of broken generators.
# because of the runtime debugging system we have to keep track of the
# number of frames to skip. that's what RUNTIME_EXCEPTION_OFFSET is for.
if sys.version_info < (2, 5):