diff options
Diffstat (limited to 'tests/test_lexnparse.py')
-rw-r--r-- | tests/test_lexnparse.py | 1030 |
1 files changed, 1030 insertions, 0 deletions
diff --git a/tests/test_lexnparse.py b/tests/test_lexnparse.py new file mode 100644 index 0000000..c02adad --- /dev/null +++ b/tests/test_lexnparse.py @@ -0,0 +1,1030 @@ +import pytest + +from jinja2 import Environment +from jinja2 import nodes +from jinja2 import Template +from jinja2 import TemplateSyntaxError +from jinja2 import UndefinedError +from jinja2.lexer import Token +from jinja2.lexer import TOKEN_BLOCK_BEGIN +from jinja2.lexer import TOKEN_BLOCK_END +from jinja2.lexer import TOKEN_EOF +from jinja2.lexer import TokenStream + + +class TestTokenStream: + test_tokens = [ + Token(1, TOKEN_BLOCK_BEGIN, ""), + Token(2, TOKEN_BLOCK_END, ""), + ] + + def test_simple(self, env): + ts = TokenStream(self.test_tokens, "foo", "bar") + assert ts.current.type is TOKEN_BLOCK_BEGIN + assert bool(ts) + assert not bool(ts.eos) + next(ts) + assert ts.current.type is TOKEN_BLOCK_END + assert bool(ts) + assert not bool(ts.eos) + next(ts) + assert ts.current.type is TOKEN_EOF + assert not bool(ts) + assert bool(ts.eos) + + def test_iter(self, env): + token_types = [t.type for t in TokenStream(self.test_tokens, "foo", "bar")] + assert token_types == [ + "block_begin", + "block_end", + ] + + +class TestLexer: + def test_raw1(self, env): + tmpl = env.from_string( + "{% raw %}foo{% endraw %}|" + "{%raw%}{{ bar }}|{% baz %}{% endraw %}" + ) + assert tmpl.render() == "foo|{{ bar }}|{% baz %}" + + def test_raw2(self, env): + tmpl = env.from_string("1 {%- raw -%} 2 {%- endraw -%} 3") + assert tmpl.render() == "123" + + def test_raw3(self, env): + # The second newline after baz exists because it is AFTER the + # {% raw %} and is ignored. + env = Environment(lstrip_blocks=True, trim_blocks=True) + tmpl = env.from_string("bar\n{% raw %}\n {{baz}}2 spaces\n{% endraw %}\nfoo") + assert tmpl.render(baz="test") == "bar\n\n {{baz}}2 spaces\nfoo" + + def test_raw4(self, env): + # The trailing dash of the {% raw -%} cleans both the spaces and + # newlines up to the first character of data. + env = Environment(lstrip_blocks=True, trim_blocks=False) + tmpl = env.from_string( + "bar\n{%- raw -%}\n\n \n 2 spaces\n space{%- endraw -%}\nfoo" + ) + assert tmpl.render() == "bar2 spaces\n spacefoo" + + def test_balancing(self, env): + env = Environment("{%", "%}", "${", "}") + tmpl = env.from_string( + """{% for item in seq + %}${{'foo': item}|upper}{% endfor %}""" + ) + assert tmpl.render(seq=list(range(3))) == "{'FOO': 0}{'FOO': 1}{'FOO': 2}" + + def test_comments(self, env): + env = Environment("<!--", "-->", "{", "}") + tmpl = env.from_string( + """\ +<ul> +<!--- for item in seq --> + <li>{item}</li> +<!--- endfor --> +</ul>""" + ) + assert tmpl.render(seq=list(range(3))) == ( + "<ul>\n <li>0</li>\n <li>1</li>\n <li>2</li>\n</ul>" + ) + + def test_string_escapes(self, env): + for char in "\0", "\u2668", "\xe4", "\t", "\r", "\n": + tmpl = env.from_string(f"{{{{ {char!r} }}}}") + assert tmpl.render() == char + assert env.from_string('{{ "\N{HOT SPRINGS}" }}').render() == "\u2668" + + def test_bytefallback(self, env): + from pprint import pformat + + tmpl = env.from_string("""{{ 'foo'|pprint }}|{{ 'bär'|pprint }}""") + assert tmpl.render() == pformat("foo") + "|" + pformat("bär") + + def test_operators(self, env): + from jinja2.lexer import operators + + for test, expect in operators.items(): + if test in "([{}])": + continue + stream = env.lexer.tokenize(f"{{{{ {test} }}}}") + next(stream) + assert stream.current.type == expect + + def test_normalizing(self, env): + for seq in "\r", "\r\n", "\n": + env = Environment(newline_sequence=seq) + tmpl = env.from_string("1\n2\r\n3\n4\n") + result = tmpl.render() + assert result.replace(seq, "X") == "1X2X3X4" + + def test_trailing_newline(self, env): + for keep in [True, False]: + env = Environment(keep_trailing_newline=keep) + for template, expected in [ + ("", {}), + ("no\nnewline", {}), + ("with\nnewline\n", {False: "with\nnewline"}), + ("with\nseveral\n\n\n", {False: "with\nseveral\n\n"}), + ]: + tmpl = env.from_string(template) + expect = expected.get(keep, template) + result = tmpl.render() + assert result == expect, (keep, template, result, expect) + + @pytest.mark.parametrize( + ("name", "valid"), + [ + ("foo", True), + ("föö", True), + ("き", True), + ("_", True), + ("1a", False), # invalid ascii start + ("a-", False), # invalid ascii continue + ("\U0001f40da", False), # invalid unicode start + ("a🐍\U0001f40d", False), # invalid unicode continue + # start characters not matched by \w + ("\u1885", True), + ("\u1886", True), + ("\u2118", True), + ("\u212e", True), + # continue character not matched by \w + ("\xb7", False), + ("a\xb7", True), + ], + ) + def test_name(self, env, name, valid): + t = "{{ " + name + " }}" + + if valid: + # valid for version being tested, shouldn't raise + env.from_string(t) + else: + pytest.raises(TemplateSyntaxError, env.from_string, t) + + def test_lineno_with_strip(self, env): + tokens = env.lex( + """\ +<html> + <body> + {%- block content -%} + <hr> + {{ item }} + {% endblock %} + </body> +</html>""" + ) + for tok in tokens: + lineno, token_type, value = tok + if token_type == "name" and value == "item": + assert lineno == 5 + break + + +class TestParser: + def test_php_syntax(self, env): + env = Environment("<?", "?>", "<?=", "?>", "<!--", "-->") + tmpl = env.from_string( + """\ +<!-- I'm a comment, I'm not interesting -->\ +<? for item in seq -?> + <?= item ?> +<?- endfor ?>""" + ) + assert tmpl.render(seq=list(range(5))) == "01234" + + def test_erb_syntax(self, env): + env = Environment("<%", "%>", "<%=", "%>", "<%#", "%>") + tmpl = env.from_string( + """\ +<%# I'm a comment, I'm not interesting %>\ +<% for item in seq -%> + <%= item %> +<%- endfor %>""" + ) + assert tmpl.render(seq=list(range(5))) == "01234" + + def test_comment_syntax(self, env): + env = Environment("<!--", "-->", "${", "}", "<!--#", "-->") + tmpl = env.from_string( + """\ +<!--# I'm a comment, I'm not interesting -->\ +<!-- for item in seq ---> + ${item} +<!--- endfor -->""" + ) + assert tmpl.render(seq=list(range(5))) == "01234" + + def test_balancing(self, env): + tmpl = env.from_string("""{{{'foo':'bar'}.foo}}""") + assert tmpl.render() == "bar" + + def test_start_comment(self, env): + tmpl = env.from_string( + """{# foo comment +and bar comment #} +{% macro blub() %}foo{% endmacro %} +{{ blub() }}""" + ) + assert tmpl.render().strip() == "foo" + + def test_line_syntax(self, env): + env = Environment("<%", "%>", "${", "}", "<%#", "%>", "%") + tmpl = env.from_string( + """\ +<%# regular comment %> +% for item in seq: + ${item} +% endfor""" + ) + assert [ + int(x.strip()) for x in tmpl.render(seq=list(range(5))).split() + ] == list(range(5)) + + env = Environment("<%", "%>", "${", "}", "<%#", "%>", "%", "##") + tmpl = env.from_string( + """\ +<%# regular comment %> +% for item in seq: + ${item} ## the rest of the stuff +% endfor""" + ) + assert [ + int(x.strip()) for x in tmpl.render(seq=list(range(5))).split() + ] == list(range(5)) + + def test_line_syntax_priority(self, env): + # XXX: why is the whitespace there in front of the newline? + env = Environment("{%", "%}", "${", "}", "/*", "*/", "##", "#") + tmpl = env.from_string( + """\ +/* ignore me. + I'm a multiline comment */ +## for item in seq: +* ${item} # this is just extra stuff +## endfor""" + ) + assert tmpl.render(seq=[1, 2]).strip() == "* 1\n* 2" + env = Environment("{%", "%}", "${", "}", "/*", "*/", "#", "##") + tmpl = env.from_string( + """\ +/* ignore me. + I'm a multiline comment */ +# for item in seq: +* ${item} ## this is just extra stuff + ## extra stuff i just want to ignore +# endfor""" + ) + assert tmpl.render(seq=[1, 2]).strip() == "* 1\n\n* 2" + + def test_error_messages(self, env): + def assert_error(code, expected): + with pytest.raises(TemplateSyntaxError, match=expected): + Template(code) + + assert_error( + "{% for item in seq %}...{% endif %}", + "Encountered unknown tag 'endif'. Jinja was looking " + "for the following tags: 'endfor' or 'else'. The " + "innermost block that needs to be closed is 'for'.", + ) + assert_error( + "{% if foo %}{% for item in seq %}...{% endfor %}{% endfor %}", + "Encountered unknown tag 'endfor'. Jinja was looking for " + "the following tags: 'elif' or 'else' or 'endif'. The " + "innermost block that needs to be closed is 'if'.", + ) + assert_error( + "{% if foo %}", + "Unexpected end of template. Jinja was looking for the " + "following tags: 'elif' or 'else' or 'endif'. The " + "innermost block that needs to be closed is 'if'.", + ) + assert_error( + "{% for item in seq %}", + "Unexpected end of template. Jinja was looking for the " + "following tags: 'endfor' or 'else'. The innermost block " + "that needs to be closed is 'for'.", + ) + assert_error( + "{% block foo-bar-baz %}", + "Block names in Jinja have to be valid Python identifiers " + "and may not contain hyphens, use an underscore instead.", + ) + assert_error("{% unknown_tag %}", "Encountered unknown tag 'unknown_tag'.") + + +class TestSyntax: + def test_call(self, env): + env = Environment() + env.globals["foo"] = lambda a, b, c, e, g: a + b + c + e + g + tmpl = env.from_string("{{ foo('a', c='d', e='f', *['b'], **{'g': 'h'}) }}") + assert tmpl.render() == "abdfh" + + def test_slicing(self, env): + tmpl = env.from_string("{{ [1, 2, 3][:] }}|{{ [1, 2, 3][::-1] }}") + assert tmpl.render() == "[1, 2, 3]|[3, 2, 1]" + + def test_attr(self, env): + tmpl = env.from_string("{{ foo.bar }}|{{ foo['bar'] }}") + assert tmpl.render(foo={"bar": 42}) == "42|42" + + def test_subscript(self, env): + tmpl = env.from_string("{{ foo[0] }}|{{ foo[-1] }}") + assert tmpl.render(foo=[0, 1, 2]) == "0|2" + + def test_tuple(self, env): + tmpl = env.from_string("{{ () }}|{{ (1,) }}|{{ (1, 2) }}") + assert tmpl.render() == "()|(1,)|(1, 2)" + + def test_math(self, env): + tmpl = env.from_string("{{ (1 + 1 * 2) - 3 / 2 }}|{{ 2**3 }}") + assert tmpl.render() == "1.5|8" + + def test_div(self, env): + tmpl = env.from_string("{{ 3 // 2 }}|{{ 3 / 2 }}|{{ 3 % 2 }}") + assert tmpl.render() == "1|1.5|1" + + def test_unary(self, env): + tmpl = env.from_string("{{ +3 }}|{{ -3 }}") + assert tmpl.render() == "3|-3" + + def test_concat(self, env): + tmpl = env.from_string("{{ [1, 2] ~ 'foo' }}") + assert tmpl.render() == "[1, 2]foo" + + @pytest.mark.parametrize( + ("a", "op", "b"), + [ + (1, ">", 0), + (1, ">=", 1), + (2, "<", 3), + (3, "<=", 4), + (4, "==", 4), + (4, "!=", 5), + ], + ) + def test_compare(self, env, a, op, b): + t = env.from_string(f"{{{{ {a} {op} {b} }}}}") + assert t.render() == "True" + + def test_compare_parens(self, env): + t = env.from_string("{{ i * (j < 5) }}") + assert t.render(i=2, j=3) == "2" + + @pytest.mark.parametrize( + ("src", "expect"), + [ + ("{{ 4 < 2 < 3 }}", "False"), + ("{{ a < b < c }}", "False"), + ("{{ 4 > 2 > 3 }}", "False"), + ("{{ a > b > c }}", "False"), + ("{{ 4 > 2 < 3 }}", "True"), + ("{{ a > b < c }}", "True"), + ], + ) + def test_compare_compound(self, env, src, expect): + t = env.from_string(src) + assert t.render(a=4, b=2, c=3) == expect + + def test_inop(self, env): + tmpl = env.from_string("{{ 1 in [1, 2, 3] }}|{{ 1 not in [1, 2, 3] }}") + assert tmpl.render() == "True|False" + + @pytest.mark.parametrize("value", ("[]", "{}", "()")) + def test_collection_literal(self, env, value): + t = env.from_string(f"{{{{ {value} }}}}") + assert t.render() == value + + @pytest.mark.parametrize( + ("value", "expect"), + ( + ("1", "1"), + ("123", "123"), + ("12_34_56", "123456"), + ("1.2", "1.2"), + ("34.56", "34.56"), + ("3_4.5_6", "34.56"), + ("1e0", "1.0"), + ("10e1", "100.0"), + ("2.5e100", "2.5e+100"), + ("2.5e+100", "2.5e+100"), + ("25.6e-10", "2.56e-09"), + ("1_2.3_4e5_6", "1.234e+57"), + ("0", "0"), + ("0_00", "0"), + ("0b1001_1111", "159"), + ("0o123", "83"), + ("0o1_23", "83"), + ("0x123abc", "1194684"), + ("0x12_3abc", "1194684"), + ), + ) + def test_numeric_literal(self, env, value, expect): + t = env.from_string(f"{{{{ {value} }}}}") + assert t.render() == expect + + def test_bool(self, env): + tmpl = env.from_string( + "{{ true and false }}|{{ false or true }}|{{ not false }}" + ) + assert tmpl.render() == "False|True|True" + + def test_grouping(self, env): + tmpl = env.from_string( + "{{ (true and false) or (false and true) and not false }}" + ) + assert tmpl.render() == "False" + + def test_django_attr(self, env): + tmpl = env.from_string("{{ [1, 2, 3].0 }}|{{ [[1]].0.0 }}") + assert tmpl.render() == "1|1" + + def test_conditional_expression(self, env): + tmpl = env.from_string("""{{ 0 if true else 1 }}""") + assert tmpl.render() == "0" + + def test_short_conditional_expression(self, env): + tmpl = env.from_string("<{{ 1 if false }}>") + assert tmpl.render() == "<>" + + tmpl = env.from_string("<{{ (1 if false).bar }}>") + pytest.raises(UndefinedError, tmpl.render) + + def test_filter_priority(self, env): + tmpl = env.from_string('{{ "foo"|upper + "bar"|upper }}') + assert tmpl.render() == "FOOBAR" + + def test_function_calls(self, env): + tests = [ + (True, "*foo, bar"), + (True, "*foo, *bar"), + (True, "**foo, *bar"), + (True, "**foo, bar"), + (True, "**foo, **bar"), + (True, "**foo, bar=42"), + (False, "foo, bar"), + (False, "foo, bar=42"), + (False, "foo, bar=23, *args"), + (False, "foo, *args, bar=23"), + (False, "a, b=c, *d, **e"), + (False, "*foo, bar=42"), + (False, "*foo, **bar"), + (False, "*foo, bar=42, **baz"), + (False, "foo, *args, bar=23, **baz"), + ] + for should_fail, sig in tests: + if should_fail: + with pytest.raises(TemplateSyntaxError): + env.from_string(f"{{{{ foo({sig}) }}}}") + else: + env.from_string(f"foo({sig})") + + def test_tuple_expr(self, env): + for tmpl in [ + "{{ () }}", + "{{ (1, 2) }}", + "{{ (1, 2,) }}", + "{{ 1, }}", + "{{ 1, 2 }}", + "{% for foo, bar in seq %}...{% endfor %}", + "{% for x in foo, bar %}...{% endfor %}", + "{% for x in foo, %}...{% endfor %}", + ]: + assert env.from_string(tmpl) + + def test_trailing_comma(self, env): + tmpl = env.from_string("{{ (1, 2,) }}|{{ [1, 2,] }}|{{ {1: 2,} }}") + assert tmpl.render().lower() == "(1, 2)|[1, 2]|{1: 2}" + + def test_block_end_name(self, env): + env.from_string("{% block foo %}...{% endblock foo %}") + pytest.raises( + TemplateSyntaxError, env.from_string, "{% block x %}{% endblock y %}" + ) + + def test_constant_casing(self, env): + for const in True, False, None: + const = str(const) + tmpl = env.from_string( + f"{{{{ {const} }}}}|{{{{ {const.lower()} }}}}|{{{{ {const.upper()} }}}}" + ) + assert tmpl.render() == f"{const}|{const}|" + + def test_test_chaining(self, env): + pytest.raises( + TemplateSyntaxError, env.from_string, "{{ foo is string is sequence }}" + ) + assert env.from_string("{{ 42 is string or 42 is number }}").render() == "True" + + def test_string_concatenation(self, env): + tmpl = env.from_string('{{ "foo" "bar" "baz" }}') + assert tmpl.render() == "foobarbaz" + + def test_notin(self, env): + bar = range(100) + tmpl = env.from_string("""{{ not 42 in bar }}""") + assert tmpl.render(bar=bar) == "False" + + def test_operator_precedence(self, env): + tmpl = env.from_string("""{{ 2 * 3 + 4 % 2 + 1 - 2 }}""") + assert tmpl.render() == "5" + + def test_implicit_subscribed_tuple(self, env): + class Foo: + def __getitem__(self, x): + return x + + t = env.from_string("{{ foo[1, 2] }}") + assert t.render(foo=Foo()) == "(1, 2)" + + def test_raw2(self, env): + tmpl = env.from_string("{% raw %}{{ FOO }} and {% BAR %}{% endraw %}") + assert tmpl.render() == "{{ FOO }} and {% BAR %}" + + def test_const(self, env): + tmpl = env.from_string( + "{{ true }}|{{ false }}|{{ none }}|" + "{{ none is defined }}|{{ missing is defined }}" + ) + assert tmpl.render() == "True|False|None|True|False" + + def test_neg_filter_priority(self, env): + node = env.parse("{{ -1|foo }}") + assert isinstance(node.body[0].nodes[0], nodes.Filter) + assert isinstance(node.body[0].nodes[0].node, nodes.Neg) + + def test_const_assign(self, env): + constass1 = """{% set true = 42 %}""" + constass2 = """{% for none in seq %}{% endfor %}""" + for tmpl in constass1, constass2: + pytest.raises(TemplateSyntaxError, env.from_string, tmpl) + + def test_localset(self, env): + tmpl = env.from_string( + """{% set foo = 0 %}\ +{% for item in [1, 2] %}{% set foo = 1 %}{% endfor %}\ +{{ foo }}""" + ) + assert tmpl.render() == "0" + + def test_parse_unary(self, env): + tmpl = env.from_string('{{ -foo["bar"] }}') + assert tmpl.render(foo={"bar": 42}) == "-42" + tmpl = env.from_string('{{ -foo["bar"]|abs }}') + assert tmpl.render(foo={"bar": 42}) == "42" + + +class TestLstripBlocks: + def test_lstrip(self, env): + 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): + 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): + env = Environment(lstrip_blocks=True, trim_blocks=False) + tmpl = env.from_string(""" {%+ if True %}\n {%+ endif %}""") + assert tmpl.render() == " \n " + + def test_lstrip_blocks_false_with_no_lstrip(self, env): + # Test that + is a NOP (but does not cause an error) if lstrip_blocks=False + env = Environment(lstrip_blocks=False, trim_blocks=False) + tmpl = env.from_string(""" {% if True %}\n {% endif %}""") + assert tmpl.render() == " \n " + tmpl = env.from_string(""" {%+ if True %}\n {%+ endif %}""") + assert tmpl.render() == " \n " + + def test_lstrip_endline(self, env): + 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): + 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): + 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): + 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): + 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): + 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): + 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): + 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): + 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): + 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(f"{x}\n" for x in range(5)) + + def test_lstrip_angle_bracket_compact(self, env): + 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(f"{x}\n" for x in range(5)) + + def test_lstrip_blocks_outside_with_new_line(self): + env = Environment(lstrip_blocks=True, trim_blocks=False) + tmpl = env.from_string( + " {% if kvs %}(\n" + " {% for k, v in kvs %}{{ k }}={{ v }} {% endfor %}\n" + " ){% endif %}" + ) + out = tmpl.render(kvs=[("a", 1), ("b", 2)]) + assert out == "(\na=1 b=2 \n )" + + def test_lstrip_trim_blocks_outside_with_new_line(self): + env = Environment(lstrip_blocks=True, trim_blocks=True) + tmpl = env.from_string( + " {% if kvs %}(\n" + " {% for k, v in kvs %}{{ k }}={{ v }} {% endfor %}\n" + " ){% endif %}" + ) + out = tmpl.render(kvs=[("a", 1), ("b", 2)]) + assert out == "(\na=1 b=2 )" + + def test_lstrip_blocks_inside_with_new_line(self): + env = Environment(lstrip_blocks=True, trim_blocks=False) + tmpl = env.from_string( + " ({% if kvs %}\n" + " {% for k, v in kvs %}{{ k }}={{ v }} {% endfor %}\n" + " {% endif %})" + ) + out = tmpl.render(kvs=[("a", 1), ("b", 2)]) + assert out == " (\na=1 b=2 \n)" + + def test_lstrip_trim_blocks_inside_with_new_line(self): + env = Environment(lstrip_blocks=True, trim_blocks=True) + tmpl = env.from_string( + " ({% if kvs %}\n" + " {% for k, v in kvs %}{{ k }}={{ v }} {% endfor %}\n" + " {% endif %})" + ) + out = tmpl.render(kvs=[("a", 1), ("b", 2)]) + assert out == " (a=1 b=2 )" + + def test_lstrip_blocks_without_new_line(self): + env = Environment(lstrip_blocks=True, trim_blocks=False) + tmpl = env.from_string( + " {% if kvs %}" + " {% for k, v in kvs %}{{ k }}={{ v }} {% endfor %}" + " {% endif %}" + ) + out = tmpl.render(kvs=[("a", 1), ("b", 2)]) + assert out == " a=1 b=2 " + + def test_lstrip_trim_blocks_without_new_line(self): + env = Environment(lstrip_blocks=True, trim_blocks=True) + tmpl = env.from_string( + " {% if kvs %}" + " {% for k, v in kvs %}{{ k }}={{ v }} {% endfor %}" + " {% endif %}" + ) + out = tmpl.render(kvs=[("a", 1), ("b", 2)]) + assert out == " a=1 b=2 " + + def test_lstrip_blocks_consume_after_without_new_line(self): + env = Environment(lstrip_blocks=True, trim_blocks=False) + tmpl = env.from_string( + " {% if kvs -%}" + " {% for k, v in kvs %}{{ k }}={{ v }} {% endfor -%}" + " {% endif -%}" + ) + out = tmpl.render(kvs=[("a", 1), ("b", 2)]) + assert out == "a=1 b=2 " + + def test_lstrip_trim_blocks_consume_before_without_new_line(self): + env = Environment(lstrip_blocks=False, trim_blocks=False) + tmpl = env.from_string( + " {%- if kvs %}" + " {%- for k, v in kvs %}{{ k }}={{ v }} {% endfor -%}" + " {%- endif %}" + ) + out = tmpl.render(kvs=[("a", 1), ("b", 2)]) + assert out == "a=1 b=2 " + + def test_lstrip_trim_blocks_comment(self): + env = Environment(lstrip_blocks=True, trim_blocks=True) + tmpl = env.from_string(" {# 1 space #}\n {# 2 spaces #} {# 4 spaces #}") + out = tmpl.render() + assert out == " " * 4 + + def test_lstrip_trim_blocks_raw(self): + env = Environment(lstrip_blocks=True, trim_blocks=True) + tmpl = env.from_string("{{x}}\n{%- raw %} {% endraw -%}\n{{ y }}") + out = tmpl.render(x=1, y=2) + assert out == "1 2" + + def test_php_syntax_with_manual(self, env): + 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): + 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(f" {x}\n" for x in range(5)) + + def test_php_syntax_compact(self, env): + 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(f" {x}\n" for x in range(5)) + + def test_erb_syntax(self, env): + 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(f" {x}\n" for x in range(5)) + + def test_erb_syntax_with_manual(self, env): + 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): + 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): + 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" + + +class TestTrimBlocks: + def test_trim(self, env): + env = Environment(trim_blocks=True, lstrip_blocks=False) + tmpl = env.from_string(" {% if True %}\n {% endif %}") + assert tmpl.render() == " " + + def test_no_trim(self, env): + env = Environment(trim_blocks=True, lstrip_blocks=False) + tmpl = env.from_string(" {% if True +%}\n {% endif %}") + assert tmpl.render() == " \n " + + def test_no_trim_outer(self, env): + env = Environment(trim_blocks=True, lstrip_blocks=False) + tmpl = env.from_string("{% if True %}X{% endif +%}\nmore things") + assert tmpl.render() == "X\nmore things" + + def test_lstrip_no_trim(self, env): + env = Environment(trim_blocks=True, lstrip_blocks=True) + tmpl = env.from_string(" {% if True +%}\n {% endif %}") + assert tmpl.render() == "\n" + + def test_trim_blocks_false_with_no_trim(self, env): + # Test that + is a NOP (but does not cause an error) if trim_blocks=False + env = Environment(trim_blocks=False, lstrip_blocks=False) + tmpl = env.from_string(" {% if True %}\n {% endif %}") + assert tmpl.render() == " \n " + tmpl = env.from_string(" {% if True +%}\n {% endif %}") + assert tmpl.render() == " \n " + + tmpl = env.from_string(" {# comment #}\n ") + assert tmpl.render() == " \n " + tmpl = env.from_string(" {# comment +#}\n ") + assert tmpl.render() == " \n " + + tmpl = env.from_string(" {% raw %}{% endraw %}\n ") + assert tmpl.render() == " \n " + tmpl = env.from_string(" {% raw %}{% endraw +%}\n ") + assert tmpl.render() == " \n " + + def test_trim_nested(self, env): + env = Environment(trim_blocks=True, lstrip_blocks=True) + tmpl = env.from_string( + " {% if True %}\na {% if True %}\nb {% endif %}\nc {% endif %}" + ) + assert tmpl.render() == "a b c " + + def test_no_trim_nested(self, env): + env = Environment(trim_blocks=True, lstrip_blocks=True) + tmpl = env.from_string( + " {% if True +%}\na {% if True +%}\nb {% endif +%}\nc {% endif %}" + ) + assert tmpl.render() == "\na \nb \nc " + + def test_comment_trim(self, env): + env = Environment(trim_blocks=True, lstrip_blocks=True) + tmpl = env.from_string(""" {# comment #}\n\n """) + assert tmpl.render() == "\n " + + def test_comment_no_trim(self, env): + env = Environment(trim_blocks=True, lstrip_blocks=True) + tmpl = env.from_string(""" {# comment +#}\n\n """) + assert tmpl.render() == "\n\n " + + def test_multiple_comment_trim_lstrip(self, env): + env = Environment(trim_blocks=True, lstrip_blocks=True) + tmpl = env.from_string( + " {# comment #}\n\n{# comment2 #}\n \n{# comment3 #}\n\n " + ) + assert tmpl.render() == "\n \n\n " + + def test_multiple_comment_no_trim_lstrip(self, env): + env = Environment(trim_blocks=True, lstrip_blocks=True) + tmpl = env.from_string( + " {# comment +#}\n\n{# comment2 +#}\n \n{# comment3 +#}\n\n " + ) + assert tmpl.render() == "\n\n\n \n\n\n " + + def test_raw_trim_lstrip(self, env): + env = Environment(trim_blocks=True, lstrip_blocks=True) + tmpl = env.from_string("{{x}}{% raw %}\n\n {% endraw %}\n\n{{ y }}") + assert tmpl.render(x=1, y=2) == "1\n\n\n2" + + def test_raw_no_trim_lstrip(self, env): + env = Environment(trim_blocks=False, lstrip_blocks=True) + tmpl = env.from_string("{{x}}{% raw %}\n\n {% endraw +%}\n\n{{ y }}") + assert tmpl.render(x=1, y=2) == "1\n\n\n\n2" + + # raw blocks do not process inner text, so start tag cannot ignore trim + with pytest.raises(TemplateSyntaxError): + tmpl = env.from_string("{{x}}{% raw +%}\n\n {% endraw +%}\n\n{{ y }}") + + def test_no_trim_angle_bracket(self, env): + env = Environment( + "<%", "%>", "${", "}", "<%#", "%>", lstrip_blocks=True, trim_blocks=True + ) + tmpl = env.from_string(" <% if True +%>\n\n <% endif %>") + assert tmpl.render() == "\n\n" + + tmpl = env.from_string(" <%# comment +%>\n\n ") + assert tmpl.render() == "\n\n " + + def test_no_trim_php_syntax(self, env): + env = Environment( + "<?", + "?>", + "<?=", + "?>", + "<!--", + "-->", + lstrip_blocks=False, + trim_blocks=True, + ) + tmpl = env.from_string(" <? if True +?>\n\n <? endif ?>") + assert tmpl.render() == " \n\n " + tmpl = env.from_string(" <!-- comment +-->\n\n ") + assert tmpl.render() == " \n\n " |