diff options
| author | Michael Williamson <mike@zwobble.org> | 2016-03-28 23:20:56 +0100 |
|---|---|---|
| committer | Michael Williamson <mike@zwobble.org> | 2016-03-28 23:20:56 +0100 |
| commit | 1b5ee524dd76729a4d790e14245a5a9d7b6dd974 (patch) | |
| tree | a88697626b0dbf1defc11510c269d35e7c1f75bc /src/main/java/org/zwobble/mammoth/internal/conversion/DocumentToHtml.java | |
| parent | 0610baf0b3464b49042b00f5518a501e9b11f3e3 (diff) | |
Move conversion classes to conversion namespace
Diffstat (limited to 'src/main/java/org/zwobble/mammoth/internal/conversion/DocumentToHtml.java')
| -rw-r--r-- | src/main/java/org/zwobble/mammoth/internal/conversion/DocumentToHtml.java | 249 |
1 files changed, 249 insertions, 0 deletions
diff --git a/src/main/java/org/zwobble/mammoth/internal/conversion/DocumentToHtml.java b/src/main/java/org/zwobble/mammoth/internal/conversion/DocumentToHtml.java new file mode 100644 index 0000000..deab0d3 --- /dev/null +++ b/src/main/java/org/zwobble/mammoth/internal/conversion/DocumentToHtml.java @@ -0,0 +1,249 @@ +package org.zwobble.mammoth.internal.conversion; + +import org.zwobble.mammoth.internal.documents.*; +import org.zwobble.mammoth.internal.html.Html; +import org.zwobble.mammoth.internal.html.HtmlNode; +import org.zwobble.mammoth.internal.results.InternalResult; +import org.zwobble.mammoth.internal.styles.HtmlPath; +import org.zwobble.mammoth.internal.styles.StyleMap; +import org.zwobble.mammoth.internal.util.Lists; + +import java.io.IOException; +import java.util.*; + +import static org.zwobble.mammoth.internal.util.Lists.*; +import static org.zwobble.mammoth.internal.util.Maps.map; +import static org.zwobble.mammoth.internal.util.Streams.toByteArray; + +public class DocumentToHtml { + public static InternalResult<List<HtmlNode>> convertToHtml(Document document, DocumentToHtmlOptions options) { + DocumentToHtml documentConverter = new DocumentToHtml(options); + return new InternalResult<>( + documentConverter.convertToHtml(document), + documentConverter.warnings); + } + + private static List<Note> findNotes(Document document, Iterable<NoteReference> noteReferences) { + return eagerMap( + noteReferences, + // TODO: handle missing notes + reference -> document.getNotes().findNote(reference.getNoteType(), reference.getNoteId()).get()); + } + + public static InternalResult<List<HtmlNode>> convertToHtml(DocumentElement element, DocumentToHtmlOptions options) { + DocumentToHtml documentConverter = new DocumentToHtml(options); + return new InternalResult<>( + documentConverter.convertToHtml(element), + documentConverter.warnings); + } + + private final String idPrefix; + private final boolean preserveEmptyParagraphs; + private final StyleMap styleMap; + private final List<NoteReference> noteReferences = new ArrayList<>(); + private final Set<String> warnings = new HashSet<>(); + + private DocumentToHtml(DocumentToHtmlOptions options) { + this.idPrefix = options.idPrefix(); + this.preserveEmptyParagraphs = options.shouldPreserveEmptyParagraphs(); + this.styleMap = options.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", + eagerMap(notes, this::convertToHtml)); + + return eagerConcat(mainBody, list(noteNode)); + } + } + + private HtmlNode convertToHtml(Note note) { + String id = generateNoteHtmlId(note.getNoteType(), note.getId()); + String referenceId = generateNoteRefHtmlId(note.getNoteType(), note.getId()); + List<HtmlNode> noteBody = 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.) + HtmlNode backLink = Html.collapsibleElement("p", list( + Html.text(" "), + Html.element("a", map("href", "#" + referenceId), list(Html.text("↑"))))); + return Html.element("li", map("id", id), eagerConcat(noteBody, list(backLink))); + } + + 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("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().orElse(HtmlPath.collapsibleElement("s")).wrap(nodes); + } + if (run.isUnderline()) { + nodes = styleMap.getUnderline().orElse(HtmlPath.EMPTY).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 = styleMap.getItalic().orElse(HtmlPath.collapsibleElement("em")).wrap(nodes); + } + if (run.isBold()) { + nodes = styleMap.getBold().orElse(HtmlPath.collapsibleElement("strong")).wrap(nodes); + } + HtmlPath mapping = styleMap.getRunHtmlPath(run) + .orElseGet(() -> { + if (run.getStyle().isPresent()) { + warnings.add("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", + Lists.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 { + Map<String, String> attributes = new HashMap<>(); + + String base64 = Base64.getEncoder().encodeToString(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)); + } catch (IOException exception) { + warnings.add(exception.getMessage()); + return Lists.<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; + } +} |
