diff --git a/CHANGES.rst b/CHANGES.rst index 36db0843a..88d0f3e5a 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -1,5 +1,19 @@ .. currentmodule:: jinja2 +Unreleased + +- Fix compiler. As compiled jinja blocks does not have info about + parent template scopes like autoescape, due to which it is ignoring those inherited + properties. So we made blocks compilation volatile, so that it will figure correct + property during rendering time. :issue:`1898` + +- Fix NodeParser error. Ideally child templates body should contain blocks as first + child. because only blocks in child template are going to be replaced with + blocks in main template. So each node in child template body is inspected to + identify first block node and moving it into the template body. + These blocks will seamlessly placed in the main template body. :issue:`1898` + + Version 3.1.3 ------------- diff --git a/src/jinja2/compiler.py b/src/jinja2/compiler.py index 7dfac0a71..19b6ad28a 100644 --- a/src/jinja2/compiler.py +++ b/src/jinja2/compiler.py @@ -920,6 +920,9 @@ def visit_Template( # interesting issues with identifier tracking. block_frame = Frame(eval_ctx) block_frame.block_frame = True + # during compile time we are not aware of parent template configuration, + # so it's better to decide child blocks configuration at runtime. + block_frame.eval_ctx.volatile = True undeclared = find_undeclared(block.body, ("self", "super")) if "self" in undeclared: ref = block_frame.symbols.declare_parameter("self") @@ -1406,7 +1409,7 @@ def _make_finalize(self) -> _FinalizeInfo: if pass_arg is None: - def finalize(value: t.Any) -> t.Any: + def finalize(value: t.Any) -> t.Any: # noqa: F811 return default(env_finalize(value)) else: @@ -1414,7 +1417,7 @@ def finalize(value: t.Any) -> t.Any: if pass_arg == "environment": - def finalize(value: t.Any) -> t.Any: + def finalize(value: t.Any) -> t.Any: # noqa: F811 return default(env_finalize(self.environment, value)) self._finalize = self._FinalizeInfo(finalize, src) diff --git a/src/jinja2/parser.py b/src/jinja2/parser.py index 12703a97a..678c83a6d 100644 --- a/src/jinja2/parser.py +++ b/src/jinja2/parser.py @@ -1031,4 +1031,46 @@ def parse(self) -> nodes.Template: """Parse the whole template into a `Template` node.""" result = nodes.Template(self.subparse(), lineno=1) result.set_environment(self.environment) + # ideally child templates body should contain blocks as first child. + # because only blocks are going to be replaced with blocks in main template. + # So we are using below method to move blocks as first child + # (items in template-node body list ). + # and other non-block nodes will remain as it is. And while rendering those + # non-block nodes will be ignored automatically. + result = self.parse_extend_blocks(result) return result + + def parse_extend_blocks(self, template: nodes.Template) -> nodes.Template: + """Parse the whole template and move first found Block Node to the top.""" + + have_extends = template.find(nodes.Extends) is not None + + if not have_extends: + return template + + new_body = [] + for node in template.body: + new_body.append(node) + new_body.extend(self.extract_first_child_block(node)) + template.body = new_body + return template + + def extract_first_child_block( + self, template: nodes.Node + ) -> typing.List[nodes.Node]: + """Remove first found block node from each child node.""" + new_child: typing.List[nodes.Node] = [] + counter = 0 + if not hasattr(template, "body") or isinstance(template, nodes.Block): + return new_child + while len(template.body) > counter: + if not hasattr(template.body[counter], "body"): + counter += 1 + continue + + if isinstance(template.body[counter], nodes.Block): + new_child.append(template.body.pop(counter)) + continue + new_child.extend(self.extract_first_child_block(template.body[counter])) + counter += 1 + return new_child diff --git a/tests/test_regression.py b/tests/test_regression.py index 46e492bdd..cbcf93ccc 100644 --- a/tests/test_regression.py +++ b/tests/test_regression.py @@ -1,3 +1,5 @@ +import textwrap + import pytest from jinja2 import DictLoader @@ -8,6 +10,7 @@ from jinja2 import TemplateNotFound from jinja2 import TemplateSyntaxError from jinja2.utils import pass_context +from jinja2.utils import select_autoescape class TestCorner: @@ -736,6 +739,50 @@ def test_nested_loop_scoping(self, env): ) assert tmpl.render() == "hellohellohello" + def test_autoescape_block_inheritance(self, env): + text = "hel'lo" + output = "Subject: hel'lo\nBody: hel'lo" + templates = { + "base.html": textwrap.dedent( + """ + Subject: {% autoescape false %}{% block subject %} + {% endblock %}{% endautoescape %} + Body: {% block body %}{% endblock %} + """ + ).strip(), + "test1.html": textwrap.dedent( + """ + {% extends 'base.html' %} + {% block subject %}{{ text }}{% endblock %} + {% block body %}{{ text }}{% endblock %} + """ + ).strip(), + "test2.html": textwrap.dedent( + """ + {% extends 'base.html' %} + {% autoescape false -%} + {% block subject %}{{ text }}{% endblock %} + {%- endautoescape %} + {% block body %}{{ text }}{% endblock %} + """ + ).strip(), + "test3.html": textwrap.dedent( + """ + {% extends 'base.html' %} + {% block subject %} + {%- autoescape false %}{{ text }}{% endautoescape -%} + {% endblock %} + {% block body %}{{ text }}{% endblock %} + """ + ).strip(), + } + + env = Environment(loader=DictLoader(templates), autoescape=select_autoescape()) + + assert env.get_template("test1.html").render(text=text) == output + assert env.get_template("test2.html").render(text=text) == output + assert env.get_template("test3.html").render(text=text) == output + @pytest.mark.parametrize("unicode_char", ["\N{FORM FEED}", "\x85"]) def test_unicode_whitespace(env, unicode_char):