1import re
2from io import BytesIO
3
4import pytest
5
6from jinja2 import contextfunction
7from jinja2 import DictLoader
8from jinja2 import Environment
9from jinja2 import nodes
10from jinja2.exceptions import TemplateAssertionError
11from jinja2.ext import Extension
12from jinja2.lexer import count_newlines
13from jinja2.lexer import Token
14
15importable_object = 23
16
17_gettext_re = re.compile(r"_\((.*?)\)", re.DOTALL)
18
19
20i18n_templates = {
21    "master.html": '<title>{{ page_title|default(_("missing")) }}</title>'
22    "{% block body %}{% endblock %}",
23    "child.html": '{% extends "master.html" %}{% block body %}'
24    "{% trans %}watch out{% endtrans %}{% endblock %}",
25    "plural.html": "{% trans user_count %}One user online{% pluralize %}"
26    "{{ user_count }} users online{% endtrans %}",
27    "plural2.html": "{% trans user_count=get_user_count() %}{{ user_count }}s"
28    "{% pluralize %}{{ user_count }}p{% endtrans %}",
29    "stringformat.html": '{{ _("User: %(num)s")|format(num=user_count) }}',
30}
31
32newstyle_i18n_templates = {
33    "master.html": '<title>{{ page_title|default(_("missing")) }}</title>'
34    "{% block body %}{% endblock %}",
35    "child.html": '{% extends "master.html" %}{% block body %}'
36    "{% trans %}watch out{% endtrans %}{% endblock %}",
37    "plural.html": "{% trans user_count %}One user online{% pluralize %}"
38    "{{ user_count }} users online{% endtrans %}",
39    "stringformat.html": '{{ _("User: %(num)s", num=user_count) }}',
40    "ngettext.html": '{{ ngettext("%(num)s apple", "%(num)s apples", apples) }}',
41    "ngettext_long.html": "{% trans num=apples %}{{ num }} apple{% pluralize %}"
42    "{{ num }} apples{% endtrans %}",
43    "transvars1.html": "{% trans %}User: {{ num }}{% endtrans %}",
44    "transvars2.html": "{% trans num=count %}User: {{ num }}{% endtrans %}",
45    "transvars3.html": "{% trans count=num %}User: {{ count }}{% endtrans %}",
46    "novars.html": "{% trans %}%(hello)s{% endtrans %}",
47    "vars.html": "{% trans %}{{ foo }}%(foo)s{% endtrans %}",
48    "explicitvars.html": '{% trans foo="42" %}%(foo)s{% endtrans %}',
49}
50
51
52languages = {
53    "de": {
54        "missing": "fehlend",
55        "watch out": "pass auf",
56        "One user online": "Ein Benutzer online",
57        "%(user_count)s users online": "%(user_count)s Benutzer online",
58        "User: %(num)s": "Benutzer: %(num)s",
59        "User: %(count)s": "Benutzer: %(count)s",
60        "%(num)s apple": "%(num)s Apfel",
61        "%(num)s apples": "%(num)s Äpfel",
62    }
63}
64
65
66@contextfunction
67def gettext(context, string):
68    language = context.get("LANGUAGE", "en")
69    return languages.get(language, {}).get(string, string)
70
71
72@contextfunction
73def ngettext(context, s, p, n):
74    language = context.get("LANGUAGE", "en")
75    if n != 1:
76        return languages.get(language, {}).get(p, p)
77    return languages.get(language, {}).get(s, s)
78
79
80i18n_env = Environment(
81    loader=DictLoader(i18n_templates), extensions=["jinja2.ext.i18n"]
82)
83i18n_env.globals.update({"_": gettext, "gettext": gettext, "ngettext": ngettext})
84i18n_env_trimmed = Environment(extensions=["jinja2.ext.i18n"])
85i18n_env_trimmed.policies["ext.i18n.trimmed"] = True
86i18n_env_trimmed.globals.update(
87    {"_": gettext, "gettext": gettext, "ngettext": ngettext}
88)
89
90newstyle_i18n_env = Environment(
91    loader=DictLoader(newstyle_i18n_templates), extensions=["jinja2.ext.i18n"]
92)
93newstyle_i18n_env.install_gettext_callables(gettext, ngettext, newstyle=True)
94
95
96class ExampleExtension(Extension):
97    tags = {"test"}
98    ext_attr = 42
99    context_reference_node_cls = nodes.ContextReference
100
101    def parse(self, parser):
102        return nodes.Output(
103            [
104                self.call_method(
105                    "_dump",
106                    [
107                        nodes.EnvironmentAttribute("sandboxed"),
108                        self.attr("ext_attr"),
109                        nodes.ImportedName(__name__ + ".importable_object"),
110                        self.context_reference_node_cls(),
111                    ],
112                )
113            ]
114        ).set_lineno(next(parser.stream).lineno)
115
116    def _dump(self, sandboxed, ext_attr, imported_object, context):
117        return (
118            f"{sandboxed}|{ext_attr}|{imported_object}|{context.blocks}"
119            f"|{context.get('test_var')}"
120        )
121
122
123class DerivedExampleExtension(ExampleExtension):
124    context_reference_node_cls = nodes.DerivedContextReference
125
126
127class PreprocessorExtension(Extension):
128    def preprocess(self, source, name, filename=None):
129        return source.replace("[[TEST]]", "({{ foo }})")
130
131
132class StreamFilterExtension(Extension):
133    def filter_stream(self, stream):
134        for token in stream:
135            if token.type == "data":
136                yield from self.interpolate(token)
137            else:
138                yield token
139
140    def interpolate(self, token):
141        pos = 0
142        end = len(token.value)
143        lineno = token.lineno
144        while 1:
145            match = _gettext_re.search(token.value, pos)
146            if match is None:
147                break
148            value = token.value[pos : match.start()]
149            if value:
150                yield Token(lineno, "data", value)
151            lineno += count_newlines(token.value)
152            yield Token(lineno, "variable_begin", None)
153            yield Token(lineno, "name", "gettext")
154            yield Token(lineno, "lparen", None)
155            yield Token(lineno, "string", match.group(1))
156            yield Token(lineno, "rparen", None)
157            yield Token(lineno, "variable_end", None)
158            pos = match.end()
159        if pos < end:
160            yield Token(lineno, "data", token.value[pos:])
161
162
163class TestExtensions:
164    def test_extend_late(self):
165        env = Environment()
166        env.add_extension("jinja2.ext.autoescape")
167        t = env.from_string('{% autoescape true %}{{ "<test>" }}{% endautoescape %}')
168        assert t.render() == "&lt;test&gt;"
169
170    def test_loop_controls(self):
171        env = Environment(extensions=["jinja2.ext.loopcontrols"])
172
173        tmpl = env.from_string(
174            """
175            {%- for item in [1, 2, 3, 4] %}
176                {%- if item % 2 == 0 %}{% continue %}{% endif -%}
177                {{ item }}
178            {%- endfor %}"""
179        )
180        assert tmpl.render() == "13"
181
182        tmpl = env.from_string(
183            """
184            {%- for item in [1, 2, 3, 4] %}
185                {%- if item > 2 %}{% break %}{% endif -%}
186                {{ item }}
187            {%- endfor %}"""
188        )
189        assert tmpl.render() == "12"
190
191    def test_do(self):
192        env = Environment(extensions=["jinja2.ext.do"])
193        tmpl = env.from_string(
194            """
195            {%- set items = [] %}
196            {%- for char in "foo" %}
197                {%- do items.append(loop.index0 ~ char) %}
198            {%- endfor %}{{ items|join(', ') }}"""
199        )
200        assert tmpl.render() == "0f, 1o, 2o"
201
202    def test_extension_nodes(self):
203        env = Environment(extensions=[ExampleExtension])
204        tmpl = env.from_string("{% test %}")
205        assert tmpl.render() == "False|42|23|{}|None"
206
207    def test_contextreference_node_passes_context(self):
208        env = Environment(extensions=[ExampleExtension])
209        tmpl = env.from_string('{% set test_var="test_content" %}{% test %}')
210        assert tmpl.render() == "False|42|23|{}|test_content"
211
212    def test_contextreference_node_can_pass_locals(self):
213        env = Environment(extensions=[DerivedExampleExtension])
214        tmpl = env.from_string(
215            '{% for test_var in ["test_content"] %}{% test %}{% endfor %}'
216        )
217        assert tmpl.render() == "False|42|23|{}|test_content"
218
219    def test_identifier(self):
220        assert ExampleExtension.identifier == __name__ + ".ExampleExtension"
221
222    def test_rebinding(self):
223        original = Environment(extensions=[ExampleExtension])
224        overlay = original.overlay()
225        for env in original, overlay:
226            for ext in env.extensions.values():
227                assert ext.environment is env
228
229    def test_preprocessor_extension(self):
230        env = Environment(extensions=[PreprocessorExtension])
231        tmpl = env.from_string("{[[TEST]]}")
232        assert tmpl.render(foo=42) == "{(42)}"
233
234    def test_streamfilter_extension(self):
235        env = Environment(extensions=[StreamFilterExtension])
236        env.globals["gettext"] = lambda x: x.upper()
237        tmpl = env.from_string("Foo _(bar) Baz")
238        out = tmpl.render()
239        assert out == "Foo BAR Baz"
240
241    def test_extension_ordering(self):
242        class T1(Extension):
243            priority = 1
244
245        class T2(Extension):
246            priority = 2
247
248        env = Environment(extensions=[T1, T2])
249        ext = list(env.iter_extensions())
250        assert ext[0].__class__ is T1
251        assert ext[1].__class__ is T2
252
253    def test_debug(self):
254        env = Environment(extensions=["jinja2.ext.debug"])
255        t = env.from_string("Hello\n{% debug %}\nGoodbye")
256        out = t.render()
257
258        for value in ("context", "cycler", "filters", "abs", "tests", "!="):
259            assert f"'{value}'" in out
260
261
262class TestInternationalization:
263    def test_trans(self):
264        tmpl = i18n_env.get_template("child.html")
265        assert tmpl.render(LANGUAGE="de") == "<title>fehlend</title>pass auf"
266
267    def test_trans_plural(self):
268        tmpl = i18n_env.get_template("plural.html")
269        assert tmpl.render(LANGUAGE="de", user_count=1) == "Ein Benutzer online"
270        assert tmpl.render(LANGUAGE="de", user_count=2) == "2 Benutzer online"
271
272    def test_trans_plural_with_functions(self):
273        tmpl = i18n_env.get_template("plural2.html")
274
275        def get_user_count():
276            get_user_count.called += 1
277            return 1
278
279        get_user_count.called = 0
280        assert tmpl.render(LANGUAGE="de", get_user_count=get_user_count) == "1s"
281        assert get_user_count.called == 1
282
283    def test_complex_plural(self):
284        tmpl = i18n_env.from_string(
285            "{% trans foo=42, count=2 %}{{ count }} item{% "
286            "pluralize count %}{{ count }} items{% endtrans %}"
287        )
288        assert tmpl.render() == "2 items"
289        pytest.raises(
290            TemplateAssertionError,
291            i18n_env.from_string,
292            "{% trans foo %}...{% pluralize bar %}...{% endtrans %}",
293        )
294
295    def test_trans_stringformatting(self):
296        tmpl = i18n_env.get_template("stringformat.html")
297        assert tmpl.render(LANGUAGE="de", user_count=5) == "Benutzer: 5"
298
299    def test_trimmed(self):
300        tmpl = i18n_env.from_string(
301            "{%- trans trimmed %}  hello\n  world  {% endtrans -%}"
302        )
303        assert tmpl.render() == "hello world"
304
305    def test_trimmed_policy(self):
306        s = "{%- trans %}  hello\n  world  {% endtrans -%}"
307        tmpl = i18n_env.from_string(s)
308        trimmed_tmpl = i18n_env_trimmed.from_string(s)
309        assert tmpl.render() == "  hello\n  world  "
310        assert trimmed_tmpl.render() == "hello world"
311
312    def test_trimmed_policy_override(self):
313        tmpl = i18n_env_trimmed.from_string(
314            "{%- trans notrimmed %}  hello\n  world  {% endtrans -%}"
315        )
316        assert tmpl.render() == "  hello\n  world  "
317
318    def test_trimmed_vars(self):
319        tmpl = i18n_env.from_string(
320            '{%- trans trimmed x="world" %}  hello\n  {{ x }} {% endtrans -%}'
321        )
322        assert tmpl.render() == "hello world"
323
324    def test_trimmed_varname_trimmed(self):
325        # unlikely variable name, but when used as a variable
326        # it should not enable trimming
327        tmpl = i18n_env.from_string(
328            "{%- trans trimmed = 'world' %}  hello\n  {{ trimmed }}  {% endtrans -%}"
329        )
330        assert tmpl.render() == "  hello\n  world  "
331
332    def test_extract(self):
333        from jinja2.ext import babel_extract
334
335        source = BytesIO(
336            b"""
337            {{ gettext('Hello World') }}
338            {% trans %}Hello World{% endtrans %}
339            {% trans %}{{ users }} user{% pluralize %}{{ users }} users{% endtrans %}
340            """
341        )
342        assert list(babel_extract(source, ("gettext", "ngettext", "_"), [], {})) == [
343            (2, "gettext", "Hello World", []),
344            (3, "gettext", "Hello World", []),
345            (4, "ngettext", ("%(users)s user", "%(users)s users", None), []),
346        ]
347
348    def test_extract_trimmed(self):
349        from jinja2.ext import babel_extract
350
351        source = BytesIO(
352            b"""
353            {{ gettext(' Hello  \n  World') }}
354            {% trans trimmed %} Hello  \n  World{% endtrans %}
355            {% trans trimmed %}{{ users }} \n user
356            {%- pluralize %}{{ users }} \n users{% endtrans %}
357            """
358        )
359        assert list(babel_extract(source, ("gettext", "ngettext", "_"), [], {})) == [
360            (2, "gettext", " Hello  \n  World", []),
361            (4, "gettext", "Hello World", []),
362            (6, "ngettext", ("%(users)s user", "%(users)s users", None), []),
363        ]
364
365    def test_extract_trimmed_option(self):
366        from jinja2.ext import babel_extract
367
368        source = BytesIO(
369            b"""
370            {{ gettext(' Hello  \n  World') }}
371            {% trans %} Hello  \n  World{% endtrans %}
372            {% trans %}{{ users }} \n user
373            {%- pluralize %}{{ users }} \n users{% endtrans %}
374            """
375        )
376        opts = {"trimmed": "true"}
377        assert list(babel_extract(source, ("gettext", "ngettext", "_"), [], opts)) == [
378            (2, "gettext", " Hello  \n  World", []),
379            (4, "gettext", "Hello World", []),
380            (6, "ngettext", ("%(users)s user", "%(users)s users", None), []),
381        ]
382
383    def test_comment_extract(self):
384        from jinja2.ext import babel_extract
385
386        source = BytesIO(
387            b"""
388            {# trans first #}
389            {{ gettext('Hello World') }}
390            {% trans %}Hello World{% endtrans %}{# trans second #}
391            {#: third #}
392            {% trans %}{{ users }} user{% pluralize %}{{ users }} users{% endtrans %}
393            """
394        )
395        assert list(
396            babel_extract(source, ("gettext", "ngettext", "_"), ["trans", ":"], {})
397        ) == [
398            (3, "gettext", "Hello World", ["first"]),
399            (4, "gettext", "Hello World", ["second"]),
400            (6, "ngettext", ("%(users)s user", "%(users)s users", None), ["third"]),
401        ]
402
403
404class TestScope:
405    def test_basic_scope_behavior(self):
406        # This is what the old with statement compiled down to
407        class ScopeExt(Extension):
408            tags = {"scope"}
409
410            def parse(self, parser):
411                node = nodes.Scope(lineno=next(parser.stream).lineno)
412                assignments = []
413                while parser.stream.current.type != "block_end":
414                    lineno = parser.stream.current.lineno
415                    if assignments:
416                        parser.stream.expect("comma")
417                    target = parser.parse_assign_target()
418                    parser.stream.expect("assign")
419                    expr = parser.parse_expression()
420                    assignments.append(nodes.Assign(target, expr, lineno=lineno))
421                node.body = assignments + list(
422                    parser.parse_statements(("name:endscope",), drop_needle=True)
423                )
424                return node
425
426        env = Environment(extensions=[ScopeExt])
427        tmpl = env.from_string(
428            """\
429        {%- scope a=1, b=2, c=b, d=e, e=5 -%}
430            {{ a }}|{{ b }}|{{ c }}|{{ d }}|{{ e }}
431        {%- endscope -%}
432        """
433        )
434        assert tmpl.render(b=3, e=4) == "1|2|2|4|5"
435
436
437class TestNewstyleInternationalization:
438    def test_trans(self):
439        tmpl = newstyle_i18n_env.get_template("child.html")
440        assert tmpl.render(LANGUAGE="de") == "<title>fehlend</title>pass auf"
441
442    def test_trans_plural(self):
443        tmpl = newstyle_i18n_env.get_template("plural.html")
444        assert tmpl.render(LANGUAGE="de", user_count=1) == "Ein Benutzer online"
445        assert tmpl.render(LANGUAGE="de", user_count=2) == "2 Benutzer online"
446
447    def test_complex_plural(self):
448        tmpl = newstyle_i18n_env.from_string(
449            "{% trans foo=42, count=2 %}{{ count }} item{% "
450            "pluralize count %}{{ count }} items{% endtrans %}"
451        )
452        assert tmpl.render() == "2 items"
453        pytest.raises(
454            TemplateAssertionError,
455            i18n_env.from_string,
456            "{% trans foo %}...{% pluralize bar %}...{% endtrans %}",
457        )
458
459    def test_trans_stringformatting(self):
460        tmpl = newstyle_i18n_env.get_template("stringformat.html")
461        assert tmpl.render(LANGUAGE="de", user_count=5) == "Benutzer: 5"
462
463    def test_newstyle_plural(self):
464        tmpl = newstyle_i18n_env.get_template("ngettext.html")
465        assert tmpl.render(LANGUAGE="de", apples=1) == "1 Apfel"
466        assert tmpl.render(LANGUAGE="de", apples=5) == "5 Äpfel"
467
468    def test_autoescape_support(self):
469        env = Environment(extensions=["jinja2.ext.autoescape", "jinja2.ext.i18n"])
470        env.install_gettext_callables(
471            lambda x: "<strong>Wert: %(name)s</strong>",
472            lambda s, p, n: s,
473            newstyle=True,
474        )
475        t = env.from_string(
476            '{% autoescape ae %}{{ gettext("foo", name='
477            '"<test>") }}{% endautoescape %}'
478        )
479        assert t.render(ae=True) == "<strong>Wert: &lt;test&gt;</strong>"
480        assert t.render(ae=False) == "<strong>Wert: <test></strong>"
481
482    def test_autoescape_macros(self):
483        env = Environment(autoescape=False, extensions=["jinja2.ext.autoescape"])
484        template = (
485            "{% macro m() %}<html>{% endmacro %}"
486            "{% autoescape true %}{{ m() }}{% endautoescape %}"
487        )
488        assert env.from_string(template).render() == "<html>"
489
490    def test_num_used_twice(self):
491        tmpl = newstyle_i18n_env.get_template("ngettext_long.html")
492        assert tmpl.render(apples=5, LANGUAGE="de") == "5 Äpfel"
493
494    def test_num_called_num(self):
495        source = newstyle_i18n_env.compile(
496            """
497            {% trans num=3 %}{{ num }} apple{% pluralize
498            %}{{ num }} apples{% endtrans %}
499        """,
500            raw=True,
501        )
502        # quite hacky, but the only way to properly test that.  The idea is
503        # that the generated code does not pass num twice (although that
504        # would work) for better performance.  This only works on the
505        # newstyle gettext of course
506        assert (
507            re.search(r"u?'%\(num\)s apple', u?'%\(num\)s apples', 3", source)
508            is not None
509        )
510
511    def test_trans_vars(self):
512        t1 = newstyle_i18n_env.get_template("transvars1.html")
513        t2 = newstyle_i18n_env.get_template("transvars2.html")
514        t3 = newstyle_i18n_env.get_template("transvars3.html")
515        assert t1.render(num=1, LANGUAGE="de") == "Benutzer: 1"
516        assert t2.render(count=23, LANGUAGE="de") == "Benutzer: 23"
517        assert t3.render(num=42, LANGUAGE="de") == "Benutzer: 42"
518
519    def test_novars_vars_escaping(self):
520        t = newstyle_i18n_env.get_template("novars.html")
521        assert t.render() == "%(hello)s"
522        t = newstyle_i18n_env.get_template("vars.html")
523        assert t.render(foo="42") == "42%(foo)s"
524        t = newstyle_i18n_env.get_template("explicitvars.html")
525        assert t.render() == "%(foo)s"
526
527
528class TestAutoEscape:
529    def test_scoped_setting(self):
530        env = Environment(extensions=["jinja2.ext.autoescape"], autoescape=True)
531        tmpl = env.from_string(
532            """
533            {{ "<HelloWorld>" }}
534            {% autoescape false %}
535                {{ "<HelloWorld>" }}
536            {% endautoescape %}
537            {{ "<HelloWorld>" }}
538        """
539        )
540        assert tmpl.render().split() == [
541            "&lt;HelloWorld&gt;",
542            "<HelloWorld>",
543            "&lt;HelloWorld&gt;",
544        ]
545
546        env = Environment(extensions=["jinja2.ext.autoescape"], autoescape=False)
547        tmpl = env.from_string(
548            """
549            {{ "<HelloWorld>" }}
550            {% autoescape true %}
551                {{ "<HelloWorld>" }}
552            {% endautoescape %}
553            {{ "<HelloWorld>" }}
554        """
555        )
556        assert tmpl.render().split() == [
557            "<HelloWorld>",
558            "&lt;HelloWorld&gt;",
559            "<HelloWorld>",
560        ]
561
562    def test_nonvolatile(self):
563        env = Environment(extensions=["jinja2.ext.autoescape"], autoescape=True)
564        tmpl = env.from_string('{{ {"foo": "<test>"}|xmlattr|escape }}')
565        assert tmpl.render() == ' foo="&lt;test&gt;"'
566        tmpl = env.from_string(
567            '{% autoescape false %}{{ {"foo": "<test>"}'
568            "|xmlattr|escape }}{% endautoescape %}"
569        )
570        assert tmpl.render() == " foo=&#34;&amp;lt;test&amp;gt;&#34;"
571
572    def test_volatile(self):
573        env = Environment(extensions=["jinja2.ext.autoescape"], autoescape=True)
574        tmpl = env.from_string(
575            '{% autoescape foo %}{{ {"foo": "<test>"}'
576            "|xmlattr|escape }}{% endautoescape %}"
577        )
578        assert tmpl.render(foo=False) == " foo=&#34;&amp;lt;test&amp;gt;&#34;"
579        assert tmpl.render(foo=True) == ' foo="&lt;test&gt;"'
580
581    def test_scoping(self):
582        env = Environment(extensions=["jinja2.ext.autoescape"])
583        tmpl = env.from_string(
584            '{% autoescape true %}{% set x = "<x>" %}{{ x }}'
585            '{% endautoescape %}{{ x }}{{ "<y>" }}'
586        )
587        assert tmpl.render(x=1) == "&lt;x&gt;1<y>"
588
589    def test_volatile_scoping(self):
590        env = Environment(extensions=["jinja2.ext.autoescape"])
591        tmplsource = """
592        {% autoescape val %}
593            {% macro foo(x) %}
594                [{{ x }}]
595            {% endmacro %}
596            {{ foo().__class__.__name__ }}
597        {% endautoescape %}
598        {{ '<testing>' }}
599        """
600        tmpl = env.from_string(tmplsource)
601        assert tmpl.render(val=True).split()[0] == "Markup"
602        assert tmpl.render(val=False).split()[0] == "str"
603
604        # looking at the source we should see <testing> there in raw
605        # (and then escaped as well)
606        env = Environment(extensions=["jinja2.ext.autoescape"])
607        pysource = env.compile(tmplsource, raw=True)
608        assert "<testing>\\n" in pysource
609
610        env = Environment(extensions=["jinja2.ext.autoescape"], autoescape=True)
611        pysource = env.compile(tmplsource, raw=True)
612        assert "&lt;testing&gt;\\n" in pysource
613
614    def test_overlay_scopes(self):
615        class MagicScopeExtension(Extension):
616            tags = {"overlay"}
617
618            def parse(self, parser):
619                node = nodes.OverlayScope(lineno=next(parser.stream).lineno)
620                node.body = list(
621                    parser.parse_statements(("name:endoverlay",), drop_needle=True)
622                )
623                node.context = self.call_method("get_scope")
624                return node
625
626            def get_scope(self):
627                return {"x": [1, 2, 3]}
628
629        env = Environment(extensions=[MagicScopeExtension])
630
631        tmpl = env.from_string(
632            """
633            {{- x }}|{% set z = 99 %}
634            {%- overlay %}
635                {{- y }}|{{ z }}|{% for item in x %}[{{ item }}]{% endfor %}
636            {%- endoverlay %}|
637            {{- x -}}
638        """
639        )
640        assert tmpl.render(x=42, y=23) == "42|23|99|[1][2][3]|42"
641