1import pytest
2
3from jinja2 import Environment
4from jinja2 import nodes
5from jinja2 import Template
6from jinja2 import TemplateSyntaxError
7from jinja2 import UndefinedError
8from jinja2.lexer import Token
9from jinja2.lexer import TOKEN_BLOCK_BEGIN
10from jinja2.lexer import TOKEN_BLOCK_END
11from jinja2.lexer import TOKEN_EOF
12from jinja2.lexer import TokenStream
13
14
15class TestTokenStream:
16    test_tokens = [
17        Token(1, TOKEN_BLOCK_BEGIN, ""),
18        Token(2, TOKEN_BLOCK_END, ""),
19    ]
20
21    def test_simple(self, env):
22        ts = TokenStream(self.test_tokens, "foo", "bar")
23        assert ts.current.type is TOKEN_BLOCK_BEGIN
24        assert bool(ts)
25        assert not bool(ts.eos)
26        next(ts)
27        assert ts.current.type is TOKEN_BLOCK_END
28        assert bool(ts)
29        assert not bool(ts.eos)
30        next(ts)
31        assert ts.current.type is TOKEN_EOF
32        assert not bool(ts)
33        assert bool(ts.eos)
34
35    def test_iter(self, env):
36        token_types = [t.type for t in TokenStream(self.test_tokens, "foo", "bar")]
37        assert token_types == [
38            "block_begin",
39            "block_end",
40        ]
41
42
43class TestLexer:
44    def test_raw1(self, env):
45        tmpl = env.from_string(
46            "{% raw %}foo{% endraw %}|"
47            "{%raw%}{{ bar }}|{% baz %}{%       endraw    %}"
48        )
49        assert tmpl.render() == "foo|{{ bar }}|{% baz %}"
50
51    def test_raw2(self, env):
52        tmpl = env.from_string("1  {%- raw -%}   2   {%- endraw -%}   3")
53        assert tmpl.render() == "123"
54
55    def test_raw3(self, env):
56        # The second newline after baz exists because it is AFTER the
57        # {% raw %} and is ignored.
58        env = Environment(lstrip_blocks=True, trim_blocks=True)
59        tmpl = env.from_string("bar\n{% raw %}\n  {{baz}}2 spaces\n{% endraw %}\nfoo")
60        assert tmpl.render(baz="test") == "bar\n\n  {{baz}}2 spaces\nfoo"
61
62    def test_raw4(self, env):
63        # The trailing dash of the {% raw -%} cleans both the spaces and
64        # newlines up to the first character of data.
65        env = Environment(lstrip_blocks=True, trim_blocks=False)
66        tmpl = env.from_string(
67            "bar\n{%- raw -%}\n\n  \n  2 spaces\n space{%- endraw -%}\nfoo"
68        )
69        assert tmpl.render() == "bar2 spaces\n spacefoo"
70
71    def test_balancing(self, env):
72        env = Environment("{%", "%}", "${", "}")
73        tmpl = env.from_string(
74            """{% for item in seq
75            %}${{'foo': item}|upper}{% endfor %}"""
76        )
77        assert tmpl.render(seq=list(range(3))) == "{'FOO': 0}{'FOO': 1}{'FOO': 2}"
78
79    def test_comments(self, env):
80        env = Environment("<!--", "-->", "{", "}")
81        tmpl = env.from_string(
82            """\
83<ul>
84<!--- for item in seq -->
85  <li>{item}</li>
86<!--- endfor -->
87</ul>"""
88        )
89        assert tmpl.render(seq=list(range(3))) == (
90            "<ul>\n  <li>0</li>\n  <li>1</li>\n  <li>2</li>\n</ul>"
91        )
92
93    def test_string_escapes(self, env):
94        for char in "\0", "\u2668", "\xe4", "\t", "\r", "\n":
95            tmpl = env.from_string(f"{{{{ {char!r} }}}}")
96            assert tmpl.render() == char
97        assert env.from_string('{{ "\N{HOT SPRINGS}" }}').render() == "\u2668"
98
99    def test_bytefallback(self, env):
100        from pprint import pformat
101
102        tmpl = env.from_string("""{{ 'foo'|pprint }}|{{ 'bär'|pprint }}""")
103        assert tmpl.render() == pformat("foo") + "|" + pformat("bär")
104
105    def test_operators(self, env):
106        from jinja2.lexer import operators
107
108        for test, expect in operators.items():
109            if test in "([{}])":
110                continue
111            stream = env.lexer.tokenize(f"{{{{ {test} }}}}")
112            next(stream)
113            assert stream.current.type == expect
114
115    def test_normalizing(self, env):
116        for seq in "\r", "\r\n", "\n":
117            env = Environment(newline_sequence=seq)
118            tmpl = env.from_string("1\n2\r\n3\n4\n")
119            result = tmpl.render()
120            assert result.replace(seq, "X") == "1X2X3X4"
121
122    def test_trailing_newline(self, env):
123        for keep in [True, False]:
124            env = Environment(keep_trailing_newline=keep)
125            for template, expected in [
126                ("", {}),
127                ("no\nnewline", {}),
128                ("with\nnewline\n", {False: "with\nnewline"}),
129                ("with\nseveral\n\n\n", {False: "with\nseveral\n\n"}),
130            ]:
131                tmpl = env.from_string(template)
132                expect = expected.get(keep, template)
133                result = tmpl.render()
134                assert result == expect, (keep, template, result, expect)
135
136    @pytest.mark.parametrize(
137        ("name", "valid"),
138        [
139            ("foo", True),
140            ("föö", True),
141            ("き", True),
142            ("_", True),
143            ("1a", False),  # invalid ascii start
144            ("a-", False),  # invalid ascii continue
145            ("\U0001f40da", False),  # invalid unicode start
146            ("a��\U0001f40d", False),  # invalid unicode continue
147            # start characters not matched by \w
148            ("\u1885", True),
149            ("\u1886", True),
150            ("\u2118", True),
151            ("\u212e", True),
152            # continue character not matched by \w
153            ("\xb7", False),
154            ("a\xb7", True),
155        ],
156    )
157    def test_name(self, env, name, valid):
158        t = "{{ " + name + " }}"
159
160        if valid:
161            # valid for version being tested, shouldn't raise
162            env.from_string(t)
163        else:
164            pytest.raises(TemplateSyntaxError, env.from_string, t)
165
166    def test_lineno_with_strip(self, env):
167        tokens = env.lex(
168            """\
169<html>
170    <body>
171    {%- block content -%}
172        <hr>
173        {{ item }}
174    {% endblock %}
175    </body>
176</html>"""
177        )
178        for tok in tokens:
179            lineno, token_type, value = tok
180            if token_type == "name" and value == "item":
181                assert lineno == 5
182                break
183
184
185class TestParser:
186    def test_php_syntax(self, env):
187        env = Environment("<?", "?>", "<?=", "?>", "<!--", "-->")
188        tmpl = env.from_string(
189            """\
190<!-- I'm a comment, I'm not interesting -->\
191<? for item in seq -?>
192    <?= item ?>
193<?- endfor ?>"""
194        )
195        assert tmpl.render(seq=list(range(5))) == "01234"
196
197    def test_erb_syntax(self, env):
198        env = Environment("<%", "%>", "<%=", "%>", "<%#", "%>")
199        tmpl = env.from_string(
200            """\
201<%# I'm a comment, I'm not interesting %>\
202<% for item in seq -%>
203    <%= item %>
204<%- endfor %>"""
205        )
206        assert tmpl.render(seq=list(range(5))) == "01234"
207
208    def test_comment_syntax(self, env):
209        env = Environment("<!--", "-->", "${", "}", "<!--#", "-->")
210        tmpl = env.from_string(
211            """\
212<!--# I'm a comment, I'm not interesting -->\
213<!-- for item in seq --->
214    ${item}
215<!--- endfor -->"""
216        )
217        assert tmpl.render(seq=list(range(5))) == "01234"
218
219    def test_balancing(self, env):
220        tmpl = env.from_string("""{{{'foo':'bar'}.foo}}""")
221        assert tmpl.render() == "bar"
222
223    def test_start_comment(self, env):
224        tmpl = env.from_string(
225            """{# foo comment
226and bar comment #}
227{% macro blub() %}foo{% endmacro %}
228{{ blub() }}"""
229        )
230        assert tmpl.render().strip() == "foo"
231
232    def test_line_syntax(self, env):
233        env = Environment("<%", "%>", "${", "}", "<%#", "%>", "%")
234        tmpl = env.from_string(
235            """\
236<%# regular comment %>
237% for item in seq:
238    ${item}
239% endfor"""
240        )
241        assert [
242            int(x.strip()) for x in tmpl.render(seq=list(range(5))).split()
243        ] == list(range(5))
244
245        env = Environment("<%", "%>", "${", "}", "<%#", "%>", "%", "##")
246        tmpl = env.from_string(
247            """\
248<%# regular comment %>
249% for item in seq:
250    ${item} ## the rest of the stuff
251% endfor"""
252        )
253        assert [
254            int(x.strip()) for x in tmpl.render(seq=list(range(5))).split()
255        ] == list(range(5))
256
257    def test_line_syntax_priority(self, env):
258        # XXX: why is the whitespace there in front of the newline?
259        env = Environment("{%", "%}", "${", "}", "/*", "*/", "##", "#")
260        tmpl = env.from_string(
261            """\
262/* ignore me.
263   I'm a multiline comment */
264## for item in seq:
265* ${item}          # this is just extra stuff
266## endfor"""
267        )
268        assert tmpl.render(seq=[1, 2]).strip() == "* 1\n* 2"
269        env = Environment("{%", "%}", "${", "}", "/*", "*/", "#", "##")
270        tmpl = env.from_string(
271            """\
272/* ignore me.
273   I'm a multiline comment */
274# for item in seq:
275* ${item}          ## this is just extra stuff
276    ## extra stuff i just want to ignore
277# endfor"""
278        )
279        assert tmpl.render(seq=[1, 2]).strip() == "* 1\n\n* 2"
280
281    def test_error_messages(self, env):
282        def assert_error(code, expected):
283            with pytest.raises(TemplateSyntaxError, match=expected):
284                Template(code)
285
286        assert_error(
287            "{% for item in seq %}...{% endif %}",
288            "Encountered unknown tag 'endif'. Jinja was looking "
289            "for the following tags: 'endfor' or 'else'. The "
290            "innermost block that needs to be closed is 'for'.",
291        )
292        assert_error(
293            "{% if foo %}{% for item in seq %}...{% endfor %}{% endfor %}",
294            "Encountered unknown tag 'endfor'. Jinja was looking for "
295            "the following tags: 'elif' or 'else' or 'endif'. The "
296            "innermost block that needs to be closed is 'if'.",
297        )
298        assert_error(
299            "{% if foo %}",
300            "Unexpected end of template. Jinja was looking for the "
301            "following tags: 'elif' or 'else' or 'endif'. The "
302            "innermost block that needs to be closed is 'if'.",
303        )
304        assert_error(
305            "{% for item in seq %}",
306            "Unexpected end of template. Jinja was looking for the "
307            "following tags: 'endfor' or 'else'. The innermost block "
308            "that needs to be closed is 'for'.",
309        )
310        assert_error(
311            "{% block foo-bar-baz %}",
312            "Block names in Jinja have to be valid Python identifiers "
313            "and may not contain hyphens, use an underscore instead.",
314        )
315        assert_error("{% unknown_tag %}", "Encountered unknown tag 'unknown_tag'.")
316
317
318class TestSyntax:
319    def test_call(self, env):
320        env = Environment()
321        env.globals["foo"] = lambda a, b, c, e, g: a + b + c + e + g
322        tmpl = env.from_string("{{ foo('a', c='d', e='f', *['b'], **{'g': 'h'}) }}")
323        assert tmpl.render() == "abdfh"
324
325    def test_slicing(self, env):
326        tmpl = env.from_string("{{ [1, 2, 3][:] }}|{{ [1, 2, 3][::-1] }}")
327        assert tmpl.render() == "[1, 2, 3]|[3, 2, 1]"
328
329    def test_attr(self, env):
330        tmpl = env.from_string("{{ foo.bar }}|{{ foo['bar'] }}")
331        assert tmpl.render(foo={"bar": 42}) == "42|42"
332
333    def test_subscript(self, env):
334        tmpl = env.from_string("{{ foo[0] }}|{{ foo[-1] }}")
335        assert tmpl.render(foo=[0, 1, 2]) == "0|2"
336
337    def test_tuple(self, env):
338        tmpl = env.from_string("{{ () }}|{{ (1,) }}|{{ (1, 2) }}")
339        assert tmpl.render() == "()|(1,)|(1, 2)"
340
341    def test_math(self, env):
342        tmpl = env.from_string("{{ (1 + 1 * 2) - 3 / 2 }}|{{ 2**3 }}")
343        assert tmpl.render() == "1.5|8"
344
345    def test_div(self, env):
346        tmpl = env.from_string("{{ 3 // 2 }}|{{ 3 / 2 }}|{{ 3 % 2 }}")
347        assert tmpl.render() == "1|1.5|1"
348
349    def test_unary(self, env):
350        tmpl = env.from_string("{{ +3 }}|{{ -3 }}")
351        assert tmpl.render() == "3|-3"
352
353    def test_concat(self, env):
354        tmpl = env.from_string("{{ [1, 2] ~ 'foo' }}")
355        assert tmpl.render() == "[1, 2]foo"
356
357    @pytest.mark.parametrize(
358        ("a", "op", "b"),
359        [
360            (1, ">", 0),
361            (1, ">=", 1),
362            (2, "<", 3),
363            (3, "<=", 4),
364            (4, "==", 4),
365            (4, "!=", 5),
366        ],
367    )
368    def test_compare(self, env, a, op, b):
369        t = env.from_string(f"{{{{ {a} {op} {b} }}}}")
370        assert t.render() == "True"
371
372    def test_compare_parens(self, env):
373        t = env.from_string("{{ i * (j < 5) }}")
374        assert t.render(i=2, j=3) == "2"
375
376    @pytest.mark.parametrize(
377        ("src", "expect"),
378        [
379            ("{{ 4 < 2 < 3 }}", "False"),
380            ("{{ a < b < c }}", "False"),
381            ("{{ 4 > 2 > 3 }}", "False"),
382            ("{{ a > b > c }}", "False"),
383            ("{{ 4 > 2 < 3 }}", "True"),
384            ("{{ a > b < c }}", "True"),
385        ],
386    )
387    def test_compare_compound(self, env, src, expect):
388        t = env.from_string(src)
389        assert t.render(a=4, b=2, c=3) == expect
390
391    def test_inop(self, env):
392        tmpl = env.from_string("{{ 1 in [1, 2, 3] }}|{{ 1 not in [1, 2, 3] }}")
393        assert tmpl.render() == "True|False"
394
395    @pytest.mark.parametrize("value", ("[]", "{}", "()"))
396    def test_collection_literal(self, env, value):
397        t = env.from_string(f"{{{{ {value} }}}}")
398        assert t.render() == value
399
400    @pytest.mark.parametrize(
401        ("value", "expect"),
402        (
403            ("1", "1"),
404            ("123", "123"),
405            ("12_34_56", "123456"),
406            ("1.2", "1.2"),
407            ("34.56", "34.56"),
408            ("3_4.5_6", "34.56"),
409            ("1e0", "1.0"),
410            ("10e1", "100.0"),
411            ("2.5e100", "2.5e+100"),
412            ("2.5e+100", "2.5e+100"),
413            ("25.6e-10", "2.56e-09"),
414            ("1_2.3_4e5_6", "1.234e+57"),
415        ),
416    )
417    def test_numeric_literal(self, env, value, expect):
418        t = env.from_string(f"{{{{ {value} }}}}")
419        assert t.render() == expect
420
421    def test_bool(self, env):
422        tmpl = env.from_string(
423            "{{ true and false }}|{{ false or true }}|{{ not false }}"
424        )
425        assert tmpl.render() == "False|True|True"
426
427    def test_grouping(self, env):
428        tmpl = env.from_string(
429            "{{ (true and false) or (false and true) and not false }}"
430        )
431        assert tmpl.render() == "False"
432
433    def test_django_attr(self, env):
434        tmpl = env.from_string("{{ [1, 2, 3].0 }}|{{ [[1]].0.0 }}")
435        assert tmpl.render() == "1|1"
436
437    def test_conditional_expression(self, env):
438        tmpl = env.from_string("""{{ 0 if true else 1 }}""")
439        assert tmpl.render() == "0"
440
441    def test_short_conditional_expression(self, env):
442        tmpl = env.from_string("<{{ 1 if false }}>")
443        assert tmpl.render() == "<>"
444
445        tmpl = env.from_string("<{{ (1 if false).bar }}>")
446        pytest.raises(UndefinedError, tmpl.render)
447
448    def test_filter_priority(self, env):
449        tmpl = env.from_string('{{ "foo"|upper + "bar"|upper }}')
450        assert tmpl.render() == "FOOBAR"
451
452    def test_function_calls(self, env):
453        tests = [
454            (True, "*foo, bar"),
455            (True, "*foo, *bar"),
456            (True, "**foo, *bar"),
457            (True, "**foo, bar"),
458            (True, "**foo, **bar"),
459            (True, "**foo, bar=42"),
460            (False, "foo, bar"),
461            (False, "foo, bar=42"),
462            (False, "foo, bar=23, *args"),
463            (False, "foo, *args, bar=23"),
464            (False, "a, b=c, *d, **e"),
465            (False, "*foo, bar=42"),
466            (False, "*foo, **bar"),
467            (False, "*foo, bar=42, **baz"),
468            (False, "foo, *args, bar=23, **baz"),
469        ]
470        for should_fail, sig in tests:
471            if should_fail:
472                with pytest.raises(TemplateSyntaxError):
473                    env.from_string(f"{{{{ foo({sig}) }}}}")
474            else:
475                env.from_string(f"foo({sig})")
476
477    def test_tuple_expr(self, env):
478        for tmpl in [
479            "{{ () }}",
480            "{{ (1, 2) }}",
481            "{{ (1, 2,) }}",
482            "{{ 1, }}",
483            "{{ 1, 2 }}",
484            "{% for foo, bar in seq %}...{% endfor %}",
485            "{% for x in foo, bar %}...{% endfor %}",
486            "{% for x in foo, %}...{% endfor %}",
487        ]:
488            assert env.from_string(tmpl)
489
490    def test_trailing_comma(self, env):
491        tmpl = env.from_string("{{ (1, 2,) }}|{{ [1, 2,] }}|{{ {1: 2,} }}")
492        assert tmpl.render().lower() == "(1, 2)|[1, 2]|{1: 2}"
493
494    def test_block_end_name(self, env):
495        env.from_string("{% block foo %}...{% endblock foo %}")
496        pytest.raises(
497            TemplateSyntaxError, env.from_string, "{% block x %}{% endblock y %}"
498        )
499
500    def test_constant_casing(self, env):
501        for const in True, False, None:
502            const = str(const)
503            tmpl = env.from_string(
504                f"{{{{ {const} }}}}|{{{{ {const.lower()} }}}}|{{{{ {const.upper()} }}}}"
505            )
506            assert tmpl.render() == f"{const}|{const}|"
507
508    def test_test_chaining(self, env):
509        pytest.raises(
510            TemplateSyntaxError, env.from_string, "{{ foo is string is sequence }}"
511        )
512        assert env.from_string("{{ 42 is string or 42 is number }}").render() == "True"
513
514    def test_string_concatenation(self, env):
515        tmpl = env.from_string('{{ "foo" "bar" "baz" }}')
516        assert tmpl.render() == "foobarbaz"
517
518    def test_notin(self, env):
519        bar = range(100)
520        tmpl = env.from_string("""{{ not 42 in bar }}""")
521        assert tmpl.render(bar=bar) == "False"
522
523    def test_operator_precedence(self, env):
524        tmpl = env.from_string("""{{ 2 * 3 + 4 % 2 + 1 - 2 }}""")
525        assert tmpl.render() == "5"
526
527    def test_implicit_subscribed_tuple(self, env):
528        class Foo:
529            def __getitem__(self, x):
530                return x
531
532        t = env.from_string("{{ foo[1, 2] }}")
533        assert t.render(foo=Foo()) == "(1, 2)"
534
535    def test_raw2(self, env):
536        tmpl = env.from_string("{% raw %}{{ FOO }} and {% BAR %}{% endraw %}")
537        assert tmpl.render() == "{{ FOO }} and {% BAR %}"
538
539    def test_const(self, env):
540        tmpl = env.from_string(
541            "{{ true }}|{{ false }}|{{ none }}|"
542            "{{ none is defined }}|{{ missing is defined }}"
543        )
544        assert tmpl.render() == "True|False|None|True|False"
545
546    def test_neg_filter_priority(self, env):
547        node = env.parse("{{ -1|foo }}")
548        assert isinstance(node.body[0].nodes[0], nodes.Filter)
549        assert isinstance(node.body[0].nodes[0].node, nodes.Neg)
550
551    def test_const_assign(self, env):
552        constass1 = """{% set true = 42 %}"""
553        constass2 = """{% for none in seq %}{% endfor %}"""
554        for tmpl in constass1, constass2:
555            pytest.raises(TemplateSyntaxError, env.from_string, tmpl)
556
557    def test_localset(self, env):
558        tmpl = env.from_string(
559            """{% set foo = 0 %}\
560{% for item in [1, 2] %}{% set foo = 1 %}{% endfor %}\
561{{ foo }}"""
562        )
563        assert tmpl.render() == "0"
564
565    def test_parse_unary(self, env):
566        tmpl = env.from_string('{{ -foo["bar"] }}')
567        assert tmpl.render(foo={"bar": 42}) == "-42"
568        tmpl = env.from_string('{{ -foo["bar"]|abs }}')
569        assert tmpl.render(foo={"bar": 42}) == "42"
570
571
572class TestLstripBlocks:
573    def test_lstrip(self, env):
574        env = Environment(lstrip_blocks=True, trim_blocks=False)
575        tmpl = env.from_string("""    {% if True %}\n    {% endif %}""")
576        assert tmpl.render() == "\n"
577
578    def test_lstrip_trim(self, env):
579        env = Environment(lstrip_blocks=True, trim_blocks=True)
580        tmpl = env.from_string("""    {% if True %}\n    {% endif %}""")
581        assert tmpl.render() == ""
582
583    def test_no_lstrip(self, env):
584        env = Environment(lstrip_blocks=True, trim_blocks=False)
585        tmpl = env.from_string("""    {%+ if True %}\n    {%+ endif %}""")
586        assert tmpl.render() == "    \n    "
587
588    def test_lstrip_blocks_false_with_no_lstrip(self, env):
589        # Test that + is a NOP (but does not cause an error) if lstrip_blocks=False
590        env = Environment(lstrip_blocks=False, trim_blocks=False)
591        tmpl = env.from_string("""    {% if True %}\n    {% endif %}""")
592        assert tmpl.render() == "    \n    "
593        tmpl = env.from_string("""    {%+ if True %}\n    {%+ endif %}""")
594        assert tmpl.render() == "    \n    "
595
596    def test_lstrip_endline(self, env):
597        env = Environment(lstrip_blocks=True, trim_blocks=False)
598        tmpl = env.from_string("""    hello{% if True %}\n    goodbye{% endif %}""")
599        assert tmpl.render() == "    hello\n    goodbye"
600
601    def test_lstrip_inline(self, env):
602        env = Environment(lstrip_blocks=True, trim_blocks=False)
603        tmpl = env.from_string("""    {% if True %}hello    {% endif %}""")
604        assert tmpl.render() == "hello    "
605
606    def test_lstrip_nested(self, env):
607        env = Environment(lstrip_blocks=True, trim_blocks=False)
608        tmpl = env.from_string(
609            """    {% if True %}a {% if True %}b {% endif %}c {% endif %}"""
610        )
611        assert tmpl.render() == "a b c "
612
613    def test_lstrip_left_chars(self, env):
614        env = Environment(lstrip_blocks=True, trim_blocks=False)
615        tmpl = env.from_string(
616            """    abc {% if True %}
617        hello{% endif %}"""
618        )
619        assert tmpl.render() == "    abc \n        hello"
620
621    def test_lstrip_embeded_strings(self, env):
622        env = Environment(lstrip_blocks=True, trim_blocks=False)
623        tmpl = env.from_string("""    {% set x = " {% str %} " %}{{ x }}""")
624        assert tmpl.render() == " {% str %} "
625
626    def test_lstrip_preserve_leading_newlines(self, env):
627        env = Environment(lstrip_blocks=True, trim_blocks=False)
628        tmpl = env.from_string("""\n\n\n{% set hello = 1 %}""")
629        assert tmpl.render() == "\n\n\n"
630
631    def test_lstrip_comment(self, env):
632        env = Environment(lstrip_blocks=True, trim_blocks=False)
633        tmpl = env.from_string(
634            """    {# if True #}
635hello
636    {#endif#}"""
637        )
638        assert tmpl.render() == "\nhello\n"
639
640    def test_lstrip_angle_bracket_simple(self, env):
641        env = Environment(
642            "<%",
643            "%>",
644            "${",
645            "}",
646            "<%#",
647            "%>",
648            "%",
649            "##",
650            lstrip_blocks=True,
651            trim_blocks=True,
652        )
653        tmpl = env.from_string("""    <% if True %>hello    <% endif %>""")
654        assert tmpl.render() == "hello    "
655
656    def test_lstrip_angle_bracket_comment(self, env):
657        env = Environment(
658            "<%",
659            "%>",
660            "${",
661            "}",
662            "<%#",
663            "%>",
664            "%",
665            "##",
666            lstrip_blocks=True,
667            trim_blocks=True,
668        )
669        tmpl = env.from_string("""    <%# if True %>hello    <%# endif %>""")
670        assert tmpl.render() == "hello    "
671
672    def test_lstrip_angle_bracket(self, env):
673        env = Environment(
674            "<%",
675            "%>",
676            "${",
677            "}",
678            "<%#",
679            "%>",
680            "%",
681            "##",
682            lstrip_blocks=True,
683            trim_blocks=True,
684        )
685        tmpl = env.from_string(
686            """\
687    <%# regular comment %>
688    <% for item in seq %>
689${item} ## the rest of the stuff
690   <% endfor %>"""
691        )
692        assert tmpl.render(seq=range(5)) == "".join(f"{x}\n" for x in range(5))
693
694    def test_lstrip_angle_bracket_compact(self, env):
695        env = Environment(
696            "<%",
697            "%>",
698            "${",
699            "}",
700            "<%#",
701            "%>",
702            "%",
703            "##",
704            lstrip_blocks=True,
705            trim_blocks=True,
706        )
707        tmpl = env.from_string(
708            """\
709    <%#regular comment%>
710    <%for item in seq%>
711${item} ## the rest of the stuff
712   <%endfor%>"""
713        )
714        assert tmpl.render(seq=range(5)) == "".join(f"{x}\n" for x in range(5))
715
716    def test_lstrip_blocks_outside_with_new_line(self):
717        env = Environment(lstrip_blocks=True, trim_blocks=False)
718        tmpl = env.from_string(
719            "  {% if kvs %}(\n"
720            "   {% for k, v in kvs %}{{ k }}={{ v }} {% endfor %}\n"
721            "  ){% endif %}"
722        )
723        out = tmpl.render(kvs=[("a", 1), ("b", 2)])
724        assert out == "(\na=1 b=2 \n  )"
725
726    def test_lstrip_trim_blocks_outside_with_new_line(self):
727        env = Environment(lstrip_blocks=True, trim_blocks=True)
728        tmpl = env.from_string(
729            "  {% if kvs %}(\n"
730            "   {% for k, v in kvs %}{{ k }}={{ v }} {% endfor %}\n"
731            "  ){% endif %}"
732        )
733        out = tmpl.render(kvs=[("a", 1), ("b", 2)])
734        assert out == "(\na=1 b=2   )"
735
736    def test_lstrip_blocks_inside_with_new_line(self):
737        env = Environment(lstrip_blocks=True, trim_blocks=False)
738        tmpl = env.from_string(
739            "  ({% if kvs %}\n"
740            "   {% for k, v in kvs %}{{ k }}={{ v }} {% endfor %}\n"
741            "  {% endif %})"
742        )
743        out = tmpl.render(kvs=[("a", 1), ("b", 2)])
744        assert out == "  (\na=1 b=2 \n)"
745
746    def test_lstrip_trim_blocks_inside_with_new_line(self):
747        env = Environment(lstrip_blocks=True, trim_blocks=True)
748        tmpl = env.from_string(
749            "  ({% if kvs %}\n"
750            "   {% for k, v in kvs %}{{ k }}={{ v }} {% endfor %}\n"
751            "  {% endif %})"
752        )
753        out = tmpl.render(kvs=[("a", 1), ("b", 2)])
754        assert out == "  (a=1 b=2 )"
755
756    def test_lstrip_blocks_without_new_line(self):
757        env = Environment(lstrip_blocks=True, trim_blocks=False)
758        tmpl = env.from_string(
759            "  {% if kvs %}"
760            "   {% for k, v in kvs %}{{ k }}={{ v }} {% endfor %}"
761            "  {% endif %}"
762        )
763        out = tmpl.render(kvs=[("a", 1), ("b", 2)])
764        assert out == "   a=1 b=2   "
765
766    def test_lstrip_trim_blocks_without_new_line(self):
767        env = Environment(lstrip_blocks=True, trim_blocks=True)
768        tmpl = env.from_string(
769            "  {% if kvs %}"
770            "   {% for k, v in kvs %}{{ k }}={{ v }} {% endfor %}"
771            "  {% endif %}"
772        )
773        out = tmpl.render(kvs=[("a", 1), ("b", 2)])
774        assert out == "   a=1 b=2   "
775
776    def test_lstrip_blocks_consume_after_without_new_line(self):
777        env = Environment(lstrip_blocks=True, trim_blocks=False)
778        tmpl = env.from_string(
779            "  {% if kvs -%}"
780            "   {% for k, v in kvs %}{{ k }}={{ v }} {% endfor -%}"
781            "  {% endif -%}"
782        )
783        out = tmpl.render(kvs=[("a", 1), ("b", 2)])
784        assert out == "a=1 b=2 "
785
786    def test_lstrip_trim_blocks_consume_before_without_new_line(self):
787        env = Environment(lstrip_blocks=False, trim_blocks=False)
788        tmpl = env.from_string(
789            "  {%- if kvs %}"
790            "   {%- for k, v in kvs %}{{ k }}={{ v }} {% endfor -%}"
791            "  {%- endif %}"
792        )
793        out = tmpl.render(kvs=[("a", 1), ("b", 2)])
794        assert out == "a=1 b=2 "
795
796    def test_lstrip_trim_blocks_comment(self):
797        env = Environment(lstrip_blocks=True, trim_blocks=True)
798        tmpl = env.from_string(" {# 1 space #}\n  {# 2 spaces #}    {# 4 spaces #}")
799        out = tmpl.render()
800        assert out == " " * 4
801
802    def test_lstrip_trim_blocks_raw(self):
803        env = Environment(lstrip_blocks=True, trim_blocks=True)
804        tmpl = env.from_string("{{x}}\n{%- raw %} {% endraw -%}\n{{ y }}")
805        out = tmpl.render(x=1, y=2)
806        assert out == "1 2"
807
808    def test_php_syntax_with_manual(self, env):
809        env = Environment(
810            "<?", "?>", "<?=", "?>", "<!--", "-->", lstrip_blocks=True, trim_blocks=True
811        )
812        tmpl = env.from_string(
813            """\
814    <!-- I'm a comment, I'm not interesting -->
815    <? for item in seq -?>
816        <?= item ?>
817    <?- endfor ?>"""
818        )
819        assert tmpl.render(seq=range(5)) == "01234"
820
821    def test_php_syntax(self, env):
822        env = Environment(
823            "<?", "?>", "<?=", "?>", "<!--", "-->", lstrip_blocks=True, trim_blocks=True
824        )
825        tmpl = env.from_string(
826            """\
827    <!-- I'm a comment, I'm not interesting -->
828    <? for item in seq ?>
829        <?= item ?>
830    <? endfor ?>"""
831        )
832        assert tmpl.render(seq=range(5)) == "".join(f"        {x}\n" for x in range(5))
833
834    def test_php_syntax_compact(self, env):
835        env = Environment(
836            "<?", "?>", "<?=", "?>", "<!--", "-->", lstrip_blocks=True, trim_blocks=True
837        )
838        tmpl = env.from_string(
839            """\
840    <!-- I'm a comment, I'm not interesting -->
841    <?for item in seq?>
842        <?=item?>
843    <?endfor?>"""
844        )
845        assert tmpl.render(seq=range(5)) == "".join(f"        {x}\n" for x in range(5))
846
847    def test_erb_syntax(self, env):
848        env = Environment(
849            "<%", "%>", "<%=", "%>", "<%#", "%>", lstrip_blocks=True, trim_blocks=True
850        )
851        tmpl = env.from_string(
852            """\
853<%# I'm a comment, I'm not interesting %>
854    <% for item in seq %>
855    <%= item %>
856    <% endfor %>
857"""
858        )
859        assert tmpl.render(seq=range(5)) == "".join(f"    {x}\n" for x in range(5))
860
861    def test_erb_syntax_with_manual(self, env):
862        env = Environment(
863            "<%", "%>", "<%=", "%>", "<%#", "%>", lstrip_blocks=True, trim_blocks=True
864        )
865        tmpl = env.from_string(
866            """\
867<%# I'm a comment, I'm not interesting %>
868    <% for item in seq -%>
869        <%= item %>
870    <%- endfor %>"""
871        )
872        assert tmpl.render(seq=range(5)) == "01234"
873
874    def test_erb_syntax_no_lstrip(self, env):
875        env = Environment(
876            "<%", "%>", "<%=", "%>", "<%#", "%>", lstrip_blocks=True, trim_blocks=True
877        )
878        tmpl = env.from_string(
879            """\
880<%# I'm a comment, I'm not interesting %>
881    <%+ for item in seq -%>
882        <%= item %>
883    <%- endfor %>"""
884        )
885        assert tmpl.render(seq=range(5)) == "    01234"
886
887    def test_comment_syntax(self, env):
888        env = Environment(
889            "<!--",
890            "-->",
891            "${",
892            "}",
893            "<!--#",
894            "-->",
895            lstrip_blocks=True,
896            trim_blocks=True,
897        )
898        tmpl = env.from_string(
899            """\
900<!--# I'm a comment, I'm not interesting -->\
901<!-- for item in seq --->
902    ${item}
903<!--- endfor -->"""
904        )
905        assert tmpl.render(seq=range(5)) == "01234"
906
907
908class TestTrimBlocks:
909    def test_trim(self, env):
910        env = Environment(trim_blocks=True, lstrip_blocks=False)
911        tmpl = env.from_string("    {% if True %}\n    {% endif %}")
912        assert tmpl.render() == "        "
913
914    def test_no_trim(self, env):
915        env = Environment(trim_blocks=True, lstrip_blocks=False)
916        tmpl = env.from_string("    {% if True +%}\n    {% endif %}")
917        assert tmpl.render() == "    \n    "
918
919    def test_no_trim_outer(self, env):
920        env = Environment(trim_blocks=True, lstrip_blocks=False)
921        tmpl = env.from_string("{% if True %}X{% endif +%}\nmore things")
922        assert tmpl.render() == "X\nmore things"
923
924    def test_lstrip_no_trim(self, env):
925        env = Environment(trim_blocks=True, lstrip_blocks=True)
926        tmpl = env.from_string("    {% if True +%}\n    {% endif %}")
927        assert tmpl.render() == "\n"
928
929    def test_trim_blocks_false_with_no_trim(self, env):
930        # Test that + is a NOP (but does not cause an error) if trim_blocks=False
931        env = Environment(trim_blocks=False, lstrip_blocks=False)
932        tmpl = env.from_string("    {% if True %}\n    {% endif %}")
933        assert tmpl.render() == "    \n    "
934        tmpl = env.from_string("    {% if True +%}\n    {% endif %}")
935        assert tmpl.render() == "    \n    "
936
937        tmpl = env.from_string("    {# comment #}\n    ")
938        assert tmpl.render() == "    \n    "
939        tmpl = env.from_string("    {# comment +#}\n    ")
940        assert tmpl.render() == "    \n    "
941
942        tmpl = env.from_string("    {% raw %}{% endraw %}\n    ")
943        assert tmpl.render() == "    \n    "
944        tmpl = env.from_string("    {% raw %}{% endraw +%}\n    ")
945        assert tmpl.render() == "    \n    "
946
947    def test_trim_nested(self, env):
948        env = Environment(trim_blocks=True, lstrip_blocks=True)
949        tmpl = env.from_string(
950            "    {% if True %}\na {% if True %}\nb {% endif %}\nc {% endif %}"
951        )
952        assert tmpl.render() == "a b c "
953
954    def test_no_trim_nested(self, env):
955        env = Environment(trim_blocks=True, lstrip_blocks=True)
956        tmpl = env.from_string(
957            "    {% if True +%}\na {% if True +%}\nb {% endif +%}\nc {% endif %}"
958        )
959        assert tmpl.render() == "\na \nb \nc "
960
961    def test_comment_trim(self, env):
962        env = Environment(trim_blocks=True, lstrip_blocks=True)
963        tmpl = env.from_string("""    {# comment #}\n\n  """)
964        assert tmpl.render() == "\n  "
965
966    def test_comment_no_trim(self, env):
967        env = Environment(trim_blocks=True, lstrip_blocks=True)
968        tmpl = env.from_string("""    {# comment +#}\n\n  """)
969        assert tmpl.render() == "\n\n  "
970
971    def test_multiple_comment_trim_lstrip(self, env):
972        env = Environment(trim_blocks=True, lstrip_blocks=True)
973        tmpl = env.from_string(
974            "   {# comment #}\n\n{# comment2 #}\n   \n{# comment3 #}\n\n "
975        )
976        assert tmpl.render() == "\n   \n\n "
977
978    def test_multiple_comment_no_trim_lstrip(self, env):
979        env = Environment(trim_blocks=True, lstrip_blocks=True)
980        tmpl = env.from_string(
981            "   {# comment +#}\n\n{# comment2 +#}\n   \n{# comment3 +#}\n\n "
982        )
983        assert tmpl.render() == "\n\n\n   \n\n\n "
984
985    def test_raw_trim_lstrip(self, env):
986        env = Environment(trim_blocks=True, lstrip_blocks=True)
987        tmpl = env.from_string("{{x}}{% raw %}\n\n    {% endraw %}\n\n{{ y }}")
988        assert tmpl.render(x=1, y=2) == "1\n\n\n2"
989
990    def test_raw_no_trim_lstrip(self, env):
991        env = Environment(trim_blocks=False, lstrip_blocks=True)
992        tmpl = env.from_string("{{x}}{% raw %}\n\n      {% endraw +%}\n\n{{ y }}")
993        assert tmpl.render(x=1, y=2) == "1\n\n\n\n2"
994
995        # raw blocks do not process inner text, so start tag cannot ignore trim
996        with pytest.raises(TemplateSyntaxError):
997            tmpl = env.from_string("{{x}}{% raw +%}\n\n  {% endraw +%}\n\n{{ y }}")
998
999    def test_no_trim_angle_bracket(self, env):
1000        env = Environment(
1001            "<%", "%>", "${", "}", "<%#", "%>", lstrip_blocks=True, trim_blocks=True,
1002        )
1003        tmpl = env.from_string("    <% if True +%>\n\n    <% endif %>")
1004        assert tmpl.render() == "\n\n"
1005
1006        tmpl = env.from_string("    <%# comment +%>\n\n   ")
1007        assert tmpl.render() == "\n\n   "
1008
1009    def test_no_trim_php_syntax(self, env):
1010        env = Environment(
1011            "<?",
1012            "?>",
1013            "<?=",
1014            "?>",
1015            "<!--",
1016            "-->",
1017            lstrip_blocks=False,
1018            trim_blocks=True,
1019        )
1020        tmpl = env.from_string("    <? if True +?>\n\n    <? endif ?>")
1021        assert tmpl.render() == "    \n\n    "
1022        tmpl = env.from_string("    <!-- comment +-->\n\n    ")
1023        assert tmpl.render() == "    \n\n    "
1024