Add HtmlRenderer asbtraction

This commit is contained in:
Dimitry Ivanov 2018-08-18 16:19:20 +03:00
parent 84a50be0dd
commit 617a1c8d8f
17 changed files with 489 additions and 82 deletions

View File

@ -45,6 +45,16 @@ public interface HtmlTag {
@NonNull
Map<String, String> attributes();
boolean isInline();
boolean isBlock();
@NonNull
Inline getAsInline();
@NonNull
Block getAsBlock();
/**
* Represents <em>really</em> inline HTML tags (unlile commonmark definitions)
*/

View File

@ -79,6 +79,28 @@ abstract class HtmlTagImpl implements HtmlTag {
", attributes=" + attributes +
'}';
}
@Override
public boolean isInline() {
return true;
}
@Override
public boolean isBlock() {
return false;
}
@NonNull
@Override
public Inline getAsInline() {
return this;
}
@NonNull
@Override
public Block getAsBlock() {
throw new ClassCastException("Cannot cast Inline instance to Block");
}
}
static class BlockImpl extends HtmlTagImpl implements Block {
@ -151,6 +173,28 @@ abstract class HtmlTagImpl implements HtmlTag {
return attributes;
}
@Override
public boolean isInline() {
return false;
}
@Override
public boolean isBlock() {
return true;
}
@NonNull
@Override
public Inline getAsInline() {
throw new ClassCastException("Cannot cast Block instance to Inline");
}
@NonNull
@Override
public Block getAsBlock() {
return this;
}
@Override
public String toString() {
return "BlockImpl{" +

View File

@ -2,6 +2,7 @@ package ru.noties.markwon.html.impl;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.support.annotation.VisibleForTesting;
import java.io.IOException;
import java.util.ArrayList;
@ -41,7 +42,8 @@ public class MarkwonHtmlParserImpl extends MarkwonHtmlParser {
}
// https://developer.mozilla.org/en-US/docs/Web/HTML/Inline_elements
private static final Set<String> INLINE_TAGS;
@VisibleForTesting
static final Set<String> INLINE_TAGS;
private static final Set<String> VOID_TAGS;

View File

@ -42,22 +42,7 @@ public class MarkwonHtmlParserImplTest {
});
// all inline tags are parsed as ones
final List<String> tags = Arrays.asList(
"a", "abbr", "acronym",
"b", "bdo", "big", "br", "button",
"cite", "code",
"dfn",
"em",
"i", "img", "input",
"kbd",
"label",
"map",
"object",
"q",
"samp", "script", "select", "small", "span", "strong", "sub", "sup",
"textarea", "time", "tt",
"var"
);
final List<String> tags = new ArrayList<>(MarkwonHtmlParserImpl.INLINE_TAGS);
final StringBuilder html = new StringBuilder();
for (String tag : tags) {

View File

@ -15,6 +15,9 @@ android {
dependencies {
api project(':markwon-html-parser-api')
api project(':markwon-html-parser-impl')
deps.with {
api it['support-annotations']
api it['commonmark']

View File

@ -3,9 +3,10 @@ package ru.noties.markwon;
import android.content.Context;
import android.support.annotation.NonNull;
import ru.noties.markwon.html.api.MarkwonHtmlParser;
import ru.noties.markwon.renderer.ImageSizeResolver;
import ru.noties.markwon.renderer.ImageSizeResolverDef;
import ru.noties.markwon.renderer.html.SpannableHtmlParser;
import ru.noties.markwon.renderer.html2.MarkwonHtmlRenderer;
import ru.noties.markwon.spans.AsyncDrawable;
import ru.noties.markwon.spans.LinkSpan;
import ru.noties.markwon.spans.SpannableTheme;
@ -29,11 +30,13 @@ public class SpannableConfiguration {
private final SyntaxHighlight syntaxHighlight;
private final LinkSpan.Resolver linkResolver;
private final UrlProcessor urlProcessor;
private final SpannableHtmlParser htmlParser;
// private final SpannableHtmlParser htmlParser;
private final ImageSizeResolver imageSizeResolver;
private final SpannableFactory factory; // @since 1.1.0
private final boolean softBreakAddsNewLine; // @since 1.1.1
private final boolean trimWhiteSpaceEnd; // @since 2.0.0
private final MarkwonHtmlParser htmlParser; // @since 2.0.0
private final MarkwonHtmlRenderer htmlRenderer; // @since 2.0.0
private SpannableConfiguration(@NonNull Builder builder) {
this.theme = builder.theme;
@ -41,11 +44,13 @@ public class SpannableConfiguration {
this.syntaxHighlight = builder.syntaxHighlight;
this.linkResolver = builder.linkResolver;
this.urlProcessor = builder.urlProcessor;
this.htmlParser = builder.htmlParser;
// this.htmlParser = builder.htmlParser;
this.imageSizeResolver = builder.imageSizeResolver;
this.factory = builder.factory;
this.softBreakAddsNewLine = builder.softBreakAddsNewLine;
this.trimWhiteSpaceEnd = builder.trimWhiteSpaceEnd;
this.htmlParser = builder.htmlParser;
this.htmlRenderer = builder.htmlRenderer;
}
@NonNull
@ -73,10 +78,10 @@ public class SpannableConfiguration {
return urlProcessor;
}
@NonNull
public SpannableHtmlParser htmlParser() {
return htmlParser;
}
// @NonNull
// public SpannableHtmlParser htmlParser() {
// return htmlParser;
// }
@NonNull
public ImageSizeResolver imageSizeResolver() {
@ -104,6 +109,22 @@ public class SpannableConfiguration {
return trimWhiteSpaceEnd;
}
/**
* @since 2.0.0
*/
@NonNull
public MarkwonHtmlParser htmlParser() {
return htmlParser;
}
/**
* @since 2.0.0
*/
@NonNull
public MarkwonHtmlRenderer htmlRenderer() {
return htmlRenderer;
}
@SuppressWarnings("unused")
public static class Builder {
@ -113,11 +134,13 @@ public class SpannableConfiguration {
private SyntaxHighlight syntaxHighlight;
private LinkSpan.Resolver linkResolver;
private UrlProcessor urlProcessor;
private SpannableHtmlParser htmlParser;
// private SpannableHtmlParser htmlParser;
private ImageSizeResolver imageSizeResolver;
private SpannableFactory factory; // @since 1.1.0
private boolean softBreakAddsNewLine; // @since 1.1.1
private boolean trimWhiteSpaceEnd = true; // @since 2.0.0
private MarkwonHtmlParser htmlParser; // @since 2.0.0
private MarkwonHtmlRenderer htmlRenderer; // @since 2.0.0
Builder(@NonNull Context context) {
this.context = context;
@ -153,11 +176,11 @@ public class SpannableConfiguration {
return this;
}
@NonNull
public Builder htmlParser(@NonNull SpannableHtmlParser htmlParser) {
this.htmlParser = htmlParser;
return this;
}
// @NonNull
// public Builder htmlParser(@NonNull SpannableHtmlParser htmlParser) {
// this.htmlParser = htmlParser;
// return this;
// }
/**
* @since 1.0.1
@ -202,6 +225,24 @@ public class SpannableConfiguration {
return this;
}
/**
* @since 2.0.0
*/
@NonNull
public Builder htmlParser(@NonNull MarkwonHtmlParser htmlParser) {
this.htmlParser = htmlParser;
return this;
}
/**
* @since 2.0.0
*/
@NonNull
public Builder htmlRenderer(@NonNull MarkwonHtmlRenderer htmlRenderer) {
this.htmlRenderer = htmlRenderer;
return this;
}
@NonNull
public SpannableConfiguration build() {
@ -234,16 +275,31 @@ public class SpannableConfiguration {
factory = SpannableFactoryDef.create();
}
// @since 2.0.0
if (htmlParser == null) {
htmlParser = SpannableHtmlParser.create(
factory,
theme,
asyncDrawableLoader,
urlProcessor,
linkResolver,
imageSizeResolver);
try {
// if impl artifact was excluded -> fallback to no-op implementation
htmlParser = ru.noties.markwon.html.impl.MarkwonHtmlParserImpl.create();
} catch (Throwable t) {
htmlParser = MarkwonHtmlParser.noOp();
}
}
// @since 2.0.0
if (htmlRenderer == null) {
htmlRenderer = MarkwonHtmlRenderer.create();
}
// if (htmlParser == null) {
// htmlParser = SpannableHtmlParser.create(
// factory,
// theme,
// asyncDrawableLoader,
// urlProcessor,
// linkResolver,
// imageSizeResolver);
// }
return new SpannableConfiguration(this);
}
}

View File

@ -43,6 +43,7 @@ import java.util.List;
import ru.noties.markwon.SpannableBuilder;
import ru.noties.markwon.SpannableConfiguration;
import ru.noties.markwon.SpannableFactory;
import ru.noties.markwon.html.api.MarkwonHtmlParser;
import ru.noties.markwon.renderer.html.SpannableHtmlParser;
import ru.noties.markwon.spans.SpannableTheme;
import ru.noties.markwon.spans.TableRowSpan;
@ -54,7 +55,8 @@ public class SpannableMarkdownVisitor extends AbstractVisitor {
private final SpannableConfiguration configuration;
private final SpannableBuilder builder;
private final Deque<HtmlInlineItem> htmlInlineItems;
private final MarkwonHtmlParser htmlParser;
// private final Deque<HtmlInlineItem> htmlInlineItems;
private final SpannableTheme theme;
private final SpannableFactory factory;
@ -72,7 +74,8 @@ public class SpannableMarkdownVisitor extends AbstractVisitor {
) {
this.configuration = configuration;
this.builder = builder;
this.htmlInlineItems = new ArrayDeque<>(2);
this.htmlParser = configuration.htmlParser();
// this.htmlInlineItems = new ArrayDeque<>(2);
this.theme = configuration.theme();
this.factory = configuration.factory();
@ -82,6 +85,8 @@ public class SpannableMarkdownVisitor extends AbstractVisitor {
public void visit(Document document) {
super.visit(document);
configuration.htmlRenderer().render(configuration, builder, htmlParser);
if (configuration.trimWhiteSpaceEnd()) {
builder.trimWhiteSpaceEnd();
}
@ -445,47 +450,59 @@ public class SpannableMarkdownVisitor extends AbstractVisitor {
@Override
public void visit(HtmlBlock htmlBlock) {
// http://spec.commonmark.org/0.18/#html-blocks
final Spanned spanned = configuration.htmlParser().getSpanned(null, htmlBlock.getLiteral());
if (!TextUtils.isEmpty(spanned)) {
builder.append(spanned);
}
// // http://spec.commonmark.org/0.18/#html-blocks
// final Spanned spanned = configuration.htmlParser().getSpanned(null, htmlBlock.getLiteral());
// if (!TextUtils.isEmpty(spanned)) {
// builder.append(spanned);
// }
// htmlParser.processFragment(builder, htmlBlock.getLiteral());
visitHtml(htmlBlock.getLiteral());
}
@Override
public void visit(HtmlInline htmlInline) {
final SpannableHtmlParser htmlParser = configuration.htmlParser();
final SpannableHtmlParser.Tag tag = htmlParser.parseTag(htmlInline.getLiteral());
visitHtml(htmlInline.getLiteral());
if (tag != null) {
// htmlParser.processFragment(builder, htmlInline.getLiteral());
final boolean voidTag = tag.voidTag();
if (!voidTag && tag.opening()) {
// push in stack
htmlInlineItems.push(new HtmlInlineItem(tag, builder.length()));
visitChildren(htmlInline);
} else {
// final SpannableHtmlParser htmlParser = configuration.htmlParser();
// final SpannableHtmlParser.Tag tag = htmlParser.parseTag(htmlInline.getLiteral());
//
// if (tag != null) {
//
// final boolean voidTag = tag.voidTag();
// if (!voidTag && tag.opening()) {
// // push in stack
// htmlInlineItems.push(new HtmlInlineItem(tag, builder.length()));
// visitChildren(htmlInline);
// } else {
//
// if (!voidTag) {
// if (htmlInlineItems.size() > 0) {
// final HtmlInlineItem item = htmlInlineItems.pop();
// final Object span = htmlParser.getSpanForTag(item.tag);
// setSpan(item.start, span);
// }
// } else {
//
// final Spanned html = htmlParser.getSpanned(tag, htmlInline.getLiteral());
// if (!TextUtils.isEmpty(html)) {
// builder.append(html);
// }
//
// }
// }
// } else {
// // todo, should we append just literal?
//// builder.append(htmlInline.getLiteral());
// visitChildren(htmlInline);
// }
}
if (!voidTag) {
if (htmlInlineItems.size() > 0) {
final HtmlInlineItem item = htmlInlineItems.pop();
final Object span = htmlParser.getSpanForTag(item.tag);
setSpan(item.start, span);
}
} else {
final Spanned html = htmlParser.getSpanned(tag, htmlInline.getLiteral());
if (!TextUtils.isEmpty(html)) {
builder.append(html);
}
}
}
} else {
// todo, should we append just literal?
// builder.append(htmlInline.getLiteral());
visitChildren(htmlInline);
private void visitHtml(@Nullable String html) {
if (html != null) {
htmlParser.processFragment(builder, html);
}
}
@ -552,14 +569,14 @@ public class SpannableMarkdownVisitor extends AbstractVisitor {
return out;
}
private static class HtmlInlineItem {
final SpannableHtmlParser.Tag tag;
final int start;
HtmlInlineItem(SpannableHtmlParser.Tag tag, int start) {
this.tag = tag;
this.start = start;
}
}
// private static class HtmlInlineItem {
//
// final SpannableHtmlParser.Tag tag;
// final int start;
//
// HtmlInlineItem(SpannableHtmlParser.Tag tag, int start) {
// this.tag = tag;
// this.start = start;
// }
// }
}

View File

@ -0,0 +1,82 @@
package ru.noties.markwon.renderer.html2;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import java.util.Collections;
import java.util.HashMap;
import java.util.Locale;
import java.util.Map;
import ru.noties.markwon.SpannableBuilder;
import ru.noties.markwon.SpannableConfiguration;
import ru.noties.markwon.html.api.MarkwonHtmlParser;
import ru.noties.markwon.renderer.html2.tag.EmphasisHandler;
import ru.noties.markwon.renderer.html2.tag.LinkHandler;
import ru.noties.markwon.renderer.html2.tag.StrikeHandler;
import ru.noties.markwon.renderer.html2.tag.StrongEmphasisHandler;
import ru.noties.markwon.renderer.html2.tag.SubScriptHandler;
import ru.noties.markwon.renderer.html2.tag.SuperScriptHandler;
import ru.noties.markwon.renderer.html2.tag.TagHandler;
import ru.noties.markwon.renderer.html2.tag.UnderlineHandler;
/**
* @since 2.0.0
*/
public abstract class MarkwonHtmlRenderer {
public abstract void render(
@NonNull SpannableConfiguration configuration,
@NonNull SpannableBuilder builder,
@NonNull MarkwonHtmlParser parser
);
@Nullable
public abstract TagHandler tagHandler(@NonNull String tagName);
@NonNull
public static MarkwonHtmlRenderer create() {
final EmphasisHandler emphasisHandler = new EmphasisHandler();
final StrongEmphasisHandler strongEmphasisHandler = new StrongEmphasisHandler();
final StrikeHandler strikeHandler = new StrikeHandler();
return builder()
.handler("i", emphasisHandler)
.handler("em", emphasisHandler)
.handler("cite", emphasisHandler)
.handler("dfn", emphasisHandler)
.handler("b", strongEmphasisHandler)
.handler("strong", strongEmphasisHandler)
.handler("sup", new SuperScriptHandler())
.handler("sub", new SubScriptHandler())
.handler("u", new UnderlineHandler())
.handler("del", strikeHandler)
.handler("s", strikeHandler)
.handler("strike", strikeHandler)
.handler("a", new LinkHandler())
.build();
}
@NonNull
public static Builder builder() {
return new Builder();
}
public static class Builder {
private final Map<String, TagHandler> tagHandlers = new HashMap<>(2);
public Builder handler(@NonNull String tagName, @NonNull TagHandler tagHandler) {
tagHandlers.put(tagName.toLowerCase(Locale.US), tagHandler);
return this;
}
@NonNull
public MarkwonHtmlRenderer build() {
return new MarkwonHtmlRendererImpl(
Collections.unmodifiableMap(tagHandlers)
);
}
}
}

View File

@ -0,0 +1,81 @@
package ru.noties.markwon.renderer.html2;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.text.Spanned;
import java.util.List;
import java.util.Map;
import ru.noties.markwon.SpannableBuilder;
import ru.noties.markwon.SpannableConfiguration;
import ru.noties.markwon.html.api.HtmlTag;
import ru.noties.markwon.html.api.MarkwonHtmlParser;
import ru.noties.markwon.renderer.html2.tag.TagHandler;
class MarkwonHtmlRendererImpl extends MarkwonHtmlRenderer {
private final Map<String, TagHandler> tagHandlers;
MarkwonHtmlRendererImpl(@NonNull Map<String, TagHandler> tagHandlers) {
this.tagHandlers = tagHandlers;
}
@Override
public void render(
@NonNull final SpannableConfiguration configuration,
@NonNull final SpannableBuilder builder,
@NonNull MarkwonHtmlParser parser) {
final int length = builder.length();
parser.flushInlineTags(length, new MarkwonHtmlParser.FlushAction<HtmlTag.Inline>() {
@Override
public void apply(@NonNull List<HtmlTag.Inline> tags) {
TagHandler handler;
for (HtmlTag.Inline inline : tags) {
handler = tagHandler(inline.name());
if (handler != null) {
setSpans(builder, handler.getSpans(configuration, inline), inline.start(), inline.end());
}
}
}
});
parser.flushBlockTags(length, new MarkwonHtmlParser.FlushAction<HtmlTag.Block>() {
@Override
public void apply(@NonNull List<HtmlTag.Block> tags) {
TagHandler handler;
for (HtmlTag.Block block : tags) {
handler = tagHandler(block.name());
if (handler != null) {
setSpans(builder, handler.getSpans(configuration, block), block.start(), block.end());
} else {
// see if any of children can be handled
apply(block.children());
}
}
}
});
parser.reset();
}
@Nullable
@Override
public TagHandler tagHandler(@NonNull String tagName) {
return tagHandlers.get(tagName);
}
private static void setSpans(@NonNull SpannableBuilder builder, @Nullable Object spans, int start, int end) {
if (spans != null) {
if (spans.getClass().isArray()) {
for (Object o : ((Object[]) spans)) {
builder.setSpan(o, start, end, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
}
} else {
builder.setSpan(spans, start, end, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
}
}
}
}

View File

@ -0,0 +1,15 @@
package ru.noties.markwon.renderer.html2.tag;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import ru.noties.markwon.SpannableConfiguration;
import ru.noties.markwon.html.api.HtmlTag;
public class EmphasisHandler implements TagHandler {
@Nullable
@Override
public Object getSpans(@NonNull SpannableConfiguration configuration, @NonNull HtmlTag tag) {
return configuration.factory().emphasis();
}
}

View File

@ -0,0 +1,24 @@
package ru.noties.markwon.renderer.html2.tag;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.text.TextUtils;
import ru.noties.markwon.SpannableConfiguration;
import ru.noties.markwon.html.api.HtmlTag;
public class LinkHandler implements TagHandler {
@Nullable
@Override
public Object getSpans(@NonNull SpannableConfiguration configuration, @NonNull HtmlTag tag) {
final String destination = tag.attributes().get("src");
if (!TextUtils.isEmpty(destination)) {
return configuration.factory().link(
configuration.theme(),
configuration.urlProcessor().process(destination),
configuration.linkResolver()
);
}
return null;
}
}

View File

@ -0,0 +1,15 @@
package ru.noties.markwon.renderer.html2.tag;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import ru.noties.markwon.SpannableConfiguration;
import ru.noties.markwon.html.api.HtmlTag;
public class StrikeHandler implements TagHandler {
@Nullable
@Override
public Object getSpans(@NonNull SpannableConfiguration configuration, @NonNull HtmlTag tag) {
return configuration.factory().strikethrough();
}
}

View File

@ -0,0 +1,15 @@
package ru.noties.markwon.renderer.html2.tag;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import ru.noties.markwon.SpannableConfiguration;
import ru.noties.markwon.html.api.HtmlTag;
public class StrongEmphasisHandler implements TagHandler {
@Nullable
@Override
public Object getSpans(@NonNull SpannableConfiguration configuration, @NonNull HtmlTag tag) {
return configuration.factory().strongEmphasis();
}
}

View File

@ -0,0 +1,15 @@
package ru.noties.markwon.renderer.html2.tag;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import ru.noties.markwon.SpannableConfiguration;
import ru.noties.markwon.html.api.HtmlTag;
public class SubScriptHandler implements TagHandler {
@Nullable
@Override
public Object getSpans(@NonNull SpannableConfiguration configuration, @NonNull HtmlTag tag) {
return configuration.factory().subScript(configuration.theme());
}
}

View File

@ -0,0 +1,15 @@
package ru.noties.markwon.renderer.html2.tag;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import ru.noties.markwon.SpannableConfiguration;
import ru.noties.markwon.html.api.HtmlTag;
public class SuperScriptHandler implements TagHandler {
@Nullable
@Override
public Object getSpans(@NonNull SpannableConfiguration configuration, @NonNull HtmlTag tag) {
return configuration.factory().superScript(configuration.theme());
}
}

View File

@ -0,0 +1,13 @@
package ru.noties.markwon.renderer.html2.tag;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import ru.noties.markwon.SpannableConfiguration;
import ru.noties.markwon.html.api.HtmlTag;
public interface TagHandler {
@Nullable
Object getSpans(@NonNull SpannableConfiguration configuration, @NonNull HtmlTag tag);
}

View File

@ -0,0 +1,15 @@
package ru.noties.markwon.renderer.html2.tag;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import ru.noties.markwon.SpannableConfiguration;
import ru.noties.markwon.html.api.HtmlTag;
public class UnderlineHandler implements TagHandler {
@Nullable
@Override
public Object getSpans(@NonNull SpannableConfiguration configuration, @NonNull HtmlTag tag) {
return configuration.factory().underline();
}
}