Skip to content
This repository

lstrip_blocks feature: Smarter whitespace control #139

Closed
wants to merge 12 commits into from

3 participants

Kristi Ryan Madsen Armin Ronacher
Kristi
kristi commented

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>
Ryan Madsen

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

Armin Ronacher
Owner

I like that, thanks a lot. Merged.

Armin Ronacher mitsuhiko closed this
Akbar Gumbira akbargumbira referenced this pull request from a commit in timlinux/flask_user_map
Akbar Gumbira 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.
40  docs/templates.rst
Source Rendered
@@ -156,9 +156,45 @@ In the default configuration, a single trailing newline is stripped if
156 156
 present, and whitespace is not further modified by the template engine. Each
157 157
 whitespace (spaces, tabs, newlines etc.) is returned unchanged.  If the
158 158
 application configures Jinja to `trim_blocks` the first newline after a a
159  
-template tag is removed automatically (like in PHP).
  159
+template tag is removed automatically (like in PHP). The `lstrip_blocks`
  160
+option can also be set to strip tabs and spaces from the beginning of
  161
+line to the start of a block. (Nothing will be stripped if there are
  162
+other characters before the start of the block.)
  163
+
  164
+With both `trim_blocks` and `lstrip_blocks` enabled you can put block tags
  165
+on their own lines, and the entire block line will be removed when
  166
+rendered, preserving the whitespace of the contents.  For example,
  167
+without the `trim_blocks` and `lstrip_blocks` options, this template::
  168
+
  169
+    <div>
  170
+        {% if True %}
  171
+            yay
  172
+        {% endif %}
  173
+    </div>
  174
+
  175
+gets rendered with blank lines inside the div::
  176
+
  177
+    <div>
  178
+    
  179
+            yay
  180
+    
  181
+    </div>
  182
+
  183
+But with both `trim_blocks` and `lstrip_blocks` enabled, the lines with the 
  184
+template blocks are removed while preserving the whitespace of the contents::
  185
+    
  186
+    <div>
  187
+            yay
  188
+    </div>
  189
+
  190
+You can manually disable the `lstrip_blocks` behavior by putting a
  191
+plus sign (``+``) at the start of a block::
  192
+
  193
+    <div>
  194
+            {%+ if something %}yay{% endif %}
  195
+    </div>
160 196
 
161  
-But you can also strip whitespace in templates by hand.  If you put an minus
  197
+You can also strip whitespace in templates by hand.  If you put an minus
162 198
 sign (``-``) to the start or end of an block (for example a for tag), a
163 199
 comment or variable expression you can remove the whitespaces after or before
164 200
 that block::
1  jinja2/defaults.py
@@ -21,6 +21,7 @@
21 21
 LINE_STATEMENT_PREFIX = None
22 22
 LINE_COMMENT_PREFIX = None
23 23
 TRIM_BLOCKS = False
  24
+LSTRIP_BLOCKS = False
24 25
 NEWLINE_SEQUENCE = '\n'
25 26
 
26 27
 
11  jinja2/environment.py
@@ -134,6 +134,10 @@ class Environment(object):
134 134
             If this is set to ``True`` the first newline after a block is
135 135
             removed (block, not variable tag!).  Defaults to `False`.
136 136
 
  137
+        `lstrip_blocks`
  138
+            If this is set to ``True`` leading spaces and tabs are stripped
  139
+            from the start of a line to a block.  Defaults to `False`.
  140
+
137 141
         `newline_sequence`
138 142
             The sequence that starts a newline.  Must be one of ``'\r'``,
139 143
             ``'\n'`` or ``'\r\n'``.  The default is ``'\n'`` which is a
@@ -224,6 +228,7 @@ def __init__(self,
224 228
                  line_statement_prefix=LINE_STATEMENT_PREFIX,
225 229
                  line_comment_prefix=LINE_COMMENT_PREFIX,
226 230
                  trim_blocks=TRIM_BLOCKS,
  231
+                 lstrip_blocks=LSTRIP_BLOCKS,
227 232
                  newline_sequence=NEWLINE_SEQUENCE,
228 233
                  extensions=(),
229 234
                  optimized=True,
@@ -255,6 +260,7 @@ def __init__(self,
255 260
         self.line_statement_prefix = line_statement_prefix
256 261
         self.line_comment_prefix = line_comment_prefix
257 262
         self.trim_blocks = trim_blocks
  263
+        self.lstrip_blocks = lstrip_blocks
258 264
         self.newline_sequence = newline_sequence
259 265
 
260 266
         # runtime information
@@ -300,7 +306,8 @@ def overlay(self, block_start_string=missing, block_end_string=missing,
300 306
                 variable_start_string=missing, variable_end_string=missing,
301 307
                 comment_start_string=missing, comment_end_string=missing,
302 308
                 line_statement_prefix=missing, line_comment_prefix=missing,
303  
-                trim_blocks=missing, extensions=missing, optimized=missing,
  309
+                trim_blocks=missing, lstrip_blocks=missing,
  310
+                extensions=missing, optimized=missing,
304 311
                 undefined=missing, finalize=missing, autoescape=missing,
305 312
                 loader=missing, cache_size=missing, auto_reload=missing,
306 313
                 bytecode_cache=missing):
@@ -816,6 +823,7 @@ def __new__(cls, source,
816 823
                 line_statement_prefix=LINE_STATEMENT_PREFIX,
817 824
                 line_comment_prefix=LINE_COMMENT_PREFIX,
818 825
                 trim_blocks=TRIM_BLOCKS,
  826
+                lstrip_blocks=LSTRIP_BLOCKS,
819 827
                 newline_sequence=NEWLINE_SEQUENCE,
820 828
                 extensions=(),
821 829
                 optimized=True,
@@ -826,6 +834,7 @@ def __new__(cls, source,
826 834
             block_start_string, block_end_string, variable_start_string,
827 835
             variable_end_string, comment_start_string, comment_end_string,
828 836
             line_statement_prefix, line_comment_prefix, trim_blocks,
  837
+            lstrip_blocks,
829 838
             newline_sequence, frozenset(extensions), optimized, undefined,
830 839
             finalize, autoescape, None, 0, False, None)
831 840
         return env.from_string(source, template_class=cls)
43  jinja2/lexer.py
@@ -383,6 +383,7 @@ def get_lexer(environment):
383 383
            environment.line_statement_prefix,
384 384
            environment.line_comment_prefix,
385 385
            environment.trim_blocks,
  386
+           environment.lstrip_blocks,
386 387
            environment.newline_sequence)
387 388
     lexer = _lexer_cache.get(key)
388 389
     if lexer is None:
@@ -425,6 +426,42 @@ def __init__(self, environment):
425 426
         # block suffix if trimming is enabled
426 427
         block_suffix_re = environment.trim_blocks and '\\n?' or ''
427 428
 
  429
+        # strip leading spaces if lstrip_blocks is enabled
  430
+        prefix_re = {}
  431
+        if environment.lstrip_blocks:
  432
+            # use '{%+' to manually disable lstrip_blocks behavior
  433
+            no_lstrip_re = e('+')
  434
+            # detect overlap between block and variable or comment strings
  435
+            block_diff = c(r'^%s(.*)' % e(environment.block_start_string))
  436
+            # make sure we don't mistake a block for a variable or a comment
  437
+            m = block_diff.match(environment.comment_start_string)
  438
+            no_lstrip_re += m and r'|%s' % e(m.group(1)) or ''
  439
+            m = block_diff.match(environment.variable_start_string)
  440
+            no_lstrip_re += m and r'|%s' % e(m.group(1)) or ''
  441
+
  442
+            # detect overlap between comment and variable strings
  443
+            comment_diff = c(r'^%s(.*)' % e(environment.comment_start_string))
  444
+            m = comment_diff.match(environment.variable_start_string)
  445
+            no_variable_re = m and r'(?!%s)' % e(m.group(1)) or ''
  446
+
  447
+            lstrip_re = r'^[ \t]*'
  448
+            block_prefix_re = r'%s%s(?!%s)|%s\+?' % (
  449
+                    lstrip_re,
  450
+                    e(environment.block_start_string),
  451
+                    no_lstrip_re,
  452
+                    e(environment.block_start_string),
  453
+                    )
  454
+            comment_prefix_re = r'%s%s%s|%s\+?' % (
  455
+                    lstrip_re,
  456
+                    e(environment.comment_start_string),
  457
+                    no_variable_re,
  458
+                    e(environment.comment_start_string),
  459
+                    )
  460
+            prefix_re['block'] = block_prefix_re
  461
+            prefix_re['comment'] = comment_prefix_re
  462
+        else:
  463
+            block_prefix_re = '%s' % e(environment.block_start_string)
  464
+
428 465
         self.newline_sequence = environment.newline_sequence
429 466
 
430 467
         # global lexing rules
@@ -434,11 +471,11 @@ def __init__(self, environment):
434 471
                 (c('(.*?)(?:%s)' % '|'.join(
435 472
                     [r'(?P<raw_begin>(?:\s*%s\-|%s)\s*raw\s*(?:\-%s\s*|%s))' % (
436 473
                         e(environment.block_start_string),
437  
-                        e(environment.block_start_string),
  474
+                        block_prefix_re,
438 475
                         e(environment.block_end_string),
439 476
                         e(environment.block_end_string)
440 477
                     )] + [
441  
-                        r'(?P<%s_begin>\s*%s\-|%s)' % (n, r, r)
  478
+                        r'(?P<%s_begin>\s*%s\-|%s)' % (n, r, prefix_re.get(n,r))
442 479
                         for n, r in root_tag_rules
443 480
                     ])), (TOKEN_DATA, '#bygroup'), '#bygroup'),
444 481
                 # data
@@ -472,7 +509,7 @@ def __init__(self, environment):
472 509
             TOKEN_RAW_BEGIN: [
473 510
                 (c('(.*?)((?:\s*%s\-|%s)\s*endraw\s*(?:\-%s\s*|%s%s))' % (
474 511
                     e(environment.block_start_string),
475  
-                    e(environment.block_start_string),
  512
+                    block_prefix_re,
476 513
                     e(environment.block_end_string),
477 514
                     e(environment.block_end_string),
478 515
                     block_suffix_re
166  jinja2/testsuite/lexnparse.py
@@ -379,9 +379,175 @@ def test_parse_unary(self):
379 379
         assert tmpl.render(foo={'bar': 42}) == '42'
380 380
 
381 381
 
  382
+class LstripBlocksTestCase(JinjaTestCase):
  383
+
  384
+    def test_lstrip(self):
  385
+        env = Environment(lstrip_blocks=True, trim_blocks=False)
  386
+        tmpl = env.from_string('''    {% if True %}\n    {% endif %}''')
  387
+        assert tmpl.render() == "\n"
  388
+
  389
+    def test_lstrip_trim(self):
  390
+        env = Environment(lstrip_blocks=True, trim_blocks=True)
  391
+        tmpl = env.from_string('''    {% if True %}\n    {% endif %}''')
  392
+        assert tmpl.render() == ""
  393
+
  394
+    def test_no_lstrip(self):
  395
+        env = Environment(lstrip_blocks=True, trim_blocks=False)
  396
+        tmpl = env.from_string('''    {%+ if True %}\n    {%+ endif %}''')
  397
+        assert tmpl.render() == "    \n    "
  398
+
  399
+    def test_lstrip_endline(self):
  400
+        env = Environment(lstrip_blocks=True, trim_blocks=False)
  401
+        tmpl = env.from_string('''    hello{% if True %}\n    goodbye{% endif %}''')
  402
+        assert tmpl.render() == "    hello\n    goodbye"
  403
+
  404
+    def test_lstrip_inline(self):
  405
+        env = Environment(lstrip_blocks=True, trim_blocks=False)
  406
+        tmpl = env.from_string('''    {% if True %}hello    {% endif %}''')
  407
+        assert tmpl.render() == 'hello    '
  408
+
  409
+    def test_lstrip_nested(self):
  410
+        env = Environment(lstrip_blocks=True, trim_blocks=False)
  411
+        tmpl = env.from_string('''    {% if True %}a {% if True %}b {% endif %}c {% endif %}''')
  412
+        assert tmpl.render() == 'a b c '
  413
+
  414
+    def test_lstrip_left_chars(self):
  415
+        env = Environment(lstrip_blocks=True, trim_blocks=False)
  416
+        tmpl = env.from_string('''    abc {% if True %}
  417
+        hello{% endif %}''')
  418
+        assert tmpl.render() == '    abc \n        hello'
  419
+
  420
+    def test_lstrip_embeded_strings(self):
  421
+        env = Environment(lstrip_blocks=True, trim_blocks=False)
  422
+        tmpl = env.from_string('''    {% set x = " {% str %} " %}{{ x }}''')
  423
+        assert tmpl.render() == ' {% str %} '
  424
+
  425
+    def test_lstrip_preserve_leading_newlines(self):
  426
+        env = Environment(lstrip_blocks=True, trim_blocks=False)
  427
+        tmpl = env.from_string('''\n\n\n{% set hello = 1 %}''')
  428
+        assert tmpl.render() == '\n\n\n'
  429
+        
  430
+    def test_lstrip_comment(self):
  431
+        env = Environment(lstrip_blocks=True, trim_blocks=False)
  432
+        tmpl = env.from_string('''    {# if True #}
  433
+hello
  434
+    {#endif#}''')
  435
+        assert tmpl.render() == '\nhello\n'
  436
+
  437
+    def test_lstrip_angle_bracket_simple(self):
  438
+        env = Environment('<%', '%>', '${', '}', '<%#', '%>', '%', '##',
  439
+            lstrip_blocks=True, trim_blocks=True)
  440
+        tmpl = env.from_string('''    <% if True %>hello    <% endif %>''')
  441
+        assert tmpl.render() == 'hello    '
  442
+
  443
+    def test_lstrip_angle_bracket_comment(self):
  444
+        env = Environment('<%', '%>', '${', '}', '<%#', '%>', '%', '##',
  445
+            lstrip_blocks=True, trim_blocks=True)
  446
+        tmpl = env.from_string('''    <%# if True %>hello    <%# endif %>''')
  447
+        assert tmpl.render() == 'hello    '
  448
+
  449
+    def test_lstrip_angle_bracket(self):
  450
+        env = Environment('<%', '%>', '${', '}', '<%#', '%>', '%', '##',
  451
+            lstrip_blocks=True, trim_blocks=True)
  452
+        tmpl = env.from_string('''\
  453
+    <%# regular comment %>
  454
+    <% for item in seq %>
  455
+${item} ## the rest of the stuff
  456
+   <% endfor %>''')
  457
+        assert tmpl.render(seq=range(5)) == \
  458
+                ''.join('%s\n' % x for x in range(5))
  459
+        
  460
+    def test_lstrip_angle_bracket_compact(self):
  461
+        env = Environment('<%', '%>', '${', '}', '<%#', '%>', '%', '##',
  462
+            lstrip_blocks=True, trim_blocks=True)
  463
+        tmpl = env.from_string('''\
  464
+    <%#regular comment%>
  465
+    <%for item in seq%>
  466
+${item} ## the rest of the stuff
  467
+   <%endfor%>''')
  468
+        assert tmpl.render(seq=range(5)) == \
  469
+                ''.join('%s\n' % x for x in range(5))
  470
+        
  471
+    def test_php_syntax_with_manual(self):
  472
+        env = Environment('<?', '?>', '<?=', '?>', '<!--', '-->',
  473
+            lstrip_blocks=True, trim_blocks=True)
  474
+        tmpl = env.from_string('''\
  475
+    <!-- I'm a comment, I'm not interesting -->
  476
+    <? for item in seq -?>
  477
+        <?= item ?>
  478
+    <?- endfor ?>''')
  479
+        assert tmpl.render(seq=range(5)) == '01234'
  480
+
  481
+    def test_php_syntax(self):
  482
+        env = Environment('<?', '?>', '<?=', '?>', '<!--', '-->',
  483
+            lstrip_blocks=True, trim_blocks=True)
  484
+        tmpl = env.from_string('''\
  485
+    <!-- I'm a comment, I'm not interesting -->
  486
+    <? for item in seq ?>
  487
+        <?= item ?>
  488
+    <? endfor ?>''')
  489
+        assert tmpl.render(seq=range(5)) == ''.join('        %s\n' % x for x in range(5))
  490
+
  491
+    def test_php_syntax_compact(self):
  492
+        env = Environment('<?', '?>', '<?=', '?>', '<!--', '-->',
  493
+            lstrip_blocks=True, trim_blocks=True)
  494
+        tmpl = env.from_string('''\
  495
+    <!-- I'm a comment, I'm not interesting -->
  496
+    <?for item in seq?>
  497
+        <?=item?>
  498
+    <?endfor?>''')
  499
+        assert tmpl.render(seq=range(5)) == ''.join('        %s\n' % x for x in range(5))
  500
+
  501
+    def test_erb_syntax(self):
  502
+        env = Environment('<%', '%>', '<%=', '%>', '<%#', '%>',
  503
+            lstrip_blocks=True, trim_blocks=True)
  504
+        #env.from_string('')
  505
+        #for n,r in env.lexer.rules.iteritems():
  506
+        #    print n
  507
+        #print env.lexer.rules['root'][0][0].pattern
  508
+        #print "'%s'" % tmpl.render(seq=range(5))
  509
+        tmpl = env.from_string('''\
  510
+<%# I'm a comment, I'm not interesting %>
  511
+    <% for item in seq %>
  512
+    <%= item %>
  513
+    <% endfor %>
  514
+''')
  515
+        assert tmpl.render(seq=range(5)) == ''.join('    %s\n' % x for x in range(5))
  516
+
  517
+    def test_erb_syntax_with_manual(self):
  518
+        env = Environment('<%', '%>', '<%=', '%>', '<%#', '%>',
  519
+            lstrip_blocks=True, trim_blocks=True)
  520
+        tmpl = env.from_string('''\
  521
+<%# I'm a comment, I'm not interesting %>
  522
+    <% for item in seq -%>
  523
+        <%= item %>
  524
+    <%- endfor %>''')
  525
+        assert tmpl.render(seq=range(5)) == '01234'
  526
+
  527
+    def test_erb_syntax_no_lstrip(self):
  528
+        env = Environment('<%', '%>', '<%=', '%>', '<%#', '%>',
  529
+            lstrip_blocks=True, trim_blocks=True)
  530
+        tmpl = env.from_string('''\
  531
+<%# I'm a comment, I'm not interesting %>
  532
+    <%+ for item in seq -%>
  533
+        <%= item %>
  534
+    <%- endfor %>''')
  535
+        assert tmpl.render(seq=range(5)) == '    01234'
  536
+
  537
+    def test_comment_syntax(self):
  538
+        env = Environment('<!--', '-->', '${', '}', '<!--#', '-->',
  539
+            lstrip_blocks=True, trim_blocks=True)
  540
+        tmpl = env.from_string('''\
  541
+<!--# I'm a comment, I'm not interesting -->\
  542
+<!-- for item in seq --->
  543
+    ${item}
  544
+<!--- endfor -->''')
  545
+        assert tmpl.render(seq=range(5)) == '01234'
  546
+
382 547
 def suite():
383 548
     suite = unittest.TestSuite()
384 549
     suite.addTest(unittest.makeSuite(LexerTestCase))
385 550
     suite.addTest(unittest.makeSuite(ParserTestCase))
386 551
     suite.addTest(unittest.makeSuite(SyntaxTestCase))
  552
+    suite.addTest(unittest.makeSuite(LstripBlocksTestCase))
387 553
     return suite
Commit_comment_tip

Tip: You can add notes to lines in a file. Hover to the left of a line to make a note

Something went wrong with that request. Please try again.