From c264e80c9d01289398d67b31307bc4d7eca27739 Mon Sep 17 00:00:00 2001 From: Michael Williamson Date: Sat, 4 Jun 2016 15:51:41 +0100 Subject: Add support for reading table cells spanning multiple rows --- dotnet/README.md | 4 +- .../mammoth/internal/documents/TableCell.java | 4 +- .../mammoth/internal/docx/BodyXmlReader.java | 103 +++++++++++++++++-- .../zwobble/mammoth/internal/docx/ReadResult.java | 16 ++- .../mammoth/internal/xml/NullXmlElement.java | 5 + .../mammoth/internal/xml/XmlElementLike.java | 1 + .../tests/documents/DocumentElementMakers.java | 4 +- .../zwobble/mammoth/tests/docx/BodyXmlTests.java | 109 ++++++++++++++++++++- .../mammoth/tests/docx/OfficeXmlBuilders.java | 29 ++++++ 9 files changed, 261 insertions(+), 14 deletions(-) create mode 100644 src/test/java/org/zwobble/mammoth/tests/docx/OfficeXmlBuilders.java diff --git a/dotnet/README.md b/dotnet/README.md index 82aebb5..1265c85 100644 --- a/dotnet/README.md +++ b/dotnet/README.md @@ -57,8 +57,8 @@ Install-Package Mammoth * [WordPress](https://wordpress.org/plugins/mammoth-docx-converter/). -* [Java](https://github.com/mwilliamson/java-mammoth). - Available on Maven Central. +* [Java/JVM](https://github.com/mwilliamson/java-mammoth). + Available [on Maven Central](http://search.maven.org/#search|ga|1|g%3A%22org.zwobble.mammoth%22%20AND%20a%3A%22mammoth%22). ## Usage diff --git a/src/main/java/org/zwobble/mammoth/internal/documents/TableCell.java b/src/main/java/org/zwobble/mammoth/internal/documents/TableCell.java index 69f8fa8..b108de8 100644 --- a/src/main/java/org/zwobble/mammoth/internal/documents/TableCell.java +++ b/src/main/java/org/zwobble/mammoth/internal/documents/TableCell.java @@ -3,10 +3,12 @@ package org.zwobble.mammoth.internal.documents; import java.util.List; public class TableCell implements DocumentElement, HasChildren { + private final int rowspan; private final int colspan; private final List children; - public TableCell(int colspan, List children) { + public TableCell(int rowspan, int colspan, List children) { + this.rowspan = rowspan; this.children = children; this.colspan = colspan; } diff --git a/src/main/java/org/zwobble/mammoth/internal/docx/BodyXmlReader.java b/src/main/java/org/zwobble/mammoth/internal/docx/BodyXmlReader.java index 5d16704..95467b0 100644 --- a/src/main/java/org/zwobble/mammoth/internal/docx/BodyXmlReader.java +++ b/src/main/java/org/zwobble/mammoth/internal/docx/BodyXmlReader.java @@ -2,6 +2,7 @@ package org.zwobble.mammoth.internal.docx; import org.zwobble.mammoth.internal.documents.*; import org.zwobble.mammoth.internal.results.InternalResult; +import org.zwobble.mammoth.internal.util.Casts; import org.zwobble.mammoth.internal.util.InputStreamSupplier; import org.zwobble.mammoth.internal.util.Optionals; import org.zwobble.mammoth.internal.xml.XmlElement; @@ -9,14 +10,15 @@ import org.zwobble.mammoth.internal.xml.XmlElementLike; import org.zwobble.mammoth.internal.xml.XmlElementList; import org.zwobble.mammoth.internal.xml.XmlNode; -import java.util.Optional; -import java.util.Set; +import java.util.*; import java.util.function.Function; import static org.zwobble.mammoth.internal.docx.ReadResult.EMPTY_SUCCESS; import static org.zwobble.mammoth.internal.docx.ReadResult.success; import static org.zwobble.mammoth.internal.util.Iterables.lazyFilter; import static org.zwobble.mammoth.internal.util.Lists.list; +import static org.zwobble.mammoth.internal.util.Maps.entry; +import static org.zwobble.mammoth.internal.util.Maps.lookup; import static org.zwobble.mammoth.internal.util.Sets.set; import static org.zwobble.mammoth.internal.util.Strings.trimLeft; @@ -62,7 +64,7 @@ public class BodyXmlReader { return readBreak(element); case "w:tbl": - return readElements(element.getChildren()).map(Table::new); + return readTable(element); case "w:tr": return readElements(element.getChildren()).map(TableRow::new); case "w:tc": @@ -231,13 +233,102 @@ public class BodyXmlReader { } } + private ReadResult readTable(XmlElement element) { + return readElements(element.getChildren()) + .flatMap(this::calculateRowspans) + .map(Table::new); + } + + private ReadResult calculateRowspans(List rows) { + for (DocumentElement rowElement : rows) { + Optional row = Casts.tryCast(TableRow.class, rowElement); + if (!row.isPresent()) { + return ReadResult.withWarning(rows, "unexpected non-row element in table, cell merging may be incorrect"); + } else { + for (DocumentElement cell : row.get().getChildren()) { + if (!(cell instanceof UnmergedTableCell)) { + return ReadResult.withWarning(rows, "unexpected non-cell element in table row, cell merging may be incorrect"); + } + } + } + } + + + Map, Integer> rowspans = new HashMap<>(); + Set> merged = new HashSet<>(); + + Map> lastCellForColumn = new HashMap<>(); + for (int rowIndex = 0; rowIndex < rows.size(); rowIndex += 1) { + TableRow row = (TableRow) rows.get(rowIndex); + int columnIndex = 0; + for (int cellIndex = 0; cellIndex < row.getChildren().size(); cellIndex += 1) { + UnmergedTableCell cell = (UnmergedTableCell) row.getChildren().get(cellIndex); + Optional> spanningCell = lookup(lastCellForColumn, columnIndex); + Map.Entry position = entry(rowIndex, cellIndex); + if (cell.vmerge && spanningCell.isPresent()) { + rowspans.put(spanningCell.get(), lookup(rowspans, spanningCell.get()).get() + 1); + merged.add(position); + } else { + lastCellForColumn.put(columnIndex, position); + rowspans.put(position, 1); + } + columnIndex += cell.colspan; + } + } + + List mergedRows = new ArrayList<>(); + for (int rowIndex = 0; rowIndex < rows.size(); rowIndex += 1) { + TableRow row = (TableRow) rows.get(rowIndex); + + List mergedCells = new ArrayList<>(); + for (int cellIndex = 0; cellIndex < row.getChildren().size(); cellIndex += 1) { + UnmergedTableCell cell = (UnmergedTableCell) row.getChildren().get(cellIndex); + Map.Entry position = entry(rowIndex, cellIndex); + if (!merged.contains(position)) { + mergedCells.add(new TableCell( + lookup(rowspans, position).get(), + cell.colspan, + cell.children + )); + } + } + + mergedRows.add(new TableRow(mergedCells)); + } + return success(mergedRows); + } + private ReadResult readTableCell(XmlElement element) { - Optional gridSpan = element - .findChildOrEmpty("w:tcPr") + XmlElementLike properties = element.findChildOrEmpty("w:tcPr"); + Optional gridSpan = properties .findChildOrEmpty("w:gridSpan") .getAttributeOrNone("w:val"); int colspan = gridSpan.map(Integer::parseInt).orElse(1); - return readElements(element.getChildren()).map(children -> new TableCell(colspan, children)); + return readElements(element.getChildren()) + .map(children -> new UnmergedTableCell(readVmerge(properties), colspan, children)); + } + + private boolean readVmerge(XmlElementLike properties) { + return properties.findChild("w:vMerge") + .map(element -> element.getAttributeOrNone("w:val").map(val -> val.equals("continue")).orElse(true)) + .orElse(false); + } + + private static class UnmergedTableCell implements DocumentElement { + private final boolean vmerge; + private final int colspan; + private final List children; + + private UnmergedTableCell(boolean vmerge, int colspan, List children) { + this.vmerge = vmerge; + this.colspan = colspan; + this.children = children; + } + + @Override + public T accept(DocumentElementVisitor visitor) { + return visitor.visit(new TableCell(1, colspan, children)); + } } private ReadResult readHyperlink(XmlElement element) { diff --git a/src/main/java/org/zwobble/mammoth/internal/docx/ReadResult.java b/src/main/java/org/zwobble/mammoth/internal/docx/ReadResult.java index fa38338..beff7f7 100644 --- a/src/main/java/org/zwobble/mammoth/internal/docx/ReadResult.java +++ b/src/main/java/org/zwobble/mammoth/internal/docx/ReadResult.java @@ -42,11 +42,15 @@ public class ReadResult { } public static ReadResult emptyWithWarning(String warning) { - return new ReadResult(list(), list(), list(warning)); + return withWarning(list(), warning); } public static ReadResult withWarning(DocumentElement element, String warning) { - return new ReadResult(list(element), list(), list(warning)); + return withWarning(list(element), warning); + } + + public static ReadResult withWarning(List elements, String warning) { + return new ReadResult(elements, list(), list(warning)); } private final List elements; @@ -66,6 +70,14 @@ public class ReadResult { warnings); } + public ReadResult flatMap(Function, ReadResult> function) { + ReadResult result = function.apply(elements); + return new ReadResult( + result.elements, + eagerConcat(extra, result.extra), + lazyConcat(warnings, result.warnings)); + } + public ReadResult toExtra() { return new ReadResult(list(), eagerConcat(extra, elements), warnings); } diff --git a/src/main/java/org/zwobble/mammoth/internal/xml/NullXmlElement.java b/src/main/java/org/zwobble/mammoth/internal/xml/NullXmlElement.java index 4d32d38..a3b0921 100644 --- a/src/main/java/org/zwobble/mammoth/internal/xml/NullXmlElement.java +++ b/src/main/java/org/zwobble/mammoth/internal/xml/NullXmlElement.java @@ -16,6 +16,11 @@ public class NullXmlElement implements XmlElementLike { return false; } + @Override + public Optional findChild(String name) { + return Optional.empty(); + } + @Override public XmlElementLike findChildOrEmpty(String name) { return this; diff --git a/src/main/java/org/zwobble/mammoth/internal/xml/XmlElementLike.java b/src/main/java/org/zwobble/mammoth/internal/xml/XmlElementLike.java index 41df49f..4683209 100644 --- a/src/main/java/org/zwobble/mammoth/internal/xml/XmlElementLike.java +++ b/src/main/java/org/zwobble/mammoth/internal/xml/XmlElementLike.java @@ -5,6 +5,7 @@ import java.util.Optional; public interface XmlElementLike { boolean hasChild(String name); + Optional findChild(String name); XmlElementLike findChildOrEmpty(String name); Optional getAttributeOrNone(String name); List getChildren(); diff --git a/src/test/java/org/zwobble/mammoth/tests/documents/DocumentElementMakers.java b/src/test/java/org/zwobble/mammoth/tests/documents/DocumentElementMakers.java index 04d8aef..17cd18e 100644 --- a/src/test/java/org/zwobble/mammoth/tests/documents/DocumentElementMakers.java +++ b/src/test/java/org/zwobble/mammoth/tests/documents/DocumentElementMakers.java @@ -13,7 +13,8 @@ import static com.natpryce.makeiteasy.MakeItEasy.with; import static com.natpryce.makeiteasy.Property.newProperty; import static org.zwobble.mammoth.internal.util.Lists.list; -public class DocumentElementMakers { +public class +DocumentElementMakers { public static final Property> STYLE = newProperty(); public static final Property> NUMBERING = newProperty(); public static final Property BOLD = newProperty(); @@ -42,6 +43,7 @@ public class DocumentElementMakers { public static final Instantiator TABLE_CELL = propertyLookup -> new TableCell( + 1, propertyLookup.valueOf(COLSPAN, 1), propertyLookup.valueOf(CHILDREN, list())); 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 0752d3a..f67560c 100644 --- a/src/test/java/org/zwobble/mammoth/tests/docx/BodyXmlTests.java +++ b/src/test/java/org/zwobble/mammoth/tests/docx/BodyXmlTests.java @@ -36,6 +36,7 @@ import static org.zwobble.mammoth.tests.ResultMatchers.isInternalResult; import static org.zwobble.mammoth.tests.documents.DocumentElementMakers.*; import static org.zwobble.mammoth.tests.docx.BodyXmlReaderMakers.*; import static org.zwobble.mammoth.tests.docx.BodyXmlReaderMakers.NUMBERING; +import static org.zwobble.mammoth.tests.docx.OfficeXmlBuilders.*; public class BodyXmlTests { @@ -324,7 +325,7 @@ public class BodyXmlTests { readSuccess(a(bodyReader), element), deepEquals(new Table(list( new TableRow(list( - new TableCell(1, list( + new TableCell(1, 1, list( make(a(PARAGRAPH)) )) )) @@ -346,7 +347,7 @@ public class BodyXmlTests { readSuccess(a(bodyReader), element), deepEquals(new Table(list( new TableRow(list( - new TableCell(2, list( + new TableCell(1, 2, list( make(a(PARAGRAPH)) )) )) @@ -354,6 +355,110 @@ public class BodyXmlTests { ); } + @Test + public void vmergeIsReadAsRowspanForTableCell() { + XmlElement element = element("w:tbl", list( + wTr(wTc()), + wTr(wTc(wTcPr(wVmerge("restart")))), + wTr(wTc(wTcPr(wVmerge("continue")))), + wTr(wTc(wTcPr(wVmerge("continue")))), + wTr(wTc()) + )); + + assertThat( + readSuccess(a(bodyReader), element), + deepEquals(new Table(list( + new TableRow(list(new TableCell(1, 1, list()))), + new TableRow(list(new TableCell(3, 1, list()))), + new TableRow(list()), + new TableRow(list()), + new TableRow(list(new TableCell(1, 1, list()))) + ))) + ); + } + + @Test + public void vmergeWithoutValIsTreatedAsContinue() { + XmlElement element = element("w:tbl", list( + wTr(wTc(wTcPr(wVmerge("restart")))), + wTr(wTc(wTcPr(element("w:vMerge")))) + )); + + assertThat( + readSuccess(a(bodyReader), element), + deepEquals(new Table(list( + new TableRow(list(new TableCell(2, 1, list()))), + new TableRow(list()) + ))) + ); + } + + @Test + public void vmergeAccountsForCellsSpanningColumns() { + XmlElement element = element("w:tbl", list( + wTr(wTc(), wTc(), wTc(wTcPr(wVmerge("restart")))), + wTr(wTc(wTcPr(wGridspan("2"))), wTc(wTcPr(wVmerge("continue")))), + wTr(wTc(), wTc(), wTc(wTcPr(wVmerge("continue")))), + wTr(wTc(), wTc(), wTc()) + )); + + assertThat( + readSuccess(a(bodyReader), element), + deepEquals(new Table(list( + new TableRow(list(new TableCell(1, 1, list()), new TableCell(1, 1, list()), new TableCell(3, 1, list()))), + new TableRow(list(new TableCell(1, 2, list()))), + new TableRow(list(new TableCell(1, 1, list()), new TableCell(1, 1, list()))), + new TableRow(list(new TableCell(1, 1, list()), new TableCell(1, 1, list()), new TableCell(1, 1, list()))) + ))) + ); + } + + @Test + public void noVerticalCellMergingIfMergedCellsDoNotLineUp() { + XmlElement element = element("w:tbl", list( + wTr(wTc(wTcPr(wGridspan("2"))), wTc(wTcPr(wVmerge("restart")))), + wTr(wTc(), wTc(wTcPr(wVmerge("continue")))) + )); + + assertThat( + readSuccess(a(bodyReader), element), + deepEquals(new Table(list( + new TableRow(list(new TableCell(1, 2, list()), new TableCell(1, 1, list()))), + new TableRow(list(new TableCell(1, 1, list()), new TableCell(1, 1, list()))) + ))) + ); + } + + @Test + public void warningIfNonRowInTable() { + XmlElement element = element("w:tbl", list( + element("w:p") + )); + + assertThat( + read(a(bodyReader), element), + isInternalResult( + deepEquals(new Table(list(make(a(PARAGRAPH))))), + list("unexpected non-row element in table, cell merging may be incorrect") + ) + ); + } + + @Test + public void warningIfNonCellInTableRow() { + XmlElement element = element("w:tbl", list( + wTr(element("w:p")) + )); + + assertThat( + read(a(bodyReader), element), + isInternalResult( + deepEquals(new Table(list(new TableRow(list(make(a(PARAGRAPH))))))), + list("unexpected non-cell element in table row, cell merging may be incorrect") + ) + ); + } + @Test public void hyperlinkIsReadIfItHasARelationshipId() { Relationships relationships = new Relationships( diff --git a/src/test/java/org/zwobble/mammoth/tests/docx/OfficeXmlBuilders.java b/src/test/java/org/zwobble/mammoth/tests/docx/OfficeXmlBuilders.java new file mode 100644 index 0000000..71c2cba --- /dev/null +++ b/src/test/java/org/zwobble/mammoth/tests/docx/OfficeXmlBuilders.java @@ -0,0 +1,29 @@ +package org.zwobble.mammoth.tests.docx; + +import org.zwobble.mammoth.internal.xml.XmlElement; + +import static java.util.Arrays.asList; +import static org.zwobble.mammoth.internal.util.Maps.map; +import static org.zwobble.mammoth.internal.xml.XmlNodes.element; + +public class OfficeXmlBuilders { + public static XmlElement wTr(XmlElement... children) { + return element("w:tr", asList(children)); + } + + public static XmlElement wTc(XmlElement... children) { + return element("w:tc", asList(children)); + } + + public static XmlElement wTcPr(XmlElement... children) { + return element("w:tcPr", asList(children)); + } + + public static XmlElement wGridspan(String val) { + return element("w:gridSpan", map("w:val", val)); + } + + public static XmlElement wVmerge(String val) { + return element("w:vMerge", map("w:val", val)); + } +} -- cgit v1.2.3