From a8290b919e189038161c6d629daafae88587a432 Mon Sep 17 00:00:00 2001 From: CAOShurong <170531907+CAOShurong@users.noreply.github.com> Date: Thu, 13 Aug 2026 20:24:20 +0800 Subject: [PATCH] Fix escaped delimiters split across Word runs Join only the four supported escaped Jinja delimiter tokens when Word has divided their characters into separate runs. Keep matching within one paragraph and add regression coverage for single-run, split-run, and false-positive cases. Assisted-by: OpenAI Codex Signed-off-by: CAOShurong <170531907+CAOShurong@users.noreply.github.com> --- docxtpl/template.py | 9 ++++++++ tests/escaping_delimiters.py | 43 ++++++++++++++++++++++++++++++++++++ 2 files changed, 52 insertions(+) create mode 100644 tests/escaping_delimiters.py diff --git a/docxtpl/template.py b/docxtpl/template.py index f20280a..72f66ff 100644 --- a/docxtpl/template.py +++ b/docxtpl/template.py @@ -87,6 +87,15 @@ def patch_xml(self, src_xml): strip all unnecessary xml tags, manage table cell background color and colspan, unescape html entities, etc...""" + # Join escaped Jinja delimiters that Word has split across runs. Limit the + # removable XML to tags inside one paragraph so unrelated text is untouched. + split_run_gap = ( + r"(?:(?:(?!))<[^>]+>)*]*)?>)?" + ) + for escaped_delimiter in ("{_{", "}_}", "{_%", "%_}"): + pattern = split_run_gap.join(map(re.escape, escaped_delimiter)) + src_xml = re.sub(pattern, escaped_delimiter, src_xml) + # replace {{ by {{ ( works with {{ }} {% and %} {# and #}) src_xml = re.sub( r"(?<={)(<[^>]*>)+(?=[\{%\#])|(?<=[%\}\#])(<[^>]*>)+(?=\})", diff --git a/tests/escaping_delimiters.py b/tests/escaping_delimiters.py new file mode 100644 index 0000000..de6f182 --- /dev/null +++ b/tests/escaping_delimiters.py @@ -0,0 +1,43 @@ +from pathlib import Path + +from docx import Document +from docxtpl import DocxTemplate + + +output_dir = Path(__file__).parent / "output" +output_dir.mkdir(exist_ok=True) +template_path = output_dir / "escaping_delimiters_split_tpl.docx" +result_path = output_dir / "escaping_delimiters_split.docx" + +escaped_delimiters = ("{_%", "%_}", "{_{", "}_}") +expected_delimiters = ("{%", "%}", "{{", "}}") + +template = Document() + +for delimiter in escaped_delimiters: + template.add_paragraph(delimiter) + +template.add_paragraph() + +for delimiter in escaped_delimiters: + paragraph = template.add_paragraph() + for character in delimiter: + paragraph.add_run(character) + +# These are not escaped delimiters and must not be changed. +template.add_paragraph("{a_{hello") +template.add_paragraph("{f_{") +template.save(template_path) + +doc = DocxTemplate(template_path) +doc.render({}) +doc.save(result_path) + +paragraphs = [paragraph.text for paragraph in Document(result_path).paragraphs] +assert paragraphs == [ + *expected_delimiters, + "", + *expected_delimiters, + "{a_{hello", + "{f_{", +]