diff --git a/django/template/base.py b/django/template/base.py index d7bc59d668..37e7243d5c 100644 --- a/django/template/base.py +++ b/django/template/base.py @@ -89,11 +89,6 @@ UNKNOWN_SOURCE = "" # than instantiating SimpleLazyObject with _lazy_re_compile(). tag_re = re.compile(r"({%.*?%}|{{.*?}}|{#.*?#})") -combined_partial_re = re.compile( - r"{%\s*partialdef\s+(?P[\w-]+)(?:\s+inline)?\s*%}" - r"|{%\s*endpartialdef(?:\s+[\w-]+)?\s*%}" -) - logger = logging.getLogger("django.template") @@ -301,29 +296,26 @@ class PartialTemplate: Wraps nodelist as a partial, in order to be able to bind context. """ - def __init__(self, nodelist, origin, name): + def __init__(self, nodelist, origin, name, source_start=None, source_end=None): self.nodelist = nodelist self.origin = origin self.name = name + # If available (debug mode), the absolute character offsets in the + # template.source correspond to the full partial region. + self._source_start = source_start + self._source_end = source_end def get_exception_info(self, exception, token): template = self.origin.loader.get_template(self.origin.template_name) return template.get_exception_info(exception, token) - def find_partial_source(self, full_source, partial_name): - start_match = None - nesting = 0 - - for match in combined_partial_re.finditer(full_source): - if name := match["name"]: # Opening tag. - if start_match is None and name == partial_name: - start_match = match - if start_match is not None: - nesting += 1 - elif start_match is not None: - nesting -= 1 - if nesting == 0: - return full_source[start_match.start() : match.end()] + def find_partial_source(self, full_source): + if ( + self._source_start is not None + and self._source_end is not None + and 0 <= self._source_start <= self._source_end <= len(full_source) + ): + return full_source[self._source_start : self._source_end] return "" @@ -337,7 +329,7 @@ class PartialTemplate: RuntimeWarning, stacklevel=2, ) - return self.find_partial_source(template.source, self.name) + return self.find_partial_source(template.source) def _render(self, context): return self.nodelist.render(context) diff --git a/django/template/defaulttags.py b/django/template/defaulttags.py index 4cbaf852e1..ac3d5de901 100644 --- a/django/template/defaulttags.py +++ b/django/template/defaulttags.py @@ -1235,11 +1235,18 @@ def partialdef_func(parser, token): # Parse the content until the end tag. valid_endpartials = ("endpartialdef", f"endpartialdef {partial_name}") + + pos_open = getattr(token, "position", None) + source_start = pos_open[0] if isinstance(pos_open, tuple) else None + nodelist = parser.parse(valid_endpartials) endpartial = parser.next_token() if endpartial.contents not in valid_endpartials: parser.invalid_block_tag(endpartial, "endpartialdef", valid_endpartials) + pos_close = getattr(endpartial, "position", None) + source_end = pos_close[1] if isinstance(pos_close, tuple) else None + # Store the partial nodelist in the parser.extra_data attribute. partials = parser.extra_data.setdefault("partials", {}) if partial_name in partials: @@ -1247,7 +1254,13 @@ def partialdef_func(parser, token): f"Partial '{partial_name}' is already defined in the " f"'{parser.origin.name}' template." ) - partials[partial_name] = PartialTemplate(nodelist, parser.origin, partial_name) + partials[partial_name] = PartialTemplate( + nodelist, + parser.origin, + partial_name, + source_start=source_start, + source_end=source_end, + ) return PartialDefNode(partial_name, inline, nodelist) diff --git a/tests/template_tests/test_partials.py b/tests/template_tests/test_partials.py index cc3ef7fb25..9762436fdc 100644 --- a/tests/template_tests/test_partials.py +++ b/tests/template_tests/test_partials.py @@ -32,33 +32,6 @@ class PartialTagsTests(TestCase): ): engine.get_template(template_name) - def test_template_source_is_correct(self): - partial = engine.get_template("partial_examples.html#test-partial") - msg = ( - "PartialTemplate.source is only available when " - "template debugging is enabled." - ) - with self.assertRaisesMessage(RuntimeWarning, msg): - self.assertEqual( - partial.template.source, - "{% partialdef test-partial %}\n" - "TEST-PARTIAL-CONTENT\n" - "{% endpartialdef %}", - ) - - def test_template_source_inline_is_correct(self): - partial = engine.get_template("partial_examples.html#inline-partial") - msg = ( - "PartialTemplate.source is only available when " - "template debugging is enabled." - ) - with self.assertRaisesMessage(RuntimeWarning, msg): - self.assertEqual( - partial.template.source, - "{% partialdef inline-partial inline %}\nINLINE-CONTENT\n" - "{% endpartialdef %}", - ) - def test_full_template_from_loader(self): template = engine.get_template("partial_examples.html") rendered = template.render({}) @@ -172,12 +145,7 @@ class PartialTagsTests(TestCase): "PartialTemplate.source is only available when template " "debugging is enabled.", ): - self.assertEqual( - partial.template.source, - "{% partialdef test-partial %}\n" - "TEST-PARTIAL-CONTENT\n" - "{% endpartialdef %}", - ) + self.assertEqual(partial.template.source, "") class RobustPartialHandlingTests(TestCase): @@ -287,6 +255,20 @@ INLINE-CONTENT {% endpartialdef %}""" self.assertEqual(partial_proxy.source.strip(), expected.strip()) + def test_find_partial_source_fallback_cases(self): + cases = {"None offsets": (None, None), "Out of bounds offsets": (10, 20)} + for name, (source_start, source_end) in cases.items(): + with self.subTest(name): + partial = PartialTemplate( + NodeList(), + Origin("test"), + "test", + source_start=source_start, + source_end=source_end, + ) + result = partial.find_partial_source("nonexistent-partial") + self.assertEqual(result, "") + @setup( { "empty_partial_template": ("{% partialdef empty %}{% endpartialdef %}"), @@ -297,7 +279,7 @@ INLINE-CONTENT template = self.engine.get_template("empty_partial_template") partial_proxy = template.extra_data["partials"]["empty"] - result = partial_proxy.find_partial_source(template.source, "empty") + result = partial_proxy.find_partial_source(template.source) self.assertEqual(result, "{% partialdef empty %}{% endpartialdef %}") @setup( @@ -315,10 +297,10 @@ INLINE-CONTENT empty_proxy = template.extra_data["partials"]["empty"] other_proxy = template.extra_data["partials"]["other"] - empty_result = empty_proxy.find_partial_source(template.source, "empty") + empty_result = empty_proxy.find_partial_source(template.source) self.assertEqual(empty_result, "{% partialdef empty %}{% endpartialdef %}") - other_result = other_proxy.find_partial_source(template.source, "other") + other_result = other_proxy.find_partial_source(template.source) self.assertEqual(other_result, "{% partialdef other %}...{% endpartialdef %}") def test_partials_with_duplicate_names(self): @@ -368,7 +350,7 @@ INLINE-CONTENT template = self.engine.get_template("named_end_tag_template") partial_proxy = template.extra_data["partials"]["thing"] - result = partial_proxy.find_partial_source(template.source, "thing") + result = partial_proxy.find_partial_source(template.source) self.assertEqual( result, "{% partialdef thing %}CONTENT{% endpartialdef thing %}" ) @@ -389,7 +371,7 @@ INLINE-CONTENT empty_proxy = template.extra_data["partials"]["outer"] other_proxy = template.extra_data["partials"]["inner"] - outer_result = empty_proxy.find_partial_source(template.source, "outer") + outer_result = empty_proxy.find_partial_source(template.source) self.assertEqual( outer_result, ( @@ -398,7 +380,7 @@ INLINE-CONTENT ), ) - inner_result = other_proxy.find_partial_source(template.source, "inner") + inner_result = other_proxy.find_partial_source(template.source) self.assertEqual(inner_result, "{% partialdef inner %}...{% endpartialdef %}") @setup( @@ -417,7 +399,7 @@ INLINE-CONTENT empty_proxy = template.extra_data["partials"]["outer"] other_proxy = template.extra_data["partials"]["inner"] - outer_result = empty_proxy.find_partial_source(template.source, "outer") + outer_result = empty_proxy.find_partial_source(template.source) self.assertEqual( outer_result, ( @@ -426,7 +408,7 @@ INLINE-CONTENT ), ) - inner_result = other_proxy.find_partial_source(template.source, "inner") + inner_result = other_proxy.find_partial_source(template.source) self.assertEqual( inner_result, "{% partialdef inner %}...{% endpartialdef inner %}" ) @@ -447,7 +429,7 @@ INLINE-CONTENT empty_proxy = template.extra_data["partials"]["outer"] other_proxy = template.extra_data["partials"]["inner"] - outer_result = empty_proxy.find_partial_source(template.source, "outer") + outer_result = empty_proxy.find_partial_source(template.source) self.assertEqual( outer_result, ( @@ -456,7 +438,7 @@ INLINE-CONTENT ), ) - inner_result = other_proxy.find_partial_source(template.source, "inner") + inner_result = other_proxy.find_partial_source(template.source) self.assertEqual(inner_result, "{% partialdef inner %}...{% endpartialdef %}") @setup( @@ -475,7 +457,7 @@ INLINE-CONTENT empty_proxy = template.extra_data["partials"]["outer"] other_proxy = template.extra_data["partials"]["inner"] - outer_result = empty_proxy.find_partial_source(template.source, "outer") + outer_result = empty_proxy.find_partial_source(template.source) self.assertEqual( outer_result, ( @@ -484,7 +466,138 @@ INLINE-CONTENT ), ) - inner_result = other_proxy.find_partial_source(template.source, "inner") + inner_result = other_proxy.find_partial_source(template.source) self.assertEqual( inner_result, "{% partialdef inner %}...{% endpartialdef inner %}" ) + + @setup( + { + "partial_embedded_in_verbatim": ( + "{% verbatim %}\n" + "{% partialdef testing-name %}\n" + "

Should be ignored

" + "{% endpartialdef testing-name %}\n" + "{% endverbatim %}\n" + "{% partialdef testing-name %}\n" + "

Content

\n" + "{% endpartialdef %}\n" + ), + }, + debug_only=True, + ) + def test_partial_template_embedded_in_verbatim(self): + template = self.engine.get_template("partial_embedded_in_verbatim") + partial_template = template.extra_data["partials"]["testing-name"] + self.assertEqual( + partial_template.source, + "{% partialdef testing-name %}\n

Content

\n{% endpartialdef %}", + ) + + @setup( + { + "partial_debug_source": ( + "{% partialdef testing-name %}\n" + "

Content

\n" + "{% endpartialdef %}\n" + ), + }, + debug_only=True, + ) + def test_partial_source_uses_offsets_in_debug(self): + template = self.engine.get_template("partial_debug_source") + partial_template = template.extra_data["partials"]["testing-name"] + + self.assertEqual(partial_template._source_start, 0) + self.assertEqual(partial_template._source_end, 64) + expected = template.source[ + partial_template._source_start : partial_template._source_end + ] + self.assertEqual(partial_template.source, expected) + + @setup( + { + "partial_embedded_in_named_verbatim": ( + "{% verbatim block1 %}\n" + "{% partialdef testing-name %}\n" + "{% endverbatim block1 %}\n" + "{% partialdef testing-name %}\n" + "

Named Content

\n" + "{% endpartialdef %}\n" + ), + }, + debug_only=True, + ) + def test_partial_template_embedded_in_named_verbatim(self): + template = self.engine.get_template("partial_embedded_in_named_verbatim") + partial_template = template.extra_data["partials"]["testing-name"] + self.assertEqual( + "{% partialdef testing-name %}\n

Named Content

\n{% endpartialdef %}", + partial_template.source, + ) + + @setup( + { + "partial_embedded_in_comment_block": ( + "{% comment %}\n" + "{% partialdef testing-name %}\n" + "{% endcomment %}\n" + "{% partialdef testing-name %}\n" + "

Comment Content

\n" + "{% endpartialdef %}\n" + ), + }, + debug_only=True, + ) + def test_partial_template_embedded_in_comment_block(self): + template = self.engine.get_template("partial_embedded_in_comment_block") + partial_template = template.extra_data["partials"]["testing-name"] + self.assertEqual( + partial_template.source, + "{% partialdef testing-name %}\n" + "

Comment Content

\n" + "{% endpartialdef %}", + ) + + @setup( + { + "partial_embedded_in_inline_comment": ( + "{# {% partialdef testing-name %} #}\n" + "{% partialdef testing-name %}\n" + "

Inline Comment Content

\n" + "{% endpartialdef %}\n" + ), + }, + debug_only=True, + ) + def test_partial_template_embedded_in_inline_comment(self): + template = self.engine.get_template("partial_embedded_in_inline_comment") + partial_template = template.extra_data["partials"]["testing-name"] + self.assertEqual( + partial_template.source, + "{% partialdef testing-name %}\n" + "

Inline Comment Content

\n" + "{% endpartialdef %}", + ) + + @setup( + { + "partial_contains_fake_end_inside_verbatim": ( + "{% partialdef testing-name %}\n" + "{% verbatim %}{% endpartialdef %}{% endverbatim %}\n" + "

Body

\n" + "{% endpartialdef %}\n" + ), + }, + debug_only=True, + ) + def test_partial_template_contains_fake_end_inside_verbatim(self): + template = self.engine.get_template("partial_contains_fake_end_inside_verbatim") + partial_template = template.extra_data["partials"]["testing-name"] + self.assertEqual( + partial_template.source, + "{% partialdef testing-name %}\n" + "{% verbatim %}{% endpartialdef %}{% endverbatim %}\n" + "

Body

\n" + "{% endpartialdef %}", + )