diff --git a/docxtpl/template.py b/docxtpl/template.py index f20280a..027df22 100644 --- a/docxtpl/template.py +++ b/docxtpl/template.py @@ -377,6 +377,28 @@ def render_footnotes( xml = self.render_xml_part(xml, part, context, jinja_env) part._blob = xml.encode("utf-8") + def render_hyperlinks( + self, context: Dict[str, Any], jinja_env: Optional[Environment] = None + ) -> None: + """Render jinja placeholders in external hyperlink URL targets. + + Hyperlink labels are stored in document XML and are already handled by + render_xml_part(), but their URLs are stored as external relationships + (e.g. word/_rels/document.xml.rels) and must be rendered separately. + """ + for part in self.docx.part.package.parts: + for rel in part.rels.values(): + if not rel.is_external or rel.reltype != REL_TYPE.HYPERLINK: + continue + target = rel.target_ref + if "{{" not in target and "{%" not in target and "{#" not in target: + continue + if jinja_env: + template = jinja_env.from_string(target) + else: + template = Template(target) + rel._target = template.render(context) + def resolve_listing(self, xml): def resolve_text(run_properties, paragraph_properties, m): @@ -511,6 +533,8 @@ def render( self.render_footnotes(context, jinja_env) + self.render_hyperlinks(context, jinja_env) + # set rendered flag self.is_rendered = True @@ -915,8 +939,15 @@ def get_undeclared_template_variables( else: env = Environment() - parse_content = env.parse(xml) - all_variables = meta.find_undeclared_variables(parse_content) + all_variables = meta.find_undeclared_variables(env.parse(xml)) + for part in temp_doc.part.package.parts: + for rel in part.rels.values(): + if not rel.is_external or rel.reltype != REL_TYPE.HYPERLINK: + continue + target = rel.target_ref + if "{{" not in target and "{%" not in target and "{#" not in target: + continue + all_variables |= meta.find_undeclared_variables(env.parse(target)) # If context is provided, return only variables that are not in the context if context is not None: diff --git a/tests/hyperlink.py b/tests/hyperlink.py new file mode 100644 index 0000000..4ffd202 --- /dev/null +++ b/tests/hyperlink.py @@ -0,0 +1,26 @@ +# -*- coding: utf-8 -*- +import re +import zipfile + +from docxtpl import DocxTemplate + +tpl = DocxTemplate("templates/hyperlink_tpl.docx") +context = {"foo": "https://example.com"} +tpl.render(context) +output_path = "output/hyperlink.docx" +tpl.save(output_path) + +with zipfile.ZipFile(output_path) as docx: + rels = docx.read("word/_rels/document.xml.rels").decode() + document = docx.read("word/document.xml").decode() + +assert 'Target="https://example.com"' in rels, rels +assert "{{ foo }}" not in rels, rels +assert re.search(r"]*>https://example.com", document), document + +undeclared = DocxTemplate("templates/hyperlink_tpl.docx").get_undeclared_template_variables( + context=context +) +assert undeclared == set(), undeclared + +print("hyperlink test passed") diff --git a/tests/templates/hyperlink_tpl.docx b/tests/templates/hyperlink_tpl.docx new file mode 100644 index 0000000..7f4511e Binary files /dev/null and b/tests/templates/hyperlink_tpl.docx differ