- Added `urlencode` filter that automatically quotes values for
URL safe usage with utf-8 as only supported encoding. If applications
want to change this encoding they can override the filter.
+- Added `keep-trailing-newline` configuration to environments and
+ templates to optionally preserve the final trailing newline.
- Accessing `last` on the loop context no longer causes the iterator
to be consumed into a list.
+- Python requirement changed: 2.6, 2.7 or >= 3.3 are required now,
+ supported by same source code, using the "six" compatibility library.
+- Allow `contextfunction` and other decorators to be applied to `__call__`.
+- Added support for changing from newline to different signs in the `wordwrap`
+ filter.
+- Added support for ignoring memcache errors silently.
++- Added support for keeping the trailing newline in templates.
Version 2.6
-----------
In the default configuration, a single trailing newline is stripped if
present, and whitespace is not further modified by the template engine. Each
whitespace (spaces, tabs, newlines etc.) is returned unchanged. If the
--application configures Jinja to `trim_blocks` the first newline after a a
- template tag is removed automatically (like in PHP). To keep the
- single trailing newline when it is present, configure Jinja to
- `keep_trailing_newline`.
-
- But you can also strip whitespace in templates by hand. If you put an minus
++application configures Jinja to `trim_blocks` the first newline after a
+ template tag is removed automatically (like in PHP). The `lstrip_blocks`
+ option can also be set to strip tabs and spaces from the beginning of
+ line to the start of a block. (Nothing will be stripped if there are
+ other characters before the start of the block.)
+
+ With both `trim_blocks` and `lstrip_blocks` enabled you can put block tags
+ on their own lines, and the entire block line will be removed when
+ rendered, preserving the whitespace of the contents. For example,
+ without the `trim_blocks` and `lstrip_blocks` options, this template::
+
+ <div>
+ {% if True %}
+ yay
+ {% endif %}
+ </div>
+
+ gets rendered with blank lines inside the div::
+
+ <div>
+
+ yay
+
+ </div>
+
+ But with both `trim_blocks` and `lstrip_blocks` enabled, the lines with the
+ template blocks are removed while preserving the whitespace of the contents::
+
+ <div>
+ yay
+ </div>
+
+ You can manually disable the `lstrip_blocks` behavior by putting a
+ plus sign (``+``) at the start of a block::
+
+ <div>
+ {%+ if something %}yay{% endif %}
+ </div>
+
+ You can also strip whitespace in templates by hand. If you put an minus
sign (``-``) to the start or end of an block (for example a for tag), a
comment or variable expression you can remove the whitespaces after or before
that block::
If :ref:`line-statements` are enabled they strip leading whitespace
automatically up to the beginning of the line.
++Jinja2 by default also removes trailing newlines. To keep the single
++trailing newline when it is present, configure Jinja to
++`keep_trailing_newline`.
++
.. admonition:: Note
You must not use a whitespace between the tag and the minus sign.
LINE_STATEMENT_PREFIX = None
LINE_COMMENT_PREFIX = None
TRIM_BLOCKS = False
+ LSTRIP_BLOCKS = False
NEWLINE_SEQUENCE = '\n'
+KEEP_TRAILING_NEWLINE = False
# default filters, tests and namespace
import os
import sys
from jinja2 import nodes
-from jinja2.defaults import *
+from jinja2.defaults import BLOCK_START_STRING, \
+ BLOCK_END_STRING, VARIABLE_START_STRING, VARIABLE_END_STRING, \
+ COMMENT_START_STRING, COMMENT_END_STRING, LINE_STATEMENT_PREFIX, \
+ LINE_COMMENT_PREFIX, TRIM_BLOCKS, NEWLINE_SEQUENCE, \
+ DEFAULT_FILTERS, DEFAULT_TESTS, DEFAULT_NAMESPACE, \
- KEEP_TRAILING_NEWLINE
++ KEEP_TRAILING_NEWLINE, LSTRIP_BLOCKS
from jinja2.lexer import get_lexer, TokenStream
from jinja2.parser import Parser
from jinja2.optimizer import optimize
line_statement_prefix=LINE_STATEMENT_PREFIX,
line_comment_prefix=LINE_COMMENT_PREFIX,
trim_blocks=TRIM_BLOCKS,
+ lstrip_blocks=LSTRIP_BLOCKS,
newline_sequence=NEWLINE_SEQUENCE,
+ keep_trailing_newline=KEEP_TRAILING_NEWLINE,
extensions=(),
optimized=True,
undefined=Undefined,
self.line_statement_prefix = line_statement_prefix
self.line_comment_prefix = line_comment_prefix
self.trim_blocks = trim_blocks
+ self.lstrip_blocks = lstrip_blocks
self.newline_sequence = newline_sequence
+ self.keep_trailing_newline = keep_trailing_newline
# runtime information
self.undefined = undefined
line_statement_prefix=LINE_STATEMENT_PREFIX,
line_comment_prefix=LINE_COMMENT_PREFIX,
trim_blocks=TRIM_BLOCKS,
+ lstrip_blocks=LSTRIP_BLOCKS,
newline_sequence=NEWLINE_SEQUENCE,
+ keep_trailing_newline=KEEP_TRAILING_NEWLINE,
extensions=(),
optimized=True,
undefined=Undefined,
block_start_string, block_end_string, variable_start_string,
variable_end_string, comment_start_string, comment_end_string,
line_statement_prefix, line_comment_prefix, trim_blocks,
- newline_sequence, keep_trailing_newline, frozenset(extensions),
- optimized, undefined, finalize, autoescape, None, 0, False, None)
- lstrip_blocks,
- newline_sequence, frozenset(extensions), optimized, undefined,
- finalize, autoescape, None, 0, False, None)
++ lstrip_blocks, newline_sequence, keep_trailing_newline,
++ frozenset(extensions), optimized, undefined, finalize, autoescape,
++ None, 0, False, None)
return env.from_string(source, template_class=cls)
@classmethod
:copyright: (c) 2010 by the Jinja Team.
:license: BSD.
"""
-from collections import deque
from jinja2 import nodes
--from jinja2.defaults import *
++from jinja2.defaults import BLOCK_START_STRING, \
++ BLOCK_END_STRING, VARIABLE_START_STRING, VARIABLE_END_STRING, \
++ COMMENT_START_STRING, COMMENT_END_STRING, LINE_STATEMENT_PREFIX, \
++ LINE_COMMENT_PREFIX, TRIM_BLOCKS, NEWLINE_SEQUENCE, \
++ KEEP_TRAILING_NEWLINE, LSTRIP_BLOCKS
from jinja2.environment import Environment
-from jinja2.runtime import Undefined, concat
+from jinja2.runtime import concat
from jinja2.exceptions import TemplateAssertionError, TemplateSyntaxError
-from jinja2.utils import contextfunction, import_string, Markup, next
+from jinja2.utils import contextfunction, import_string, Markup
+import six
# the only real useful gettext functions for a Jinja template. Note
options.get('line_statement_prefix') or LINE_STATEMENT_PREFIX,
options.get('line_comment_prefix') or LINE_COMMENT_PREFIX,
getbool(options, 'trim_blocks', TRIM_BLOCKS),
- NEWLINE_SEQUENCE, frozenset(extensions),
++ getbool(options, 'lstrip_blocks', LSTRIP_BLOCKS),
+ NEWLINE_SEQUENCE,
+ getbool(options, 'keep_trailing_newline', KEEP_TRAILING_NEWLINE),
+ frozenset(extensions),
cache_size=0,
auto_reload=False
)
environment.line_statement_prefix,
environment.line_comment_prefix,
environment.trim_blocks,
- environment.newline_sequence)
+ environment.lstrip_blocks,
+ environment.newline_sequence,
+ environment.keep_trailing_newline)
lexer = _lexer_cache.get(key)
if lexer is None:
lexer = Lexer(environment)
# block suffix if trimming is enabled
block_suffix_re = environment.trim_blocks and '\\n?' or ''
+ # strip leading spaces if lstrip_blocks is enabled
+ prefix_re = {}
+ if environment.lstrip_blocks:
+ # use '{%+' to manually disable lstrip_blocks behavior
+ no_lstrip_re = e('+')
+ # detect overlap between block and variable or comment strings
+ block_diff = c(r'^%s(.*)' % e(environment.block_start_string))
+ # make sure we don't mistake a block for a variable or a comment
+ m = block_diff.match(environment.comment_start_string)
+ no_lstrip_re += m and r'|%s' % e(m.group(1)) or ''
+ m = block_diff.match(environment.variable_start_string)
+ no_lstrip_re += m and r'|%s' % e(m.group(1)) or ''
+
+ # detect overlap between comment and variable strings
+ comment_diff = c(r'^%s(.*)' % e(environment.comment_start_string))
+ m = comment_diff.match(environment.variable_start_string)
+ no_variable_re = m and r'(?!%s)' % e(m.group(1)) or ''
+
+ lstrip_re = r'^[ \t]*'
+ block_prefix_re = r'%s%s(?!%s)|%s\+?' % (
+ lstrip_re,
+ e(environment.block_start_string),
+ no_lstrip_re,
+ e(environment.block_start_string),
+ )
+ comment_prefix_re = r'%s%s%s|%s\+?' % (
+ lstrip_re,
+ e(environment.comment_start_string),
+ no_variable_re,
+ e(environment.comment_start_string),
+ )
+ prefix_re['block'] = block_prefix_re
+ prefix_re['comment'] = comment_prefix_re
+ else:
+ block_prefix_re = '%s' % e(environment.block_start_string)
+
self.newline_sequence = environment.newline_sequence
+ self.keep_trailing_newline = environment.keep_trailing_newline
# global lexing rules
self.rules = {
assert tmpl.render(foo={'bar': 42}) == '42'
+ class LstripBlocksTestCase(JinjaTestCase):
+
+ def test_lstrip(self):
+ env = Environment(lstrip_blocks=True, trim_blocks=False)
+ tmpl = env.from_string(''' {% if True %}\n {% endif %}''')
+ assert tmpl.render() == "\n"
+
+ def test_lstrip_trim(self):
+ env = Environment(lstrip_blocks=True, trim_blocks=True)
+ tmpl = env.from_string(''' {% if True %}\n {% endif %}''')
+ assert tmpl.render() == ""
+
+ def test_no_lstrip(self):
+ env = Environment(lstrip_blocks=True, trim_blocks=False)
+ tmpl = env.from_string(''' {%+ if True %}\n {%+ endif %}''')
+ assert tmpl.render() == " \n "
+
+ def test_lstrip_endline(self):
+ env = Environment(lstrip_blocks=True, trim_blocks=False)
+ tmpl = env.from_string(''' hello{% if True %}\n goodbye{% endif %}''')
+ assert tmpl.render() == " hello\n goodbye"
+
+ def test_lstrip_inline(self):
+ env = Environment(lstrip_blocks=True, trim_blocks=False)
+ tmpl = env.from_string(''' {% if True %}hello {% endif %}''')
+ assert tmpl.render() == 'hello '
+
+ def test_lstrip_nested(self):
+ env = Environment(lstrip_blocks=True, trim_blocks=False)
+ tmpl = env.from_string(''' {% if True %}a {% if True %}b {% endif %}c {% endif %}''')
+ assert tmpl.render() == 'a b c '
+
+ def test_lstrip_left_chars(self):
+ env = Environment(lstrip_blocks=True, trim_blocks=False)
+ tmpl = env.from_string(''' abc {% if True %}
+ hello{% endif %}''')
+ assert tmpl.render() == ' abc \n hello'
+
+ def test_lstrip_embeded_strings(self):
+ env = Environment(lstrip_blocks=True, trim_blocks=False)
+ tmpl = env.from_string(''' {% set x = " {% str %} " %}{{ x }}''')
+ assert tmpl.render() == ' {% str %} '
+
+ def test_lstrip_preserve_leading_newlines(self):
+ env = Environment(lstrip_blocks=True, trim_blocks=False)
+ tmpl = env.from_string('''\n\n\n{% set hello = 1 %}''')
+ assert tmpl.render() == '\n\n\n'
+
+ def test_lstrip_comment(self):
+ env = Environment(lstrip_blocks=True, trim_blocks=False)
+ tmpl = env.from_string(''' {# if True #}
+ hello
+ {#endif#}''')
+ assert tmpl.render() == '\nhello\n'
+
+ def test_lstrip_angle_bracket_simple(self):
+ env = Environment('<%', '%>', '${', '}', '<%#', '%>', '%', '##',
+ lstrip_blocks=True, trim_blocks=True)
+ tmpl = env.from_string(''' <% if True %>hello <% endif %>''')
+ assert tmpl.render() == 'hello '
+
+ def test_lstrip_angle_bracket_comment(self):
+ env = Environment('<%', '%>', '${', '}', '<%#', '%>', '%', '##',
+ lstrip_blocks=True, trim_blocks=True)
+ tmpl = env.from_string(''' <%# if True %>hello <%# endif %>''')
+ assert tmpl.render() == 'hello '
+
+ def test_lstrip_angle_bracket(self):
+ env = Environment('<%', '%>', '${', '}', '<%#', '%>', '%', '##',
+ lstrip_blocks=True, trim_blocks=True)
+ tmpl = env.from_string('''\
+ <%# regular comment %>
+ <% for item in seq %>
+ ${item} ## the rest of the stuff
+ <% endfor %>''')
+ assert tmpl.render(seq=range(5)) == \
+ ''.join('%s\n' % x for x in range(5))
+
+ def test_lstrip_angle_bracket_compact(self):
+ env = Environment('<%', '%>', '${', '}', '<%#', '%>', '%', '##',
+ lstrip_blocks=True, trim_blocks=True)
+ tmpl = env.from_string('''\
+ <%#regular comment%>
+ <%for item in seq%>
+ ${item} ## the rest of the stuff
+ <%endfor%>''')
+ assert tmpl.render(seq=range(5)) == \
+ ''.join('%s\n' % x for x in range(5))
+
+ def test_php_syntax_with_manual(self):
+ env = Environment('<?', '?>', '<?=', '?>', '<!--', '-->',
+ lstrip_blocks=True, trim_blocks=True)
+ tmpl = env.from_string('''\
+ <!-- I'm a comment, I'm not interesting -->
+ <? for item in seq -?>
+ <?= item ?>
+ <?- endfor ?>''')
+ assert tmpl.render(seq=range(5)) == '01234'
+
+ def test_php_syntax(self):
+ env = Environment('<?', '?>', '<?=', '?>', '<!--', '-->',
+ lstrip_blocks=True, trim_blocks=True)
+ tmpl = env.from_string('''\
+ <!-- I'm a comment, I'm not interesting -->
+ <? for item in seq ?>
+ <?= item ?>
+ <? endfor ?>''')
+ assert tmpl.render(seq=range(5)) == ''.join(' %s\n' % x for x in range(5))
+
+ def test_php_syntax_compact(self):
+ env = Environment('<?', '?>', '<?=', '?>', '<!--', '-->',
+ lstrip_blocks=True, trim_blocks=True)
+ tmpl = env.from_string('''\
+ <!-- I'm a comment, I'm not interesting -->
+ <?for item in seq?>
+ <?=item?>
+ <?endfor?>''')
+ assert tmpl.render(seq=range(5)) == ''.join(' %s\n' % x for x in range(5))
+
+ def test_erb_syntax(self):
+ env = Environment('<%', '%>', '<%=', '%>', '<%#', '%>',
+ lstrip_blocks=True, trim_blocks=True)
+ #env.from_string('')
+ #for n,r in env.lexer.rules.iteritems():
+ # print n
+ #print env.lexer.rules['root'][0][0].pattern
+ #print "'%s'" % tmpl.render(seq=range(5))
+ tmpl = env.from_string('''\
+ <%# I'm a comment, I'm not interesting %>
+ <% for item in seq %>
+ <%= item %>
+ <% endfor %>
+ ''')
+ assert tmpl.render(seq=range(5)) == ''.join(' %s\n' % x for x in range(5))
+
+ def test_erb_syntax_with_manual(self):
+ env = Environment('<%', '%>', '<%=', '%>', '<%#', '%>',
+ lstrip_blocks=True, trim_blocks=True)
+ tmpl = env.from_string('''\
+ <%# I'm a comment, I'm not interesting %>
+ <% for item in seq -%>
+ <%= item %>
+ <%- endfor %>''')
+ assert tmpl.render(seq=range(5)) == '01234'
+
+ def test_erb_syntax_no_lstrip(self):
+ env = Environment('<%', '%>', '<%=', '%>', '<%#', '%>',
+ lstrip_blocks=True, trim_blocks=True)
+ tmpl = env.from_string('''\
+ <%# I'm a comment, I'm not interesting %>
+ <%+ for item in seq -%>
+ <%= item %>
+ <%- endfor %>''')
+ assert tmpl.render(seq=range(5)) == ' 01234'
+
+ def test_comment_syntax(self):
+ env = Environment('<!--', '-->', '${', '}', '<!--#', '-->',
+ lstrip_blocks=True, trim_blocks=True)
+ tmpl = env.from_string('''\
+ <!--# I'm a comment, I'm not interesting -->\
+ <!-- for item in seq --->
+ ${item}
+ <!--- endfor -->''')
+ assert tmpl.render(seq=range(5)) == '01234'
+
def suite():
suite = unittest.TestSuite()
+ suite.addTest(unittest.makeSuite(TokenStreamTestCase))
suite.addTest(unittest.makeSuite(LexerTestCase))
suite.addTest(unittest.makeSuite(ParserTestCase))
suite.addTest(unittest.makeSuite(SyntaxTestCase))