From b31c5a4b4ccb835a583efa33cee178e122d564de Mon Sep 17 00:00:00 2001 From: Michael Williamson Date: Sun, 19 Feb 2017 18:16:52 +0000 Subject: Handle hyperlinks created with complex fields --- .../internal/docx/StatefulBodyXmlReader.java | 539 +++++++++++++++++++++ 1 file changed, 539 insertions(+) create mode 100644 src/main/java/org/zwobble/mammoth/internal/docx/StatefulBodyXmlReader.java (limited to 'src/main/java/org/zwobble/mammoth/internal/docx/StatefulBodyXmlReader.java') 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 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 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 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 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> readRunStyle(XmlElementLike properties) { + return readStyle(properties, "w:rStyle", "Run", styles::findCharacterStyleById); + } + + ReadResult readElements(Iterable nodes) { + return ReadResult.flatMap(lazyFilter(nodes, XmlElement.class), this::readElement); + } + + private ReadResult readParagraph(XmlElement element) { + XmlElementLike properties = element.findChildOrEmpty("w:pPr"); + Optional 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 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> readParagraphStyle(XmlElementLike properties) { + return readStyle(properties, "w:pStyle", "Paragraph", styles::findParagraphStyleById); + } + + private InternalResult> readStyle( + XmlElementLike properties, + String styleTagName, + String styleType, + Function> findStyleById) + { + return readVal(properties, styleTagName) + .map(styleId -> findStyleById(styleType, styleId, findStyleById)) + .orElse(InternalResult.empty()); + } + + private InternalResult> findStyleById( + String styleType, + String styleId, + Function> findStyleById) + { + Optional