From 29bf2c08bafb5e7a727ff854684b8f9ceeb14767 Mon Sep 17 00:00:00 2001 From: Noethix55555 <277300782+Noethix55555@users.noreply.github.com> Date: Tue, 16 Jun 2026 23:21:45 -0400 Subject: [PATCH 1/2] fix: allow % and } in {%p %}/{%tr %}/{%tc %}/{%r %} expressions patch_xml matched the expression body of these directive tags with [^}%]*, which stops at the first % or }. Tags whose expression contains a modulo (if i % 2 == 0) or a literal brace (set d = {"x":1}) were not stripped of their surrounding /// wrapper, so the literal {%p ...%} reached Jinja and raised TemplateSyntaxError: Encountered unknown tag 'p'. Match up to the closing %} or }} instead of excluding the characters. --- docxtpl/template.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docxtpl/template.py b/docxtpl/template.py index f20280a..7886480 100644 --- a/docxtpl/template.py +++ b/docxtpl/template.py @@ -183,7 +183,7 @@ def cellbg(m): # by {% xxx %} or {{ xx }} without any surrounding tags : # This is mandatory to have jinja2 generating correct xml code pat = ( - r"](?:(?!]).)*({%%|{{)%(y)s ([^}%%]*(?:%%}|}})).*?" + r"](?:(?!]).)*({%%|{{)%(y)s ((?:(?!%%}|}}).)*(?:%%}|}})).*?" % {"y": y} ) src_xml = re.sub(pat, r"\1 \2", src_xml, flags=re.DOTALL) From 2c534f0323f889f8cf780844a2d0261b6e11e7cf Mon Sep 17 00:00:00 2001 From: Noethix55555 <277300782+Noethix55555@users.noreply.github.com> Date: Wed, 24 Jun 2026 12:53:46 -0400 Subject: [PATCH 2/2] test: cover {%p %}/{%tr %} expressions containing % and } Add a regression test for patch_xml() relocating {%p %}/{%tr %} directives whose Jinja expression contains a "%" (modulo / literal percent) or a "}" (dict literal). These previously broke the relocation regex. Co-Authored-By: Claude Opus 4.8 (1M context) --- tests/patch_xml_modulo_brace.py | 44 +++++++++++++++++++++++++++++++++ 1 file changed, 44 insertions(+) create mode 100644 tests/patch_xml_modulo_brace.py diff --git a/tests/patch_xml_modulo_brace.py b/tests/patch_xml_modulo_brace.py new file mode 100644 index 0000000..616dc2d --- /dev/null +++ b/tests/patch_xml_modulo_brace.py @@ -0,0 +1,44 @@ +# Regression test for the {%p %}/{%tr %}/{%tc %}/{%r %} relocation regex. +# +# patch_xml() moves a {%p ... %} (or tr/tc/r) directive out of its surrounding +# /// tags. The capture group used to be `[^}%]*`, which +# stopped at the first "%" or "}" *inside* the expression. As a result a +# directive whose Jinja expression contained a "%" (e.g. a modulo operation or a +# literal percent) or a "}" (e.g. a dict literal) was silently left in place, +# producing invalid XML. The expression is now captured up to the real closing +# "%}"/"}}", so such expressions round-trip correctly. + +from docxtpl import DocxTemplate + +# patch_xml() is a pure string transformation and never opens the file, so we +# can exercise it without a real .docx template. +tpl = DocxTemplate("dummy.docx") + +CASES = [ + # "%" inside the expression (modulo / literal percent) + ( + "{%p set x = a % b %}", + "{% set x = a % b %}", + ), + # "}" inside the expression (dict literal) + ( + "{%p set d = {'a': 1} %}", + "{% set d = {'a': 1} %}", + ), + # same fix for a table-row directive carrying a "%" + ( + "{%tr for x in items if x % 2 %}" + "", + "{% for x in items if x % 2 %}", + ), +] + +for src_xml, expected in CASES: + result = tpl.patch_xml(src_xml) + assert result == expected, "patch_xml(%r) -> %r, expected %r" % ( + src_xml, + result, + expected, + ) + +print("patch_xml_modulo_brace: OK")