aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorMichael Williamson <mike@zwobble.org>2025-06-02 18:50:31 +0100
committerMichael Williamson <mike@zwobble.org>2025-06-02 18:50:31 +0100
commitee76b524a54ff235416458abb8b17bedcb478eb8 (patch)
treec639fa0f66fdbe78b4c76eb562cb42d751caa03e
parent3f5dd013a9259827aef9c63d8c33ed4aab88cdc6 (diff)
Handle SDTs representing checkboxes wrapped in other elements
-rw-r--r--NEWS4
-rw-r--r--lib/docx/body-reader.js62
-rw-r--r--lib/transforms.js1
-rw-r--r--test/docx/body-reader.tests.js85
4 files changed, 137 insertions, 15 deletions
diff --git a/NEWS b/NEWS
index bdbebd7..f9b245c 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/lib/docx/body-reader.js b/lib/docx/body-reader.js
index 93144c3..2ab5ba7 100644
--- a/lib/docx/body-reader.js
+++ b/lib/docx/body-reader.js
@@ -8,6 +8,7 @@ var documents = require("../documents");
var Result = require("../results").Result;
var warning = require("../results").warning;
var xml = require("../xml");
+var transforms = require("../transforms");
var uris = require("./uris");
function createBodyReader(options) {
@@ -385,21 +386,52 @@ function BodyReader(options) {
},
"w:sdt": function(element) {
- var checkbox = element
- .firstOrEmpty("w:sdtPr")
- .first("wordml:checkbox");
-
- if (checkbox) {
- var checkedElement = checkbox.first("wordml:checked");
- var isChecked = !!checkedElement && readBooleanAttributeValue(
- checkedElement.attributes["wordml:val"]
- );
- return elementResult(documents.checkbox({
- checked: isChecked
- }));
- } else {
- return readXmlElements(element.firstOrEmpty("w:sdtContent").children);
- }
+ var contentResult = readXmlElements(element.firstOrEmpty("w:sdtContent").children);
+ return contentResult.map(function(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.
+
+ var checkbox = element
+ .firstOrEmpty("w:sdtPr")
+ .first("wordml:checkbox");
+
+ if (checkbox) {
+ var checkedElement = checkbox.first("wordml:checked");
+ var isChecked = !!checkedElement && readBooleanAttributeValue(
+ checkedElement.attributes["wordml:val"]
+ );
+ var documentCheckbox = documents.checkbox({
+ checked: isChecked
+ });
+
+ var hasCheckbox = false;
+ var replacedContent = content.map(transforms._elementsOfType(
+ documents.types.text,
+ function(text) {
+ if (text.value.length > 0 && !hasCheckbox) {
+ hasCheckbox = true;
+ return documentCheckbox;
+ } else {
+ return text;
+ }
+ }
+ ));
+
+ if (hasCheckbox) {
+ return replacedContent;
+ } else {
+ return documentCheckbox;
+ }
+
+ } else {
+ return content;
+ }
+ });
},
"w:ins": readChildElements,
diff --git a/lib/transforms.js b/lib/transforms.js
index 01ba0d8..3c5cf73 100644
--- a/lib/transforms.js
+++ b/lib/transforms.js
@@ -3,6 +3,7 @@ var _ = require("underscore");
exports.paragraph = paragraph;
exports.run = run;
exports._elements = elements;
+exports._elementsOfType = elementsOfType;
exports.getDescendantsOfType = getDescendantsOfType;
exports.getDescendants = getDescendants;
diff --git a/test/docx/body-reader.tests.js b/test/docx/body-reader.tests.js
index 8f36998..9413b2e 100644
--- a/test/docx/body-reader.tests.js
+++ b/test/docx/body-reader.tests.js
@@ -724,6 +724,91 @@ test("checkboxes", {
var result = readXmlElementValue(sdtXml);
assertThat(result, isCheckbox({checked: equalTo(true)}));
+ },
+
+ "when structured document tag checkbox has sdtContent then checkbox replaces single character": function() {
+ var tableXml = new XmlElement("w:tbl", {}, [
+ row(
+ 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("☐")
+ ])
+ ])
+ ])
+ ])
+ ])
+ ])
+ )
+ ]);
+
+ var result = readXmlElementValue(tableXml);
+
+ assert.deepEqual(result, new documents.Table([
+ new documents.TableRow([
+ new documents.TableCell([
+ new documents.Paragraph([
+ new documents.Run([
+ new documents.Checkbox({checked: true})
+ ])
+ ])
+ ])
+ ])
+ ]));
+ },
+
+ "when structured document tag checkbox has sdtContent then deleted content is ignored": function() {
+ var tableXml = new XmlElement("w:tbl", {}, [
+ row(
+ 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("☐")
+ ])
+ ])
+ ])
+ ])
+ ])
+ ])
+ ])
+ )
+ ]);
+
+ var result = readXmlElementValue(tableXml);
+
+ assert.deepEqual(result, new documents.Table([
+ new documents.TableRow([
+ new documents.TableCell([
+ new documents.Paragraph([
+ new documents.Run([
+ new documents.Checkbox({checked: true})
+ ])
+ ])
+ ])
+ ])
+ ]));
}
});