summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--coverage/templite.py100
-rw-r--r--tests/test_templite.py22
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ǝɥ",