summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorMichael Williamson <mike@zwobble.org>2016-05-10 21:25:10 +0100
committerMichael Williamson <mike@zwobble.org>2016-05-10 21:25:10 +0100
commit1417c93bebe0426be97d8f334fc279a6eb10deda (patch)
treef3c3505e247f7f5a7a0e93b452d38dae8815a580
parentcda64f608faf532bb67a131453269edfa888955a (diff)
Add support for table cells spanning multiple rows
-rw-r--r--NEWS4
-rw-r--r--lib/documents.js5
-rw-r--r--lib/docx/body-reader.js98
-rw-r--r--tests/docx/body-reader.tests.js88
4 files changed, 177 insertions, 18 deletions
diff --git a/NEWS b/NEWS
index c42e2cc..9e2ea04 100644
--- a/NEWS
+++ b/NEWS
@@ -1,3 +1,7 @@
+# 1.0.2
+
+* Add support for table cells spanning multiple rows.
+
# 1.0.1
* Add support for table cells spanning multiple columns.
diff --git a/lib/documents.js b/lib/documents.js
index 8b979c2..5665198 100644
--- a/lib/documents.js
+++ b/lib/documents.js
@@ -1,6 +1,6 @@
var _ = require("underscore");
-var types = {
+var types = exports.types = {
document: "document",
paragraph: "paragraph",
run: "run",
@@ -144,7 +144,8 @@ function TableCell(children, options) {
return {
type: types.tableCell,
children: children,
- colSpan: options.colSpan == null ? 1 : options.colSpan
+ colSpan: options.colSpan == null ? 1 : options.colSpan,
+ rowSpan: options.rowSpan == null ? 1 : options.rowSpan
};
}
diff --git a/lib/docx/body-reader.js b/lib/docx/body-reader.js
index 126896c..70d5402 100644
--- a/lib/docx/body-reader.js
+++ b/lib/docx/body-reader.js
@@ -157,22 +157,9 @@ function BodyReader(options) {
}
});
},
- "w:tbl": function(element) {
- return readXmlElements(element.children).map(documents.Table);
- },
- "w:tr": function(element) {
- return readXmlElements(element.children).map(documents.TableRow);
- },
- "w:tc": function(element) {
- return readXmlElements(element.children).map(function(children) {
- var gridSpan = element
- .firstOrEmpty("w:tcPr")
- .firstOrEmpty("w:gridSpan")
- .attributes["w:val"];
- var colSpan = gridSpan ? parseInt(gridSpan) : 1;
- return documents.TableCell(children, {colSpan: colSpan});
- });
- },
+ "w:tbl": readTable,
+ "w:tr": readTableRow,
+ "w:tc": readTableCell,
"w:footnoteReference": noteReferenceReader("footnote"),
"w:endnoteReference": noteReferenceReader("endnote"),
"w:br": function(element) {
@@ -225,6 +212,85 @@ function BodyReader(options) {
return numbering.findLevel(numId, level);
}
}
+
+ function readTable(element) {
+ return readXmlElements(element.children)
+ .flatMap(calculateRowSpans)
+ .map(documents.Table);
+ }
+
+ function readTableRow(element) {
+ return readXmlElements(element.children).map(documents.TableRow);
+ }
+
+ function readTableCell(element) {
+ return readXmlElements(element.children).map(function(children) {
+ var properties = element.firstOrEmpty("w:tcPr");
+
+ var gridSpan = properties.firstOrEmpty("w:gridSpan").attributes["w:val"];
+ var colSpan = gridSpan ? parseInt(gridSpan) : 1;
+
+ var cell = documents.TableCell(children, {colSpan: colSpan});
+ cell._vMerge = readVMerge(properties);
+ return cell;
+ });
+ }
+
+ function readVMerge(properties) {
+ var element = properties.first("w:vMerge");
+ if (element) {
+ var val = element.attributes["w:val"];
+ return val ? val : "continue";
+ } else {
+ return null;
+ }
+ }
+
+ function calculateRowSpans(rows) {
+ var unexpectedNonRows = _.any(rows, function(row) {
+ return row.type !== documents.types.tableRow;
+ });
+ if (unexpectedNonRows) {
+ return elementResultWithMessages(rows, [warning(
+ "unexpected non-row element in table, cell merging may be incorrect"
+ )]);
+ }
+ var unexpectedNonCells = _.any(rows, function(row) {
+ return _.any(row.children, function(cell) {
+ return cell.type !== documents.types.tableCell;
+ });
+ });
+ if (unexpectedNonCells) {
+ return elementResultWithMessages(rows, [warning(
+ "unexpected non-cell element in table row, cell merging may be incorrect"
+ )]);
+ }
+
+ var columns = {};
+
+ rows.forEach(function(row, rowIndex) {
+ var cellIndex = 0;
+ row.children.forEach(function(cell) {
+ if (cell._vMerge === "continue" && columns[cellIndex]) {
+ columns[cellIndex].rowSpan++;
+ } else {
+ columns[cellIndex] = cell;
+ }
+ cellIndex += cell.colSpan;
+ });
+ });
+
+ rows.forEach(function(row, rowIndex) {
+ row.children = row.children.filter(function(cell) {
+ return cell._vMerge !== "continue";
+ });
+ row.children.forEach(function(cell) {
+ delete cell._vMerge;
+ });
+ });
+
+ return elementResult(rows);
+ }
function readDrawingElement(element) {
var blips = element
diff --git a/tests/docx/body-reader.tests.js b/tests/docx/body-reader.tests.js
index debc8d3..51d46fe 100644
--- a/tests/docx/body-reader.tests.js
+++ b/tests/docx/body-reader.tests.js
@@ -282,6 +282,94 @@ describe("readXmlElement: ", function() {
]));
});
+ test("w:vMerge is read as rowSpan for table cell", function() {
+ var tableXml = new XmlElement("w:tbl", {}, [
+ row(emptyCell()),
+ row(emptyCell(vMerge("restart"))),
+ row(emptyCell(vMerge("continue"))),
+ row(emptyCell(vMerge("continue"))),
+ row(emptyCell())
+ ]);
+ var result = readXmlElement(tableXml);
+ assert.deepEqual(result.value, new documents.Table([
+ docRow([docEmptyCell()]),
+ docRow([docEmptyCell({rowSpan: 3})]),
+ docRow([]),
+ docRow([]),
+ docRow([docEmptyCell()])
+ ]));
+ });
+
+ test("w:vMerge without val is treated as continue", function() {
+ var tableXml = new XmlElement("w:tbl", {}, [
+ row(emptyCell(vMerge("restart"))),
+ row(emptyCell(vMerge())),
+ ]);
+ var result = readXmlElement(tableXml);
+ assert.deepEqual(result.value, new documents.Table([
+ docRow([docEmptyCell({rowSpan: 2})]),
+ docRow([])
+ ]));
+ });
+
+ test("w:vMerge accounts for cells spanning columns", function() {
+ var tableXml = new XmlElement("w:tbl", {}, [
+ row(emptyCell(), emptyCell(), emptyCell(vMerge("restart"))),
+ row(emptyCell(gridSpan("2")), emptyCell(vMerge("continue"))),
+ row(emptyCell(), emptyCell(), emptyCell(vMerge("continue"))),
+ row(emptyCell(), emptyCell(), emptyCell())
+ ]);
+ var result = readXmlElement(tableXml);
+ assert.deepEqual(result.value, new documents.Table([
+ docRow([docEmptyCell(), docEmptyCell(), docEmptyCell({rowSpan: 3})]),
+ docRow([docEmptyCell({colSpan: 2})]),
+ docRow([docEmptyCell(), docEmptyCell()]),
+ docRow([docEmptyCell(), docEmptyCell(), docEmptyCell()])
+ ]));
+ });
+
+ test("warning if non-row in table", function() {
+ var tableXml = new XmlElement("w:tbl", {}, [
+ new XmlElement("w:p")
+ ]);
+ var result = readXmlElement(tableXml);
+ assert.deepEqual(result.messages, [warning("unexpected non-row element in table, cell merging may be incorrect")]);
+ });
+
+ test("warning if non-cell in table", function() {
+ var tableXml = new XmlElement("w:tbl", {}, [
+ row(new XmlElement("w:p"))
+ ]);
+ var result = readXmlElement(tableXml);
+ assert.deepEqual(result.messages, [warning("unexpected non-cell element in table row, cell merging may be incorrect")]);
+ });
+
+ function row() {
+ return new XmlElement("w:tr", {}, Array.prototype.slice.call(arguments));
+ }
+
+ function emptyCell() {
+ return new XmlElement("w:tc", {}, [
+ new XmlElement("w:tcPr", {}, Array.prototype.slice.call(arguments))
+ ]);
+ }
+
+ function vMerge(val) {
+ return new XmlElement("w:vMerge", {"w:val": val}, []);
+ }
+
+ function gridSpan(val) {
+ return new XmlElement("w:gridSpan", {"w:val": val});
+ }
+
+ function docRow(children) {
+ return new documents.TableRow(children);
+ }
+
+ function docEmptyCell(properties) {
+ return new documents.TableCell([], properties);
+ }
+
test("emits warning on unrecognised element", function() {
var unrecognisedElement = new XmlElement("w:not-an-element");
var result = readXmlElement(unrecognisedElement);