diff options
| author | Michael Williamson <mike@zwobble.org> | 2016-02-24 08:16:16 +0000 |
|---|---|---|
| committer | Michael Williamson <mike@zwobble.org> | 2016-02-24 08:16:16 +0000 |
| commit | 3e2786a004eddf968a5fa7d5a92dff5b1b5f7734 (patch) | |
| tree | 7fa1bcc1e484cd0ef782f64baf504ee83333d397 /src/main/java/org/zwobble/mammoth/internal | |
| parent | 0dc3a12affc2e46c1a0df2899cf51a1ae3fdbd61 (diff) | |
Create internal package
Diffstat (limited to 'src/main/java/org/zwobble/mammoth/internal')
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("&", "&").replace("<", "<").replace(">", ">"); + } + + private static String escapeAttributeValue(String value) { + return escapeText(value).replace("\"", """); + } +} 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)); + } + } +} |
