summaryrefslogtreecommitdiff
path: root/src/main/java/org
diff options
context:
space:
mode:
authorMichael Williamson <mike@zwobble.org>2017-02-19 18:16:52 +0000
committerMichael Williamson <mike@zwobble.org>2017-02-19 19:21:02 +0000
commitb31c5a4b4ccb835a583efa33cee178e122d564de (patch)
tree2dd68868d5cc1bbeaa5283798122b4cc4e5c2b2d /src/main/java/org
parent2e819654db985bf8a7e7ed76e0a43dc2f46eec8d (diff)
Handle hyperlinks created with complex fields
Diffstat (limited to 'src/main/java/org')
-rw-r--r--src/main/java/org/zwobble/mammoth/internal/docx/BodyXmlReader.java452
-rw-r--r--src/main/java/org/zwobble/mammoth/internal/docx/StatefulBodyXmlReader.java539
-rw-r--r--src/main/java/org/zwobble/mammoth/internal/util/Iterables.java14
3 files changed, 573 insertions, 432 deletions
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 4cbdda7..ace6628 100644
--- a/src/main/java/org/zwobble/mammoth/internal/docx/BodyXmlReader.java
+++ b/src/main/java/org/zwobble/mammoth/internal/docx/BodyXmlReader.java
@@ -1,34 +1,10 @@
package org.zwobble.mammoth.internal.docx;
import org.zwobble.mammoth.internal.archives.Archive;
-import org.zwobble.mammoth.internal.archives.Archives;
-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.Lists;
-import org.zwobble.mammoth.internal.util.Optionals;
import org.zwobble.mammoth.internal.xml.XmlElement;
-import org.zwobble.mammoth.internal.xml.XmlElementLike;
-import org.zwobble.mammoth.internal.xml.XmlElementList;
import org.zwobble.mammoth.internal.xml.XmlNode;
-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.docx.Uris.uriToZipEntryName;
-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;
-
public class BodyXmlReader {
- private static final Set<String> IMAGE_TYPES_SUPPORTED_BY_BROWSERS = set(
- "image/png", "image/gif", "image/jpeg", "image/svg+xml", "image/tiff");
-
private final Styles styles;
private final Numbering numbering;
private final Relationships relationships;
@@ -42,7 +18,8 @@ public class BodyXmlReader {
Relationships relationships,
ContentTypes contentTypes,
Archive file,
- FileReader fileReader)
+ FileReader fileReader
+ )
{
this.styles = styles;
this.numbering = numbering;
@@ -52,414 +29,25 @@ public class BodyXmlReader {
this.fileReader = fileReader;
}
- public ReadResult readElement(XmlElement element) {
- switch (element.getName()) {
- case "w:t":
- return success(new Text(element.innerText()));
- case "w:r":
- return readRun(element);
- case "w:p":
- return readParagraph(element);
-
- case "w:tab":
- return success(Tab.TAB);
- case "w:br":
- return readBreak(element);
-
- case "w:tbl":
- return readTable(element);
- case "w:tr":
- return readElements(element.getChildren()).map(TableRow::new);
- case "w:tc":
- return readTableCell(element);
-
- case "w:hyperlink":
- return readHyperlink(element);
- case "w:bookmarkStart":
- return readBookmark(element);
- case "w:footnoteReference":
- return readNoteReference(NoteType.FOOTNOTE, element);
- case "w:endnoteReference":
- return readNoteReference(NoteType.ENDNOTE, element);
- case "w:commentReference":
- return readCommentReference(element);
-
- case "w:pict":
- return readPict(element);
-
- case "v:imagedata":
- return readImagedata(element);
-
- case "wp:inline":
- case "wp:anchor":
- return readInline(element);
-
- case "w:sdt":
- return readSdt(element);
-
- case "w:ins":
- case "w:smartTag":
- case "w:drawing":
- case "v:roundrect":
- case "v:shape":
- case "v:textbox":
- case "w:txbxContent":
- return readElements(element.getChildren());
-
- case "office-word:wrap":
- case "v:shadow":
- case "v:shapetype":
- case "w:bookmarkEnd":
- case "w:sectPr":
- case "w:proofErr":
- case "w:lastRenderedPageBreak":
- case "w:commentRangeStart":
- case "w:commentRangeEnd":
- case "w:del":
- case "w:footnoteRef":
- case "w:endnoteRef":
- case "w:annotationRef":
- case "w:pPr":
- case "w:rPr":
- case "w:tblPr":
- case "w:tblGrid":
- case "w:tcPr":
- return EMPTY_SUCCESS;
-
- default:
- String warning = "An unrecognised element was ignored: " + element.getName();
- return ReadResult.emptyWithWarning(warning);
- }
- }
-
- private ReadResult readRun(XmlElement element) {
- XmlElementLike properties = element.findChildOrEmpty("w:rPr");
- return ReadResult.map(
- readRunStyle(properties),
- readElements(element.getChildren()),
- (style, children) -> new Run(
- isBold(properties),
- isItalic(properties),
- isUnderline(properties),
- isStrikethrough(properties),
- readVerticalAlignment(properties),
- style,
- children));
- }
-
- private boolean isBold(XmlElementLike properties) {
- return readBooleanElement(properties, "w:b");
- }
-
- private boolean isItalic(XmlElementLike properties) {
- return readBooleanElement(properties, "w:i");
- }
-
- private boolean isUnderline(XmlElementLike properties) {
- return readBooleanElement(properties, "w:u");
- }
-
- private boolean isStrikethrough(XmlElementLike properties) {
- return readBooleanElement(properties, "w:strike");
- }
-
- private boolean readBooleanElement(XmlElementLike properties, String tagName) {
- return properties.findChild(tagName)
- .map(child -> child.getAttributeOrNone("w:val")
- .map(value -> !value.equals("false") && !value.equals("0"))
- .orElse(true))
- .orElse(false);
- }
-
- private VerticalAlignment readVerticalAlignment(XmlElementLike properties) {
- String verticalAlignment = readVal(properties, "w:vertAlign").orElse("");
- switch (verticalAlignment) {
- case "superscript":
- return VerticalAlignment.SUPERSCRIPT;
- case "subscript":
- return VerticalAlignment.SUBSCRIPT;
- default:
- // TODO: warn if set?
- return VerticalAlignment.BASELINE;
- }
- }
-
- private InternalResult<Optional<Style>> readRunStyle(XmlElementLike properties) {
- return readStyle(properties, "w:rStyle", "Run", styles::findCharacterStyleById);
- }
-
- public ReadResult readElements(Iterable<XmlNode> nodes) {
- return ReadResult.flatMap(lazyFilter(nodes, XmlElement.class), this::readElement);
+ ReadResult readElements(Iterable<XmlNode> nodes) {
+ return new StatefulBodyXmlReader(
+ styles,
+ numbering,
+ relationships,
+ contentTypes,
+ file,
+ fileReader
+ ).readElements(nodes);
}
- private ReadResult readParagraph(XmlElement element) {
- XmlElementLike properties = element.findChildOrEmpty("w:pPr");
- Optional<NumberingLevel> numbering = readNumbering(properties);
- return ReadResult.map(
- readParagraphStyle(properties),
- readElements(element.getChildren()),
- (style, children) -> new Paragraph(style, numbering, children)).appendExtra();
- }
-
- private InternalResult<Optional<Style>> readParagraphStyle(XmlElementLike properties) {
- return readStyle(properties, "w:pStyle", "Paragraph", styles::findParagraphStyleById);
- }
-
- private InternalResult<Optional<Style>> readStyle(
- XmlElementLike properties,
- String styleTagName,
- String styleType,
- Function<String, Optional<Style>> findStyleById)
- {
- return readVal(properties, styleTagName)
- .map(styleId -> findStyleById(styleType, styleId, findStyleById))
- .orElse(InternalResult.empty());
- }
-
- private InternalResult<Optional<Style>> findStyleById(
- String styleType,
- String styleId,
- Function<String, Optional<Style>> findStyleById)
- {
- Optional<Style> style = findStyleById.apply(styleId);
- if (style.isPresent()) {
- return InternalResult.success(style);
- } else {
- return new InternalResult<>(
- Optional.of(new Style(styleId, Optional.empty())),
- list(styleType + " style with ID " + styleId + " was referenced but not defined in the document"));
- }
-
- }
-
- private Optional<NumberingLevel> readNumbering(XmlElementLike properties) {
- XmlElementLike numberingProperties = properties.findChildOrEmpty("w:numPr");
- return Optionals.flatMap(
- readVal(numberingProperties, "w:numId"),
- readVal(numberingProperties, "w:ilvl"),
- numbering::findLevel);
- }
-
- private ReadResult readBreak(XmlElement element) {
- String breakType = element.getAttributeOrNone("w:type").orElse("textWrapping");
- switch (breakType) {
- case "textWrapping":
- return success(Break.LINE_BREAK);
- case "page":
- return success(Break.PAGE_BREAK);
- case "column":
- return success(Break.COLUMN_BREAK);
- default:
- return ReadResult.emptyWithWarning("Unsupported break type: " + breakType);
- }
- }
-
- private ReadResult readTable(XmlElement element) {
- return readElements(element.getChildren())
- .flatMap(this::calculateRowspans)
- .map(Table::new);
- }
-
- private ReadResult calculateRowspans(List<DocumentElement> rows) {
- Optional<String> error = checkTableRows(rows);
- if (error.isPresent()) {
- return ReadResult.withWarning(rows, error.get());
- }
-
- Map<Map.Entry<Integer, Integer>, Integer> rowspans = new HashMap<>();
- Set<Map.Entry<Integer, Integer>> merged = new HashSet<>();
-
- Map<Integer, Map.Entry<Integer, Integer>> 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<Map.Entry<Integer, Integer>> spanningCell = lookup(lastCellForColumn, columnIndex);
- Map.Entry<Integer, Integer> 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;
- }
- }
-
- return success(Lists.eagerMapWithIndex(rows, (rowIndex, rowElement) -> {
- TableRow row = (TableRow) rowElement;
-
- List<DocumentElement> mergedCells = new ArrayList<>();
- for (int cellIndex = 0; cellIndex < row.getChildren().size(); cellIndex += 1) {
- UnmergedTableCell cell = (UnmergedTableCell) row.getChildren().get(cellIndex);
- Map.Entry<Integer, Integer> position = entry(rowIndex, cellIndex);
- if (!merged.contains(position)) {
- mergedCells.add(new TableCell(
- lookup(rowspans, position).get(),
- cell.colspan,
- cell.children
- ));
- }
- }
-
- return new TableRow(mergedCells);
- }));
- }
-
- private Optional<String> checkTableRows(List<DocumentElement> rows) {
- for (DocumentElement rowElement : rows) {
- Optional<TableRow> row = Casts.tryCast(TableRow.class, rowElement);
- if (!row.isPresent()) {
- return Optional.of("unexpected non-row element in table, cell merging may be incorrect");
- } else {
- for (DocumentElement cell : row.get().getChildren()) {
- if (!(cell instanceof UnmergedTableCell)) {
- return Optional.of("unexpected non-cell element in table row, cell merging may be incorrect");
- }
- }
- }
- }
- return Optional.empty();
- }
-
- private ReadResult readTableCell(XmlElement element) {
- XmlElementLike properties = element.findChildOrEmpty("w:tcPr");
- Optional<String> gridSpan = properties
- .findChildOrEmpty("w:gridSpan")
- .getAttributeOrNone("w:val");
- int colspan = gridSpan.map(Integer::parseInt).orElse(1);
- 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<DocumentElement> children;
-
- private UnmergedTableCell(boolean vmerge, int colspan, List<DocumentElement> children) {
- this.vmerge = vmerge;
- this.colspan = colspan;
- this.children = children;
- }
-
- @Override
- public <T> T accept(DocumentElementVisitor<T> visitor) {
- return visitor.visit(new TableCell(1, colspan, children));
- }
- }
-
- private ReadResult readHyperlink(XmlElement element) {
- Optional<String> relationshipId = element.getAttributeOrNone("r:id");
- Optional<String> anchor = element.getAttributeOrNone("w:anchor");
- ReadResult childrenResult = readElements(element.getChildren());
- if (relationshipId.isPresent()) {
- return childrenResult.map(children -> Hyperlink.href(
- relationships.findRelationshipById(relationshipId.get()).getTarget(), children));
- } else if (anchor.isPresent()) {
- return childrenResult.map(children -> Hyperlink.anchor(
- anchor.get(), children));
- } else {
- return childrenResult;
- }
- }
-
- private ReadResult readBookmark(XmlElement element) {
- String name = element.getAttribute("w:name");
- if (name.equals("_GoBack")) {
- return ReadResult.EMPTY_SUCCESS;
- } else {
- return success(new Bookmark(name));
- }
- }
-
- private ReadResult readNoteReference(NoteType noteType, XmlElement element) {
- String noteId = element.getAttribute("w:id");
- return success(new NoteReference(noteType, noteId));
- }
-
- private ReadResult readCommentReference(XmlElement element) {
- String commentId = element.getAttribute("w:id");
- return success(new CommentReference(commentId));
- }
-
- private ReadResult readPict(XmlElement element) {
- return readElements(element.getChildren()).toExtra();
- }
-
- private ReadResult readImagedata(XmlElement element) {
- return element.getAttributeOrNone("r:id")
- .map(relationshipId -> {
- Optional<String> title = element.getAttributeOrNone("o:title");
- String imagePath = relationshipIdToDocxPath(relationshipId);
- return readImage(imagePath, title, () -> Archives.getInputStream(file, imagePath));
- })
- .orElse(ReadResult.emptyWithWarning("A v:imagedata element without a relationship ID was ignored"));
- }
-
- private ReadResult readInline(XmlElement element) {
- XmlElementLike properties = element.findChildOrEmpty("wp:docPr");
- Optional<String> altText = Optionals.first(
- properties.getAttributeOrNone("descr").filter(description -> !description.trim().isEmpty()),
- properties.getAttributeOrNone("title")
- );
- XmlElementList blips = element.findChildren("a:graphic")
- .findChildren("a:graphicData")
- .findChildren("pic:pic")
- .findChildren("pic:blipFill")
- .findChildren("a:blip");
- return readBlips(blips, altText);
- }
-
- private ReadResult readBlips(XmlElementList blips, Optional<String> altText) {
- return ReadResult.flatMap(blips, blip -> readBlip(blip, altText));
- }
-
- private ReadResult readBlip(XmlElement blip, Optional<String> altText) {
- Optional<String> embedRelationshipId = blip.getAttributeOrNone("r:embed");
- Optional<String> linkRelationshipId = blip.getAttributeOrNone("r:link");
- if (embedRelationshipId.isPresent()) {
- String imagePath = relationshipIdToDocxPath(embedRelationshipId.get());
- return readImage(imagePath, altText, () -> Archives.getInputStream(file, imagePath));
- } else if (linkRelationshipId.isPresent()) {
- String imagePath = relationships.findRelationshipById(linkRelationshipId.get()).getTarget();
- return readImage(imagePath, altText, () -> fileReader.getInputStream(imagePath));
- } else {
- // TODO: emit warning
- return ReadResult.EMPTY_SUCCESS;
- }
- }
-
- private ReadResult readImage(String imagePath, Optional<String> altText, InputStreamSupplier open) {
- Optional<String> contentType = contentTypes.findContentType(imagePath);
- Image image = new Image(altText, contentType, open);
-
- String contentTypeString = contentType.orElse("(unknown)");
- if (IMAGE_TYPES_SUPPORTED_BY_BROWSERS.contains(contentTypeString)) {
- return success(image);
- } else {
- return ReadResult.withWarning(image, "Image of type " + contentTypeString + " is unlikely to display in web browsers");
- }
- }
-
- private ReadResult readSdt(XmlElement element) {
- return readElements(element.findChildOrEmpty("w:sdtContent").getChildren());
- }
-
- private String relationshipIdToDocxPath(String relationshipId) {
- Relationship relationship = relationships.findRelationshipById(relationshipId);
- return uriToZipEntryName("word", relationship.getTarget());
- }
-
- private Optional<String> readVal(XmlElementLike element, String name) {
- return element.findChildOrEmpty(name).getAttributeOrNone("w:val");
+ public ReadResult readElement(XmlElement element) {
+ return new StatefulBodyXmlReader(
+ styles,
+ numbering,
+ relationships,
+ contentTypes,
+ file,
+ fileReader
+ ).readElement(element);
}
}
diff --git a/src/main/java/org/zwobble/mammoth/internal/docx/StatefulBodyXmlReader.java b/src/main/java/org/zwobble/mammoth/internal/docx/StatefulBodyXmlReader.java
new file mode 100644
index 0000000..f02a2e1
--- /dev/null
+++ b/src/main/java/org/zwobble/mammoth/internal/docx/StatefulBodyXmlReader.java
@@ -0,0 +1,539 @@
+package org.zwobble.mammoth.internal.docx;
+
+import org.zwobble.mammoth.internal.archives.Archive;
+import org.zwobble.mammoth.internal.archives.Archives;
+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.Lists;
+import org.zwobble.mammoth.internal.util.Optionals;
+import org.zwobble.mammoth.internal.xml.XmlElement;
+import org.zwobble.mammoth.internal.xml.XmlElementLike;
+import org.zwobble.mammoth.internal.xml.XmlElementList;
+import org.zwobble.mammoth.internal.xml.XmlNode;
+
+import java.util.*;
+import java.util.function.Function;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+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.docx.Uris.uriToZipEntryName;
+import static org.zwobble.mammoth.internal.util.Iterables.lazyFilter;
+import static org.zwobble.mammoth.internal.util.Iterables.tryGetLast;
+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;
+
+class StatefulBodyXmlReader {
+ private static final Set<String> IMAGE_TYPES_SUPPORTED_BY_BROWSERS = set(
+ "image/png", "image/gif", "image/jpeg", "image/svg+xml", "image/tiff");
+
+ private final Styles styles;
+ private final Numbering numbering;
+ private final Relationships relationships;
+ private final ContentTypes contentTypes;
+ private final Archive file;
+ private final FileReader fileReader;
+ private final StringBuilder currentInstrText;
+ private final Queue<ComplexField> complexFields;
+
+ private interface ComplexField {
+ ComplexField UNKNOWN = new ComplexField() {};
+
+ static ComplexField hyperlink(String href) {
+ return new HyperlinkComplexField(href);
+ }
+ }
+
+ private static class HyperlinkComplexField implements ComplexField {
+ private final String href;
+
+ private HyperlinkComplexField(String href) {
+ this.href = href;
+ }
+ }
+
+ StatefulBodyXmlReader(
+ Styles styles,
+ Numbering numbering,
+ Relationships relationships,
+ ContentTypes contentTypes,
+ Archive file,
+ FileReader fileReader
+ )
+ {
+ this.styles = styles;
+ this.numbering = numbering;
+ this.relationships = relationships;
+ this.contentTypes = contentTypes;
+ this.file = file;
+ this.fileReader = fileReader;
+ this.currentInstrText = new StringBuilder();
+ this.complexFields = Collections.asLifoQueue(new ArrayDeque<>());
+ }
+
+ ReadResult readElement(XmlElement element) {
+ switch (element.getName()) {
+ case "w:t":
+ return success(new Text(element.innerText()));
+ case "w:r":
+ return readRun(element);
+ case "w:p":
+ return readParagraph(element);
+
+ case "w:fldChar":
+ return readFieldChar(element);
+ case "w:instrText":
+ return readInstrText(element);
+
+ case "w:tab":
+ return success(Tab.TAB);
+ case "w:br":
+ return readBreak(element);
+
+ case "w:tbl":
+ return readTable(element);
+ case "w:tr":
+ return readElements(element.getChildren()).map(TableRow::new);
+ case "w:tc":
+ return readTableCell(element);
+
+ case "w:hyperlink":
+ return readHyperlink(element);
+ case "w:bookmarkStart":
+ return readBookmark(element);
+ case "w:footnoteReference":
+ return readNoteReference(NoteType.FOOTNOTE, element);
+ case "w:endnoteReference":
+ return readNoteReference(NoteType.ENDNOTE, element);
+ case "w:commentReference":
+ return readCommentReference(element);
+
+ case "w:pict":
+ return readPict(element);
+
+ case "v:imagedata":
+ return readImagedata(element);
+
+ case "wp:inline":
+ case "wp:anchor":
+ return readInline(element);
+
+ case "w:sdt":
+ return readSdt(element);
+
+ case "w:ins":
+ case "w:smartTag":
+ case "w:drawing":
+ case "v:roundrect":
+ case "v:shape":
+ case "v:textbox":
+ case "w:txbxContent":
+ return readElements(element.getChildren());
+
+ case "office-word:wrap":
+ case "v:shadow":
+ case "v:shapetype":
+ case "w:bookmarkEnd":
+ case "w:sectPr":
+ case "w:proofErr":
+ case "w:lastRenderedPageBreak":
+ case "w:commentRangeStart":
+ case "w:commentRangeEnd":
+ case "w:del":
+ case "w:footnoteRef":
+ case "w:endnoteRef":
+ case "w:annotationRef":
+ case "w:pPr":
+ case "w:rPr":
+ case "w:tblPr":
+ case "w:tblGrid":
+ case "w:tcPr":
+ return EMPTY_SUCCESS;
+
+ default:
+ String warning = "An unrecognised element was ignored: " + element.getName();
+ return ReadResult.emptyWithWarning(warning);
+ }
+ }
+
+ private ReadResult readRun(XmlElement element) {
+ XmlElementLike properties = element.findChildOrEmpty("w:rPr");
+ return ReadResult.map(
+ readRunStyle(properties),
+ readElements(element.getChildren()),
+ (style, children) -> {
+ Optional<String> hyperlinkHref = currentHyperlinkHref();
+ if (hyperlinkHref.isPresent()) {
+ children = list(Hyperlink.href(hyperlinkHref.get(), children));
+ }
+
+ return new Run(
+ isBold(properties),
+ isItalic(properties),
+ isUnderline(properties),
+ isStrikethrough(properties),
+ readVerticalAlignment(properties),
+ style,
+ children
+ );
+ }
+ );
+ }
+
+ private Optional<String> currentHyperlinkHref() {
+ return tryGetLast(lazyFilter(this.complexFields, HyperlinkComplexField.class))
+ .map(field -> field.href);
+ }
+
+ private boolean isBold(XmlElementLike properties) {
+ return readBooleanElement(properties, "w:b");
+ }
+
+ private boolean isItalic(XmlElementLike properties) {
+ return readBooleanElement(properties, "w:i");
+ }
+
+ private boolean isUnderline(XmlElementLike properties) {
+ return readBooleanElement(properties, "w:u");
+ }
+
+ private boolean isStrikethrough(XmlElementLike properties) {
+ return readBooleanElement(properties, "w:strike");
+ }
+
+ private boolean readBooleanElement(XmlElementLike properties, String tagName) {
+ return properties.findChild(tagName)
+ .map(child -> child.getAttributeOrNone("w:val")
+ .map(value -> !value.equals("false") && !value.equals("0"))
+ .orElse(true))
+ .orElse(false);
+ }
+
+ private VerticalAlignment readVerticalAlignment(XmlElementLike properties) {
+ String verticalAlignment = readVal(properties, "w:vertAlign").orElse("");
+ switch (verticalAlignment) {
+ case "superscript":
+ return VerticalAlignment.SUPERSCRIPT;
+ case "subscript":
+ return VerticalAlignment.SUBSCRIPT;
+ default:
+ // TODO: warn if set?
+ return VerticalAlignment.BASELINE;
+ }
+ }
+
+ private InternalResult<Optional<Style>> readRunStyle(XmlElementLike properties) {
+ return readStyle(properties, "w:rStyle", "Run", styles::findCharacterStyleById);
+ }
+
+ ReadResult readElements(Iterable<XmlNode> nodes) {
+ return ReadResult.flatMap(lazyFilter(nodes, XmlElement.class), this::readElement);
+ }
+
+ private ReadResult readParagraph(XmlElement element) {
+ XmlElementLike properties = element.findChildOrEmpty("w:pPr");
+ Optional<NumberingLevel> numbering = readNumbering(properties);
+ return ReadResult.map(
+ readParagraphStyle(properties),
+ readElements(element.getChildren()),
+ (style, children) -> new Paragraph(style, numbering, children)).appendExtra();
+ }
+
+ private ReadResult readFieldChar(XmlElement element) {
+ String type = element.getAttributeOrNone("w:fldCharType").orElse("");
+ if (type.equals("begin")) {
+ currentInstrText.setLength(0);
+ } else if (type.equals("end")) {
+ complexFields.remove();
+ } else if (type.equals("separate")) {
+ String instrText = currentInstrText.toString();
+ ComplexField complexField = parseHyperlinkFieldCode(instrText)
+ .map(href -> ComplexField.hyperlink(href))
+ .orElse(ComplexField.UNKNOWN);
+ complexFields.add(complexField);
+ }
+ return ReadResult.EMPTY_SUCCESS;
+ }
+
+ private ReadResult readInstrText(XmlElement element) {
+ currentInstrText.append(element.innerText());
+ return ReadResult.EMPTY_SUCCESS;
+ }
+
+ private Optional<String> parseHyperlinkFieldCode(String instrText) {
+ Pattern pattern = Pattern.compile("\\s*HYPERLINK \"(.*)\"");
+ Matcher matcher = pattern.matcher(instrText);
+ if (matcher.lookingAt()) {
+ return Optional.of(matcher.group(1));
+ } else {
+ return Optional.empty();
+ }
+ }
+
+ private InternalResult<Optional<Style>> readParagraphStyle(XmlElementLike properties) {
+ return readStyle(properties, "w:pStyle", "Paragraph", styles::findParagraphStyleById);
+ }
+
+ private InternalResult<Optional<Style>> readStyle(
+ XmlElementLike properties,
+ String styleTagName,
+ String styleType,
+ Function<String, Optional<Style>> findStyleById)
+ {
+ return readVal(properties, styleTagName)
+ .map(styleId -> findStyleById(styleType, styleId, findStyleById))
+ .orElse(InternalResult.empty());
+ }
+
+ private InternalResult<Optional<Style>> findStyleById(
+ String styleType,
+ String styleId,
+ Function<String, Optional<Style>> findStyleById)
+ {
+ Optional<Style> style = findStyleById.apply(styleId);
+ if (style.isPresent()) {
+ return InternalResult.success(style);
+ } else {
+ return new InternalResult<>(
+ Optional.of(new Style(styleId, Optional.empty())),
+ list(styleType + " style with ID " + styleId + " was referenced but not defined in the document"));
+ }
+
+ }
+
+ private Optional<NumberingLevel> readNumbering(XmlElementLike properties) {
+ XmlElementLike numberingProperties = properties.findChildOrEmpty("w:numPr");
+ return Optionals.flatMap(
+ readVal(numberingProperties, "w:numId"),
+ readVal(numberingProperties, "w:ilvl"),
+ numbering::findLevel);
+ }
+
+ private ReadResult readBreak(XmlElement element) {
+ String breakType = element.getAttributeOrNone("w:type").orElse("textWrapping");
+ switch (breakType) {
+ case "textWrapping":
+ return success(Break.LINE_BREAK);
+ case "page":
+ return success(Break.PAGE_BREAK);
+ case "column":
+ return success(Break.COLUMN_BREAK);
+ default:
+ return ReadResult.emptyWithWarning("Unsupported break type: " + breakType);
+ }
+ }
+
+ private ReadResult readTable(XmlElement element) {
+ return readElements(element.getChildren())
+ .flatMap(this::calculateRowspans)
+ .map(Table::new);
+ }
+
+ private ReadResult calculateRowspans(List<DocumentElement> rows) {
+ Optional<String> error = checkTableRows(rows);
+ if (error.isPresent()) {
+ return ReadResult.withWarning(rows, error.get());
+ }
+
+ Map<Map.Entry<Integer, Integer>, Integer> rowspans = new HashMap<>();
+ Set<Map.Entry<Integer, Integer>> merged = new HashSet<>();
+
+ Map<Integer, Map.Entry<Integer, Integer>> 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<Map.Entry<Integer, Integer>> spanningCell = lookup(lastCellForColumn, columnIndex);
+ Map.Entry<Integer, Integer> 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;
+ }
+ }
+
+ return success(Lists.eagerMapWithIndex(rows, (rowIndex, rowElement) -> {
+ TableRow row = (TableRow) rowElement;
+
+ List<DocumentElement> mergedCells = new ArrayList<>();
+ for (int cellIndex = 0; cellIndex < row.getChildren().size(); cellIndex += 1) {
+ UnmergedTableCell cell = (UnmergedTableCell) row.getChildren().get(cellIndex);
+ Map.Entry<Integer, Integer> position = entry(rowIndex, cellIndex);
+ if (!merged.contains(position)) {
+ mergedCells.add(new TableCell(
+ lookup(rowspans, position).get(),
+ cell.colspan,
+ cell.children
+ ));
+ }
+ }
+
+ return new TableRow(mergedCells);
+ }));
+ }
+
+ private Optional<String> checkTableRows(List<DocumentElement> rows) {
+ for (DocumentElement rowElement : rows) {
+ Optional<TableRow> row = Casts.tryCast(TableRow.class, rowElement);
+ if (!row.isPresent()) {
+ return Optional.of("unexpected non-row element in table, cell merging may be incorrect");
+ } else {
+ for (DocumentElement cell : row.get().getChildren()) {
+ if (!(cell instanceof UnmergedTableCell)) {
+ return Optional.of("unexpected non-cell element in table row, cell merging may be incorrect");
+ }
+ }
+ }
+ }
+ return Optional.empty();
+ }
+
+ private ReadResult readTableCell(XmlElement element) {
+ XmlElementLike properties = element.findChildOrEmpty("w:tcPr");
+ Optional<String> gridSpan = properties
+ .findChildOrEmpty("w:gridSpan")
+ .getAttributeOrNone("w:val");
+ int colspan = gridSpan.map(Integer::parseInt).orElse(1);
+ 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<DocumentElement> children;
+
+ private UnmergedTableCell(boolean vmerge, int colspan, List<DocumentElement> children) {
+ this.vmerge = vmerge;
+ this.colspan = colspan;
+ this.children = children;
+ }
+
+ @Override
+ public <T> T accept(DocumentElementVisitor<T> visitor) {
+ return visitor.visit(new TableCell(1, colspan, children));
+ }
+ }
+
+ private ReadResult readHyperlink(XmlElement element) {
+ Optional<String> relationshipId = element.getAttributeOrNone("r:id");
+ Optional<String> anchor = element.getAttributeOrNone("w:anchor");
+ ReadResult childrenResult = readElements(element.getChildren());
+ if (relationshipId.isPresent()) {
+ return childrenResult.map(children -> Hyperlink.href(
+ relationships.findRelationshipById(relationshipId.get()).getTarget(), children));
+ } else if (anchor.isPresent()) {
+ return childrenResult.map(children -> Hyperlink.anchor(
+ anchor.get(), children));
+ } else {
+ return childrenResult;
+ }
+ }
+
+ private ReadResult readBookmark(XmlElement element) {
+ String name = element.getAttribute("w:name");
+ if (name.equals("_GoBack")) {
+ return ReadResult.EMPTY_SUCCESS;
+ } else {
+ return success(new Bookmark(name));
+ }
+ }
+
+ private ReadResult readNoteReference(NoteType noteType, XmlElement element) {
+ String noteId = element.getAttribute("w:id");
+ return success(new NoteReference(noteType, noteId));
+ }
+
+ private ReadResult readCommentReference(XmlElement element) {
+ String commentId = element.getAttribute("w:id");
+ return success(new CommentReference(commentId));
+ }
+
+ private ReadResult readPict(XmlElement element) {
+ return readElements(element.getChildren()).toExtra();
+ }
+
+ private ReadResult readImagedata(XmlElement element) {
+ return element.getAttributeOrNone("r:id")
+ .map(relationshipId -> {
+ Optional<String> title = element.getAttributeOrNone("o:title");
+ String imagePath = relationshipIdToDocxPath(relationshipId);
+ return readImage(imagePath, title, () -> Archives.getInputStream(file, imagePath));
+ })
+ .orElse(ReadResult.emptyWithWarning("A v:imagedata element without a relationship ID was ignored"));
+ }
+
+ private ReadResult readInline(XmlElement element) {
+ XmlElementLike properties = element.findChildOrEmpty("wp:docPr");
+ Optional<String> altText = Optionals.first(
+ properties.getAttributeOrNone("descr").filter(description -> !description.trim().isEmpty()),
+ properties.getAttributeOrNone("title")
+ );
+ XmlElementList blips = element.findChildren("a:graphic")
+ .findChildren("a:graphicData")
+ .findChildren("pic:pic")
+ .findChildren("pic:blipFill")
+ .findChildren("a:blip");
+ return readBlips(blips, altText);
+ }
+
+ private ReadResult readBlips(XmlElementList blips, Optional<String> altText) {
+ return ReadResult.flatMap(blips, blip -> readBlip(blip, altText));
+ }
+
+ private ReadResult readBlip(XmlElement blip, Optional<String> altText) {
+ Optional<String> embedRelationshipId = blip.getAttributeOrNone("r:embed");
+ Optional<String> linkRelationshipId = blip.getAttributeOrNone("r:link");
+ if (embedRelationshipId.isPresent()) {
+ String imagePath = relationshipIdToDocxPath(embedRelationshipId.get());
+ return readImage(imagePath, altText, () -> Archives.getInputStream(file, imagePath));
+ } else if (linkRelationshipId.isPresent()) {
+ String imagePath = relationships.findRelationshipById(linkRelationshipId.get()).getTarget();
+ return readImage(imagePath, altText, () -> fileReader.getInputStream(imagePath));
+ } else {
+ // TODO: emit warning
+ return ReadResult.EMPTY_SUCCESS;
+ }
+ }
+
+ private ReadResult readImage(String imagePath, Optional<String> altText, InputStreamSupplier open) {
+ Optional<String> contentType = contentTypes.findContentType(imagePath);
+ Image image = new Image(altText, contentType, open);
+
+ String contentTypeString = contentType.orElse("(unknown)");
+ if (IMAGE_TYPES_SUPPORTED_BY_BROWSERS.contains(contentTypeString)) {
+ return success(image);
+ } else {
+ return ReadResult.withWarning(image, "Image of type " + contentTypeString + " is unlikely to display in web browsers");
+ }
+ }
+
+ private ReadResult readSdt(XmlElement element) {
+ return readElements(element.findChildOrEmpty("w:sdtContent").getChildren());
+ }
+
+ private String relationshipIdToDocxPath(String relationshipId) {
+ Relationship relationship = relationships.findRelationshipById(relationshipId);
+ return uriToZipEntryName("word", relationship.getTarget());
+ }
+
+ private Optional<String> readVal(XmlElementLike element, String name) {
+ return element.findChildOrEmpty(name).getAttributeOrNone("w:val");
+ }
+}
diff --git a/src/main/java/org/zwobble/mammoth/internal/util/Iterables.java b/src/main/java/org/zwobble/mammoth/internal/util/Iterables.java
index 88e906e..85fdaee 100644
--- a/src/main/java/org/zwobble/mammoth/internal/util/Iterables.java
+++ b/src/main/java/org/zwobble/mammoth/internal/util/Iterables.java
@@ -54,6 +54,20 @@ public class Iterables {
}
}
+ public static <T> Optional<T> tryGetLast(Iterable<? extends T> iterable) {
+ Iterator<? extends T> iterator = iterable.iterator();
+ if (!iterator.hasNext()) {
+ return Optional.empty();
+ }
+
+ while (true) {
+ T last = iterator.next();
+ if (!iterator.hasNext()) {
+ return Optional.of(last);
+ }
+ }
+ }
+
public static <T, R> Iterable<R> lazyFlatMap(Iterable<T> iterable, Function<T, Iterable<R>> function) {
return lazyFlatten(lazyMap(iterable, function));
}