Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

lstrip_blocks feature: Smarter whitespace control #139

Closed
wants to merge 12 commits into from
40 changes: 38 additions & 2 deletions docs/templates.rst
Original file line number Original file line Diff line number Diff line change
Expand Up @@ -156,9 +156,45 @@ In the default configuration, a single trailing newline is stripped if
present, and whitespace is not further modified by the template engine. Each present, and whitespace is not further modified by the template engine. Each
whitespace (spaces, tabs, newlines etc.) is returned unchanged. If the whitespace (spaces, tabs, newlines etc.) is returned unchanged. If the
application configures Jinja to `trim_blocks` the first newline after a a application configures Jinja to `trim_blocks` the first newline after a a
template tag is removed automatically (like in PHP). 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>


But you can also strip whitespace in templates by hand. If you put an minus 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 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 comment or variable expression you can remove the whitespaces after or before
that block:: that block::
Expand Down
1 change: 1 addition & 0 deletions jinja2/defaults.py
Original file line number Original file line Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
LINE_STATEMENT_PREFIX = None LINE_STATEMENT_PREFIX = None
LINE_COMMENT_PREFIX = None LINE_COMMENT_PREFIX = None
TRIM_BLOCKS = False TRIM_BLOCKS = False
LSTRIP_BLOCKS = False
NEWLINE_SEQUENCE = '\n' NEWLINE_SEQUENCE = '\n'




Expand Down
11 changes: 10 additions & 1 deletion jinja2/environment.py
Original file line number Original file line Diff line number Diff line change
Expand Up @@ -134,6 +134,10 @@ class Environment(object):
If this is set to ``True`` the first newline after a block is If this is set to ``True`` the first newline after a block is
removed (block, not variable tag!). Defaults to `False`. removed (block, not variable tag!). Defaults to `False`.


`lstrip_blocks`
If this is set to ``True`` leading spaces and tabs are stripped
from the start of a line to a block. Defaults to `False`.

`newline_sequence` `newline_sequence`
The sequence that starts a newline. Must be one of ``'\r'``, The sequence that starts a newline. Must be one of ``'\r'``,
``'\n'`` or ``'\r\n'``. The default is ``'\n'`` which is a ``'\n'`` or ``'\r\n'``. The default is ``'\n'`` which is a
Expand Down Expand Up @@ -224,6 +228,7 @@ def __init__(self,
line_statement_prefix=LINE_STATEMENT_PREFIX, line_statement_prefix=LINE_STATEMENT_PREFIX,
line_comment_prefix=LINE_COMMENT_PREFIX, line_comment_prefix=LINE_COMMENT_PREFIX,
trim_blocks=TRIM_BLOCKS, trim_blocks=TRIM_BLOCKS,
lstrip_blocks=LSTRIP_BLOCKS,
newline_sequence=NEWLINE_SEQUENCE, newline_sequence=NEWLINE_SEQUENCE,
extensions=(), extensions=(),
optimized=True, optimized=True,
Expand Down Expand Up @@ -255,6 +260,7 @@ def __init__(self,
self.line_statement_prefix = line_statement_prefix self.line_statement_prefix = line_statement_prefix
self.line_comment_prefix = line_comment_prefix self.line_comment_prefix = line_comment_prefix
self.trim_blocks = trim_blocks self.trim_blocks = trim_blocks
self.lstrip_blocks = lstrip_blocks
self.newline_sequence = newline_sequence self.newline_sequence = newline_sequence


# runtime information # runtime information
Expand Down Expand Up @@ -300,7 +306,8 @@ def overlay(self, block_start_string=missing, block_end_string=missing,
variable_start_string=missing, variable_end_string=missing, variable_start_string=missing, variable_end_string=missing,
comment_start_string=missing, comment_end_string=missing, comment_start_string=missing, comment_end_string=missing,
line_statement_prefix=missing, line_comment_prefix=missing, line_statement_prefix=missing, line_comment_prefix=missing,
trim_blocks=missing, extensions=missing, optimized=missing, trim_blocks=missing, lstrip_blocks=missing,
extensions=missing, optimized=missing,
undefined=missing, finalize=missing, autoescape=missing, undefined=missing, finalize=missing, autoescape=missing,
loader=missing, cache_size=missing, auto_reload=missing, loader=missing, cache_size=missing, auto_reload=missing,
bytecode_cache=missing): bytecode_cache=missing):
Expand Down Expand Up @@ -816,6 +823,7 @@ def __new__(cls, source,
line_statement_prefix=LINE_STATEMENT_PREFIX, line_statement_prefix=LINE_STATEMENT_PREFIX,
line_comment_prefix=LINE_COMMENT_PREFIX, line_comment_prefix=LINE_COMMENT_PREFIX,
trim_blocks=TRIM_BLOCKS, trim_blocks=TRIM_BLOCKS,
lstrip_blocks=LSTRIP_BLOCKS,
newline_sequence=NEWLINE_SEQUENCE, newline_sequence=NEWLINE_SEQUENCE,
extensions=(), extensions=(),
optimized=True, optimized=True,
Expand All @@ -826,6 +834,7 @@ def __new__(cls, source,
block_start_string, block_end_string, variable_start_string, block_start_string, block_end_string, variable_start_string,
variable_end_string, comment_start_string, comment_end_string, variable_end_string, comment_start_string, comment_end_string,
line_statement_prefix, line_comment_prefix, trim_blocks, line_statement_prefix, line_comment_prefix, trim_blocks,
lstrip_blocks,
newline_sequence, frozenset(extensions), optimized, undefined, newline_sequence, frozenset(extensions), optimized, undefined,
finalize, autoescape, None, 0, False, None) finalize, autoescape, None, 0, False, None)
return env.from_string(source, template_class=cls) return env.from_string(source, template_class=cls)
Expand Down
43 changes: 40 additions & 3 deletions jinja2/lexer.py
Original file line number Original file line Diff line number Diff line change
Expand Up @@ -383,6 +383,7 @@ def get_lexer(environment):
environment.line_statement_prefix, environment.line_statement_prefix,
environment.line_comment_prefix, environment.line_comment_prefix,
environment.trim_blocks, environment.trim_blocks,
environment.lstrip_blocks,
environment.newline_sequence) environment.newline_sequence)
lexer = _lexer_cache.get(key) lexer = _lexer_cache.get(key)
if lexer is None: if lexer is None:
Expand Down Expand Up @@ -425,6 +426,42 @@ def __init__(self, environment):
# block suffix if trimming is enabled # block suffix if trimming is enabled
block_suffix_re = environment.trim_blocks and '\\n?' or '' 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.newline_sequence = environment.newline_sequence


# global lexing rules # global lexing rules
Expand All @@ -434,11 +471,11 @@ def __init__(self, environment):
(c('(.*?)(?:%s)' % '|'.join( (c('(.*?)(?:%s)' % '|'.join(
[r'(?P<raw_begin>(?:\s*%s\-|%s)\s*raw\s*(?:\-%s\s*|%s))' % ( [r'(?P<raw_begin>(?:\s*%s\-|%s)\s*raw\s*(?:\-%s\s*|%s))' % (
e(environment.block_start_string), e(environment.block_start_string),
e(environment.block_start_string), block_prefix_re,
e(environment.block_end_string), e(environment.block_end_string),
e(environment.block_end_string) e(environment.block_end_string)
)] + [ )] + [
r'(?P<%s_begin>\s*%s\-|%s)' % (n, r, r) r'(?P<%s_begin>\s*%s\-|%s)' % (n, r, prefix_re.get(n,r))
for n, r in root_tag_rules for n, r in root_tag_rules
])), (TOKEN_DATA, '#bygroup'), '#bygroup'), ])), (TOKEN_DATA, '#bygroup'), '#bygroup'),
# data # data
Expand Down Expand Up @@ -472,7 +509,7 @@ def __init__(self, environment):
TOKEN_RAW_BEGIN: [ TOKEN_RAW_BEGIN: [
(c('(.*?)((?:\s*%s\-|%s)\s*endraw\s*(?:\-%s\s*|%s%s))' % ( (c('(.*?)((?:\s*%s\-|%s)\s*endraw\s*(?:\-%s\s*|%s%s))' % (
e(environment.block_start_string), e(environment.block_start_string),
e(environment.block_start_string), block_prefix_re,
e(environment.block_end_string), e(environment.block_end_string),
e(environment.block_end_string), e(environment.block_end_string),
block_suffix_re block_suffix_re
Expand Down
166 changes: 166 additions & 0 deletions jinja2/testsuite/lexnparse.py
Original file line number Original file line Diff line number Diff line change
Expand Up @@ -379,9 +379,175 @@ def test_parse_unary(self):
assert tmpl.render(foo={'bar': 42}) == '42' 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(): def suite():
suite = unittest.TestSuite() suite = unittest.TestSuite()
suite.addTest(unittest.makeSuite(LexerTestCase)) suite.addTest(unittest.makeSuite(LexerTestCase))
suite.addTest(unittest.makeSuite(ParserTestCase)) suite.addTest(unittest.makeSuite(ParserTestCase))
suite.addTest(unittest.makeSuite(SyntaxTestCase)) suite.addTest(unittest.makeSuite(SyntaxTestCase))
suite.addTest(unittest.makeSuite(LstripBlocksTestCase))
return suite return suite