Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP

Loading…

lstrip_blocks feature: Smarter whitespace control #139

Closed
wants to merge 12 commits into from

3 participants

@kristi

Jinja's whitespace control using {%- and -%} is convenient, but tedious to apply to many blocks to get cleanly indented output.

The new lstrip_blocks option removes spaces and tabs from the beginning of a line up to a block tag. If there are characters other than spaces or tabs preceding the block tag, nothing is stripped. So the spaces would be removed from

    {% set x = 1 %}

but not from

    hello {% set x = 1 %}

Often, a block tag appears on its own line in a template. Using lstrip_blocks and the existing trim_blocks options, the entire tag line will be automatically removed when rendering a template.

Example template:

<ul>
    {% for post in posts %}
        <li>{{ post.title }}</li>
    {% endfor %}
</ul>

The rendered output:

<ul>
        <li>Post One</li>
        <li>Post Two</li>
        <li>Post Three</li>
</ul>
@rmadsen

This would be a very useful for ASCII output, where whitespace is significant (for table alignment, etc).

@mitsuhiko
Owner

I like that, thanks a lot. Merged.

@mitsuhiko mitsuhiko closed this
@akbargumbira akbargumbira referenced this pull request from a commit in kartoza/flask_user_map
@akbargumbira akbargumbira Using template for user info popup content. 364367e
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
This page is out of date. Refresh to see the latest.
View
40 docs/templates.rst
@@ -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
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).
+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
comment or variable expression you can remove the whitespaces after or before
that block::
View
1  jinja2/defaults.py
@@ -21,6 +21,7 @@
LINE_STATEMENT_PREFIX = None
LINE_COMMENT_PREFIX = None
TRIM_BLOCKS = False
+LSTRIP_BLOCKS = False
NEWLINE_SEQUENCE = '\n'
View
11 jinja2/environment.py
@@ -134,6 +134,10 @@ class Environment(object):
If this is set to ``True`` the first newline after a block is
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`
The sequence that starts a newline. Must be one of ``'\r'``,
``'\n'`` or ``'\r\n'``. The default is ``'\n'`` which is a
@@ -224,6 +228,7 @@ def __init__(self,
line_statement_prefix=LINE_STATEMENT_PREFIX,
line_comment_prefix=LINE_COMMENT_PREFIX,
trim_blocks=TRIM_BLOCKS,
+ lstrip_blocks=LSTRIP_BLOCKS,
newline_sequence=NEWLINE_SEQUENCE,
extensions=(),
optimized=True,
@@ -255,6 +260,7 @@ def __init__(self,
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
# runtime information
@@ -300,7 +306,8 @@ def overlay(self, block_start_string=missing, block_end_string=missing,
variable_start_string=missing, variable_end_string=missing,
comment_start_string=missing, comment_end_string=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,
loader=missing, cache_size=missing, auto_reload=missing,
bytecode_cache=missing):
@@ -816,6 +823,7 @@ def __new__(cls, source,
line_statement_prefix=LINE_STATEMENT_PREFIX,
line_comment_prefix=LINE_COMMENT_PREFIX,
trim_blocks=TRIM_BLOCKS,
+ lstrip_blocks=LSTRIP_BLOCKS,
newline_sequence=NEWLINE_SEQUENCE,
extensions=(),
optimized=True,
@@ -826,6 +834,7 @@ def __new__(cls, source,
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,
+ lstrip_blocks,
newline_sequence, frozenset(extensions), optimized, undefined,
finalize, autoescape, None, 0, False, None)
return env.from_string(source, template_class=cls)
View
43 jinja2/lexer.py
@@ -383,6 +383,7 @@ def get_lexer(environment):
environment.line_statement_prefix,
environment.line_comment_prefix,
environment.trim_blocks,
+ environment.lstrip_blocks,
environment.newline_sequence)
lexer = _lexer_cache.get(key)
if lexer is None:
@@ -425,6 +426,42 @@ def __init__(self, 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
# global lexing rules
@@ -434,11 +471,11 @@ def __init__(self, environment):
(c('(.*?)(?:%s)' % '|'.join(
[r'(?P<raw_begin>(?:\s*%s\-|%s)\s*raw\s*(?:\-%s\s*|%s))' % (
e(environment.block_start_string),
- e(environment.block_start_string),
+ block_prefix_re,
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
])), (TOKEN_DATA, '#bygroup'), '#bygroup'),
# data
@@ -472,7 +509,7 @@ def __init__(self, environment):
TOKEN_RAW_BEGIN: [
(c('(.*?)((?:\s*%s\-|%s)\s*endraw\s*(?:\-%s\s*|%s%s))' % (
e(environment.block_start_string),
- e(environment.block_start_string),
+ block_prefix_re,
e(environment.block_end_string),
e(environment.block_end_string),
block_suffix_re
View
166 jinja2/testsuite/lexnparse.py
@@ -379,9 +379,175 @@ def test_parse_unary(self):
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(LexerTestCase))
suite.addTest(unittest.makeSuite(ParserTestCase))
suite.addTest(unittest.makeSuite(SyntaxTestCase))
+ suite.addTest(unittest.makeSuite(LstripBlocksTestCase))
return suite
Something went wrong with that request. Please try again.