diff options
| author | Michael Williamson <mike@zwobble.org> | 2025-06-08 00:22:01 +0100 |
|---|---|---|
| committer | Michael Williamson <mike@zwobble.org> | 2025-06-08 00:22:01 +0100 |
| commit | 80e9fd089494996def1e2529d1fa127d3ee796dd (patch) | |
| tree | b54aa0b0959e5c83c43a78958669070e95e3fae8 | |
| parent | cc1ad1d283264284e10beeff33b140abc16156d3 (diff) | |
Handle SDTs representing checkboxes wrapped in other elements
| -rw-r--r-- | NEWS | 4 | ||||
| -rw-r--r-- | mammoth/documents.py | 3 | ||||
| -rw-r--r-- | mammoth/docx/body_xml.py | 43 | ||||
| -rw-r--r-- | mammoth/transforms.py | 8 | ||||
| -rw-r--r-- | tests/docx/body_xml_tests.py | 83 |
5 files changed, 132 insertions, 9 deletions
@@ -3,6 +3,10 @@ * Add "Heading" and "Body" styles, as found in documents created by Apple Pages, to the default style map. +* Handle structured document tags representing checkboxes wrapped in other + elements, such as table cells. Previously, the wrapping elements would have + been ignored. + # 1.9.1 * Ignore AlternateContent elements when there is no Fallback element. diff --git a/mammoth/documents.py b/mammoth/documents.py index f9c0665..e874775 100644 --- a/mammoth/documents.py +++ b/mammoth/documents.py @@ -92,6 +92,9 @@ class TableCellUnmerged: rowspan = cobble.field() vmerge = cobble.field() + def copy(self, **kwargs): + return cobble.copy(self, **kwargs) + @cobble.data class Break(Element): break_type = cobble.field() diff --git a/mammoth/docx/body_xml.py b/mammoth/docx/body_xml.py index 3de2c60..5cef312 100644 --- a/mammoth/docx/body_xml.py +++ b/mammoth/docx/body_xml.py @@ -5,6 +5,7 @@ import sys from .. import documents from .. import results from .. import lists +from .. import transforms from . import complex_fields from .dingbats import dingbats from .xmlparser import node_types, XmlElement, null_xml_element @@ -577,17 +578,49 @@ def _create_reader(numbering, content_types, relationships, styles, docx_file, f return read_child_elements(element.find_child_or_null("mc:Fallback")) def read_sdt(element): - checkbox = element.find_child_or_null("w:sdtPr").find_child("wordml:checkbox") + content_result = read_child_elements(element.find_child_or_null("w:sdtContent")) + + def handle_content(content): + # From the WordML standard: https://learn.microsoft.com/en-us/openspecs/office_standards/ms-docx/3350cb64-931f-41f7-8824-f18b2568ce66 + # + # > A CT_SdtCheckbox element that specifies that the parent + # > structured document tag is a checkbox when displayed in the + # > document. The parent structured document tag contents MUST + # > contain a single character and optionally an additional + # > character in a deleted run. + checkbox = element.find_child_or_null("w:sdtPr").find_child("wordml:checkbox") + + if checkbox is None: + return content - if checkbox is not None: checked_element = checkbox.find_child("wordml:checked") is_checked = ( checked_element is not None and read_boolean_attribute_value(checked_element.attributes.get("wordml:val")) ) - return _success(documents.checkbox(checked=is_checked)) - else: - return read_child_elements(element.find_child_or_null("w:sdtContent")) + document_checkbox = documents.checkbox(checked=is_checked) + + has_checkbox = False + + def transform_text(text): + nonlocal has_checkbox + if len(text.value) > 0 and not has_checkbox: + has_checkbox = True + return document_checkbox + else: + return text + + replaced_content = list(map( + transforms.element_of_type(documents.Text, transform_text), + content, + )) + + if has_checkbox: + return replaced_content + else: + return document_checkbox + + return content_result.map(handle_content) handlers = { "w:t": text, diff --git a/mammoth/transforms.py b/mammoth/transforms.py index 62bfc1f..cf96cd3 100644 --- a/mammoth/transforms.py +++ b/mammoth/transforms.py @@ -15,18 +15,18 @@ def element_of_type(element_type, transform): return transform(element) else: return element - + return _each_element(transform_element) def _each_element(transform_element): def transform_element_and_children(element): - if isinstance(element, documents.HasChildren): + if isinstance(element, (documents.HasChildren, documents.TableCellUnmerged)): children = list(map(transform_element_and_children, element.children)) element = element.copy(children=children) - + return transform_element(element) - + return transform_element_and_children diff --git a/tests/docx/body_xml_tests.py b/tests/docx/body_xml_tests.py index 424c2f5..994583e 100644 --- a/tests/docx/body_xml_tests.py +++ b/tests/docx/body_xml_tests.py @@ -788,6 +788,89 @@ class CheckboxTests: assert_that(result, is_checkbox(checked=True)) + def test_when_structured_document_tag_checkbox_has_sdt_content_then_checkbox_replaces_single_character(self): + element = xml_element("w:tbl", {}, [ + w_tr( + xml_element("w:sdt", {}, [ + xml_element("w:sdtPr", {}, [ + xml_element("wordml:checkbox", {}, [ + xml_element("wordml:checked", {"wordml:val": "1"}), + ]), + ]), + xml_element("w:sdtContent", {}, [ + xml_element("w:tc", {}, [ + xml_element("w:p", {}, [ + xml_element("w:r", {}, [ + xml_element("w:t", {}, [ + xml_text("☐"), + ]), + ]), + ]), + ]), + ]), + ]), + ), + ]) + + result = _read_and_get_document_xml_element(element) + + assert_equal(result, documents.table([ + documents.table_row([ + documents.table_cell([ + documents.paragraph([ + documents.run([ + documents.checkbox(checked=True), + ]), + ]), + ]), + ]), + ])) + + def test_when_structured_document_tag_checkbox_has_sdt_content_then_deleted_content_is_ignored(self): + element = xml_element("w:tbl", {}, [ + w_tr( + xml_element("w:sdt", {}, [ + xml_element("w:sdtPr", {}, [ + xml_element("wordml:checkbox", {}, [ + xml_element("wordml:checked", {"wordml:val": "1"}), + ]), + ]), + xml_element("w:sdtContent", {}, [ + xml_element("w:tc", {}, [ + xml_element("w:p", {}, [ + xml_element("w:r", {}, [ + xml_element("w:t", {}, [ + xml_text("☐"), + ]), + ]), + xml_element("w:del", {}, [ + xml_element("w:r", {}, [ + xml_element("w:t", {}, [ + xml_text("☐") + ]) + ]) + ]), + ]), + ]), + ]), + ]), + ), + ]) + + result = _read_and_get_document_xml_element(element) + + assert_equal(result, documents.table([ + documents.table_row([ + documents.table_cell([ + documents.paragraph([ + documents.run([ + documents.checkbox(checked=True), + ]), + ]), + ]), + ]), + ])) + def _complex_field_checkbox_paragraph(self, ff_data_children): return xml_element("w:p", {}, [ xml_element("w:r", {}, [ |
