summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--NEWS4
-rw-r--r--src/main/java/org/zwobble/mammoth/internal/docx/StatefulBodyXmlReader.java67
-rw-r--r--src/test/java/org/zwobble/mammoth/tests/docx/BodyXmlTests.java91
3 files changed, 154 insertions, 8 deletions
diff --git a/NEWS b/NEWS
index 7f79654..9fda7d6 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.0
* Detect checkboxes, both as complex fields and structured document tags, and
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<XmlElement> 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<XmlElement> checkbox = element
+ .findChildOrEmpty("w:sdtPr")
+ .findChild("wordml:checkbox");
+
+ if (!checkbox.isPresent()) {
+ return content;
+ }
- if (checkbox.isPresent()) {
Optional<XmlElement> 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<DocumentElement> 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 <T extends DocumentElement> Function<DocumentElement, DocumentElement> transformElementsOfType(
+ Class<T> elementClass,
+ Function<T, DocumentElement> transform
+ ) {
+ return element -> {
+ if (element instanceof HasChildren) {
+ element = ((HasChildren<DocumentElement>) 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<XmlNode> ffDataChildren) {
return element("w:p", list(
element("w:r", list(