aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--NEWS4
-rw-r--r--mammoth/documents.py3
-rw-r--r--mammoth/docx/body_xml.py43
-rw-r--r--mammoth/transforms.py8
-rw-r--r--tests/docx/body_xml_tests.py83
5 files changed, 132 insertions, 9 deletions
diff --git a/NEWS b/NEWS
index 6324c8c..68b497d 100644
--- a/NEWS
+++ b/NEWS
@@ -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", {}, [