From 7fa2cbcfafd1acf12d288a578ee52dc893d2b874 Mon Sep 17 00:00:00 2001 From: Michael Williamson Date: Sun, 8 Jun 2025 10:02:56 +0100 Subject: Handle SDTs representing checkboxes wrapped in other elements --- .../internal/docx/StatefulBodyXmlReader.java | 67 ++++++++++++++-- .../zwobble/mammoth/tests/docx/BodyXmlTests.java | 91 ++++++++++++++++++++++ 2 files changed, 150 insertions(+), 8 deletions(-) (limited to 'src') diff --git a/src/main/java/org/zwobble/mammoth/internal/docx/StatefulBodyXmlReader.java b/src/main/java/org/zwobble/mammoth/internal/docx/StatefulBodyXmlReader.java index 161ef91..30579ff 100644 --- a/src/main/java/org/zwobble/mammoth/internal/docx/StatefulBodyXmlReader.java +++ b/src/main/java/org/zwobble/mammoth/internal/docx/StatefulBodyXmlReader.java @@ -711,18 +711,69 @@ class StatefulBodyXmlReader { } private ReadResult readSdt(XmlElement element) { - Optional checkbox = element - .findChildOrEmpty("w:sdtPr") - .findChild("wordml:checkbox"); + ReadResult contentResult = readElements( + element.findChildOrEmpty("w:sdtContent").getChildren() + ); + return contentResult.map(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. + + Optional checkbox = element + .findChildOrEmpty("w:sdtPr") + .findChild("wordml:checkbox"); + + if (!checkbox.isPresent()) { + return content; + } - if (checkbox.isPresent()) { Optional checkedElement = checkbox.get().findChild("wordml:checked"); boolean isChecked = checkedElement.isPresent() && readBooleanAttributeValue(checkedElement.get().getAttributeOrNone("wordml:val")); - return success(new Checkbox(isChecked)); - } else { - return readElements(element.findChildOrEmpty("w:sdtContent").getChildren()); - } + Checkbox documentCheckbox = new Checkbox(isChecked); + + MutableBoolean hasCheckbox = new MutableBoolean(false); + List replacedContent = Lists.eagerMap( + content, + transformElementsOfType(Text.class, text -> { + if (text.getValue().length() > 0 && !hasCheckbox.get()) { + hasCheckbox.set(true); + return documentCheckbox; + } else { + return text; + } + }) + ); + + if (hasCheckbox.get()) { + return replacedContent; + } else { + return list(documentCheckbox); + } + }); + } + + private Function transformElementsOfType( + Class elementClass, + Function transform + ) { + return element -> { + if (element instanceof HasChildren) { + element = ((HasChildren) element).replaceChildren( + Lists.eagerMap(((HasChildren) element).getChildren(), transformElementsOfType(elementClass, transform)) + ); + } + + if (elementClass.isInstance(element)) { + return transform.apply((T) element); + } else { + return element; + } + }; } private String relationshipIdToDocxPath(String relationshipId) { diff --git a/src/test/java/org/zwobble/mammoth/tests/docx/BodyXmlTests.java b/src/test/java/org/zwobble/mammoth/tests/docx/BodyXmlTests.java index 7ab1ec0..a4b3bdd 100644 --- a/src/test/java/org/zwobble/mammoth/tests/docx/BodyXmlTests.java +++ b/src/test/java/org/zwobble/mammoth/tests/docx/BodyXmlTests.java @@ -765,6 +765,97 @@ public class BodyXmlTests { assertThat(result, isCheckbox(true)); } + @Test + public void whenStructuredDocumentTagCheckboxHasSdtContentThenCheckboxReplacesSingleCharacter() { + XmlElement element = element("w:tbl", list( + wTr( + element("w:sdt", list( + element("w:sdtPr", list( + element("wordml:checkbox", list( + element("wordml:checked", map("wordml:val", "1")) + )) + )), + element("w:sdtContent", list( + element("w:tc", list( + element("w:p", list( + element("w:r", list( + element("w:t", list( + textXml("☐") + )) + )) + )) + )) + )) + )) + ) + )); + + DocumentElement result = readSuccess(bodyReader(), element); + + assertThat(result, deepEquals(table(list( + tableRow(list( + tableCell( + withChildren( + paragraph(withChildren( + run(withChildren( + checkbox(true) + )) + )) + ) + ) + )) + )))); + } + + @Test + public void whenStructuredDocumentTagCheckboxHasSdtContentThenDeletedContentIsIgnored() { + XmlElement element = element("w:tbl", list( + wTr( + element("w:sdt", list( + element("w:sdtPr", list( + element("wordml:checkbox", list( + element("wordml:checked", map("wordml:val", "1")) + )) + )), + element("w:sdtContent", list( + element("w:tc", list( + element("w:p", list( + element("w:r", list( + element("w:t", list( + textXml("☐") + )) + )), + element("w:del", list( + element("w:r", list( + element("w:t", list( + textXml("☐") + )) + )) + )) + )) + )) + )) + )) + ) + )); + + DocumentElement result = readSuccess(bodyReader(), element); + + assertThat(result, deepEquals(table(list( + tableRow(list( + tableCell( + withChildren( + paragraph(withChildren( + run(withChildren( + checkbox(true) + )) + )) + ) + ) + )) + )))); + } + private XmlElement complexFieldCheckboxParagraph(List ffDataChildren) { return element("w:p", list( element("w:r", list( -- cgit v1.2.3