aboutsummaryrefslogtreecommitdiff
path: root/src/main/java/org/zwobble/mammoth/internal
diff options
context:
space:
mode:
authorMichael Williamson <mike@zwobble.org>2016-02-24 08:16:16 +0000
committerMichael Williamson <mike@zwobble.org>2016-02-24 08:16:16 +0000
commit3e2786a004eddf968a5fa7d5a92dff5b1b5f7734 (patch)
tree7fa1bcc1e484cd0ef782f64baf504ee83333d397 /src/main/java/org/zwobble/mammoth/internal
parent0dc3a12affc2e46c1a0df2899cf51a1ae3fdbd61 (diff)
Create internal package
Diffstat (limited to 'src/main/java/org/zwobble/mammoth/internal')
-rw-r--r--src/main/java/org/zwobble/mammoth/internal/DocumentConverter.java258
-rw-r--r--src/main/java/org/zwobble/mammoth/internal/documents/Bookmark.java18
-rw-r--r--src/main/java/org/zwobble/mammoth/internal/documents/Document.java21
-rw-r--r--src/main/java/org/zwobble/mammoth/internal/documents/DocumentElement.java5
-rw-r--r--src/main/java/org/zwobble/mammoth/internal/documents/DocumentElementVisitor.java20
-rw-r--r--src/main/java/org/zwobble/mammoth/internal/documents/Documents.java4
-rw-r--r--src/main/java/org/zwobble/mammoth/internal/documents/HasChildren.java7
-rw-r--r--src/main/java/org/zwobble/mammoth/internal/documents/Hyperlink.java42
-rw-r--r--src/main/java/org/zwobble/mammoth/internal/documents/Image.java37
-rw-r--r--src/main/java/org/zwobble/mammoth/internal/documents/LineBreak.java13
-rw-r--r--src/main/java/org/zwobble/mammoth/internal/documents/Note.java27
-rw-r--r--src/main/java/org/zwobble/mammoth/internal/documents/NoteReference.java32
-rw-r--r--src/main/java/org/zwobble/mammoth/internal/documents/NoteType.java6
-rw-r--r--src/main/java/org/zwobble/mammoth/internal/documents/Notes.java27
-rw-r--r--src/main/java/org/zwobble/mammoth/internal/documents/NumberingLevel.java27
-rw-r--r--src/main/java/org/zwobble/mammoth/internal/documents/Paragraph.java33
-rw-r--r--src/main/java/org/zwobble/mammoth/internal/documents/Run.java65
-rw-r--r--src/main/java/org/zwobble/mammoth/internal/documents/Style.java26
-rw-r--r--src/main/java/org/zwobble/mammoth/internal/documents/Tab.java13
-rw-r--r--src/main/java/org/zwobble/mammoth/internal/documents/Table.java21
-rw-r--r--src/main/java/org/zwobble/mammoth/internal/documents/TableCell.java21
-rw-r--r--src/main/java/org/zwobble/mammoth/internal/documents/TableRow.java21
-rw-r--r--src/main/java/org/zwobble/mammoth/internal/documents/Text.java18
-rw-r--r--src/main/java/org/zwobble/mammoth/internal/documents/VerticalAlignment.java7
-rw-r--r--src/main/java/org/zwobble/mammoth/internal/docx/BodyXmlReader.java329
-rw-r--r--src/main/java/org/zwobble/mammoth/internal/docx/ContentTypes.java45
-rw-r--r--src/main/java/org/zwobble/mammoth/internal/docx/ContentTypesXml.java38
-rw-r--r--src/main/java/org/zwobble/mammoth/internal/docx/DocumentReader.java115
-rw-r--r--src/main/java/org/zwobble/mammoth/internal/docx/DocumentXmlReader.java23
-rw-r--r--src/main/java/org/zwobble/mammoth/internal/docx/DocxFile.java15
-rw-r--r--src/main/java/org/zwobble/mammoth/internal/docx/FileReader.java8
-rw-r--r--src/main/java/org/zwobble/mammoth/internal/docx/InMemoryDocxFile.java46
-rw-r--r--src/main/java/org/zwobble/mammoth/internal/docx/NotesXmlReader.java55
-rw-r--r--src/main/java/org/zwobble/mammoth/internal/docx/Numbering.java23
-rw-r--r--src/main/java/org/zwobble/mammoth/internal/docx/NumberingXml.java56
-rw-r--r--src/main/java/org/zwobble/mammoth/internal/docx/OfficeXml.java58
-rw-r--r--src/main/java/org/zwobble/mammoth/internal/docx/ReadResult.java91
-rw-r--r--src/main/java/org/zwobble/mammoth/internal/docx/Relationship.java13
-rw-r--r--src/main/java/org/zwobble/mammoth/internal/docx/Relationships.java20
-rw-r--r--src/main/java/org/zwobble/mammoth/internal/docx/RelationshipsXml.java22
-rw-r--r--src/main/java/org/zwobble/mammoth/internal/docx/Styles.java28
-rw-r--r--src/main/java/org/zwobble/mammoth/internal/docx/StylesXml.java33
-rw-r--r--src/main/java/org/zwobble/mammoth/internal/docx/ZippedDocxFile.java30
-rw-r--r--src/main/java/org/zwobble/mammoth/internal/html/Html.java179
-rw-r--r--src/main/java/org/zwobble/mammoth/internal/html/HtmlElement.java48
-rw-r--r--src/main/java/org/zwobble/mammoth/internal/html/HtmlForceWrite.java18
-rw-r--r--src/main/java/org/zwobble/mammoth/internal/html/HtmlNode.java21
-rw-r--r--src/main/java/org/zwobble/mammoth/internal/html/HtmlSelfClosingElement.java31
-rw-r--r--src/main/java/org/zwobble/mammoth/internal/html/HtmlTextNode.java23
-rw-r--r--src/main/java/org/zwobble/mammoth/internal/html/HtmlWriter.java62
-rw-r--r--src/main/java/org/zwobble/mammoth/internal/styles/DocumentElementMatcher.java5
-rw-r--r--src/main/java/org/zwobble/mammoth/internal/styles/DocumentElementMatching.java26
-rw-r--r--src/main/java/org/zwobble/mammoth/internal/styles/HtmlPath.java51
-rw-r--r--src/main/java/org/zwobble/mammoth/internal/styles/HtmlPathElement.java38
-rw-r--r--src/main/java/org/zwobble/mammoth/internal/styles/ParagraphMatcher.java50
-rw-r--r--src/main/java/org/zwobble/mammoth/internal/styles/RunMatcher.java30
-rw-r--r--src/main/java/org/zwobble/mammoth/internal/styles/StyleMap.java57
-rw-r--r--src/main/java/org/zwobble/mammoth/internal/styles/StyleMapBuilder.java41
-rw-r--r--src/main/java/org/zwobble/mammoth/internal/styles/StyleMapping.java19
-rw-r--r--src/main/java/org/zwobble/mammoth/internal/styles/parsing/ListVar.java34
-rw-r--r--src/main/java/org/zwobble/mammoth/internal/styles/parsing/Parsing.java21
-rw-r--r--src/main/java/org/zwobble/mammoth/internal/styles/parsing/StyleMapParser.java34
-rw-r--r--src/main/java/org/zwobble/mammoth/internal/styles/parsing/StyleMappingParser.java178
-rw-r--r--src/main/java/org/zwobble/mammoth/internal/util/Casts.java13
-rw-r--r--src/main/java/org/zwobble/mammoth/internal/util/InputStreamSupplier.java9
-rw-r--r--src/main/java/org/zwobble/mammoth/internal/util/MammothIterables.java11
-rw-r--r--src/main/java/org/zwobble/mammoth/internal/util/MammothLists.java50
-rw-r--r--src/main/java/org/zwobble/mammoth/internal/util/MammothMaps.java32
-rw-r--r--src/main/java/org/zwobble/mammoth/internal/util/MammothOptionals.java30
-rw-r--r--src/main/java/org/zwobble/mammoth/internal/util/MammothStrings.java11
-rw-r--r--src/main/java/org/zwobble/mammoth/internal/xml/NullXmlElement.java25
-rw-r--r--src/main/java/org/zwobble/mammoth/internal/xml/XmlElement.java89
-rw-r--r--src/main/java/org/zwobble/mammoth/internal/xml/XmlElementLike.java9
-rw-r--r--src/main/java/org/zwobble/mammoth/internal/xml/XmlElementList.java28
-rw-r--r--src/main/java/org/zwobble/mammoth/internal/xml/XmlNode.java6
-rw-r--r--src/main/java/org/zwobble/mammoth/internal/xml/XmlNodeVisitor.java6
-rw-r--r--src/main/java/org/zwobble/mammoth/internal/xml/XmlNodes.java29
-rw-r--r--src/main/java/org/zwobble/mammoth/internal/xml/XmlTextNode.java28
-rw-r--r--src/main/java/org/zwobble/mammoth/internal/xml/parsing/ElementName.java19
-rw-r--r--src/main/java/org/zwobble/mammoth/internal/xml/parsing/SimpleSax.java64
-rw-r--r--src/main/java/org/zwobble/mammoth/internal/xml/parsing/SimpleSaxHandler.java9
-rw-r--r--src/main/java/org/zwobble/mammoth/internal/xml/parsing/XmlElementBuilder.java28
-rw-r--r--src/main/java/org/zwobble/mammoth/internal/xml/parsing/XmlParser.java81
83 files changed, 3360 insertions, 0 deletions
diff --git a/src/main/java/org/zwobble/mammoth/internal/DocumentConverter.java b/src/main/java/org/zwobble/mammoth/internal/DocumentConverter.java
new file mode 100644
index 0000000..5aa3c08
--- /dev/null
+++ b/src/main/java/org/zwobble/mammoth/internal/DocumentConverter.java
@@ -0,0 +1,258 @@
+package org.zwobble.mammoth.internal;
+
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableMap;
+import com.google.common.collect.Iterables;
+import com.google.common.io.ByteStreams;
+import org.zwobble.mammoth.internal.documents.*;
+import org.zwobble.mammoth.internal.html.Html;
+import org.zwobble.mammoth.internal.html.HtmlNode;
+import org.zwobble.mammoth.results.Result;
+import org.zwobble.mammoth.results.Warning;
+import org.zwobble.mammoth.internal.styles.HtmlPath;
+import org.zwobble.mammoth.internal.styles.StyleMap;
+import org.zwobble.mammoth.internal.util.MammothLists;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Base64;
+import java.util.List;
+import java.util.Map;
+
+import static org.zwobble.mammoth.results.Warning.warning;
+import static org.zwobble.mammoth.internal.util.MammothLists.*;
+import static org.zwobble.mammoth.internal.util.MammothMaps.map;
+
+public class DocumentConverter {
+ public static Result<List<HtmlNode>> convertToHtml(String idPrefix, boolean preserveEmptyParagraphs, StyleMap styleMap, Document document) {
+ DocumentConverter documentConverter = new DocumentConverter(idPrefix, preserveEmptyParagraphs, styleMap);
+ return new Result<>(
+ documentConverter.convertToHtml(document),
+ documentConverter.warnings.build());
+ }
+
+ private static List<Note> findNotes(Document document, Iterable<NoteReference> noteReferences) {
+ return ImmutableList.copyOf(Iterables.transform(
+ noteReferences,
+ // TODO: handle missing notes
+ reference -> document.getNotes().findNote(reference.getNoteType(), reference.getNoteId()).get()));
+ }
+
+ public static Result<List<HtmlNode>> convertToHtml(String idPrefix, boolean preserveEmptyParagraphs, StyleMap styleMap, DocumentElement element) {
+ DocumentConverter documentConverter = new DocumentConverter(idPrefix, preserveEmptyParagraphs, styleMap);
+ return new Result<>(
+ documentConverter.convertToHtml(element),
+ documentConverter.warnings.build());
+ }
+
+ private final String idPrefix;
+ private final boolean preserveEmptyParagraphs;
+ private final StyleMap styleMap;
+ private final List<NoteReference> noteReferences = new ArrayList<>();
+ private final ImmutableList.Builder<Warning> warnings = ImmutableList.builder();
+
+ private DocumentConverter(String idPrefix, boolean preserveEmptyParagraphs, StyleMap styleMap) {
+ this.idPrefix = idPrefix;
+ this.preserveEmptyParagraphs = preserveEmptyParagraphs;
+ this.styleMap = styleMap;
+ }
+
+ private List<HtmlNode> convertToHtml(Document document) {
+ List<HtmlNode> mainBody = convertChildrenToHtml(document);
+ // TODO: can you have note references inside a note?
+ List<Note> notes = findNotes(document, noteReferences);
+ if (notes.isEmpty()) {
+ return mainBody;
+ } else {
+ HtmlNode noteNode = Html.element("ol",
+ ImmutableList.copyOf(Iterables.transform(notes, this::convertToHtml)));
+
+ return ImmutableList.copyOf(Iterables.concat(mainBody, list(noteNode)));
+ }
+ }
+
+ private HtmlNode convertToHtml(Note note) {
+ String id = generateNoteHtmlId(note.getNoteType(), note.getId());
+ String referenceId = generateNoteRefHtmlId(note.getNoteType(), note.getId());
+ ImmutableList.Builder<HtmlNode> children = ImmutableList.builder();
+ children.addAll(convertToHtml(note.getBody()));
+ // TODO: we probably want this to collapse more eagerly than other collapsible elements
+ // -- for instance, any paragraph will probably do, regardless of attributes. (Possible other elements will do too.)
+ children.add(Html.collapsibleElement("p", list(
+ Html.text(" "),
+ Html.element("a", map("href", "#" + referenceId), list(Html.text("↑"))))));
+ return Html.element("li", map("id", id), children.build());
+ }
+
+ private List<HtmlNode> convertToHtml(List<DocumentElement> elements) {
+ return eagerFlatMap(
+ elements,
+ this::convertToHtml);
+ }
+
+ private List<HtmlNode> convertChildrenToHtml(HasChildren element) {
+ return convertToHtml(element.getChildren());
+ }
+
+ private List<HtmlNode> convertToHtml(DocumentElement element) {
+ return element.accept(new DocumentElementVisitor<List<HtmlNode>>() {
+ @Override
+ public List<HtmlNode> visit(Paragraph paragraph) {
+ List<HtmlNode> content = convertChildrenToHtml(paragraph);
+ List<HtmlNode> children = preserveEmptyParagraphs ? cons(Html.FORCE_WRITE, content) : content;
+ HtmlPath mapping = styleMap.getParagraphHtmlPath(paragraph)
+ .orElseGet(() -> {
+ if (paragraph.getStyle().isPresent()) {
+ warnings.add(warning("Unrecognised paragraph style: " + paragraph.getStyle().get().describe()));
+ }
+ return HtmlPath.element("p");
+ });
+ return mapping.wrap(children);
+ }
+
+ @Override
+ public List<HtmlNode> visit(Run run) {
+ List<HtmlNode> nodes = convertChildrenToHtml(run);
+ if (run.isStrikethrough()) {
+ nodes = styleMap.getStrikethrough().wrap(nodes);
+ }
+ if (run.isUnderline()) {
+ nodes = styleMap.getUnderline().wrap(nodes);
+ }
+ if (run.getVerticalAlignment() == VerticalAlignment.SUBSCRIPT) {
+ nodes = list(Html.collapsibleElement("sub", nodes));
+ }
+ if (run.getVerticalAlignment() == VerticalAlignment.SUPERSCRIPT) {
+ nodes = list(Html.collapsibleElement("sup", nodes));
+ }
+ if (run.isItalic()) {
+ nodes = list(Html.collapsibleElement("em", nodes));
+ }
+ if (run.isBold()) {
+ nodes = list(Html.collapsibleElement("strong", nodes));
+ }
+ HtmlPath mapping = styleMap.getRunHtmlPath(run)
+ .orElseGet(() -> {
+ if (run.getStyle().isPresent()) {
+ warnings.add(warning("Unrecognised run style: " + run.getStyle().get().describe()));
+ }
+ return HtmlPath.EMPTY;
+ });
+ return mapping.wrap(nodes);
+ }
+
+ @Override
+ public List<HtmlNode> visit(Text text) {
+ if (text.getValue().isEmpty()) {
+ return list();
+ } else {
+ return list(Html.text(text.getValue()));
+ }
+ }
+
+ @Override
+ public List<HtmlNode> visit(Tab tab) {
+ return list(Html.text("\t"));
+ }
+
+ @Override
+ public List<HtmlNode> visit(LineBreak lineBreak) {
+ return list(Html.selfClosingElement("br"));
+ }
+
+ @Override
+ public List<HtmlNode> visit(Table table) {
+ return list(Html.element("table", convertChildrenToHtml(table)));
+ }
+
+ @Override
+ public List<HtmlNode> visit(TableRow tableRow) {
+ return list(Html.element("tr", convertChildrenToHtml(tableRow)));
+ }
+
+ @Override
+ public List<HtmlNode> visit(TableCell tableCell) {
+ return list(Html.element("td",
+ MammothLists.cons(Html.FORCE_WRITE, convertChildrenToHtml(tableCell))));
+ }
+
+ @Override
+ public List<HtmlNode> visit(Hyperlink hyperlink) {
+ Map<String, String> attributes = map("href", generateHref(hyperlink));
+ return list(Html.collapsibleElement("a", attributes, convertChildrenToHtml(hyperlink)));
+ }
+
+ private String generateHref(Hyperlink hyperlink) {
+ if (hyperlink.getHref().isPresent()) {
+ return hyperlink.getHref().get();
+ } else if (hyperlink.getAnchor().isPresent()) {
+ return "#" + generateId(hyperlink.getAnchor().get());
+ } else {
+ return "";
+ }
+ }
+
+ @Override
+ public List<HtmlNode> visit(Bookmark bookmark) {
+ return list(Html.element("a", map("id", generateId(bookmark.getName())), list(Html.FORCE_WRITE)));
+ }
+
+ @Override
+ public List<HtmlNode> visit(NoteReference noteReference) {
+ noteReferences.add(noteReference);
+ String noteAnchor = generateNoteHtmlId(noteReference.getNoteType(), noteReference.getNoteId());
+ String noteReferenceAnchor = generateNoteRefHtmlId(noteReference.getNoteType(), noteReference.getNoteId());
+ return list(Html.element("sup", list(
+ Html.element("a", map("href", "#" + noteAnchor, "id", noteReferenceAnchor), list(
+ Html.text("[" + noteReferences.size() + "]"))))));
+ }
+
+ @Override
+ public List<HtmlNode> visit(Image image) {
+ // TODO: custom image handlers
+ // TODO: handle empty content type
+ return image.getContentType()
+ .map(contentType -> {
+ try {
+ ImmutableMap.Builder<String, String> attributes = ImmutableMap.builder();
+
+ String base64 = Base64.getEncoder().encodeToString(ByteStreams.toByteArray(image.open()));
+ String src = "data:" + contentType + ";base64," + base64;
+ attributes.put("src", src);
+
+ image.getAltText().ifPresent(altText -> attributes.put("alt", altText));
+
+ return list(Html.selfClosingElement("img", attributes.build()));
+ } catch (IOException exception) {
+ warnings.add(warning(exception.getMessage()));
+ return MammothLists.<HtmlNode>list();
+ }
+ })
+ .orElse(list());
+ }
+ });
+ }
+
+ private String generateNoteHtmlId(NoteType noteType, String noteId) {
+ return generateId(noteTypeToIdFragment(noteType) + "-" + noteId);
+ }
+
+ private String generateNoteRefHtmlId(NoteType noteType, String noteId) {
+ return generateId(noteTypeToIdFragment(noteType) + "-ref-" + noteId);
+ }
+
+ private String noteTypeToIdFragment(NoteType noteType) {
+ switch (noteType) {
+ case FOOTNOTE:
+ return "footnote";
+ case ENDNOTE:
+ return "endnote";
+ default:
+ throw new UnsupportedOperationException();
+ }
+ }
+
+ private String generateId(String bookmarkName) {
+ return idPrefix + bookmarkName;
+ }
+}
diff --git a/src/main/java/org/zwobble/mammoth/internal/documents/Bookmark.java b/src/main/java/org/zwobble/mammoth/internal/documents/Bookmark.java
new file mode 100644
index 0000000..cca6317
--- /dev/null
+++ b/src/main/java/org/zwobble/mammoth/internal/documents/Bookmark.java
@@ -0,0 +1,18 @@
+package org.zwobble.mammoth.internal.documents;
+
+public class Bookmark implements DocumentElement {
+ private final String name;
+
+ public Bookmark(String name) {
+ this.name = name;
+ }
+
+ public String getName() {
+ return name;
+ }
+
+ @Override
+ public <T> T accept(DocumentElementVisitor<T> visitor) {
+ return visitor.visit(this);
+ }
+}
diff --git a/src/main/java/org/zwobble/mammoth/internal/documents/Document.java b/src/main/java/org/zwobble/mammoth/internal/documents/Document.java
new file mode 100644
index 0000000..447d6ed
--- /dev/null
+++ b/src/main/java/org/zwobble/mammoth/internal/documents/Document.java
@@ -0,0 +1,21 @@
+package org.zwobble.mammoth.internal.documents;
+
+import java.util.List;
+
+public class Document implements HasChildren {
+ private final List<DocumentElement> children;
+ private final Notes notes;
+
+ public Document(List<DocumentElement> children, Notes notes) {
+ this.children = children;
+ this.notes = notes;
+ }
+
+ public List<DocumentElement> getChildren() {
+ return children;
+ }
+
+ public Notes getNotes() {
+ return notes;
+ }
+}
diff --git a/src/main/java/org/zwobble/mammoth/internal/documents/DocumentElement.java b/src/main/java/org/zwobble/mammoth/internal/documents/DocumentElement.java
new file mode 100644
index 0000000..b3360ed
--- /dev/null
+++ b/src/main/java/org/zwobble/mammoth/internal/documents/DocumentElement.java
@@ -0,0 +1,5 @@
+package org.zwobble.mammoth.internal.documents;
+
+public interface DocumentElement {
+ <T> T accept(DocumentElementVisitor<T> visitor);
+}
diff --git a/src/main/java/org/zwobble/mammoth/internal/documents/DocumentElementVisitor.java b/src/main/java/org/zwobble/mammoth/internal/documents/DocumentElementVisitor.java
new file mode 100644
index 0000000..569a525
--- /dev/null
+++ b/src/main/java/org/zwobble/mammoth/internal/documents/DocumentElementVisitor.java
@@ -0,0 +1,20 @@
+package org.zwobble.mammoth.internal.documents;
+
+public interface DocumentElementVisitor<T> {
+ T visit(Paragraph paragraph);
+ T visit(Run run);
+ T visit(Text text);
+
+ T visit(Tab tab);
+ T visit(LineBreak lineBreak);
+
+ T visit(Table table);
+ T visit(TableRow tableRow);
+ T visit(TableCell tableCell);
+
+ T visit(Hyperlink hyperlink);
+ T visit(Bookmark bookmark);
+ T visit(NoteReference noteReference);
+
+ T visit(Image image);
+}
diff --git a/src/main/java/org/zwobble/mammoth/internal/documents/Documents.java b/src/main/java/org/zwobble/mammoth/internal/documents/Documents.java
new file mode 100644
index 0000000..64a5d1c
--- /dev/null
+++ b/src/main/java/org/zwobble/mammoth/internal/documents/Documents.java
@@ -0,0 +1,4 @@
+package org.zwobble.mammoth.internal.documents;
+
+public class Documents {
+}
diff --git a/src/main/java/org/zwobble/mammoth/internal/documents/HasChildren.java b/src/main/java/org/zwobble/mammoth/internal/documents/HasChildren.java
new file mode 100644
index 0000000..2a5280e
--- /dev/null
+++ b/src/main/java/org/zwobble/mammoth/internal/documents/HasChildren.java
@@ -0,0 +1,7 @@
+package org.zwobble.mammoth.internal.documents;
+
+import java.util.List;
+
+public interface HasChildren {
+ List<DocumentElement> getChildren();
+}
diff --git a/src/main/java/org/zwobble/mammoth/internal/documents/Hyperlink.java b/src/main/java/org/zwobble/mammoth/internal/documents/Hyperlink.java
new file mode 100644
index 0000000..ceb558f
--- /dev/null
+++ b/src/main/java/org/zwobble/mammoth/internal/documents/Hyperlink.java
@@ -0,0 +1,42 @@
+package org.zwobble.mammoth.internal.documents;
+
+import java.util.List;
+import java.util.Optional;
+
+public class Hyperlink implements DocumentElement, HasChildren {
+ public static Hyperlink href(String href, List<DocumentElement> children) {
+ return new Hyperlink(Optional.of(href), Optional.empty(), children);
+ }
+
+ public static Hyperlink anchor(String anchor, List<DocumentElement> children) {
+ return new Hyperlink(Optional.empty(), Optional.of(anchor), children);
+ }
+
+ private final Optional<String> href;
+ private final Optional<String> anchor;
+ private final List<DocumentElement> children;
+
+ private Hyperlink(Optional<String> href, Optional<String> anchor, List<DocumentElement> children) {
+ this.href = href;
+ this.anchor = anchor;
+ this.children = children;
+ }
+
+ public Optional<String> getHref() {
+ return href;
+ }
+
+ public Optional<String> getAnchor() {
+ return anchor;
+ }
+
+ @Override
+ public List<DocumentElement> getChildren() {
+ return children;
+ }
+
+ @Override
+ public <T> T accept(DocumentElementVisitor<T> visitor) {
+ return visitor.visit(this);
+ }
+}
diff --git a/src/main/java/org/zwobble/mammoth/internal/documents/Image.java b/src/main/java/org/zwobble/mammoth/internal/documents/Image.java
new file mode 100644
index 0000000..8ceee7c
--- /dev/null
+++ b/src/main/java/org/zwobble/mammoth/internal/documents/Image.java
@@ -0,0 +1,37 @@
+package org.zwobble.mammoth.internal.documents;
+
+import org.zwobble.mammoth.internal.util.InputStreamSupplier;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.util.Optional;
+
+public class Image implements DocumentElement {
+
+ private final Optional<String> altText;
+ private final Optional<String> contentType;
+ private final InputStreamSupplier open;
+
+ public Image(Optional<String> altText, Optional<String> contentType, InputStreamSupplier open) {
+ this.altText = altText;
+ this.contentType = contentType;
+ this.open = open;
+ }
+
+ public Optional<String> getAltText() {
+ return altText;
+ }
+
+ public Optional<String> getContentType() {
+ return contentType;
+ }
+
+ public InputStream open() throws IOException {
+ return open.open();
+ }
+
+ @Override
+ public <T> T accept(DocumentElementVisitor<T> visitor) {
+ return visitor.visit(this);
+ }
+}
diff --git a/src/main/java/org/zwobble/mammoth/internal/documents/LineBreak.java b/src/main/java/org/zwobble/mammoth/internal/documents/LineBreak.java
new file mode 100644
index 0000000..00b2a06
--- /dev/null
+++ b/src/main/java/org/zwobble/mammoth/internal/documents/LineBreak.java
@@ -0,0 +1,13 @@
+package org.zwobble.mammoth.internal.documents;
+
+public class LineBreak implements DocumentElement {
+ public static final LineBreak LINE_BREAK = new LineBreak();
+
+ private LineBreak() {
+ }
+
+ @Override
+ public <T> T accept(DocumentElementVisitor<T> visitor) {
+ return visitor.visit(this);
+ }
+}
diff --git a/src/main/java/org/zwobble/mammoth/internal/documents/Note.java b/src/main/java/org/zwobble/mammoth/internal/documents/Note.java
new file mode 100644
index 0000000..d7d741b
--- /dev/null
+++ b/src/main/java/org/zwobble/mammoth/internal/documents/Note.java
@@ -0,0 +1,27 @@
+package org.zwobble.mammoth.internal.documents;
+
+import java.util.List;
+
+public class Note {
+ private final NoteType noteType;
+ private final String id;
+ private final List<DocumentElement> body;
+
+ public Note(NoteType noteType, String id, List<DocumentElement> body) {
+ this.noteType = noteType;
+ this.id = id;
+ this.body = body;
+ }
+
+ public NoteType getNoteType() {
+ return noteType;
+ }
+
+ public String getId() {
+ return id;
+ }
+
+ public List<DocumentElement> getBody() {
+ return body;
+ }
+}
diff --git a/src/main/java/org/zwobble/mammoth/internal/documents/NoteReference.java b/src/main/java/org/zwobble/mammoth/internal/documents/NoteReference.java
new file mode 100644
index 0000000..7d0beb2
--- /dev/null
+++ b/src/main/java/org/zwobble/mammoth/internal/documents/NoteReference.java
@@ -0,0 +1,32 @@
+package org.zwobble.mammoth.internal.documents;
+
+public class NoteReference implements DocumentElement {
+ public static NoteReference footnoteReference(String noteId) {
+ return new NoteReference(NoteType.FOOTNOTE, noteId);
+ }
+
+ public static NoteReference endnoteReference(String noteId) {
+ return new NoteReference(NoteType.ENDNOTE, noteId);
+ }
+
+ private final NoteType noteType;
+ private final String noteId;
+
+ public NoteReference(NoteType noteType, String noteId) {
+ this.noteType = noteType;
+ this.noteId = noteId;
+ }
+
+ public NoteType getNoteType() {
+ return noteType;
+ }
+
+ public String getNoteId() {
+ return noteId;
+ }
+
+ @Override
+ public <T> T accept(DocumentElementVisitor<T> visitor) {
+ return visitor.visit(this);
+ }
+}
diff --git a/src/main/java/org/zwobble/mammoth/internal/documents/NoteType.java b/src/main/java/org/zwobble/mammoth/internal/documents/NoteType.java
new file mode 100644
index 0000000..47defc1
--- /dev/null
+++ b/src/main/java/org/zwobble/mammoth/internal/documents/NoteType.java
@@ -0,0 +1,6 @@
+package org.zwobble.mammoth.internal.documents;
+
+public enum NoteType {
+ FOOTNOTE,
+ ENDNOTE
+}
diff --git a/src/main/java/org/zwobble/mammoth/internal/documents/Notes.java b/src/main/java/org/zwobble/mammoth/internal/documents/Notes.java
new file mode 100644
index 0000000..d8ca28d
--- /dev/null
+++ b/src/main/java/org/zwobble/mammoth/internal/documents/Notes.java
@@ -0,0 +1,27 @@
+package org.zwobble.mammoth.internal.documents;
+
+import com.google.common.collect.Maps;
+import com.google.common.collect.Multimaps;
+
+import java.util.List;
+import java.util.Map;
+import java.util.Optional;
+
+import static org.zwobble.mammoth.internal.util.MammothLists.list;
+
+public class Notes {
+ public final static Notes EMPTY = new Notes(list());
+
+ private final Map<NoteType, Map<String, Note>> notes;
+
+ public Notes(List<Note> notes) {
+ this.notes = Maps.transformValues(
+ Multimaps.index(notes, Note::getNoteType).asMap(),
+ notesOfType -> Maps.uniqueIndex(notesOfType, Note::getId));
+ }
+
+ public Optional<Note> findNote(NoteType noteType, String noteId) {
+ return Optional.ofNullable(notes.get(noteType))
+ .flatMap(notesOfType -> Optional.ofNullable(notesOfType.get(noteId)));
+ }
+}
diff --git a/src/main/java/org/zwobble/mammoth/internal/documents/NumberingLevel.java b/src/main/java/org/zwobble/mammoth/internal/documents/NumberingLevel.java
new file mode 100644
index 0000000..592d96f
--- /dev/null
+++ b/src/main/java/org/zwobble/mammoth/internal/documents/NumberingLevel.java
@@ -0,0 +1,27 @@
+package org.zwobble.mammoth.internal.documents;
+
+public class NumberingLevel {
+ public static NumberingLevel ordered(String levelIndex) {
+ return new NumberingLevel(levelIndex, true);
+ }
+
+ public static NumberingLevel unordered(String levelIndex) {
+ return new NumberingLevel(levelIndex, false);
+ }
+
+ private final String levelIndex;
+ private final boolean isOrdered;
+
+ public NumberingLevel(String levelIndex, boolean isOrdered) {
+ this.levelIndex = levelIndex;
+ this.isOrdered = isOrdered;
+ }
+
+ public String getLevelIndex() {
+ return levelIndex;
+ }
+
+ public boolean isOrdered() {
+ return isOrdered;
+ }
+}
diff --git a/src/main/java/org/zwobble/mammoth/internal/documents/Paragraph.java b/src/main/java/org/zwobble/mammoth/internal/documents/Paragraph.java
new file mode 100644
index 0000000..66ea009
--- /dev/null
+++ b/src/main/java/org/zwobble/mammoth/internal/documents/Paragraph.java
@@ -0,0 +1,33 @@
+package org.zwobble.mammoth.internal.documents;
+
+import java.util.List;
+import java.util.Optional;
+
+public class Paragraph implements DocumentElement, HasChildren {
+ private final Optional<Style> style;
+ private final Optional<NumberingLevel> numbering;
+ private final List<DocumentElement> children;
+
+ public Paragraph(Optional<Style> style, Optional<NumberingLevel> numbering, List<DocumentElement> children) {
+ this.style = style;
+ this.numbering = numbering;
+ this.children = children;
+ }
+
+ public Optional<Style> getStyle() {
+ return style;
+ }
+
+ public Optional<NumberingLevel> getNumbering() {
+ return numbering;
+ }
+
+ public List<DocumentElement> getChildren() {
+ return children;
+ }
+
+ @Override
+ public <T> T accept(DocumentElementVisitor<T> visitor) {
+ return visitor.visit(this);
+ }
+}
diff --git a/src/main/java/org/zwobble/mammoth/internal/documents/Run.java b/src/main/java/org/zwobble/mammoth/internal/documents/Run.java
new file mode 100644
index 0000000..61f865f
--- /dev/null
+++ b/src/main/java/org/zwobble/mammoth/internal/documents/Run.java
@@ -0,0 +1,65 @@
+package org.zwobble.mammoth.internal.documents;
+
+import java.util.List;
+import java.util.Optional;
+
+public class Run implements DocumentElement, HasChildren {
+ private final boolean isBold;
+ private final boolean isItalic;
+ private final boolean isUnderline;
+ private final boolean isStrikethrough;
+ private final VerticalAlignment verticalAlignment;
+ private final Optional<Style> style;
+ private final List<DocumentElement> children;
+
+ public Run(
+ boolean isBold,
+ boolean isItalic,
+ boolean isUnderline,
+ boolean isStrikethrough,
+ VerticalAlignment verticalAlignment,
+ Optional<Style> style,
+ List<DocumentElement> children)
+ {
+ this.isBold = isBold;
+ this.isItalic = isItalic;
+ this.isUnderline = isUnderline;
+ this.isStrikethrough = isStrikethrough;
+ this.verticalAlignment = verticalAlignment;
+ this.style = style;
+ this.children = children;
+ }
+
+ public boolean isBold() {
+ return isBold;
+ }
+
+ public boolean isItalic() {
+ return isItalic;
+ }
+
+ public boolean isUnderline() {
+ return isUnderline;
+ }
+
+ public boolean isStrikethrough() {
+ return isStrikethrough;
+ }
+
+ public VerticalAlignment getVerticalAlignment() {
+ return verticalAlignment;
+ }
+
+ public Optional<Style> getStyle() {
+ return style;
+ }
+
+ public List<DocumentElement> getChildren() {
+ return children;
+ }
+
+ @Override
+ public <T> T accept(DocumentElementVisitor<T> visitor) {
+ return visitor.visit(this);
+ }
+}
diff --git a/src/main/java/org/zwobble/mammoth/internal/documents/Style.java b/src/main/java/org/zwobble/mammoth/internal/documents/Style.java
new file mode 100644
index 0000000..2d7805c
--- /dev/null
+++ b/src/main/java/org/zwobble/mammoth/internal/documents/Style.java
@@ -0,0 +1,26 @@
+package org.zwobble.mammoth.internal.documents;
+
+import java.util.Optional;
+
+public class Style {
+ private final String styleId;
+ private final Optional<String> name;
+
+ public Style(String styleId, Optional<String> name) {
+ this.styleId = styleId;
+ this.name = name;
+ }
+
+ public String getStyleId() {
+ return styleId;
+ }
+
+ public Optional<String> getName() {
+ return name;
+ }
+
+ public String describe() {
+ String styleIdDescription = "Style ID: " + styleId;
+ return this.name.map(name -> name + " (" + styleIdDescription + ")").orElse(styleIdDescription);
+ }
+}
diff --git a/src/main/java/org/zwobble/mammoth/internal/documents/Tab.java b/src/main/java/org/zwobble/mammoth/internal/documents/Tab.java
new file mode 100644
index 0000000..b977d12
--- /dev/null
+++ b/src/main/java/org/zwobble/mammoth/internal/documents/Tab.java
@@ -0,0 +1,13 @@
+package org.zwobble.mammoth.internal.documents;
+
+public class Tab implements DocumentElement {
+ public static final Tab TAB = new Tab();
+
+ private Tab() {
+ }
+
+ @Override
+ public <T> T accept(DocumentElementVisitor<T> visitor) {
+ return visitor.visit(this);
+ }
+}
diff --git a/src/main/java/org/zwobble/mammoth/internal/documents/Table.java b/src/main/java/org/zwobble/mammoth/internal/documents/Table.java
new file mode 100644
index 0000000..7381c7d
--- /dev/null
+++ b/src/main/java/org/zwobble/mammoth/internal/documents/Table.java
@@ -0,0 +1,21 @@
+package org.zwobble.mammoth.internal.documents;
+
+import java.util.List;
+
+public class Table implements DocumentElement, HasChildren {
+ private final List<DocumentElement> children;
+
+ public Table(List<DocumentElement> children) {
+ this.children = children;
+ }
+
+ @Override
+ public List<DocumentElement> getChildren() {
+ return children;
+ }
+
+ @Override
+ public <T> T accept(DocumentElementVisitor<T> visitor) {
+ return visitor.visit(this);
+ }
+}
diff --git a/src/main/java/org/zwobble/mammoth/internal/documents/TableCell.java b/src/main/java/org/zwobble/mammoth/internal/documents/TableCell.java
new file mode 100644
index 0000000..f0ce7be
--- /dev/null
+++ b/src/main/java/org/zwobble/mammoth/internal/documents/TableCell.java
@@ -0,0 +1,21 @@
+package org.zwobble.mammoth.internal.documents;
+
+import java.util.List;
+
+public class TableCell implements DocumentElement, HasChildren {
+ private final List<DocumentElement> children;
+
+ public TableCell(List<DocumentElement> children) {
+ this.children = children;
+ }
+
+ @Override
+ public List<DocumentElement> getChildren() {
+ return children;
+ }
+
+ @Override
+ public <T> T accept(DocumentElementVisitor<T> visitor) {
+ return visitor.visit(this);
+ }
+}
diff --git a/src/main/java/org/zwobble/mammoth/internal/documents/TableRow.java b/src/main/java/org/zwobble/mammoth/internal/documents/TableRow.java
new file mode 100644
index 0000000..9450c88
--- /dev/null
+++ b/src/main/java/org/zwobble/mammoth/internal/documents/TableRow.java
@@ -0,0 +1,21 @@
+package org.zwobble.mammoth.internal.documents;
+
+import java.util.List;
+
+public class TableRow implements DocumentElement, HasChildren {
+ private final List<DocumentElement> children;
+
+ public TableRow(List<DocumentElement> children) {
+ this.children = children;
+ }
+
+ @Override
+ public List<DocumentElement> getChildren() {
+ return children;
+ }
+
+ @Override
+ public <T> T accept(DocumentElementVisitor<T> visitor) {
+ return visitor.visit(this);
+ }
+}
diff --git a/src/main/java/org/zwobble/mammoth/internal/documents/Text.java b/src/main/java/org/zwobble/mammoth/internal/documents/Text.java
new file mode 100644
index 0000000..64123e9
--- /dev/null
+++ b/src/main/java/org/zwobble/mammoth/internal/documents/Text.java
@@ -0,0 +1,18 @@
+package org.zwobble.mammoth.internal.documents;
+
+public class Text implements DocumentElement {
+ private final String value;
+
+ public Text(String value) {
+ this.value = value;
+ }
+
+ public String getValue() {
+ return value;
+ }
+
+ @Override
+ public <T> T accept(DocumentElementVisitor<T> visitor) {
+ return visitor.visit(this);
+ }
+}
diff --git a/src/main/java/org/zwobble/mammoth/internal/documents/VerticalAlignment.java b/src/main/java/org/zwobble/mammoth/internal/documents/VerticalAlignment.java
new file mode 100644
index 0000000..5957e8f
--- /dev/null
+++ b/src/main/java/org/zwobble/mammoth/internal/documents/VerticalAlignment.java
@@ -0,0 +1,7 @@
+package org.zwobble.mammoth.internal.documents;
+
+public enum VerticalAlignment {
+ SUPERSCRIPT,
+ SUBSCRIPT,
+ BASELINE
+}
diff --git a/src/main/java/org/zwobble/mammoth/internal/docx/BodyXmlReader.java b/src/main/java/org/zwobble/mammoth/internal/docx/BodyXmlReader.java
new file mode 100644
index 0000000..43a275a
--- /dev/null
+++ b/src/main/java/org/zwobble/mammoth/internal/docx/BodyXmlReader.java
@@ -0,0 +1,329 @@
+package org.zwobble.mammoth.internal.docx;
+
+import com.google.common.collect.ImmutableSet;
+import org.zwobble.mammoth.internal.documents.*;
+import org.zwobble.mammoth.results.Result;
+import org.zwobble.mammoth.results.Warning;
+import org.zwobble.mammoth.internal.util.InputStreamSupplier;
+import org.zwobble.mammoth.internal.util.MammothOptionals;
+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.Optional;
+import java.util.Set;
+import java.util.function.Function;
+
+import static com.google.common.collect.Iterables.filter;
+import static com.google.common.collect.Iterables.transform;
+import static org.zwobble.mammoth.internal.docx.ReadResult.EMPTY_SUCCESS;
+import static org.zwobble.mammoth.internal.docx.ReadResult.success;
+import static org.zwobble.mammoth.results.Warning.warning;
+import static org.zwobble.mammoth.internal.util.MammothLists.list;
+import static org.zwobble.mammoth.internal.util.MammothStrings.trimLeft;
+
+public class BodyXmlReader {
+ private static final Set<String> IMAGE_TYPES_SUPPORTED_BY_BROWSERS = ImmutableSet.of(
+ "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 DocxFile file;
+ private final FileReader fileReader;
+
+ public BodyXmlReader(
+ Styles styles,
+ Numbering numbering,
+ Relationships relationships,
+ ContentTypes contentTypes,
+ DocxFile file,
+ FileReader fileReader)
+ {
+ this.styles = styles;
+ this.numbering = numbering;
+ this.relationships = relationships;
+ this.contentTypes = contentTypes;
+ this.file = file;
+ 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 readElements(element.children()).map(Table::new);
+ case "w:tr":
+ return readElements(element.children()).map(TableRow::new);
+ case "w:tc":
+ return readElements(element.children()).map(TableCell::new);
+
+ 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:pict":
+ return readPict(element);
+
+ case "v:imagedata":
+ return readImagedata(element);
+
+ case "wp:inline":
+ case "wp:anchor":
+ return readInline(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.children());
+
+ 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:commentReference":
+ case "w:del":
+ case "w:footnoteRef":
+ case "w:endnoteRef":
+ case "w:pPr":
+ case "w:rPr":
+ case "w:tblPr":
+ case "w:tblGrid":
+ case "w:tcPr":
+ return EMPTY_SUCCESS;
+
+ default:
+ Warning warning = 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.children()),
+ (style, children) -> new Run(
+ isBold(properties),
+ isItalic(properties),
+ isUnderline(properties),
+ isStrikethrough(properties),
+ readVerticalAlignment(properties),
+ style,
+ children));
+ }
+
+ private boolean isBold(XmlElementLike properties) {
+ return properties.hasChild("w:b");
+ }
+
+ private boolean isItalic(XmlElementLike properties) {
+ return properties.hasChild("w:i");
+ }
+
+ private boolean isUnderline(XmlElementLike properties) {
+ return properties.hasChild("w:u");
+ }
+
+ private boolean isStrikethrough(XmlElementLike properties) {
+ return properties.hasChild("w:strike");
+ }
+
+ 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 Result<Optional<Style>> readRunStyle(XmlElementLike properties) {
+ return readStyle(properties, "w:rStyle", "Run", styles::findCharacterStyleById);
+ }
+
+ public ReadResult readElements(Iterable<XmlNode> nodes) {
+ return ReadResult.concat(
+ transform(
+ filter(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.children()),
+ (style, children) -> new Paragraph(style, numbering, children)).appendExtra();
+ }
+
+ private Result<Optional<Style>> readParagraphStyle(XmlElementLike properties) {
+ return readStyle(properties, "w:pStyle", "Paragraph", styles::findParagraphStyleById);
+ }
+
+ private Result<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(Result.empty());
+ }
+
+ private Result<Optional<Style>> findStyleById(
+ String styleType,
+ String styleId,
+ Function<String, Optional<Style>> findStyleById)
+ {
+ Optional<Style> style = findStyleById.apply(styleId);
+ if (style.isPresent()) {
+ return Result.success(style);
+ } else {
+ return new Result<>(
+ Optional.of(new Style(styleId, Optional.empty())),
+ list(warning(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 MammothOptionals.flatMap(
+ readVal(numberingProperties, "w:numId"),
+ readVal(numberingProperties, "w:ilvl"),
+ numbering::findLevel);
+ }
+
+ private ReadResult readBreak(XmlElement element) {
+ String breakType = element.getAttributeOrNone("w:type").orElse("");
+ if (breakType.equals("")) {
+ return success(LineBreak.LINE_BREAK);
+ } else {
+ return ReadResult.emptyWithWarning(warning("Unsupported break type: " + breakType));
+ }
+ }
+
+ private ReadResult readHyperlink(XmlElement element) {
+ Optional<String> relationshipId = element.getAttributeOrNone("r:id");
+ Optional<String> anchor = element.getAttributeOrNone("w:anchor");
+ ReadResult childrenResult = readElements(element.children());
+ 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 readPict(XmlElement element) {
+ return readElements(element.children()).toExtra();
+ }
+
+ private ReadResult readImagedata(XmlElement element) {
+ Optional<String> title = element.getAttributeOrNone("o:title");
+ String relationshipId = element.getAttribute("r:id");
+ String imagePath = relationshipIdToDocxPath(relationshipId);
+ return readImage(imagePath, title, () -> DocxFile.getInputStream(file, imagePath));
+ }
+
+ private ReadResult readInline(XmlElement element) {
+ Optional<String> altText = element.findChildOrEmpty("wp:docPr").getAttributeOrNone("descr");
+ 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.concat(transform(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, () -> DocxFile.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, warning("Image of type " + contentTypeString + " is unlikely to display in web browsers"));
+ }
+ }
+
+ private String relationshipIdToDocxPath(String relationshipId) {
+ Relationship relationship = relationships.findRelationshipById(relationshipId);
+ return "word/" + trimLeft(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/docx/ContentTypes.java b/src/main/java/org/zwobble/mammoth/internal/docx/ContentTypes.java
new file mode 100644
index 0000000..ce4ebac
--- /dev/null
+++ b/src/main/java/org/zwobble/mammoth/internal/docx/ContentTypes.java
@@ -0,0 +1,45 @@
+package org.zwobble.mammoth.internal.docx;
+
+import com.google.common.collect.ImmutableMap;
+import com.google.common.io.Files;
+
+import java.util.Map;
+import java.util.Optional;
+
+import static org.zwobble.mammoth.internal.util.MammothMaps.map;
+
+public class ContentTypes {
+ public static final ContentTypes DEFAULT = new ContentTypes(map(), map());
+
+ private static final Map<String, String> imageExtensions = ImmutableMap.<String, String>builder()
+ .put("png", "png")
+ .put("gif", "gif")
+ .put("jpeg", "jpeg")
+ .put("jpg", "jpeg")
+ .put("bmp", "bmp")
+ .put("tif", "tiff")
+ .put("tiff", "tiff")
+ .build();
+
+ private final Map<String, String> extensionDefaults;
+ private final Map<String, String> overrides;
+
+ public ContentTypes(Map<String, String> extensionDefaults, Map<String, String> overrides) {
+ this.extensionDefaults = extensionDefaults;
+ this.overrides = overrides;
+ }
+
+ public Optional<String> findContentType(String path) {
+ if (overrides.containsKey(path)) {
+ return Optional.ofNullable(overrides.get(path));
+ } else {
+ String extension = Files.getFileExtension(path);
+ if (extensionDefaults.containsKey(extension)) {
+ return Optional.ofNullable(extensionDefaults.get(extension));
+ } else {
+ return Optional.ofNullable(imageExtensions.get(extension.toLowerCase()))
+ .map(imageType -> "image/" + imageType);
+ }
+ }
+ }
+}
diff --git a/src/main/java/org/zwobble/mammoth/internal/docx/ContentTypesXml.java b/src/main/java/org/zwobble/mammoth/internal/docx/ContentTypesXml.java
new file mode 100644
index 0000000..4a7d3bd
--- /dev/null
+++ b/src/main/java/org/zwobble/mammoth/internal/docx/ContentTypesXml.java
@@ -0,0 +1,38 @@
+package org.zwobble.mammoth.internal.docx;
+
+import org.zwobble.mammoth.internal.xml.XmlElement;
+import org.zwobble.mammoth.internal.xml.XmlElementList;
+
+import java.util.Map;
+
+import static com.google.common.collect.Maps.immutableEntry;
+import static org.zwobble.mammoth.internal.util.MammothMaps.toMap;
+import static org.zwobble.mammoth.internal.util.MammothStrings.trimLeft;
+
+public class ContentTypesXml {
+ public static ContentTypes readContentTypesXmlElement(XmlElement element) {
+ return new ContentTypes(
+ readDefaults(element.findChildren("content-types:Default")),
+ readOverrides(element.findChildren("content-types:Override")));
+ }
+
+ private static Map<String, String> readDefaults(XmlElementList children) {
+ return toMap(children, ContentTypesXml::readDefault);
+ }
+
+ private static Map.Entry<String, String> readDefault(XmlElement element) {
+ return immutableEntry(
+ element.getAttribute("Extension"),
+ element.getAttribute("ContentType"));
+ }
+
+ private static Map<String, String> readOverrides(XmlElementList children) {
+ return toMap(children, ContentTypesXml::readOverride);
+ }
+
+ private static Map.Entry<String, String> readOverride(XmlElement element) {
+ return immutableEntry(
+ trimLeft(element.getAttribute("PartName"), '/'),
+ element.getAttribute("ContentType"));
+ }
+}
diff --git a/src/main/java/org/zwobble/mammoth/internal/docx/DocumentReader.java b/src/main/java/org/zwobble/mammoth/internal/docx/DocumentReader.java
new file mode 100644
index 0000000..4341bbf
--- /dev/null
+++ b/src/main/java/org/zwobble/mammoth/internal/docx/DocumentReader.java
@@ -0,0 +1,115 @@
+package org.zwobble.mammoth.internal.docx;
+
+import org.zwobble.mammoth.internal.documents.Document;
+import org.zwobble.mammoth.internal.documents.Notes;
+import org.zwobble.mammoth.results.Result;
+import org.zwobble.mammoth.internal.util.MammothLists;
+import org.zwobble.mammoth.internal.xml.XmlElement;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.net.URI;
+import java.net.URISyntaxException;
+import java.nio.file.Path;
+import java.util.Optional;
+
+import static org.zwobble.mammoth.internal.util.MammothLists.list;
+
+public class DocumentReader {
+ @FunctionalInterface
+ private interface BodyReaders {
+ BodyXmlReader forName(String name);
+ }
+
+ public static Result<Document> readDocument(Optional<Path> path, DocxFile zipFile) {
+ Styles styles = readStyles(zipFile);
+ Numbering numbering = readNumbering(zipFile);
+ ContentTypes contentTypes = readContentTypes(zipFile);
+ FileReader fileReader = uri -> {
+ try {
+ Optional<URI> absoluteUri = asAbsoluteUri(uri);
+ if (absoluteUri.isPresent()) {
+ return open(absoluteUri.get());
+ } else if (path.isPresent()) {
+ return open(path.get().toUri().resolve(uri));
+ } else {
+ throw new IOException("path of document is unknown, but is required for relative URI");
+ }
+ } catch (IOException exception) {
+ throw new IOException("could not open external image '" + uri + "': " + exception.getMessage());
+ }
+ };
+ BodyReaders bodyReaders = name -> {
+ Relationships relationships = readRelationships(zipFile, name);
+ return new BodyXmlReader(styles, numbering, relationships, contentTypes, zipFile, fileReader);
+ };
+ return readNotes(zipFile, bodyReaders)
+ .flatMap(notes -> {
+ DocumentXmlReader reader = new DocumentXmlReader(bodyReaders.forName("document"), notes);
+ return reader.readElement(parseOfficeXml(zipFile, "word/document.xml"));
+ });
+ }
+
+ private static InputStream open(URI uri) throws IOException {
+ return uri.toURL().openStream();
+ }
+
+ private static Optional<URI> asAbsoluteUri(String uriString) {
+ try {
+ URI uri = new URI(uriString);
+ return uri.isAbsolute() ? Optional.of(uri) : Optional.empty();
+ } catch (URISyntaxException exception) {
+ return Optional.empty();
+ }
+ }
+
+ private static Result<Notes> readNotes(DocxFile file, BodyReaders bodyReaders) {
+ return Result.map(
+ tryParseOfficeXml(file, "word/footnotes.xml")
+ .map(NotesXmlReader.footnote(bodyReaders.forName("footnotes"))::readElement)
+ .orElse(Result.success(list())),
+ tryParseOfficeXml(file, "word/endnotes.xml")
+ .map(NotesXmlReader.endnote(bodyReaders.forName("endnotes"))::readElement)
+ .orElse(Result.success(list())),
+ MammothLists::concat).map(Notes::new);
+ }
+
+ private static Styles readStyles(DocxFile file) {
+ return tryParseOfficeXml(file, "word/styles.xml")
+ .map(StylesXml::readStylesXmlElement)
+ .orElse(Styles.EMPTY);
+ }
+
+ private static Numbering readNumbering(DocxFile file) {
+ return tryParseOfficeXml(file, "word/numbering.xml")
+ .map(NumberingXml::readNumberingXmlElement)
+ .orElse(Numbering.EMPTY);
+ }
+
+ private static ContentTypes readContentTypes(DocxFile file) {
+ return tryParseOfficeXml(file, "[Content_Types].xml")
+ .map(ContentTypesXml::readContentTypesXmlElement)
+ .orElse(ContentTypes.DEFAULT);
+ }
+
+ private static Relationships readRelationships(DocxFile zipFile, String name) {
+ return tryParseOfficeXml(zipFile, "word/_rels/" + name + ".xml.rels")
+ .map(RelationshipsXml::readRelationshipsXmlElement)
+ .orElse(Relationships.EMPTY);
+ }
+
+ private static Optional<XmlElement> tryParseOfficeXml(DocxFile zipFile, String name) {
+ try {
+ return zipFile.tryGetInputStream(name).map(OfficeXml::parseXml);
+ } catch (IOException exception) {
+ // TODO: wrap in more specific exception and catch at the top
+ throw new RuntimeException(exception);
+ }
+ }
+
+ private static XmlElement parseOfficeXml(DocxFile zipFile, String name) {
+ return tryParseOfficeXml(zipFile, name)
+ // TODO: wrap in more specific exception and catch at the top
+ .orElseThrow(() -> new RuntimeException(new IOException("Missing entry in file: " + name)));
+ }
+}
diff --git a/src/main/java/org/zwobble/mammoth/internal/docx/DocumentXmlReader.java b/src/main/java/org/zwobble/mammoth/internal/docx/DocumentXmlReader.java
new file mode 100644
index 0000000..3c215fe
--- /dev/null
+++ b/src/main/java/org/zwobble/mammoth/internal/docx/DocumentXmlReader.java
@@ -0,0 +1,23 @@
+package org.zwobble.mammoth.internal.docx;
+
+import org.zwobble.mammoth.internal.documents.Document;
+import org.zwobble.mammoth.internal.documents.Notes;
+import org.zwobble.mammoth.results.Result;
+import org.zwobble.mammoth.internal.xml.XmlElement;
+
+public class DocumentXmlReader {
+ private final BodyXmlReader bodyReader;
+ private final Notes notes;
+
+ public DocumentXmlReader(BodyXmlReader bodyReader, Notes notes) {
+ this.bodyReader = bodyReader;
+ this.notes = notes;
+ }
+
+ public Result<Document> readElement(XmlElement element) {
+ XmlElement body = element.findChild("w:body");
+ return bodyReader.readElements(body.children())
+ .toResult()
+ .map(children -> new Document(children, notes));
+ }
+}
diff --git a/src/main/java/org/zwobble/mammoth/internal/docx/DocxFile.java b/src/main/java/org/zwobble/mammoth/internal/docx/DocxFile.java
new file mode 100644
index 0000000..8e73a04
--- /dev/null
+++ b/src/main/java/org/zwobble/mammoth/internal/docx/DocxFile.java
@@ -0,0 +1,15 @@
+package org.zwobble.mammoth.internal.docx;
+
+import java.io.Closeable;
+import java.io.IOException;
+import java.io.InputStream;
+import java.util.Optional;
+
+public interface DocxFile extends Closeable {
+ static InputStream getInputStream(DocxFile file, String name) throws IOException {
+ return file.tryGetInputStream(name)
+ .orElseThrow(() -> new IOException("Missing entry in file: " + name));
+ }
+
+ Optional<InputStream> tryGetInputStream(String name) throws IOException;
+}
diff --git a/src/main/java/org/zwobble/mammoth/internal/docx/FileReader.java b/src/main/java/org/zwobble/mammoth/internal/docx/FileReader.java
new file mode 100644
index 0000000..f25eb32
--- /dev/null
+++ b/src/main/java/org/zwobble/mammoth/internal/docx/FileReader.java
@@ -0,0 +1,8 @@
+package org.zwobble.mammoth.internal.docx;
+
+import java.io.IOException;
+import java.io.InputStream;
+
+public interface FileReader {
+ InputStream getInputStream(String uri) throws IOException;
+}
diff --git a/src/main/java/org/zwobble/mammoth/internal/docx/InMemoryDocxFile.java b/src/main/java/org/zwobble/mammoth/internal/docx/InMemoryDocxFile.java
new file mode 100644
index 0000000..7229ad5
--- /dev/null
+++ b/src/main/java/org/zwobble/mammoth/internal/docx/InMemoryDocxFile.java
@@ -0,0 +1,46 @@
+package org.zwobble.mammoth.internal.docx;
+
+import com.google.common.collect.ImmutableMap;
+import com.google.common.collect.Maps;
+import com.google.common.io.ByteStreams;
+
+import java.io.ByteArrayInputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.nio.charset.StandardCharsets;
+import java.util.Map;
+import java.util.Optional;
+import java.util.zip.ZipEntry;
+import java.util.zip.ZipInputStream;
+
+public class InMemoryDocxFile implements DocxFile {
+ public static DocxFile fromStream(InputStream stream) throws IOException {
+ ZipInputStream zipStream = new ZipInputStream(stream);
+ ImmutableMap.Builder<String, byte[]> entries = ImmutableMap.builder();
+ ZipEntry entry;
+ while ((entry = zipStream.getNextEntry()) != null) {
+ entries.put(entry.getName(), ByteStreams.toByteArray(zipStream));
+ }
+ return new InMemoryDocxFile(entries.build());
+ }
+
+ public static DocxFile fromStrings(Map<String, String> entries) {
+ return new InMemoryDocxFile(Maps.transformValues(entries, value -> value.getBytes(StandardCharsets.UTF_8)));
+ }
+
+ private final Map<String, byte[]> entries;
+
+ public InMemoryDocxFile(Map<String, byte[]> entries) {
+ this.entries = entries;
+ }
+
+ @Override
+ public Optional<InputStream> tryGetInputStream(String name) throws IOException {
+ return Optional.ofNullable(entries.get(name))
+ .map(ByteArrayInputStream::new);
+ }
+
+ @Override
+ public void close() throws IOException {
+ }
+}
diff --git a/src/main/java/org/zwobble/mammoth/internal/docx/NotesXmlReader.java b/src/main/java/org/zwobble/mammoth/internal/docx/NotesXmlReader.java
new file mode 100644
index 0000000..2e31aa2
--- /dev/null
+++ b/src/main/java/org/zwobble/mammoth/internal/docx/NotesXmlReader.java
@@ -0,0 +1,55 @@
+package org.zwobble.mammoth.internal.docx;
+
+import org.zwobble.mammoth.internal.documents.NoteType;
+import org.zwobble.mammoth.results.Result;
+import org.zwobble.mammoth.internal.documents.Note;
+import org.zwobble.mammoth.internal.xml.XmlElement;
+
+import java.util.List;
+
+import static com.google.common.collect.Iterables.filter;
+import static com.google.common.collect.Iterables.transform;
+
+public class NotesXmlReader {
+ public static NotesXmlReader footnote(BodyXmlReader bodyReader) {
+ return new NotesXmlReader(bodyReader, "footnote", NoteType.FOOTNOTE);
+ }
+
+ public static NotesXmlReader endnote(BodyXmlReader bodyReader) {
+ return new NotesXmlReader(bodyReader, "endnote", NoteType.ENDNOTE);
+ }
+
+ private final BodyXmlReader bodyReader;
+ private final String tagName;
+ private final NoteType noteType;
+
+ private NotesXmlReader(BodyXmlReader bodyReader, String tagName, NoteType noteType) {
+ this.bodyReader = bodyReader;
+ this.tagName = tagName;
+ this.noteType = noteType;
+ }
+
+ public Result<List<Note>> readElement(XmlElement element) {
+ Iterable<XmlElement> elements = filter(element.findChildren("w:" + tagName), this::isNoteElement);
+ return Result.concat(transform(elements, this::readNoteElement));
+ }
+
+ private boolean isNoteElement(XmlElement element) {
+ return element.getAttributeOrNone("w:type")
+ .map(type -> !isSeparatorType(type))
+ .orElse(true);
+ }
+
+ private boolean isSeparatorType(String type) {
+ return type.equals("continuationSeparator") || type.equals("separator");
+ }
+
+ private Result<Note> readNoteElement(XmlElement element) {
+ return bodyReader.readElements(element.children())
+ .toResult()
+ .map(children -> new Note(
+ noteType,
+ element.getAttribute("w:id"),
+ children));
+ }
+}
diff --git a/src/main/java/org/zwobble/mammoth/internal/docx/Numbering.java b/src/main/java/org/zwobble/mammoth/internal/docx/Numbering.java
new file mode 100644
index 0000000..f61b5db
--- /dev/null
+++ b/src/main/java/org/zwobble/mammoth/internal/docx/Numbering.java
@@ -0,0 +1,23 @@
+package org.zwobble.mammoth.internal.docx;
+
+import org.zwobble.mammoth.internal.documents.NumberingLevel;
+
+import java.util.Map;
+import java.util.Optional;
+
+import static org.zwobble.mammoth.internal.util.MammothMaps.map;
+
+public class Numbering {
+ public static final Numbering EMPTY = new Numbering(map());
+
+ private final Map<String, Map<String, NumberingLevel>> numbering;
+
+ public Numbering(Map<String, Map<String, NumberingLevel>> numbering) {
+ this.numbering = numbering;
+ }
+
+ public Optional<NumberingLevel> findLevel(String numId, String level) {
+ return Optional.ofNullable(numbering.get(numId))
+ .flatMap(levels -> Optional.ofNullable(levels.get(level)));
+ }
+}
diff --git a/src/main/java/org/zwobble/mammoth/internal/docx/NumberingXml.java b/src/main/java/org/zwobble/mammoth/internal/docx/NumberingXml.java
new file mode 100644
index 0000000..0ee480d
--- /dev/null
+++ b/src/main/java/org/zwobble/mammoth/internal/docx/NumberingXml.java
@@ -0,0 +1,56 @@
+package org.zwobble.mammoth.internal.docx;
+
+import org.zwobble.mammoth.internal.documents.NumberingLevel;
+import org.zwobble.mammoth.internal.xml.XmlElement;
+import org.zwobble.mammoth.internal.xml.XmlElementList;
+
+import java.util.Map;
+import java.util.Optional;
+
+import static com.google.common.collect.Maps.immutableEntry;
+import static org.zwobble.mammoth.internal.util.MammothMaps.toMap;
+
+public class NumberingXml {
+ public static Numbering readNumberingXmlElement(XmlElement element) {
+ Map<String, Map<String, NumberingLevel>> abstractNums = readAbstractNums(element.findChildren("w:abstractNum"));
+ return new Numbering(readNums(element.findChildren("w:num"), abstractNums));
+ }
+
+ private static Map<String, Map<String, NumberingLevel>> readAbstractNums(XmlElementList children) {
+ return toMap(children, NumberingXml::readAbstractNum);
+ }
+
+ private static Map.Entry<String, Map<String, NumberingLevel>> readAbstractNum(XmlElement element) {
+ // TODO: in python-mammoth, we allow None here. Check whether that's actually possible or not
+ String abstractNumId = element.getAttribute("w:abstractNumId");
+ return immutableEntry(abstractNumId, readAbstractNumLevels(element));
+ }
+
+ private static Map<String, NumberingLevel> readAbstractNumLevels(XmlElement element) {
+ return toMap(element.findChildren("w:lvl"), NumberingXml::readAbstractNumLevel);
+ }
+
+ private static Map.Entry<String, NumberingLevel> readAbstractNumLevel(XmlElement element) {
+ String levelIndex = element.getAttribute("w:ilvl");
+ Optional<String> numFmt = element.findChildOrEmpty("w:numFmt").getAttributeOrNone("w:val");
+ boolean isOrdered = !numFmt.equals(Optional.of("bullet"));
+ return immutableEntry(levelIndex, new NumberingLevel(levelIndex, isOrdered));
+ }
+
+ private static Map<String, Map<String, NumberingLevel>> readNums(
+ XmlElementList numElements,
+ Map<String, Map<String, NumberingLevel>> abstractNums
+ ) {
+ return toMap(numElements, numElement -> readNum(numElement, abstractNums));
+ }
+
+ private static Map.Entry<String,Map<String,NumberingLevel>> readNum(
+ XmlElement numElement,
+ Map<String, Map<String, NumberingLevel>> abstractNums
+ ) {
+ // TODO: in python-mammoth, we allow None here. Check whether that's actually possible or not
+ String numId = numElement.getAttribute("w:numId");
+ String abstractNumId = numElement.findChild("w:abstractNumId").getAttribute("w:val");
+ return immutableEntry(numId, Optional.ofNullable(abstractNums.get(abstractNumId)).get());
+ }
+}
diff --git a/src/main/java/org/zwobble/mammoth/internal/docx/OfficeXml.java b/src/main/java/org/zwobble/mammoth/internal/docx/OfficeXml.java
new file mode 100644
index 0000000..2b5c279
--- /dev/null
+++ b/src/main/java/org/zwobble/mammoth/internal/docx/OfficeXml.java
@@ -0,0 +1,58 @@
+package org.zwobble.mammoth.internal.docx;
+
+import com.google.common.collect.ImmutableBiMap;
+import com.google.common.collect.ImmutableList;
+import org.zwobble.mammoth.internal.xml.XmlElement;
+import org.zwobble.mammoth.internal.xml.XmlNode;
+import org.zwobble.mammoth.internal.xml.XmlNodeVisitor;
+import org.zwobble.mammoth.internal.xml.XmlTextNode;
+import org.zwobble.mammoth.internal.xml.parsing.XmlParser;
+
+import java.io.InputStream;
+import java.util.List;
+
+import static com.google.common.collect.Iterables.concat;
+import static com.google.common.collect.Iterables.transform;
+import static org.zwobble.mammoth.internal.util.MammothLists.list;
+
+public class OfficeXml {
+ private static ImmutableBiMap<String, String> XML_NAMESPACES = ImmutableBiMap.<String, String>builder()
+ .put("w", "http://schemas.openxmlformats.org/wordprocessingml/2006/main")
+ .put("wp", "http://schemas.openxmlformats.org/drawingml/2006/wordprocessingDrawing")
+ .put("a", "http://schemas.openxmlformats.org/drawingml/2006/main")
+ .put("pic", "http://schemas.openxmlformats.org/drawingml/2006/picture")
+ .put("content-types", "http://schemas.openxmlformats.org/package/2006/content-types")
+ .put("r", "http://schemas.openxmlformats.org/officeDocument/2006/relationships")
+ .put("relationships", "http://schemas.openxmlformats.org/package/2006/relationships")
+ .put("v", "urn:schemas-microsoft-com:vml")
+ .put("mc", "http://schemas.openxmlformats.org/markup-compatibility/2006")
+ .put("office-word", "urn:schemas-microsoft-com:office:word")
+ .build();
+
+ public static XmlElement parseXml(InputStream inputStream) {
+ XmlParser parser = new XmlParser(XML_NAMESPACES);
+ return (XmlElement)collapseAlternateContent(parser.parseStream(inputStream)).get(0);
+ }
+
+ private static List<XmlNode> collapseAlternateContent(XmlNode node) {
+ return node.accept(new XmlNodeVisitor<List<XmlNode>>() {
+ @Override
+ public List<XmlNode> visit(XmlElement element) {
+ if (element.getName().equals("mc:AlternateContent")) {
+ return element.findChild("mc:Fallback").getChildren();
+ } else {
+ XmlElement collapsedElement = new XmlElement(
+ element.getName(),
+ element.getAttributes(),
+ ImmutableList.copyOf(concat(transform(element.children(), OfficeXml::collapseAlternateContent))));
+ return list(collapsedElement);
+ }
+ }
+
+ @Override
+ public List<XmlNode> visit(XmlTextNode textNode) {
+ return list(textNode);
+ }
+ });
+ }
+}
diff --git a/src/main/java/org/zwobble/mammoth/internal/docx/ReadResult.java b/src/main/java/org/zwobble/mammoth/internal/docx/ReadResult.java
new file mode 100644
index 0000000..366fd12
--- /dev/null
+++ b/src/main/java/org/zwobble/mammoth/internal/docx/ReadResult.java
@@ -0,0 +1,91 @@
+package org.zwobble.mammoth.internal.docx;
+
+import com.google.common.collect.ImmutableList;
+import org.zwobble.mammoth.internal.documents.DocumentElement;
+import org.zwobble.mammoth.results.Result;
+import org.zwobble.mammoth.results.Warning;
+
+import java.util.List;
+import java.util.function.BiFunction;
+import java.util.function.Function;
+
+import static org.zwobble.mammoth.internal.util.MammothLists.list;
+
+public class ReadResult {
+ public static final ReadResult EMPTY_SUCCESS = success(list());
+
+ public static ReadResult concat(Iterable<ReadResult> results) {
+ ImmutableList.Builder<DocumentElement> elements = ImmutableList.builder();
+ ImmutableList.Builder<DocumentElement> extra = ImmutableList.builder();
+ ImmutableList.Builder<Warning> warnings = ImmutableList.builder();
+ for (ReadResult result : results) {
+ elements.addAll(result.elements);
+ extra.addAll(result.extra);
+ warnings.addAll(result.warnings);
+ }
+ return new ReadResult(elements.build(), extra.build(), warnings.build());
+ }
+
+ public static <T> ReadResult map(
+ Result<T> first,
+ ReadResult second,
+ BiFunction<T, List<DocumentElement>, DocumentElement> function)
+ {
+ return new ReadResult(
+ list(function.apply(first.getValue(), second.elements)),
+ second.extra,
+ concat(first.getWarnings(), second.warnings));
+ }
+
+ public static ReadResult success(DocumentElement element) {
+ return success(list(element));
+ }
+
+ public static ReadResult success(List<DocumentElement> elements) {
+ return new ReadResult(elements, list(), list());
+ }
+
+ public static ReadResult emptyWithWarning(Warning warning) {
+ return new ReadResult(list(), list(), list(warning));
+ }
+
+ public static ReadResult withWarning(DocumentElement element, Warning warning) {
+ return new ReadResult(list(element), list(), list(warning));
+ }
+
+ private final List<DocumentElement> elements;
+ private final List<DocumentElement> extra;
+ private final List<Warning> warnings;
+
+ public ReadResult(List<DocumentElement> elements, List<DocumentElement> extra, List<Warning> warnings) {
+ this.elements = elements;
+ this.extra = extra;
+ this.warnings = warnings;
+ }
+
+ public ReadResult map(Function<List<DocumentElement>, DocumentElement> function) {
+ return new ReadResult(
+ list(function.apply(elements)),
+ extra,
+ warnings);
+ }
+
+ public ReadResult toExtra() {
+ return new ReadResult(list(), concat(extra, elements), warnings);
+ }
+
+ public ReadResult appendExtra() {
+ return new ReadResult(concat(elements, extra), list(), warnings);
+ }
+
+ public Result<List<DocumentElement>> toResult() {
+ return new Result<>(elements, warnings);
+ }
+
+ private static <T> List<T> concat(List<T> first, List<T> second) {
+ ImmutableList.Builder<T> builder = ImmutableList.builder();
+ builder.addAll(first);
+ builder.addAll(second);
+ return builder.build();
+ }
+}
diff --git a/src/main/java/org/zwobble/mammoth/internal/docx/Relationship.java b/src/main/java/org/zwobble/mammoth/internal/docx/Relationship.java
new file mode 100644
index 0000000..dacd7a6
--- /dev/null
+++ b/src/main/java/org/zwobble/mammoth/internal/docx/Relationship.java
@@ -0,0 +1,13 @@
+package org.zwobble.mammoth.internal.docx;
+
+public class Relationship {
+ private final String target;
+
+ public Relationship(String target) {
+ this.target = target;
+ }
+
+ public String getTarget() {
+ return target;
+ }
+}
diff --git a/src/main/java/org/zwobble/mammoth/internal/docx/Relationships.java b/src/main/java/org/zwobble/mammoth/internal/docx/Relationships.java
new file mode 100644
index 0000000..d019b57
--- /dev/null
+++ b/src/main/java/org/zwobble/mammoth/internal/docx/Relationships.java
@@ -0,0 +1,20 @@
+package org.zwobble.mammoth.internal.docx;
+
+import java.util.Map;
+import java.util.Optional;
+
+import static org.zwobble.mammoth.internal.util.MammothMaps.map;
+
+public class Relationships {
+ public static final Relationships EMPTY = new Relationships(map());
+
+ private final Map<String, Relationship> relationships;
+
+ public Relationships(Map<String, Relationship> relationships) {
+ this.relationships = relationships;
+ }
+
+ public Relationship findRelationshipById(String id) {
+ return Optional.ofNullable(relationships.get(id)).get();
+ }
+}
diff --git a/src/main/java/org/zwobble/mammoth/internal/docx/RelationshipsXml.java b/src/main/java/org/zwobble/mammoth/internal/docx/RelationshipsXml.java
new file mode 100644
index 0000000..938776e
--- /dev/null
+++ b/src/main/java/org/zwobble/mammoth/internal/docx/RelationshipsXml.java
@@ -0,0 +1,22 @@
+package org.zwobble.mammoth.internal.docx;
+
+import org.zwobble.mammoth.internal.xml.XmlElement;
+
+import java.util.Map;
+
+import static com.google.common.collect.Maps.immutableEntry;
+import static org.zwobble.mammoth.internal.util.MammothMaps.toMap;
+
+public class RelationshipsXml {
+ public static Relationships readRelationshipsXmlElement(XmlElement element) {
+ return new Relationships(toMap(
+ element.findChildren("relationships:Relationship"),
+ RelationshipsXml::readRelationship));
+ }
+
+ private static Map.Entry<String, Relationship> readRelationship(XmlElement element) {
+ return immutableEntry(
+ element.getAttribute("Id"),
+ new Relationship(element.getAttribute("Target")));
+ }
+}
diff --git a/src/main/java/org/zwobble/mammoth/internal/docx/Styles.java b/src/main/java/org/zwobble/mammoth/internal/docx/Styles.java
new file mode 100644
index 0000000..d5e32e8
--- /dev/null
+++ b/src/main/java/org/zwobble/mammoth/internal/docx/Styles.java
@@ -0,0 +1,28 @@
+package org.zwobble.mammoth.internal.docx;
+
+import org.zwobble.mammoth.internal.documents.Style;
+
+import java.util.Map;
+import java.util.Optional;
+
+import static org.zwobble.mammoth.internal.util.MammothMaps.map;
+
+public class Styles {
+ public static final Styles EMPTY = new Styles(map(), map());
+
+ private final Map<String, Style> paragraphStyles;
+ private final Map<String, Style> characterStyles;
+
+ public Styles(Map<String, Style> paragraphStyles, Map<String, Style> characterStyles) {
+ this.paragraphStyles = paragraphStyles;
+ this.characterStyles = characterStyles;
+ }
+
+ public Optional<Style> findParagraphStyleById(String id) {
+ return Optional.ofNullable(paragraphStyles.get(id));
+ }
+
+ public Optional<Style> findCharacterStyleById(String id) {
+ return Optional.ofNullable(characterStyles.get(id));
+ }
+}
diff --git a/src/main/java/org/zwobble/mammoth/internal/docx/StylesXml.java b/src/main/java/org/zwobble/mammoth/internal/docx/StylesXml.java
new file mode 100644
index 0000000..78ba65f
--- /dev/null
+++ b/src/main/java/org/zwobble/mammoth/internal/docx/StylesXml.java
@@ -0,0 +1,33 @@
+package org.zwobble.mammoth.internal.docx;
+
+import org.zwobble.mammoth.internal.documents.Style;
+import org.zwobble.mammoth.internal.xml.XmlElement;
+import org.zwobble.mammoth.internal.xml.XmlElementList;
+
+import java.util.Map;
+import java.util.Optional;
+
+import static com.google.common.collect.Iterables.filter;
+import static com.google.common.collect.Maps.immutableEntry;
+import static org.zwobble.mammoth.internal.util.MammothMaps.toMap;
+
+public class StylesXml {
+ public static Styles readStylesXmlElement(XmlElement element) {
+ XmlElementList styleElements = element.findChildren("w:style");
+ return new Styles(
+ readStyles(styleElements, "paragraph"),
+ readStyles(styleElements, "character"));
+ }
+
+ private static Map<String, Style> readStyles(XmlElementList styleElements, String styleType) {
+ return toMap(
+ filter(styleElements, styleElement -> styleElement.getAttribute("w:type").equals(styleType)),
+ StylesXml::readStyle);
+ }
+
+ private static Map.Entry<String, Style> readStyle(XmlElement element) {
+ String styleId = element.getAttribute("w:styleId");
+ Optional<String> styleName = element.findChildOrEmpty("w:name").getAttributeOrNone("w:val");
+ return immutableEntry(styleId, new Style(styleId, styleName));
+ }
+}
diff --git a/src/main/java/org/zwobble/mammoth/internal/docx/ZippedDocxFile.java b/src/main/java/org/zwobble/mammoth/internal/docx/ZippedDocxFile.java
new file mode 100644
index 0000000..d263578
--- /dev/null
+++ b/src/main/java/org/zwobble/mammoth/internal/docx/ZippedDocxFile.java
@@ -0,0 +1,30 @@
+package org.zwobble.mammoth.internal.docx;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.util.Optional;
+import java.util.zip.ZipEntry;
+import java.util.zip.ZipFile;
+
+public class ZippedDocxFile implements DocxFile {
+ private final ZipFile zipFile;
+
+ public ZippedDocxFile(ZipFile zipFile) {
+ this.zipFile = zipFile;
+ }
+
+ @Override
+ public Optional<InputStream> tryGetInputStream(String name) throws IOException {
+ ZipEntry entry = zipFile.getEntry(name);
+ if (entry == null) {
+ return Optional.empty();
+ } else {
+ return Optional.of(zipFile.getInputStream(entry));
+ }
+ }
+
+ @Override
+ public void close() throws IOException {
+ zipFile.close();
+ }
+}
diff --git a/src/main/java/org/zwobble/mammoth/internal/html/Html.java b/src/main/java/org/zwobble/mammoth/internal/html/Html.java
new file mode 100644
index 0000000..12ac11e
--- /dev/null
+++ b/src/main/java/org/zwobble/mammoth/internal/html/Html.java
@@ -0,0 +1,179 @@
+package org.zwobble.mammoth.internal.html;
+
+import org.zwobble.mammoth.internal.util.MammothLists;
+import org.zwobble.mammoth.internal.util.MammothOptionals;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
+
+import static org.zwobble.mammoth.internal.util.Casts.tryCast;
+import static org.zwobble.mammoth.internal.util.MammothIterables.tryGetLast;
+import static org.zwobble.mammoth.internal.util.MammothLists.list;
+import static org.zwobble.mammoth.internal.util.MammothMaps.map;
+
+public class Html {
+ public static final HtmlNode FORCE_WRITE = HtmlForceWrite.FORCE_WRITE;
+
+ public static String write(List<HtmlNode> nodes) {
+ StringBuilder builder = new StringBuilder();
+ nodes.forEach(node -> HtmlWriter.write(node, builder));
+ return builder.toString();
+ }
+
+ public static HtmlNode text(String value) {
+ return new HtmlTextNode(value);
+ }
+
+ public static HtmlNode element(String tagName) {
+ return element(tagName, list());
+ }
+
+ public static HtmlNode element(String tagName, Map<String, String> attributes) {
+ return element(tagName, attributes, list());
+ }
+
+ public static HtmlNode element(String tagName, List<HtmlNode> children) {
+ return element(tagName, map(), children);
+ }
+
+ public static HtmlNode element(String tagName, Map<String, String> attributes, List<HtmlNode> children) {
+ return new HtmlElement(list(tagName), attributes, children, false);
+ }
+
+ public static HtmlNode collapsibleElement(String tagName) {
+ return collapsibleElement(list(tagName));
+ }
+
+ public static HtmlNode collapsibleElement(List<String> tagNames) {
+ return collapsibleElement(tagNames, map(), list());
+ }
+
+ public static HtmlNode collapsibleElement(String tagName, List<HtmlNode> children) {
+ return collapsibleElement(tagName, map(), children);
+ }
+
+ public static HtmlNode collapsibleElement(String tagName, Map<String, String> attributes, List<HtmlNode> children) {
+ return collapsibleElement(list(tagName), attributes, children);
+ }
+
+ public static HtmlNode collapsibleElement(List<String> tagNames, Map<String, String> attributes, List<HtmlNode> children) {
+ return new HtmlElement(tagNames, attributes, children, true);
+ }
+
+ public static HtmlNode selfClosingElement(String tagName) {
+ return selfClosingElement(tagName, map());
+ }
+
+ public static HtmlNode selfClosingElement(String tagName, Map<String, String> attributes) {
+ return new HtmlSelfClosingElement(tagName, attributes);
+ }
+
+ public static List<HtmlNode> stripEmpty(List<HtmlNode> nodes) {
+ return MammothLists.eagerFlatMap(nodes, node -> stripEmpty(node));
+ }
+
+ private static List<HtmlNode> stripEmpty(HtmlNode node) {
+ return node.accept(new HtmlNode.Mapper<List<HtmlNode>>() {
+ @Override
+ public List<HtmlNode> visit(HtmlElement element) {
+ List<HtmlNode> children = stripEmpty(element.getChildren());
+ if (children.isEmpty()) {
+ return list();
+ } else {
+ return list(new HtmlElement(
+ element.getTagNames(),
+ element.getAttributes(),
+ children,
+ element.isCollapsible()));
+ }
+ }
+
+ @Override
+ public List<HtmlNode> visit(HtmlSelfClosingElement element) {
+ return list(element);
+ }
+
+ @Override
+ public List<HtmlNode> visit(HtmlTextNode node) {
+ if (node.getValue().isEmpty()) {
+ return list();
+ } else {
+ return list(node);
+ }
+ }
+
+ @Override
+ public List<HtmlNode> visit(HtmlForceWrite forceWrite) {
+ return list(forceWrite);
+ }
+ });
+ }
+
+ public static List<HtmlNode> collapse(List<HtmlNode> nodes) {
+ List<HtmlNode> collapsed = new ArrayList<>();
+
+ for (HtmlNode node : nodes) {
+ collapsingAdd(collapsed, node);
+ }
+
+ return collapsed;
+ }
+
+ private static void collapsingAdd(List<HtmlNode> collapsed, HtmlNode node) {
+ HtmlNode collapsedNode = collapse(node);
+ if (!tryCollapse(collapsed, collapsedNode)) {
+ collapsed.add(collapsedNode);
+ }
+ }
+
+ private static HtmlNode collapse(HtmlNode node) {
+ return node.accept(new HtmlNode.Mapper<HtmlNode>() {
+ @Override
+ public HtmlNode visit(HtmlElement element) {
+ return new HtmlElement(
+ element.getTagNames(),
+ element.getAttributes(),
+ collapse(element.getChildren()),
+ element.isCollapsible());
+ }
+
+ @Override
+ public HtmlNode visit(HtmlSelfClosingElement element) {
+ return element;
+ }
+
+ @Override
+ public HtmlNode visit(HtmlTextNode node) {
+ return node;
+ }
+
+ @Override
+ public HtmlNode visit(HtmlForceWrite forceWrite) {
+ return forceWrite;
+ }
+ });
+ }
+
+ private static boolean tryCollapse(List<HtmlNode> collapsed, HtmlNode node) {
+ return MammothOptionals.map(
+ tryGetLast(collapsed).flatMap(last -> tryCast(HtmlElement.class, last)),
+ tryCast(HtmlElement.class, node),
+ (last, next) -> {
+ if (next.isCollapsible() && isMatch(last, next)) {
+ for (HtmlNode child : next.getChildren()) {
+ collapsingAdd(last.getChildren(), child);
+ }
+ return true;
+ } else {
+ return false;
+ }
+ }
+ ).orElse(false);
+ }
+
+ private static boolean isMatch(HtmlElement first, HtmlElement second) {
+ return second.getTagNames().contains(first.getTagName()) &&
+ first.getAttributes().equals(second.getAttributes());
+ }
+}
diff --git a/src/main/java/org/zwobble/mammoth/internal/html/HtmlElement.java b/src/main/java/org/zwobble/mammoth/internal/html/HtmlElement.java
new file mode 100644
index 0000000..c95f40e
--- /dev/null
+++ b/src/main/java/org/zwobble/mammoth/internal/html/HtmlElement.java
@@ -0,0 +1,48 @@
+package org.zwobble.mammoth.internal.html;
+
+import java.util.List;
+import java.util.Map;
+
+public class HtmlElement implements HtmlNode {
+ private final List<String> tagNames;
+ private final Map<String, String> attributes;
+ private final List<HtmlNode> children;
+ private final boolean collapsible;
+
+ public HtmlElement(List<String> tagNames, Map<String, String> attributes, List<HtmlNode> children, boolean collapsible) {
+ this.tagNames = tagNames;
+ this.attributes = attributes;
+ this.children = children;
+ this.collapsible = collapsible;
+ }
+
+ public List<String> getTagNames() {
+ return tagNames;
+ }
+
+ public String getTagName() {
+ return tagNames.get(0);
+ }
+
+ public Map<String, String> getAttributes() {
+ return attributes;
+ }
+
+ public List<HtmlNode> getChildren() {
+ return children;
+ }
+
+ public boolean isCollapsible() {
+ return collapsible;
+ }
+
+ @Override
+ public void visit(Visitor visitor) {
+ visitor.visit(this);
+ }
+
+ @Override
+ public <T> T accept(Mapper<T> visitor) {
+ return visitor.visit(this);
+ }
+}
diff --git a/src/main/java/org/zwobble/mammoth/internal/html/HtmlForceWrite.java b/src/main/java/org/zwobble/mammoth/internal/html/HtmlForceWrite.java
new file mode 100644
index 0000000..4b3d4bc
--- /dev/null
+++ b/src/main/java/org/zwobble/mammoth/internal/html/HtmlForceWrite.java
@@ -0,0 +1,18 @@
+package org.zwobble.mammoth.internal.html;
+
+public class HtmlForceWrite implements HtmlNode {
+ public static final HtmlForceWrite FORCE_WRITE = new HtmlForceWrite();
+
+ private HtmlForceWrite() {
+ }
+
+ @Override
+ public void visit(Visitor visitor) {
+ visitor.visit(this);
+ }
+
+ @Override
+ public <T> T accept(Mapper<T> visitor) {
+ return visitor.visit(this);
+ }
+}
diff --git a/src/main/java/org/zwobble/mammoth/internal/html/HtmlNode.java b/src/main/java/org/zwobble/mammoth/internal/html/HtmlNode.java
new file mode 100644
index 0000000..a04b57f
--- /dev/null
+++ b/src/main/java/org/zwobble/mammoth/internal/html/HtmlNode.java
@@ -0,0 +1,21 @@
+package org.zwobble.mammoth.internal.html;
+
+public interface HtmlNode {
+ // TODO: rename visit to accept
+ void visit(Visitor visitor);
+ <T> T accept(Mapper<T> visitor);
+
+ interface Visitor {
+ void visit(HtmlElement element);
+ void visit(HtmlSelfClosingElement element);
+ void visit(HtmlTextNode node);
+ void visit(HtmlForceWrite forceWrite);
+ }
+
+ interface Mapper<T> {
+ T visit(HtmlElement element);
+ T visit(HtmlSelfClosingElement element);
+ T visit(HtmlTextNode node);
+ T visit(HtmlForceWrite forceWrite);
+ }
+}
diff --git a/src/main/java/org/zwobble/mammoth/internal/html/HtmlSelfClosingElement.java b/src/main/java/org/zwobble/mammoth/internal/html/HtmlSelfClosingElement.java
new file mode 100644
index 0000000..adff96f
--- /dev/null
+++ b/src/main/java/org/zwobble/mammoth/internal/html/HtmlSelfClosingElement.java
@@ -0,0 +1,31 @@
+package org.zwobble.mammoth.internal.html;
+
+import java.util.Map;
+
+public class HtmlSelfClosingElement implements HtmlNode {
+ private final String tagName;
+ private final Map<String, String> attributes;
+
+ public HtmlSelfClosingElement(String tagName, Map<String, String> attributes) {
+ this.tagName = tagName;
+ this.attributes = attributes;
+ }
+
+ public String getTagName() {
+ return tagName;
+ }
+
+ public Map<String, String> getAttributes() {
+ return attributes;
+ }
+
+ @Override
+ public void visit(Visitor visitor) {
+ visitor.visit(this);
+ }
+
+ @Override
+ public <T> T accept(Mapper<T> visitor) {
+ return visitor.visit(this);
+ }
+}
diff --git a/src/main/java/org/zwobble/mammoth/internal/html/HtmlTextNode.java b/src/main/java/org/zwobble/mammoth/internal/html/HtmlTextNode.java
new file mode 100644
index 0000000..c26bd33
--- /dev/null
+++ b/src/main/java/org/zwobble/mammoth/internal/html/HtmlTextNode.java
@@ -0,0 +1,23 @@
+package org.zwobble.mammoth.internal.html;
+
+public class HtmlTextNode implements HtmlNode {
+ private final String value;
+
+ public HtmlTextNode(String value) {
+ this.value = value;
+ }
+
+ public String getValue() {
+ return value;
+ }
+
+ @Override
+ public void visit(Visitor visitor) {
+ visitor.visit(this);
+ }
+
+ @Override
+ public <T> T accept(Mapper<T> visitor) {
+ return visitor.visit(this);
+ }
+}
diff --git a/src/main/java/org/zwobble/mammoth/internal/html/HtmlWriter.java b/src/main/java/org/zwobble/mammoth/internal/html/HtmlWriter.java
new file mode 100644
index 0000000..164eefc
--- /dev/null
+++ b/src/main/java/org/zwobble/mammoth/internal/html/HtmlWriter.java
@@ -0,0 +1,62 @@
+package org.zwobble.mammoth.internal.html;
+
+import java.util.Map;
+
+import static org.zwobble.mammoth.internal.util.MammothLists.orderedBy;
+
+public class HtmlWriter {
+ public static void write(HtmlNode node, StringBuilder builder) {
+ node.visit(new HtmlNode.Visitor() {
+ @Override
+ public void visit(HtmlElement element) {
+ builder.append("<").append(element.getTagName());
+
+ HtmlWriter.generateAttributes(element.getAttributes(), builder);
+
+ builder.append(">");
+
+ element.getChildren().forEach(node -> write(node, builder));
+
+ builder
+ .append("</")
+ .append(element.getTagName())
+ .append(">");
+ }
+
+ @Override
+ public void visit(HtmlSelfClosingElement element) {
+ builder.append("<").append(element.getTagName());
+ HtmlWriter.generateAttributes(element.getAttributes(), builder);
+ builder.append(" />");
+ }
+
+ @Override
+ public void visit(HtmlTextNode node) {
+ builder.append(HtmlWriter.escapeText(node.getValue()));
+ }
+
+ @Override
+ public void visit(HtmlForceWrite forceWrite) {
+ }
+ });
+ }
+
+ private static void generateAttributes(Map<String, String> attributes, StringBuilder builder) {
+ for (Map.Entry<String, String> attribute : orderedBy(attributes.entrySet(), Map.Entry::getKey)) {
+ builder
+ .append(" ")
+ .append(attribute.getKey())
+ .append("=\"")
+ .append(HtmlWriter.escapeAttributeValue(attribute.getValue()))
+ .append("\"");
+ }
+ }
+
+ private static String escapeText(String text) {
+ return text.replace("&", "&amp;").replace("<", "&lt;").replace(">", "&gt;");
+ }
+
+ private static String escapeAttributeValue(String value) {
+ return escapeText(value).replace("\"", "&quot;");
+ }
+}
diff --git a/src/main/java/org/zwobble/mammoth/internal/styles/DocumentElementMatcher.java b/src/main/java/org/zwobble/mammoth/internal/styles/DocumentElementMatcher.java
new file mode 100644
index 0000000..697926c
--- /dev/null
+++ b/src/main/java/org/zwobble/mammoth/internal/styles/DocumentElementMatcher.java
@@ -0,0 +1,5 @@
+package org.zwobble.mammoth.internal.styles;
+
+public interface DocumentElementMatcher<T> {
+ boolean matches(T element);
+}
diff --git a/src/main/java/org/zwobble/mammoth/internal/styles/DocumentElementMatching.java b/src/main/java/org/zwobble/mammoth/internal/styles/DocumentElementMatching.java
new file mode 100644
index 0000000..a0c5aec
--- /dev/null
+++ b/src/main/java/org/zwobble/mammoth/internal/styles/DocumentElementMatching.java
@@ -0,0 +1,26 @@
+package org.zwobble.mammoth.internal.styles;
+
+import org.zwobble.mammoth.internal.documents.Style;
+
+import java.util.Optional;
+import java.util.function.BiPredicate;
+
+class DocumentElementMatching {
+ static boolean matchesStyle(Optional<String> styleId, Optional<String> styleName, Optional<Style> style) {
+ return matchesStyleId(styleId, style) && matchesStyleName(styleName, style);
+ }
+
+ private static boolean matchesStyleId(Optional<String> styleId, Optional<Style> style) {
+ return matches(styleId, style.map(Style::getStyleId), Object::equals);
+ }
+
+ private static boolean matchesStyleName(Optional<String> styleName, Optional<Style> style) {
+ return matches(styleName, style.flatMap(Style::getName), String::equalsIgnoreCase);
+ }
+
+ static <T> boolean matches(Optional<T> required, Optional<T> actual, BiPredicate<T, T> areEqual) {
+ return required
+ .map(requiredValue -> actual.map(actualValue -> areEqual.test(requiredValue, actualValue)).orElse(false))
+ .orElse(true);
+ }
+}
diff --git a/src/main/java/org/zwobble/mammoth/internal/styles/HtmlPath.java b/src/main/java/org/zwobble/mammoth/internal/styles/HtmlPath.java
new file mode 100644
index 0000000..a20043a
--- /dev/null
+++ b/src/main/java/org/zwobble/mammoth/internal/styles/HtmlPath.java
@@ -0,0 +1,51 @@
+package org.zwobble.mammoth.internal.styles;
+
+import com.google.common.collect.Lists;
+import org.zwobble.mammoth.internal.html.HtmlNode;
+
+import java.util.List;
+import java.util.Map;
+
+import static org.zwobble.mammoth.internal.util.MammothLists.list;
+import static org.zwobble.mammoth.internal.util.MammothMaps.map;
+
+public class HtmlPath {
+ public static final HtmlPath EMPTY = new HtmlPath(list());
+
+ public static HtmlPath element(String tagName) {
+ return element(tagName, map());
+ }
+
+ public static HtmlPath element(String tagName, Map<String, String> attributes) {
+ return new HtmlPath(list(new HtmlPathElement(list(tagName), attributes, false)));
+ }
+
+ public static HtmlPath collapsibleElement(String tagName) {
+ return collapsibleElement(tagName, map());
+ }
+
+ public static HtmlPath collapsibleElement(List<String> tagNames) {
+ return collapsibleElement(tagNames, map());
+ }
+
+ public static HtmlPath collapsibleElement(String tagName, Map<String, String> attributes) {
+ return collapsibleElement(list(tagName), attributes);
+ }
+
+ public static HtmlPath collapsibleElement(List<String> tagNames, Map<String, String> attributes) {
+ return new HtmlPath(list(new HtmlPathElement(tagNames, attributes, true)));
+ }
+
+ private final List<HtmlPathElement> elements;
+
+ public HtmlPath(List<HtmlPathElement> elements) {
+ this.elements = elements;
+ }
+
+ public List<HtmlNode> wrap(List<HtmlNode> nodes) {
+ for (HtmlPathElement element : Lists.reverse(elements)) {
+ nodes = element.wrap(nodes);
+ }
+ return nodes;
+ }
+}
diff --git a/src/main/java/org/zwobble/mammoth/internal/styles/HtmlPathElement.java b/src/main/java/org/zwobble/mammoth/internal/styles/HtmlPathElement.java
new file mode 100644
index 0000000..7f110a4
--- /dev/null
+++ b/src/main/java/org/zwobble/mammoth/internal/styles/HtmlPathElement.java
@@ -0,0 +1,38 @@
+package org.zwobble.mammoth.internal.styles;
+
+import org.zwobble.mammoth.internal.html.HtmlElement;
+import org.zwobble.mammoth.internal.html.HtmlNode;
+
+import java.util.List;
+import java.util.Map;
+
+import static org.zwobble.mammoth.internal.util.MammothLists.list;
+import static org.zwobble.mammoth.internal.util.MammothMaps.map;
+
+public class HtmlPathElement {
+ public static HtmlPathElement fresh(String tagName) {
+ return new HtmlPathElement(list(tagName), map(), false);
+ }
+
+ public static HtmlPathElement collapsible(String tagName) {
+ return collapsible(tagName, map());
+ }
+
+ public static HtmlPathElement collapsible(String tagName, Map<String, String> attributes) {
+ return new HtmlPathElement(list(tagName), attributes, true);
+ }
+
+ private final List<String> tagNames;
+ private final Map<String, String> attributes;
+ private final boolean isCollapsible;
+
+ public HtmlPathElement(List<String> tagNames, Map<String, String> attributes, boolean isCollapsible) {
+ this.tagNames = tagNames;
+ this.attributes = attributes;
+ this.isCollapsible = isCollapsible;
+ }
+
+ public List<HtmlNode> wrap(List<HtmlNode> nodes) {
+ return list(new HtmlElement(tagNames, attributes, nodes, isCollapsible));
+ }
+}
diff --git a/src/main/java/org/zwobble/mammoth/internal/styles/ParagraphMatcher.java b/src/main/java/org/zwobble/mammoth/internal/styles/ParagraphMatcher.java
new file mode 100644
index 0000000..50d5df9
--- /dev/null
+++ b/src/main/java/org/zwobble/mammoth/internal/styles/ParagraphMatcher.java
@@ -0,0 +1,50 @@
+package org.zwobble.mammoth.internal.styles;
+
+import org.zwobble.mammoth.internal.documents.NumberingLevel;
+import org.zwobble.mammoth.internal.documents.Paragraph;
+
+import java.util.Optional;
+
+public class ParagraphMatcher implements DocumentElementMatcher<Paragraph> {
+ public static final ParagraphMatcher ANY = new ParagraphMatcher(Optional.empty(), Optional.empty(), Optional.empty());
+
+ public static ParagraphMatcher styleId(String styleId) {
+ return new ParagraphMatcher(Optional.of(styleId), Optional.empty(), Optional.empty());
+ }
+
+ public static ParagraphMatcher styleName(String styleName) {
+ return new ParagraphMatcher(Optional.empty(), Optional.of(styleName), Optional.empty());
+ }
+
+ public static ParagraphMatcher orderedList(String level) {
+ return new ParagraphMatcher(Optional.empty(), Optional.empty(), Optional.of(NumberingLevel.ordered(level)));
+ }
+
+ public static ParagraphMatcher unorderedList(String level) {
+ return new ParagraphMatcher(Optional.empty(), Optional.empty(), Optional.of(NumberingLevel.unordered(level)));
+ }
+
+ private final Optional<String> styleId;
+ private final Optional<String> styleName;
+ private final Optional<NumberingLevel> numbering;
+
+ public ParagraphMatcher(Optional<String> styleId, Optional<String> styleName, Optional<NumberingLevel> numbering) {
+ this.styleId = styleId;
+ this.styleName = styleName;
+ this.numbering = numbering;
+ }
+
+ @Override
+ public boolean matches(Paragraph paragraph) {
+ return matchesStyle(paragraph) && matchesNumbering(paragraph);
+ }
+
+ private boolean matchesStyle(Paragraph paragraph) {
+ return DocumentElementMatching.matchesStyle(styleId, styleName, paragraph.getStyle());
+ }
+
+ private boolean matchesNumbering(Paragraph paragraph) {
+ return DocumentElementMatching.matches(numbering, paragraph.getNumbering(), (first, second) ->
+ first.isOrdered() == second.isOrdered() && first.getLevelIndex().equalsIgnoreCase(second.getLevelIndex()));
+ }
+}
diff --git a/src/main/java/org/zwobble/mammoth/internal/styles/RunMatcher.java b/src/main/java/org/zwobble/mammoth/internal/styles/RunMatcher.java
new file mode 100644
index 0000000..93b50dd
--- /dev/null
+++ b/src/main/java/org/zwobble/mammoth/internal/styles/RunMatcher.java
@@ -0,0 +1,30 @@
+package org.zwobble.mammoth.internal.styles;
+
+import org.zwobble.mammoth.internal.documents.Run;
+
+import java.util.Optional;
+
+public class RunMatcher implements DocumentElementMatcher<Run> {
+ public static final RunMatcher ANY = new RunMatcher(Optional.empty(), Optional.empty());
+
+ public static RunMatcher styleId(String styleId) {
+ return new RunMatcher(Optional.of(styleId), Optional.empty());
+ }
+
+ public static RunMatcher styleName(String styleName) {
+ return new RunMatcher(Optional.empty(), Optional.of(styleName));
+ }
+
+ private final Optional<String> styleId;
+ private final Optional<String> styleName;
+
+ public RunMatcher(Optional<String> styleId, Optional<String> styleName) {
+ this.styleId = styleId;
+ this.styleName = styleName;
+ }
+
+ @Override
+ public boolean matches(Run run) {
+ return DocumentElementMatching.matchesStyle(styleId, styleName, run.getStyle());
+ }
+}
diff --git a/src/main/java/org/zwobble/mammoth/internal/styles/StyleMap.java b/src/main/java/org/zwobble/mammoth/internal/styles/StyleMap.java
new file mode 100644
index 0000000..7d28bf1
--- /dev/null
+++ b/src/main/java/org/zwobble/mammoth/internal/styles/StyleMap.java
@@ -0,0 +1,57 @@
+package org.zwobble.mammoth.internal.styles;
+
+import com.google.common.collect.Iterables;
+import org.zwobble.mammoth.internal.documents.Paragraph;
+import org.zwobble.mammoth.internal.documents.Run;
+
+import java.util.List;
+import java.util.Optional;
+
+public class StyleMap {
+ public static StyleMapBuilder builder() {
+ return new StyleMapBuilder();
+ }
+
+ public static final StyleMap EMPTY = new StyleMapBuilder().build();
+
+ private final HtmlPath underline;
+ private final HtmlPath strikethrough;
+ private final List<StyleMapping<Paragraph>> paragraphStyles;
+ private final List<StyleMapping<Run>> runStyles;
+
+ public StyleMap(
+ HtmlPath underline,
+ HtmlPath strikethrough,
+ List<StyleMapping<Paragraph>> paragraphStyles,
+ List<StyleMapping<Run>> runStyles)
+ {
+ this.underline = underline;
+ this.strikethrough = strikethrough;
+ this.paragraphStyles = paragraphStyles;
+ this.runStyles = runStyles;
+ }
+
+ public HtmlPath getUnderline() {
+ return underline;
+ }
+
+ public HtmlPath getStrikethrough() {
+ return strikethrough;
+ }
+
+ public Optional<HtmlPath> getParagraphHtmlPath(Paragraph paragraph) {
+ com.google.common.base.Optional<StyleMapping<Paragraph>> mapping = Iterables.tryFind(
+ paragraphStyles,
+ styleMapping -> styleMapping.matches(paragraph));
+ return Optional.ofNullable(mapping.orNull())
+ .map(StyleMapping::getHtmlPath);
+ }
+
+ public Optional<HtmlPath> getRunHtmlPath(Run run) {
+ com.google.common.base.Optional<StyleMapping<Run>> mapping = Iterables.tryFind(
+ runStyles,
+ styleMapping -> styleMapping.matches(run));
+ return Optional.ofNullable(mapping.orNull())
+ .map(StyleMapping::getHtmlPath);
+ }
+}
diff --git a/src/main/java/org/zwobble/mammoth/internal/styles/StyleMapBuilder.java b/src/main/java/org/zwobble/mammoth/internal/styles/StyleMapBuilder.java
new file mode 100644
index 0000000..6300d29
--- /dev/null
+++ b/src/main/java/org/zwobble/mammoth/internal/styles/StyleMapBuilder.java
@@ -0,0 +1,41 @@
+package org.zwobble.mammoth.internal.styles;
+
+import com.google.common.collect.ImmutableList;
+import org.zwobble.mammoth.internal.documents.Paragraph;
+import org.zwobble.mammoth.internal.documents.Run;
+
+public class StyleMapBuilder {
+ private HtmlPath underline;
+ private HtmlPath strikethrough;
+ private final ImmutableList.Builder<StyleMapping<Paragraph>> paragraphStyles = ImmutableList.builder();
+ private final ImmutableList.Builder<StyleMapping<Run>> runStyles = ImmutableList.builder();
+
+ public StyleMapBuilder() {
+ this.underline = HtmlPath.EMPTY;
+ this.strikethrough = HtmlPath.collapsibleElement("s");
+ }
+
+ public StyleMapBuilder underline(HtmlPath path) {
+ this.underline = path;
+ return this;
+ }
+
+ public StyleMapBuilder strikethrough(HtmlPath path) {
+ this.strikethrough = path;
+ return this;
+ }
+
+ public StyleMapBuilder mapParagraph(ParagraphMatcher matcher, HtmlPath path) {
+ paragraphStyles.add(new StyleMapping<Paragraph>(matcher, path));
+ return this;
+ }
+
+ public StyleMapBuilder mapRun(RunMatcher matcher, HtmlPath path) {
+ runStyles.add(new StyleMapping<Run>(matcher, path));
+ return this;
+ }
+
+ public StyleMap build() {
+ return new StyleMap(underline, strikethrough, paragraphStyles.build(), runStyles.build());
+ }
+}
diff --git a/src/main/java/org/zwobble/mammoth/internal/styles/StyleMapping.java b/src/main/java/org/zwobble/mammoth/internal/styles/StyleMapping.java
new file mode 100644
index 0000000..5512c22
--- /dev/null
+++ b/src/main/java/org/zwobble/mammoth/internal/styles/StyleMapping.java
@@ -0,0 +1,19 @@
+package org.zwobble.mammoth.internal.styles;
+
+public class StyleMapping<T> {
+ private final DocumentElementMatcher<T> matcher;
+ private final HtmlPath htmlPath;
+
+ public StyleMapping(DocumentElementMatcher<T> matcher, HtmlPath htmlPath) {
+ this.matcher = matcher;
+ this.htmlPath = htmlPath;
+ }
+
+ public boolean matches(T element) {
+ return matcher.matches(element);
+ }
+
+ public HtmlPath getHtmlPath() {
+ return htmlPath;
+ }
+}
diff --git a/src/main/java/org/zwobble/mammoth/internal/styles/parsing/ListVar.java b/src/main/java/org/zwobble/mammoth/internal/styles/parsing/ListVar.java
new file mode 100644
index 0000000..74b3dbc
--- /dev/null
+++ b/src/main/java/org/zwobble/mammoth/internal/styles/parsing/ListVar.java
@@ -0,0 +1,34 @@
+package org.zwobble.mammoth.internal.styles.parsing;
+
+
+import com.google.common.collect.ImmutableList;
+import org.parboiled.support.Var;
+
+import java.util.List;
+
+import static org.zwobble.mammoth.internal.util.MammothLists.list;
+
+class ListVar<T> extends Var<ImmutableList.Builder<T>> {
+ boolean append(Var<T> element) {
+ return append(element.get());
+ }
+
+ boolean append(T element) {
+ if (get() == null) {
+ set(ImmutableList.builder());
+ }
+ return set(get().add(element));
+ }
+
+ public List<T> build() {
+ if (get() == null) {
+ return list();
+ } else {
+ return get().build();
+ }
+ }
+
+ public boolean isEmpty() {
+ return get() == null;
+ }
+}
diff --git a/src/main/java/org/zwobble/mammoth/internal/styles/parsing/Parsing.java b/src/main/java/org/zwobble/mammoth/internal/styles/parsing/Parsing.java
new file mode 100644
index 0000000..bef3c0b
--- /dev/null
+++ b/src/main/java/org/zwobble/mammoth/internal/styles/parsing/Parsing.java
@@ -0,0 +1,21 @@
+package org.zwobble.mammoth.internal.styles.parsing;
+
+import org.parboiled.BaseParser;
+import org.parboiled.Parboiled;
+import org.parboiled.Rule;
+import org.parboiled.parserunners.ReportingParseRunner;
+import org.parboiled.support.ParsingResult;
+
+import java.util.function.Function;
+
+public class Parsing {
+ public static <T, U extends BaseParser<T>, V> void parse(Class<U> parserType, Function<U, Rule> rule, String value) {
+ U parser = Parboiled.createParser(parserType);
+ ReportingParseRunner<T> runner = new ReportingParseRunner<>(rule.apply(parser));
+ ParsingResult<T> result = runner.run(value);
+ if (result.hasErrors()) {
+ // TODO: wrap in result
+ throw new RuntimeException("Parse error");
+ }
+ }
+}
diff --git a/src/main/java/org/zwobble/mammoth/internal/styles/parsing/StyleMapParser.java b/src/main/java/org/zwobble/mammoth/internal/styles/parsing/StyleMapParser.java
new file mode 100644
index 0000000..b43e300
--- /dev/null
+++ b/src/main/java/org/zwobble/mammoth/internal/styles/parsing/StyleMapParser.java
@@ -0,0 +1,34 @@
+package org.zwobble.mammoth.internal.styles.parsing;
+
+import org.parboiled.support.Var;
+import org.zwobble.mammoth.internal.styles.StyleMap;
+import org.zwobble.mammoth.internal.styles.StyleMapBuilder;
+
+import java.util.List;
+
+import static java.util.Arrays.asList;
+
+public class StyleMapParser {
+ public static StyleMap parse(String input) {
+ return parse(asList(input.split("\\r?\\n")));
+ }
+
+ public static StyleMap parse(List<String> lines) {
+ Var<StyleMapBuilder> styleMap = new Var<>(StyleMap.builder());
+ for (String line : lines) {
+ handleLine(styleMap, line);
+ }
+ return styleMap.get().build();
+ }
+
+ private static void handleLine(Var<StyleMapBuilder> styleMap, String line) {
+ if (line.startsWith("#")) {
+ return;
+ }
+ line = line.trim();
+ if (line.isEmpty()) {
+ return;
+ }
+ Parsing.parse(StyleMappingParser.class, parser -> parser.StyleMapping(styleMap), line);
+ }
+}
diff --git a/src/main/java/org/zwobble/mammoth/internal/styles/parsing/StyleMappingParser.java b/src/main/java/org/zwobble/mammoth/internal/styles/parsing/StyleMappingParser.java
new file mode 100644
index 0000000..5c1500c
--- /dev/null
+++ b/src/main/java/org/zwobble/mammoth/internal/styles/parsing/StyleMappingParser.java
@@ -0,0 +1,178 @@
+package org.zwobble.mammoth.internal.styles.parsing;
+
+import org.parboiled.Action;
+import org.parboiled.BaseParser;
+import org.parboiled.Context;
+import org.parboiled.Rule;
+import org.parboiled.support.Var;
+import org.zwobble.mammoth.internal.documents.NumberingLevel;
+import org.zwobble.mammoth.internal.styles.*;
+
+import java.math.BigInteger;
+import java.util.Optional;
+import java.util.function.BiFunction;
+
+import static org.zwobble.mammoth.internal.util.MammothMaps.map;
+
+public class StyleMappingParser extends BaseParser<StyleMapBuilder> {
+ Rule StyleMapping(Var<StyleMapBuilder> styleMap) {
+ Var<BiFunction<StyleMapBuilder, HtmlPath, StyleMapBuilder>> documentElementMatcher = new Var<>();
+ Var<HtmlPath> path = new Var<>();
+ return Sequence(
+ DocumentElementMatcher(documentElementMatcher), Whitespace(), "=>", Whitespace(), HtmlPath(path), EOI,
+ styleMap.set(documentElementMatcher.get().apply(styleMap.get(), path.get())));
+ }
+
+ Rule DocumentElementMatcher(Var<BiFunction<StyleMapBuilder, HtmlPath, StyleMapBuilder>> updateBuilder) {
+ Var<ParagraphMatcher> paragraphMatcher = new Var<>();
+ Var<RunMatcher> runMatcher = new Var<>();
+ return FirstOf(
+ Sequence(
+ ParagraphMatcher(paragraphMatcher),
+ new Action() {
+ @Override
+ public boolean run(Context context) {
+ updateBuilder.set((styleMap, htmlPath) -> styleMap.mapParagraph(paragraphMatcher.get(), htmlPath));
+ return true;
+ }
+ }),
+ Sequence(
+ RunMatcher(runMatcher),
+ new Action() {
+ @Override
+ public boolean run(Context context) {
+ updateBuilder.set((styleMap, htmlPath) -> styleMap.mapRun(runMatcher.get(), htmlPath));
+ return true;
+ }
+ }),
+ Sequence(
+ "u",
+ new Action() {
+ @Override
+ public boolean run(Context context) {
+ updateBuilder.set(StyleMapBuilder::underline);
+ return true;
+ }
+ }),
+ Sequence(
+ "s",
+ new Action() {
+ @Override
+ public boolean run(Context context) {
+ updateBuilder.set(StyleMapBuilder::strikethrough);
+ return true;
+ }
+ }));
+ }
+
+ public Rule ParagraphMatcher(Var<ParagraphMatcher> matcher) {
+ Var<Optional<String>> styleId = new Var<>();
+ Var<Optional<String>> styleName = new Var<>();
+ Var<Optional<NumberingLevel>> numbering = new Var<>();
+ return Sequence(
+ "p", StyleId(styleId), StyleName(styleName), Numbering(numbering),
+ matcher.set(new ParagraphMatcher(styleId.get(), styleName.get(), numbering.get())));
+ }
+
+ public Rule RunMatcher(Var<RunMatcher> matcher) {
+ Var<Optional<String>> styleId = new Var<>();
+ Var<Optional<String>> styleName = new Var<>();
+ return Sequence(
+ "r", StyleId(styleId), StyleName(styleName),
+ matcher.set(new RunMatcher(styleId.get(), styleName.get())));
+ }
+
+ Rule StyleId(Var<Optional<String>> styleId) {
+ return Sequence(styleId.set(Optional.empty()), Optional(".", Identifier(), styleId.set(Optional.of(match()))));
+ }
+
+ Rule StyleName(Var<Optional<String>> styleName) {
+ Var<String> value = new Var<>();
+ return Sequence(
+ styleName.set(Optional.empty()),
+ Optional("[style-name=", SingleQuotedString(value), "]", styleName.set(Optional.of(value.get()))));
+ }
+
+ Rule Numbering(Var<Optional<NumberingLevel>> numbering) {
+ Var<Boolean> isOrdered = new Var<>();
+ Var<BigInteger> level = new Var<>();
+ return Sequence(
+ numbering.set(Optional.empty()),
+ Optional(Sequence(
+ ":", NumberingType(isOrdered), "(", Number(level), ")",
+ numbering.set(Optional.of(new NumberingLevel(level.get().subtract(BigInteger.ONE).toString(), isOrdered.get()))))));
+ }
+
+ Rule NumberingType(Var<Boolean> isOrdered) {
+ return FirstOf(
+ Sequence("ordered-list", isOrdered.set(true)),
+ Sequence("unordered-list", isOrdered.set(false)));
+ }
+
+ Rule Number(Var<BigInteger> level) {
+ return Sequence(
+ OneOrMore(CharRange('0', '9')),
+ level.set(new BigInteger(match())));
+ }
+
+ public Rule HtmlPath(Var<HtmlPath> htmlPath) {
+ ListVar<HtmlPathElement> elements = new ListVar<>();
+ return Sequence(Optional(HtmlPathElements(elements)), htmlPath.set(new HtmlPath(elements.build())));
+ }
+
+ Rule HtmlPathElements(ListVar<HtmlPathElement> elements) {
+ Var<HtmlPathElement> element = new Var<>();
+ return Sequence(
+ HtmlPathElement(element),
+ elements.append(element),
+ ZeroOrMore(Sequence(Whitespace(), Ch('>'), Whitespace(), HtmlPathElement(element), elements.append(element))));
+ }
+
+ Rule HtmlPathElement(Var<HtmlPathElement> element) {
+ ListVar<String> tagNames = new ListVar<>();
+ ListVar<String> classNames = new ListVar<>();
+ Var<Boolean> fresh = new Var<>();
+ return Sequence(
+ TagNames(tagNames),
+ ClassNames(classNames),
+ Fresh(fresh),
+ element.set(new HtmlPathElement(
+ tagNames.build(),
+ classNames.isEmpty() ? map() : map("class", String.join(" ", classNames.build())),
+ !fresh.get())));
+ }
+
+ Rule TagNames(ListVar<String> tagNames) {
+ return Sequence(
+ Identifier(),
+ tagNames.append(match()),
+ ZeroOrMore(Sequence("|", Identifier(), tagNames.append(match()))));
+ }
+
+ Rule ClassNames(ListVar<String> classNames) {
+ Var<String> className = new Var<>();
+ return ZeroOrMore(Sequence(ClassName(className), classNames.append(className)));
+ }
+
+ Rule ClassName(Var<String> className) {
+ return Sequence(".", Identifier(), className.set(match()));
+ }
+
+ Rule Fresh(Var<Boolean> fresh) {
+ return Sequence(fresh.set(false), Optional(":fresh", fresh.set(true)));
+ }
+
+ Rule SingleQuotedString(Var<String> value) {
+ return Sequence("'", ZeroOrMore(NoneOf("\'")), value.set(match()), "'");
+ }
+
+ Rule Identifier() {
+ return Sequence(
+ FirstOf(CharRange('a', 'z'), CharRange('A', 'Z')),
+ ZeroOrMore(FirstOf(CharRange('a', 'z'), CharRange('A', 'Z'), CharRange('0', '9'), Ch('-'), Ch('_'))));
+ }
+
+ Rule Whitespace() {
+ return ZeroOrMore(AnyOf(" \t"));
+ }
+}
diff --git a/src/main/java/org/zwobble/mammoth/internal/util/Casts.java b/src/main/java/org/zwobble/mammoth/internal/util/Casts.java
new file mode 100644
index 0000000..6c48ca8
--- /dev/null
+++ b/src/main/java/org/zwobble/mammoth/internal/util/Casts.java
@@ -0,0 +1,13 @@
+package org.zwobble.mammoth.internal.util;
+
+import java.util.Optional;
+
+public class Casts {
+ public static <T> Optional<T> tryCast(Class<T> type, Object value) {
+ try {
+ return Optional.of(type.cast(value));
+ } catch (ClassCastException exception) {
+ return Optional.empty();
+ }
+ }
+}
diff --git a/src/main/java/org/zwobble/mammoth/internal/util/InputStreamSupplier.java b/src/main/java/org/zwobble/mammoth/internal/util/InputStreamSupplier.java
new file mode 100644
index 0000000..012b0d3
--- /dev/null
+++ b/src/main/java/org/zwobble/mammoth/internal/util/InputStreamSupplier.java
@@ -0,0 +1,9 @@
+package org.zwobble.mammoth.internal.util;
+
+import java.io.IOException;
+import java.io.InputStream;
+
+@FunctionalInterface
+public interface InputStreamSupplier {
+ InputStream open() throws IOException;
+}
diff --git a/src/main/java/org/zwobble/mammoth/internal/util/MammothIterables.java b/src/main/java/org/zwobble/mammoth/internal/util/MammothIterables.java
new file mode 100644
index 0000000..cd28c14
--- /dev/null
+++ b/src/main/java/org/zwobble/mammoth/internal/util/MammothIterables.java
@@ -0,0 +1,11 @@
+package org.zwobble.mammoth.internal.util;
+
+import com.google.common.collect.Iterables;
+
+import java.util.Optional;
+
+public class MammothIterables {
+ public static <T> Optional<T> tryGetLast(Iterable<T> iterable) {
+ return Optional.ofNullable(Iterables.getLast(iterable, null));
+ }
+}
diff --git a/src/main/java/org/zwobble/mammoth/internal/util/MammothLists.java b/src/main/java/org/zwobble/mammoth/internal/util/MammothLists.java
new file mode 100644
index 0000000..eff5411
--- /dev/null
+++ b/src/main/java/org/zwobble/mammoth/internal/util/MammothLists.java
@@ -0,0 +1,50 @@
+package org.zwobble.mammoth.internal.util;
+
+import com.google.common.base.Function;
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.Iterables;
+import com.google.common.collect.Ordering;
+
+import java.util.List;
+
+public class MammothLists {
+ public static <T> List<T> list() {
+ return ImmutableList.of();
+ }
+
+ public static <T> List<T> list(T value1) {
+ return ImmutableList.of(value1);
+ }
+
+ public static <T> List<T> list(T value1, T value2) {
+ return ImmutableList.of(value1, value2);
+ }
+
+ public static <T> List<T> list(T value1, T value2, T value3) {
+ return ImmutableList.of(value1, value2, value3);
+ }
+
+ public static <T> List<T> cons(T head, Iterable<T> tail) {
+ return concat(list(head), tail);
+ }
+
+ public static <T> List<T> concat(Iterable<T> first, Iterable<T> second) {
+ return ImmutableList.copyOf(Iterables.concat(first, second));
+ }
+
+ public static <T, R> List<R> eagerMap(Iterable<T> iterable, Function<T, R> function) {
+ return ImmutableList.copyOf(Iterables.transform(iterable, function));
+ }
+
+ public static <T, R> List<R> eagerFlatMap(Iterable<T> iterable, Function<T, Iterable<R>> function) {
+ return ImmutableList.copyOf(Iterables.concat(Iterables.transform(iterable, function)));
+ }
+
+ public static <T, R extends Comparable<R>> List<T> orderedBy(Iterable<T> iterable, Function<T, R> getKey) {
+ return orderBy(getKey).sortedCopy(iterable);
+ }
+
+ private static <T, R extends Comparable<R>> Ordering<T> orderBy(Function<T, R> getKey) {
+ return Ordering.from((first, second) -> getKey.apply(first).compareTo(getKey.apply(second)));
+ }
+}
diff --git a/src/main/java/org/zwobble/mammoth/internal/util/MammothMaps.java b/src/main/java/org/zwobble/mammoth/internal/util/MammothMaps.java
new file mode 100644
index 0000000..912a454
--- /dev/null
+++ b/src/main/java/org/zwobble/mammoth/internal/util/MammothMaps.java
@@ -0,0 +1,32 @@
+package org.zwobble.mammoth.internal.util;
+
+import com.google.common.collect.ImmutableMap;
+
+import java.util.Map;
+import java.util.function.Function;
+
+public class MammothMaps {
+ public static <K, V> Map<K, V> map() {
+ return ImmutableMap.of();
+ }
+
+ public static <K, V> Map<K, V> map(K key1, V value1) {
+ return ImmutableMap.of(key1, value1);
+ }
+
+ public static <K, V> Map<K, V> map(K key1, V value1, K key2, V value2) {
+ return ImmutableMap.of(key1, value1, key2, value2);
+ }
+
+ public static <K, V> Map<K, V> map(K key1, V value1, K key2, V value2, K key3, V value3) {
+ return ImmutableMap.of(key1, value1, key2, value2, key3, value3);
+ }
+
+ public static <T, K, V> Map<K, V> toMap(Iterable<T> iterable, Function<T, Map.Entry<K, V>> function) {
+ ImmutableMap.Builder<K, V> builder = ImmutableMap.builder();
+ for (T element : iterable) {
+ builder.put(function.apply(element));
+ }
+ return builder.build();
+ }
+}
diff --git a/src/main/java/org/zwobble/mammoth/internal/util/MammothOptionals.java b/src/main/java/org/zwobble/mammoth/internal/util/MammothOptionals.java
new file mode 100644
index 0000000..b94a168
--- /dev/null
+++ b/src/main/java/org/zwobble/mammoth/internal/util/MammothOptionals.java
@@ -0,0 +1,30 @@
+package org.zwobble.mammoth.internal.util;
+
+import java.util.Optional;
+import java.util.function.BiFunction;
+
+public class MammothOptionals {
+ public static <T1, T2, R> Optional<R> flatMap(
+ Optional<T1> first,
+ Optional<T2> second,
+ BiFunction<T1, T2, Optional<R>> function)
+ {
+ if (first.isPresent() && second.isPresent()) {
+ return function.apply(first.get(), second.get());
+ } else {
+ return Optional.empty();
+ }
+ }
+
+ public static <T1, T2, R> Optional<R> map(
+ Optional<T1> first,
+ Optional<T2> second,
+ BiFunction<T1, T2, R> function)
+ {
+ if (first.isPresent() && second.isPresent()) {
+ return Optional.of(function.apply(first.get(), second.get()));
+ } else {
+ return Optional.empty();
+ }
+ }
+}
diff --git a/src/main/java/org/zwobble/mammoth/internal/util/MammothStrings.java b/src/main/java/org/zwobble/mammoth/internal/util/MammothStrings.java
new file mode 100644
index 0000000..320f9fb
--- /dev/null
+++ b/src/main/java/org/zwobble/mammoth/internal/util/MammothStrings.java
@@ -0,0 +1,11 @@
+package org.zwobble.mammoth.internal.util;
+
+public class MammothStrings {
+ public static String trimLeft(String value, char character) {
+ int index = 0;
+ while (index < value.length() && value.charAt(index) == character) {
+ index++;
+ }
+ return value.substring(index);
+ }
+}
diff --git a/src/main/java/org/zwobble/mammoth/internal/xml/NullXmlElement.java b/src/main/java/org/zwobble/mammoth/internal/xml/NullXmlElement.java
new file mode 100644
index 0000000..ecc7971
--- /dev/null
+++ b/src/main/java/org/zwobble/mammoth/internal/xml/NullXmlElement.java
@@ -0,0 +1,25 @@
+package org.zwobble.mammoth.internal.xml;
+
+import java.util.Optional;
+
+public class NullXmlElement implements XmlElementLike {
+ public static final XmlElementLike INSTANCE = new NullXmlElement();
+
+ private NullXmlElement() {
+ }
+
+ @Override
+ public boolean hasChild(String name) {
+ return false;
+ }
+
+ @Override
+ public XmlElementLike findChildOrEmpty(String name) {
+ return this;
+ }
+
+ @Override
+ public Optional<String> getAttributeOrNone(String name) {
+ return Optional.empty();
+ }
+}
diff --git a/src/main/java/org/zwobble/mammoth/internal/xml/XmlElement.java b/src/main/java/org/zwobble/mammoth/internal/xml/XmlElement.java
new file mode 100644
index 0000000..157d638
--- /dev/null
+++ b/src/main/java/org/zwobble/mammoth/internal/xml/XmlElement.java
@@ -0,0 +1,89 @@
+package org.zwobble.mammoth.internal.xml;
+
+import com.google.common.base.Joiner;
+import com.google.common.collect.ImmutableList;
+
+import java.util.List;
+import java.util.Map;
+import java.util.Optional;
+
+import static com.google.common.collect.Iterables.*;
+
+public class XmlElement implements XmlNode, XmlElementLike {
+ private final String name;
+ private final Map<String, String> attributes;
+ private final List<XmlNode> children;
+
+ public XmlElement(String name, Map<String, String> attributes, List<XmlNode> children) {
+ this.name = name;
+ this.attributes = attributes;
+ this.children = children;
+ }
+
+ public String getName() {
+ return name;
+ }
+
+ public Map<String, String> getAttributes() {
+ return attributes;
+ }
+
+ public String getAttribute(String name) {
+ // TODO: throw more informative error message
+ return getAttributeOrNone(name).get();
+ }
+
+ @Override
+ public Optional<String> getAttributeOrNone(String name) {
+ return Optional.ofNullable(attributes.get(name));
+ }
+
+ public List<XmlNode> getChildren() {
+ return children;
+ }
+
+ @Override
+ public String innerText() {
+ return Joiner.on("").join(transform(children, XmlNode::innerText));
+ }
+
+ @Override
+ public <T> T accept(XmlNodeVisitor<T> visitor) {
+ return visitor.visit(this);
+ }
+
+ public Iterable<XmlNode> children() {
+ return children;
+ }
+
+ @Override
+ public String toString() {
+ return "XmlElement(name=" + name + ", attributes=" + attributes
+ + ", children=" + children + ")";
+ }
+
+ public XmlElementList findChildren(String name) {
+ Iterable<XmlElement> filtered = findChildrenIterable(name);
+ return new XmlElementList(ImmutableList.copyOf(filtered));
+ }
+
+ public XmlElement findChild(String name) {
+ return findChildrenIterable(name).iterator().next();
+ }
+
+ @Override
+ public boolean hasChild(String name) {
+ return findChildrenIterable(name).iterator().hasNext();
+ }
+
+ @Override
+ public XmlElementLike findChildOrEmpty(String name) {
+ return getFirst(findChildrenIterable(name), NullXmlElement.INSTANCE);
+ }
+
+ private Iterable<XmlElement> findChildrenIterable(String name) {
+ return filter(
+ filter(children, XmlElement.class),
+ child -> child.getName().equals(name));
+ }
+}
diff --git a/src/main/java/org/zwobble/mammoth/internal/xml/XmlElementLike.java b/src/main/java/org/zwobble/mammoth/internal/xml/XmlElementLike.java
new file mode 100644
index 0000000..bdb3cc8
--- /dev/null
+++ b/src/main/java/org/zwobble/mammoth/internal/xml/XmlElementLike.java
@@ -0,0 +1,9 @@
+package org.zwobble.mammoth.internal.xml;
+
+import java.util.Optional;
+
+public interface XmlElementLike {
+ boolean hasChild(String name);
+ XmlElementLike findChildOrEmpty(String name);
+ Optional<String> getAttributeOrNone(String name);
+}
diff --git a/src/main/java/org/zwobble/mammoth/internal/xml/XmlElementList.java b/src/main/java/org/zwobble/mammoth/internal/xml/XmlElementList.java
new file mode 100644
index 0000000..3facde4
--- /dev/null
+++ b/src/main/java/org/zwobble/mammoth/internal/xml/XmlElementList.java
@@ -0,0 +1,28 @@
+package org.zwobble.mammoth.internal.xml;
+
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.Iterables;
+
+import java.util.Iterator;
+import java.util.List;
+
+public class XmlElementList implements Iterable<XmlElement> {
+ private final List<XmlElement> elements;
+
+ public XmlElementList(List<XmlElement> elements) {
+ this.elements = elements;
+ }
+
+ @Override
+ public Iterator<XmlElement> iterator() {
+ return elements.iterator();
+ }
+
+ public XmlElementList findChildren(String name) {
+ Iterable<XmlElement> children = Iterables.concat(
+ Iterables.transform(
+ elements,
+ element -> element.findChildren(name)));
+ return new XmlElementList(ImmutableList.copyOf(children));
+ }
+}
diff --git a/src/main/java/org/zwobble/mammoth/internal/xml/XmlNode.java b/src/main/java/org/zwobble/mammoth/internal/xml/XmlNode.java
new file mode 100644
index 0000000..6f6e7fa
--- /dev/null
+++ b/src/main/java/org/zwobble/mammoth/internal/xml/XmlNode.java
@@ -0,0 +1,6 @@
+package org.zwobble.mammoth.internal.xml;
+
+public interface XmlNode {
+ String innerText();
+ <T> T accept(XmlNodeVisitor<T> visitor);
+}
diff --git a/src/main/java/org/zwobble/mammoth/internal/xml/XmlNodeVisitor.java b/src/main/java/org/zwobble/mammoth/internal/xml/XmlNodeVisitor.java
new file mode 100644
index 0000000..5bbef54
--- /dev/null
+++ b/src/main/java/org/zwobble/mammoth/internal/xml/XmlNodeVisitor.java
@@ -0,0 +1,6 @@
+package org.zwobble.mammoth.internal.xml;
+
+public interface XmlNodeVisitor<T> {
+ T visit(XmlElement element);
+ T visit(XmlTextNode textNode);
+}
diff --git a/src/main/java/org/zwobble/mammoth/internal/xml/XmlNodes.java b/src/main/java/org/zwobble/mammoth/internal/xml/XmlNodes.java
new file mode 100644
index 0000000..3b4bab7
--- /dev/null
+++ b/src/main/java/org/zwobble/mammoth/internal/xml/XmlNodes.java
@@ -0,0 +1,29 @@
+package org.zwobble.mammoth.internal.xml;
+
+import java.util.List;
+import java.util.Map;
+
+import static org.zwobble.mammoth.internal.util.MammothLists.list;
+import static org.zwobble.mammoth.internal.util.MammothMaps.map;
+
+public class XmlNodes {
+ public static XmlElement element(String name) {
+ return element(name, list());
+ }
+
+ public static XmlElement element(String name, Map<String, String> attributes) {
+ return element(name, attributes, list());
+ }
+
+ public static XmlElement element(String name, List<XmlNode> children) {
+ return element(name, map(), children);
+ }
+
+ public static XmlElement element(String name, Map<String, String> attributes, List<XmlNode> children) {
+ return new XmlElement(name, attributes, children);
+ }
+
+ public static XmlTextNode text(String value) {
+ return new XmlTextNode(value);
+ }
+}
diff --git a/src/main/java/org/zwobble/mammoth/internal/xml/XmlTextNode.java b/src/main/java/org/zwobble/mammoth/internal/xml/XmlTextNode.java
new file mode 100644
index 0000000..02380ee
--- /dev/null
+++ b/src/main/java/org/zwobble/mammoth/internal/xml/XmlTextNode.java
@@ -0,0 +1,28 @@
+package org.zwobble.mammoth.internal.xml;
+
+public class XmlTextNode implements XmlNode {
+ private final String value;
+
+ public XmlTextNode(String value) {
+ this.value = value;
+ }
+
+ public String getValue() {
+ return value;
+ }
+
+ @Override
+ public String innerText() {
+ return value;
+ }
+
+ @Override
+ public <T> T accept(XmlNodeVisitor<T> visitor) {
+ return visitor.visit(this);
+ }
+
+ @Override
+ public String toString() {
+ return "XmlTextNode(value=" + value + ")";
+ }
+}
diff --git a/src/main/java/org/zwobble/mammoth/internal/xml/parsing/ElementName.java b/src/main/java/org/zwobble/mammoth/internal/xml/parsing/ElementName.java
new file mode 100644
index 0000000..a97a683
--- /dev/null
+++ b/src/main/java/org/zwobble/mammoth/internal/xml/parsing/ElementName.java
@@ -0,0 +1,19 @@
+package org.zwobble.mammoth.internal.xml.parsing;
+
+class ElementName {
+ private final String uri;
+ private final String localName;
+
+ ElementName(String uri, String localName) {
+ this.uri = uri;
+ this.localName = localName;
+ }
+
+ String getUri() {
+ return uri;
+ }
+
+ String getLocalName() {
+ return localName;
+ }
+}
diff --git a/src/main/java/org/zwobble/mammoth/internal/xml/parsing/SimpleSax.java b/src/main/java/org/zwobble/mammoth/internal/xml/parsing/SimpleSax.java
new file mode 100644
index 0000000..047e02d
--- /dev/null
+++ b/src/main/java/org/zwobble/mammoth/internal/xml/parsing/SimpleSax.java
@@ -0,0 +1,64 @@
+package org.zwobble.mammoth.internal.xml.parsing;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.StringReader;
+import java.util.Map;
+import java.util.stream.IntStream;
+
+import javax.xml.parsers.ParserConfigurationException;
+import javax.xml.parsers.SAXParser;
+import javax.xml.parsers.SAXParserFactory;
+
+import org.xml.sax.Attributes;
+import org.xml.sax.InputSource;
+import org.xml.sax.SAXException;
+import org.xml.sax.XMLReader;
+import org.xml.sax.helpers.DefaultHandler;
+
+import static java.util.stream.Collectors.toMap;
+
+class SimpleSax {
+ static void parseStream(InputStream input, SimpleSaxHandler handler) {
+ parseInputSource(new InputSource(input), handler);
+ }
+
+ static void parseString(String value, SimpleSaxHandler handler) {
+ parseInputSource(new InputSource(new StringReader(value)), handler);
+ }
+
+ private static void parseInputSource(InputSource inputSource, SimpleSaxHandler handler) {
+ SAXParserFactory parserFactory = SAXParserFactory.newInstance();
+ parserFactory.setNamespaceAware(true);
+ try {
+ SAXParser saxParser = parserFactory.newSAXParser();
+ XMLReader xmlReader = saxParser.getXMLReader();
+ xmlReader.setContentHandler(new DefaultHandler() {
+ @Override
+ public void startElement(String uri, String localName, String qName, Attributes attributes) throws SAXException {
+ ElementName name = new ElementName(uri, localName);
+ Map<ElementName, String> attributesMap = IntStream.range(0, attributes.getLength())
+ .boxed()
+ .collect(toMap(
+ index -> new ElementName(attributes.getURI(index), attributes.getLocalName(index)),
+ attributes::getValue
+ ));
+ handler.startElement(name, attributesMap);
+ }
+
+ @Override
+ public void endElement(String uri, String localName, String qName) throws SAXException {
+ handler.endElement();
+ }
+
+ @Override
+ public void characters(char[] ch, int start, int length) throws SAXException {
+ handler.characters(new String(ch, start, length));
+ }
+ });
+ xmlReader.parse(inputSource);
+ } catch (ParserConfigurationException | SAXException | IOException exception) {
+ throw new RuntimeException(exception);
+ }
+ }
+}
diff --git a/src/main/java/org/zwobble/mammoth/internal/xml/parsing/SimpleSaxHandler.java b/src/main/java/org/zwobble/mammoth/internal/xml/parsing/SimpleSaxHandler.java
new file mode 100644
index 0000000..8358346
--- /dev/null
+++ b/src/main/java/org/zwobble/mammoth/internal/xml/parsing/SimpleSaxHandler.java
@@ -0,0 +1,9 @@
+package org.zwobble.mammoth.internal.xml.parsing;
+
+import java.util.Map;
+
+interface SimpleSaxHandler {
+ void startElement(ElementName name, Map<ElementName, String> attributes);
+ void endElement();
+ void characters(String string);
+}
diff --git a/src/main/java/org/zwobble/mammoth/internal/xml/parsing/XmlElementBuilder.java b/src/main/java/org/zwobble/mammoth/internal/xml/parsing/XmlElementBuilder.java
new file mode 100644
index 0000000..231299e
--- /dev/null
+++ b/src/main/java/org/zwobble/mammoth/internal/xml/parsing/XmlElementBuilder.java
@@ -0,0 +1,28 @@
+package org.zwobble.mammoth.internal.xml.parsing;
+
+import org.zwobble.mammoth.internal.xml.XmlElement;
+import org.zwobble.mammoth.internal.xml.XmlNode;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
+
+class XmlElementBuilder {
+ private final String name;
+ private final Map<String, String> attributes;
+ private final List<XmlNode> children;
+
+ XmlElementBuilder(String name, Map<String, String> attributes) {
+ this.name = name;
+ this.attributes = attributes;
+ this.children = new ArrayList<>();
+ }
+
+ XmlElement build() {
+ return new XmlElement(name, attributes, children);
+ }
+
+ void addChild(XmlNode node) {
+ children.add(node);
+ }
+}
diff --git a/src/main/java/org/zwobble/mammoth/internal/xml/parsing/XmlParser.java b/src/main/java/org/zwobble/mammoth/internal/xml/parsing/XmlParser.java
new file mode 100644
index 0000000..566dbd6
--- /dev/null
+++ b/src/main/java/org/zwobble/mammoth/internal/xml/parsing/XmlParser.java
@@ -0,0 +1,81 @@
+package org.zwobble.mammoth.internal.xml.parsing;
+
+import java.io.InputStream;
+import java.util.ArrayDeque;
+import java.util.Deque;
+import java.util.Map;
+
+import com.google.common.base.Strings;
+import com.google.common.collect.BiMap;
+import org.zwobble.mammoth.internal.xml.XmlElement;
+import org.zwobble.mammoth.internal.xml.XmlTextNode;
+
+import static java.util.stream.Collectors.toMap;
+
+public class XmlParser {
+ private final BiMap<String, String> namespaces;
+
+ public XmlParser(BiMap<String, String> namespaces) {
+ this.namespaces = namespaces;
+ }
+
+ public XmlElement parseStream(InputStream inputStream) {
+ NodeGenerator nodeGenerator = new NodeGenerator();
+ SimpleSax.parseStream(inputStream, nodeGenerator);
+ return nodeGenerator.getRoot();
+ }
+
+ public XmlElement parseString(String value) {
+ NodeGenerator nodeGenerator = new NodeGenerator();
+ SimpleSax.parseString(value, nodeGenerator);
+ return nodeGenerator.getRoot();
+ }
+
+ private class NodeGenerator implements SimpleSaxHandler {
+ private final Deque<XmlElementBuilder> elementStack;
+
+ public NodeGenerator() {
+ elementStack = new ArrayDeque<>();
+ }
+
+ public XmlElement getRoot() {
+ return elementStack.getFirst().build();
+ }
+
+ @Override
+ public void startElement(ElementName name, Map<ElementName, String> attributes) {
+ Map<String, String> simpleAttributes = attributes
+ .entrySet()
+ .stream()
+ .collect(toMap(
+ entry -> readName(entry.getKey()),
+ entry -> entry.getValue()));
+ XmlElementBuilder element = new XmlElementBuilder(readName(name), simpleAttributes);
+ elementStack.add(element);
+ }
+
+ private String readName(ElementName name) {
+ if (Strings.isNullOrEmpty(name.getUri())) {
+ return name.getLocalName();
+ } else if (namespaces.containsValue(name.getUri())) {
+ return namespaces.inverse().get(name.getUri()) + ":" + name.getLocalName();
+ } else {
+ return "{" + name.getUri() + "}" + name.getLocalName();
+ }
+
+ }
+
+ @Override
+ public void endElement() {
+ if (elementStack.size() > 1) {
+ XmlElementBuilder element = elementStack.removeLast();
+ elementStack.getLast().addChild(element.build());
+ }
+ }
+
+ @Override
+ public void characters(String string) {
+ elementStack.getLast().addChild(new XmlTextNode(string));
+ }
+ }
+}