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 complexFieldStack; 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.complexFieldStack = 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.complexFieldStack, 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")) { complexFieldStack.remove(); } else if (type.equals("separate")) { String instrText = currentInstrText.toString(); ComplexField complexField = parseHyperlinkFieldCode(instrText) .map(href -> ComplexField.hyperlink(href)) .orElse(ComplexField.UNKNOWN); complexFieldStack.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