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.*; import org.zwobble.mammoth.internal.xml.*; 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.*; 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.eagerConcat; 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.codepointToString; 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 List deletedParagraphContents; private interface ComplexField { ComplexField UNKNOWN = new ComplexField() {}; static ComplexField begin(XmlElement fldChar) { return new BeginComplexField(fldChar); } static ComplexField hyperlink(Function, Hyperlink> childrenToHyperlink) { return new HyperlinkComplexField(childrenToHyperlink); } static ComplexField checkbox(boolean checked) { return new CheckboxComplexField(checked); } } private static class BeginComplexField implements ComplexField { private final XmlElement fldChar; private BeginComplexField(XmlElement fldChar) { this.fldChar = fldChar; } } private static class HyperlinkComplexField implements ComplexField { private final Function, Hyperlink> childrenToHyperlink; private HyperlinkComplexField(Function, Hyperlink> childrenToHyperlink) { this.childrenToHyperlink = childrenToHyperlink; } } private static class CheckboxComplexField implements ComplexField { private final boolean checked; private CheckboxComplexField(boolean checked) { this.checked = checked; } } 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 = Queues.stack(); this.deletedParagraphContents = new ArrayList<>(); } 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:noBreakHyphen": return success(new Text("\u2011")); case "w:softHyphen": return success(new Text("\u00ad")); case "w:sym": return readSymbol(element); case "w:br": return readBreak(element); case "w:tbl": return readTable(element); case "w:tr": return readTableRow(element); 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:object": case "w:smartTag": case "w:drawing": case "v:group": case "v:rect": 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:trPr": 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 hyperlinkComplexField = currentHyperlinkComplexField(); if (hyperlinkComplexField.isPresent()) { children = list(hyperlinkComplexField.get().childrenToHyperlink.apply(children)); } return new Run( readHighlight(properties), isBold(properties), isItalic(properties), isUnderline(properties), isStrikethrough(properties), isAllCaps(properties), isSmallCaps(properties), readVerticalAlignment(properties), style, children ); } ); } private Optional currentHyperlinkComplexField() { return tryGetLast(lazyFilter(this.complexFieldStack, HyperlinkComplexField.class)); } private Optional readHighlight(XmlElementLike properties) { return readVal(properties, "w:highlight") .filter(value -> !value.isEmpty() && !value.equals("none")); } 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 properties.findChild("w:u") .flatMap(child -> child.getAttributeOrNone("w:val")) .map(value -> !value.equals("false") && !value.equals("0") && !value.equals("none")) .orElse(false); } private boolean isStrikethrough(XmlElementLike properties) { return readBooleanElement(properties, "w:strike"); } private boolean isAllCaps(XmlElementLike properties) { return readBooleanElement(properties, "w:caps"); } private boolean isSmallCaps(XmlElementLike properties) { return readBooleanElement(properties, "w:smallCaps"); } private boolean readBooleanElement(XmlElementLike properties, String tagName) { return properties.findChild(tagName) .map(child -> readBooleanAttributeValue(child.getAttributeOrNone("w:val"))) .orElse(false); } private boolean readBooleanAttributeValue(Optional valAttributeValue) { return valAttributeValue .map(value -> !value.equals("false") && !value.equals("0")) .orElse(true); } 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"); boolean isDeleted = properties .findChildOrEmpty("w:rPr") .findChild("w:del") .isPresent(); if (isDeleted) { deletedParagraphContents.addAll(element.getChildren()); return ReadResult.success(list()); } else { ParagraphIndent indent = readParagraphIndent(properties); List childrenXml = element.getChildren(); if (!deletedParagraphContents.isEmpty()) { childrenXml = eagerConcat(deletedParagraphContents, childrenXml); deletedParagraphContents = new ArrayList<>(); } return ReadResult.map( readParagraphStyle(properties), readElements(childrenXml), (style, children) -> new Paragraph(style, readNumbering(style, properties), indent, children)).appendExtra(); } } private ReadResult readFieldChar(XmlElement element) { String type = element.getAttributeOrNone("w:fldCharType").orElse(""); if (type.equals("begin")) { complexFieldStack.add(ComplexField.begin(element)); currentInstrText.setLength(0); } else if (type.equals("end")) { ComplexField complexField = complexFieldStack.remove(); if (complexField instanceof BeginComplexField) { complexField = parseCurrentInstrText(complexField); } if (complexField instanceof CheckboxComplexField) { return success(new Checkbox(((CheckboxComplexField) complexField).checked)); } } else if (type.equals("separate")) { ComplexField complexFieldSeparate = complexFieldStack.remove(); ComplexField complexField = parseCurrentInstrText(complexFieldSeparate); complexFieldStack.add(complexField); } return ReadResult.EMPTY_SUCCESS; } private ComplexField parseCurrentInstrText(ComplexField complexField) { String instrText = currentInstrText.toString(); XmlElementLike fldChar = complexField instanceof BeginComplexField ? ((BeginComplexField) complexField).fldChar : NullXmlElement.INSTANCE; return parseInstrText(instrText, fldChar); } private ComplexField parseInstrText(String instrText, XmlElementLike fldChar) { Pattern externalLinkPattern = Pattern.compile("\\s*HYPERLINK \"(.*)\""); Matcher externalLinkMatcher = externalLinkPattern.matcher(instrText); if (externalLinkMatcher.lookingAt()) { String href = externalLinkMatcher.group(1); return ComplexField.hyperlink(children -> Hyperlink.href(href, Optional.empty(), children)); } Pattern internalLinkPattern = Pattern.compile("\\s*HYPERLINK\\s+\\\\l\\s+\"(.*)\""); Matcher internalLinkMatcher = internalLinkPattern.matcher(instrText); if (internalLinkMatcher.lookingAt()) { String anchor = internalLinkMatcher.group(1); return ComplexField.hyperlink(children -> Hyperlink.anchor(anchor, Optional.empty(), children)); } Pattern checkboxPattern = Pattern.compile("\\s*FORMCHECKBOX\\s*"); Matcher checkboxMatcher = checkboxPattern.matcher(instrText); if (checkboxMatcher.lookingAt()) { XmlElementLike checkboxElement = fldChar .findChildOrEmpty("w:ffData") .findChildOrEmpty("w:checkBox"); boolean checked = checkboxElement.hasChild("w:checked") ? readBooleanElement(checkboxElement, "w:checked") : readBooleanElement(checkboxElement, "w:default"); return ComplexField.checkbox(checked); } return ComplexField.UNKNOWN; } private ReadResult readInstrText(XmlElement element) { currentInstrText.append(element.innerText()); return ReadResult.EMPTY_SUCCESS; } 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