diff options
| author | Michael Williamson <mike@zwobble.org> | 2026-03-12 21:16:34 +0000 |
|---|---|---|
| committer | Michael Williamson <mike@zwobble.org> | 2026-03-12 21:16:34 +0000 |
| commit | 87c5adb81dc4c1bd00fa776525c5953bd5f99b80 (patch) | |
| tree | fad421cbac5c53e3ce071694bd397dae8923aee2 | |
| parent | 685e1e0fb3e7dc854a124c863da45bd018b2936b (diff) | |
Handle hyperlink complex fields with unquoted hrefs
| -rw-r--r-- | NEWS | 2 | ||||
| -rw-r--r-- | mammoth/docx/body_xml.py | 18 | ||||
| -rw-r--r-- | tests/docx/body_xml_tests.py | 56 |
3 files changed, 67 insertions, 9 deletions
@@ -2,6 +2,8 @@ * Handle hyperlinked wp:anchor and wp:inline elements. +* Handle hyperlink complex fields with unquoted hrefs. + # 1.11.0 * Ignore style definitions using a style ID that has already been used. diff --git a/mammoth/docx/body_xml.py b/mammoth/docx/body_xml.py index 581c9e2..2a1ffaf 100644 --- a/mammoth/docx/body_xml.py +++ b/mammoth/docx/body_xml.py @@ -228,13 +228,19 @@ def _create_reader(numbering, content_types, relationships, styles, docx_file, f return parse_instr_text(instr_text, fld_char=fld_char) def parse_instr_text(instr_text, *, fld_char): - external_link_result = re.match(r'\s*HYPERLINK "(.*)"', instr_text) - if external_link_result is not None: - return complex_fields.hyperlink(dict(href=external_link_result.group(1))) + link_result = re.match(r'^\s*HYPERLINK\s+(\\l\s+)?(?:"(.*)"|([^\\]\S*))', instr_text) + if link_result is not None: + if link_result.group(2) is None: + location = link_result.group(3) + else: + location = link_result.group(2) + + if link_result.group(1) is None: + hyperlink_args = dict(href=location) + else: + hyperlink_args = dict(anchor=location) - internal_link_result = re.match(r'\s*HYPERLINK\s+\\l\s+"(.*)"', instr_text) - if internal_link_result is not None: - return complex_fields.hyperlink(dict(anchor=internal_link_result.group(1))) + return complex_fields.hyperlink(hyperlink_args) checkbox_result = re.match(r'\s*FORMCHECKBOX\s*', instr_text) if checkbox_result is not None: diff --git a/tests/docx/body_xml_tests.py b/tests/docx/body_xml_tests.py index 3b62944..664fb5a 100644 --- a/tests/docx/body_xml_tests.py +++ b/tests/docx/body_xml_tests.py @@ -451,10 +451,12 @@ class ComplexFieldTests(object): def _is_empty_hyperlinked_run(self): return self._is_hyperlinked_run(children=[]) - def test_runs_in_a_complex_field_for_hyperlinks_without_switch_are_read_as_external_hyperlinks(self): + def test_runs_in_a_complex_field_for_hyperlinks_without_switch_with_quoted_location_are_read_as_external_hyperlinks(self): element = xml_element("w:p", {}, [ self._BEGIN_COMPLEX_FIELD, - self._HYPERLINK_INSTRTEXT, + xml_element("w:instrText", {}, [ + xml_text(' HYPERLINK "http://example.com"') + ]), self._SEPARATE_COMPLEX_FIELD, _run_element_with_text("this is a hyperlink"), self._END_COMPLEX_FIELD, @@ -473,7 +475,31 @@ class ComplexFieldTests(object): is_empty_run, ))) - def test_runs_in_a_complex_field_for_hyperlinks_with_l_switch_are_read_as_internal_hyperlinks(self): + def test_runs_in_a_complex_field_for_hyperlinks_without_switch_with_unquoted_location_are_read_as_external_hyperlinks(self): + element = xml_element("w:p", {}, [ + self._BEGIN_COMPLEX_FIELD, + xml_element("w:instrText", {}, [ + xml_text(' HYPERLINK http://example.com') + ]), + self._SEPARATE_COMPLEX_FIELD, + _run_element_with_text("this is a hyperlink"), + self._END_COMPLEX_FIELD, + ]) + paragraph = _read_and_get_document_xml_element(element) + + assert_that(paragraph, is_paragraph(children=is_sequence( + is_empty_run, + self._is_empty_hyperlinked_run, + self._is_hyperlinked_run( + href=self._URI, + children=is_sequence( + is_text("this is a hyperlink"), + ), + ), + is_empty_run, + ))) + + def test_runs_in_a_complex_field_for_hyperlinks_with_l_switch_with_quoted_location_are_read_as_internal_hyperlinks(self): element = xml_element("w:p", {}, [ self._BEGIN_COMPLEX_FIELD, xml_element("w:instrText", {}, [ @@ -497,6 +523,30 @@ class ComplexFieldTests(object): is_empty_run, ))) + def test_runs_in_a_complex_field_for_hyperlinks_with_l_switch_with_unquoted_location_are_read_as_internal_hyperlinks(self): + element = xml_element("w:p", {}, [ + self._BEGIN_COMPLEX_FIELD, + xml_element("w:instrText", {}, [ + xml_text(' HYPERLINK \\l InternalLink'), + ]), + self._SEPARATE_COMPLEX_FIELD, + _run_element_with_text("this is a hyperlink"), + self._END_COMPLEX_FIELD, + ]) + paragraph = _read_and_get_document_xml_element(element) + + assert_that(paragraph, is_paragraph(children=is_sequence( + is_empty_run, + self._is_empty_hyperlinked_run, + self._is_hyperlinked_run( + anchor="InternalLink", + children=is_sequence( + is_text("this is a hyperlink"), + ), + ), + is_empty_run, + ))) + def test_runs_after_a_complex_field_for_hyperlinks_are_not_read_as_hyperlinks(self): element = xml_element("w:p", {}, [ self._BEGIN_COMPLEX_FIELD, |
