diff options
| author | Michael Williamson <mike@zwobble.org> | 2016-05-10 21:25:10 +0100 |
|---|---|---|
| committer | Michael Williamson <mike@zwobble.org> | 2016-05-10 21:25:10 +0100 |
| commit | 1417c93bebe0426be97d8f334fc279a6eb10deda (patch) | |
| tree | f3c3505e247f7f5a7a0e93b452d38dae8815a580 | |
| parent | cda64f608faf532bb67a131453269edfa888955a (diff) | |
Add support for table cells spanning multiple rows
| -rw-r--r-- | NEWS | 4 | ||||
| -rw-r--r-- | lib/documents.js | 5 | ||||
| -rw-r--r-- | lib/docx/body-reader.js | 98 | ||||
| -rw-r--r-- | tests/docx/body-reader.tests.js | 88 |
4 files changed, 177 insertions, 18 deletions
@@ -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); |
