diff options
author | Ned Batchelder <ned@nedbatchelder.com> | 2016-02-15 15:15:07 -0500 |
---|---|---|
committer | Ned Batchelder <ned@nedbatchelder.com> | 2016-02-15 15:15:07 -0500 |
commit | 7e10f782544e6f628c53f6ac64d510d0e2fef0af (patch) | |
tree | 4baa3f1b340dec11af2e29b11c06fa9b27272c7e | |
parent | 20033c71012732c181b3997e454f42edc46d44fd (diff) | |
download | python-coveragepy-git-7e10f782544e6f628c53f6ac64d510d0e2fef0af.tar.gz |
Templite can now collapse whitespace after tags
-rw-r--r-- | coverage/templite.py | 100 | ||||
-rw-r--r-- | tests/test_templite.py | 22 |
2 files changed, 79 insertions, 43 deletions
diff --git a/coverage/templite.py b/coverage/templite.py index f131f748..4c09c113 100644 --- a/coverage/templite.py +++ b/coverage/templite.py @@ -90,6 +90,9 @@ class Templite(object): {# This will be ignored #} + Any of these constructs can have a hypen at the end (`-}}`, `-%}`, `-#}`), + which will collapse the whitespace following the tag. + Construct a Templite with the template text, then use `render` against a dictionary context to create a finished string:: @@ -151,53 +154,64 @@ class Templite(object): # Split the text to form a list of tokens. tokens = re.split(r"(?s)({{.*?}}|{%.*?%}|{#.*?#})", text) + squash = False + for token in tokens: - if token.startswith('{#'): - # Comment: ignore it and move on. - continue - elif token.startswith('{{'): - # An expression to evaluate. - expr = self._expr_code(token[2:-2].strip()) - buffered.append("to_str(%s)" % expr) - elif token.startswith('{%'): - # Action tag: split into words and parse further. - flush_output() - words = token[2:-2].strip().split() - if words[0] == 'if': - # An if statement: evaluate the expression to determine if. - if len(words) != 2: - self._syntax_error("Don't understand if", token) - ops_stack.append('if') - code.add_line("if %s:" % self._expr_code(words[1])) - code.indent() - elif words[0] == 'for': - # A loop: iterate over expression result. - if len(words) != 4 or words[2] != 'in': - self._syntax_error("Don't understand for", token) - ops_stack.append('for') - self._variable(words[1], self.loop_vars) - code.add_line( - "for c_%s in %s:" % ( - words[1], - self._expr_code(words[3]) + if token.startswith('{'): + start, end = 2, -2 + squash = (token[-3] == '-') + if squash: + end = -3 + + if token.startswith('{#'): + # Comment: ignore it and move on. + continue + elif token.startswith('{{'): + # An expression to evaluate. + expr = self._expr_code(token[start:end].strip()) + buffered.append("to_str(%s)" % expr) + elif token.startswith('{%'): + # Action tag: split into words and parse further. + flush_output() + + words = token[start:end].strip().split() + if words[0] == 'if': + # An if statement: evaluate the expression to determine if. + if len(words) != 2: + self._syntax_error("Don't understand if", token) + ops_stack.append('if') + code.add_line("if %s:" % self._expr_code(words[1])) + code.indent() + elif words[0] == 'for': + # A loop: iterate over expression result. + if len(words) != 4 or words[2] != 'in': + self._syntax_error("Don't understand for", token) + ops_stack.append('for') + self._variable(words[1], self.loop_vars) + code.add_line( + "for c_%s in %s:" % ( + words[1], + self._expr_code(words[3]) + ) ) - ) - code.indent() - elif words[0].startswith('end'): - # Endsomething. Pop the ops stack. - if len(words) != 1: - self._syntax_error("Don't understand end", token) - end_what = words[0][3:] - if not ops_stack: - self._syntax_error("Too many ends", token) - start_what = ops_stack.pop() - if start_what != end_what: - self._syntax_error("Mismatched end tag", end_what) - code.dedent() - else: - self._syntax_error("Don't understand tag", words[0]) + code.indent() + elif words[0].startswith('end'): + # Endsomething. Pop the ops stack. + if len(words) != 1: + self._syntax_error("Don't understand end", token) + end_what = words[0][3:] + if not ops_stack: + self._syntax_error("Too many ends", token) + start_what = ops_stack.pop() + if start_what != end_what: + self._syntax_error("Mismatched end tag", end_what) + code.dedent() + else: + self._syntax_error("Don't understand tag", words[0]) else: # Literal content. If it isn't empty, output it. + if squash: + token = token.lstrip() if token: buffered.append(repr(token)) diff --git a/tests/test_templite.py b/tests/test_templite.py index 2f9b2dbd..1df942ee 100644 --- a/tests/test_templite.py +++ b/tests/test_templite.py @@ -33,6 +33,7 @@ class TempliteTest(CoverageTest): Result defaults to None so we can shorten the calls where we expect an exception and never get to the result comparison. + """ actual = Templite(text).render(ctx or {}) # If result is None, then an exception should have prevented us getting @@ -44,6 +45,7 @@ class TempliteTest(CoverageTest): """Assert that a `TempliteSyntaxError` will happen. A context manager, and the message should be `msg`. + """ pat = "^" + re.escape(msg) + "$" return self.assertRaisesRegex(TempliteSyntaxError, pat) @@ -238,6 +240,26 @@ class TempliteTest(CoverageTest): "@a0b0c0a1b1c1a2b2c2!" ) + def test_whitespace_handling(self): + self.try_render( + "@{% for n in nums %}\n" + " {% for a in abc %}{{a}}{{n}}{% endfor %}\n" + "{% endfor %}!\n", + {'nums': [0, 1, 2], 'abc': ['a', 'b', 'c']}, + "@\n a0b0c0\n\n a1b1c1\n\n a2b2c2\n!\n" + ) + self.try_render( + "@{% for n in nums -%}\n" + " {% for a in abc -%}\n" + " {# this disappears completely -#}\n" + " {{a -}}\n" + " {{n -}}\n" + " {% endfor %}\n" + "{% endfor %}!\n", + {'nums': [0, 1, 2], 'abc': ['a', 'b', 'c']}, + "@a0b0c0\na1b1c1\na2b2c2\n!\n" + ) + def test_non_ascii(self): self.try_render( u"{{where}} ollǝɥ", |