Move all html entities to markwon-html module

This commit is contained in:
Dimitry Ivanov 2018-11-26 15:16:11 +03:00
parent 2efd12f020
commit 27ed17aaff
83 changed files with 761 additions and 1797 deletions
app/src/main/java/ru/noties/markwon
markwon-html-parser-api
markwon-html
markwon-image-loader
markwon-syntax-highlight/src/main/java/ru/noties/markwon/syntax
markwon
settings.gradle

@ -77,10 +77,6 @@ public class MarkdownRenderer {
? prism4jThemeDefault
: prism4JThemeDarkula;
// final int background = isLightTheme
// ? prism4jTheme.background()
// : 0x0Fffffff;
final Markwon2 markwon2 = Markwon2.builder(context)
.use(CorePlugin.create())
.use(ImagesPlugin.createWithAssets(context))

@ -1,23 +0,0 @@
apply plugin: 'com.android.library'
android {
compileSdkVersion config['compile-sdk']
buildToolsVersion config['build-tools']
defaultConfig {
minSdkVersion config['min-sdk']
targetSdkVersion config['target-sdk']
versionCode 1
versionName version
}
}
dependencies {
deps.with {
api it['support-annotations']
}
}
registerArtifact(this)

@ -1,3 +0,0 @@
POM_NAME=Markwon
POM_ARTIFACT_ID=markwon-html-parser-api
POM_PACKAGING=aar

@ -1 +0,0 @@
<manifest package="ru.noties.markwon.html.api" />

@ -15,7 +15,7 @@ android {
dependencies {
api project(':markwon-html-parser-api')
api project(':markwon')
deps.with {
api it['support-annotations']
@ -25,6 +25,7 @@ dependencies {
deps.test.with {
testImplementation it['junit']
testImplementation it['robolectric']
testImplementation it['ix-java']
}
}

@ -1,4 +1,4 @@
package ru.noties.markwon.renderer.html2;
package ru.noties.markwon.html.impl;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;

@ -1,4 +1,4 @@
package ru.noties.markwon.renderer.html2;
package ru.noties.markwon.html.impl;
import android.support.annotation.NonNull;

@ -3,7 +3,7 @@ package ru.noties.markwon.html.impl;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import ru.noties.markwon.html.api.HtmlTag;
import ru.noties.markwon.html.HtmlTag;
/**
* This class will be used to append some text to output in order to

@ -7,7 +7,7 @@ import java.util.Collections;
import java.util.List;
import java.util.Map;
import ru.noties.markwon.html.api.HtmlTag;
import ru.noties.markwon.html.HtmlTag;
abstract class HtmlTagImpl implements HtmlTag {

@ -14,10 +14,10 @@ import java.util.Locale;
import java.util.Map;
import java.util.Set;
import ru.noties.markwon.html.api.HtmlTag;
import ru.noties.markwon.html.api.HtmlTag.Block;
import ru.noties.markwon.html.api.HtmlTag.Inline;
import ru.noties.markwon.html.api.MarkwonHtmlParser;
import ru.noties.markwon.html.HtmlTag;
import ru.noties.markwon.html.HtmlTag.Block;
import ru.noties.markwon.html.HtmlTag.Inline;
import ru.noties.markwon.html.MarkwonHtmlParser;
import ru.noties.markwon.html.impl.jsoup.nodes.Attribute;
import ru.noties.markwon.html.impl.jsoup.nodes.Attributes;
import ru.noties.markwon.html.impl.jsoup.parser.CharacterReader;

@ -0,0 +1,167 @@
package ru.noties.markwon.html.impl;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import ru.noties.markwon.MarkwonConfiguration;
import ru.noties.markwon.SpannableBuilder;
import ru.noties.markwon.html.HtmlTag;
import ru.noties.markwon.html.MarkwonHtmlParser;
import ru.noties.markwon.html.MarkwonHtmlRenderer;
import ru.noties.markwon.html.TagHandler;
import ru.noties.markwon.html.impl.tag.BlockquoteHandler;
import ru.noties.markwon.html.impl.tag.EmphasisHandler;
import ru.noties.markwon.html.impl.tag.HeadingHandler;
import ru.noties.markwon.html.impl.tag.ImageHandler;
import ru.noties.markwon.html.impl.tag.LinkHandler;
import ru.noties.markwon.html.impl.tag.ListHandler;
import ru.noties.markwon.html.impl.tag.StrikeHandler;
import ru.noties.markwon.html.impl.tag.StrongEmphasisHandler;
import ru.noties.markwon.html.impl.tag.SubScriptHandler;
import ru.noties.markwon.html.impl.tag.SuperScriptHandler;
import ru.noties.markwon.html.impl.tag.UnderlineHandler;
public class MarkwonHtmlRendererImpl extends MarkwonHtmlRenderer {
@NonNull
public static MarkwonHtmlRendererImpl create() {
return builderWithDefaults().build();
}
@NonNull
public static Builder builderWithDefaults() {
final EmphasisHandler emphasisHandler = new EmphasisHandler();
final StrongEmphasisHandler strongEmphasisHandler = new StrongEmphasisHandler();
final StrikeHandler strikeHandler = new StrikeHandler();
final UnderlineHandler underlineHandler = new UnderlineHandler();
final ListHandler listHandler = new ListHandler();
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", underlineHandler)
.handler("ins", underlineHandler)
.handler("del", strikeHandler)
.handler("s", strikeHandler)
.handler("strike", strikeHandler)
.handler("a", new LinkHandler())
.handler("ul", listHandler)
.handler("ol", listHandler)
.handler("img", ImageHandler.create())
.handler("blockquote", new BlockquoteHandler())
.handler("h1", new HeadingHandler(1))
.handler("h2", new HeadingHandler(2))
.handler("h3", new HeadingHandler(3))
.handler("h4", new HeadingHandler(4))
.handler("h5", new HeadingHandler(5))
.handler("h6", new HeadingHandler(6));
}
@NonNull
public static Builder builder() {
return new Builder();
}
public static final float SCRIPT_DEF_TEXT_SIZE_RATIO = .75F;
private final Map<String, TagHandler> tagHandlers;
private MarkwonHtmlRendererImpl(@NonNull Map<String, TagHandler> tagHandlers) {
this.tagHandlers = tagHandlers;
}
@Override
public void render(
@NonNull final MarkwonConfiguration configuration,
@NonNull final SpannableBuilder builder,
@NonNull MarkwonHtmlParser parser) {
final int end;
if (!configuration.htmlAllowNonClosedTags()) {
end = HtmlTag.NO_END;
} else {
end = builder.length();
}
parser.flushInlineTags(end, new MarkwonHtmlParser.FlushAction<HtmlTag.Inline>() {
@Override
public void apply(@NonNull List<HtmlTag.Inline> tags) {
TagHandler handler;
for (HtmlTag.Inline inline : tags) {
// if tag is not closed -> do not render
if (!inline.isClosed()) {
continue;
}
handler = tagHandler(inline.name());
if (handler != null) {
handler.handle(configuration, builder, inline);
}
}
}
});
parser.flushBlockTags(end, new MarkwonHtmlParser.FlushAction<HtmlTag.Block>() {
@Override
public void apply(@NonNull List<HtmlTag.Block> tags) {
TagHandler handler;
for (HtmlTag.Block block : tags) {
if (!block.isClosed()) {
continue;
}
handler = tagHandler(block.name());
if (handler != null) {
handler.handle(configuration, builder, block);
} 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);
}
public static class Builder {
private final Map<String, TagHandler> tagHandlers = new HashMap<>(2);
@NonNull
public Builder handler(@NonNull String tagName, @NonNull TagHandler tagHandler) {
tagHandlers.put(tagName.toLowerCase(Locale.US), tagHandler);
return this;
}
@NonNull
public MarkwonHtmlRendererImpl build() {
return new MarkwonHtmlRendererImpl(Collections.unmodifiableMap(tagHandlers));
}
}
}

@ -0,0 +1,25 @@
package ru.noties.markwon.html.impl.span;
import android.support.annotation.NonNull;
import android.text.TextPaint;
import android.text.style.MetricAffectingSpan;
import ru.noties.markwon.html.impl.MarkwonHtmlRendererImpl;
public class SubScriptSpan extends MetricAffectingSpan {
@Override
public void updateDrawState(TextPaint tp) {
apply(tp);
}
@Override
public void updateMeasureState(@NonNull TextPaint tp) {
apply(tp);
}
private void apply(TextPaint paint) {
paint.setTextSize(paint.getTextSize() * MarkwonHtmlRendererImpl.SCRIPT_DEF_TEXT_SIZE_RATIO);
paint.baselineShift -= (int) (paint.ascent() / 2);
}
}

@ -0,0 +1,25 @@
package ru.noties.markwon.html.impl.span;
import android.support.annotation.NonNull;
import android.text.TextPaint;
import android.text.style.MetricAffectingSpan;
import ru.noties.markwon.html.impl.MarkwonHtmlRendererImpl;
public class SuperScriptSpan extends MetricAffectingSpan {
@Override
public void updateDrawState(TextPaint tp) {
apply(tp);
}
@Override
public void updateMeasureState(@NonNull TextPaint tp) {
apply(tp);
}
private void apply(TextPaint paint) {
paint.setTextSize(paint.getTextSize() * MarkwonHtmlRendererImpl.SCRIPT_DEF_TEXT_SIZE_RATIO);
paint.baselineShift += (int) (paint.ascent() / 2);
}
}

@ -1,10 +1,11 @@
package ru.noties.markwon.renderer.html2.tag;
package ru.noties.markwon.html.impl.tag;
import android.support.annotation.NonNull;
import ru.noties.markwon.SpannableBuilder;
import ru.noties.markwon.MarkwonConfiguration;
import ru.noties.markwon.html.api.HtmlTag;
import ru.noties.markwon.SpannableBuilder;
import ru.noties.markwon.html.HtmlTag;
import ru.noties.markwon.html.TagHandler;
public class BlockquoteHandler extends TagHandler {

@ -1,10 +1,10 @@
package ru.noties.markwon.renderer.html2.tag;
package ru.noties.markwon.html.impl.tag;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import ru.noties.markwon.MarkwonConfiguration;
import ru.noties.markwon.html.api.HtmlTag;
import ru.noties.markwon.html.HtmlTag;
public class EmphasisHandler extends SimpleTagHandler {
@Nullable

@ -1,10 +1,10 @@
package ru.noties.markwon.renderer.html2.tag;
package ru.noties.markwon.html.impl.tag;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import ru.noties.markwon.MarkwonConfiguration;
import ru.noties.markwon.html.api.HtmlTag;
import ru.noties.markwon.html.HtmlTag;
public class HeadingHandler extends SimpleTagHandler {

@ -1,4 +1,4 @@
package ru.noties.markwon.renderer.html2.tag;
package ru.noties.markwon.html.impl.tag;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
@ -7,9 +7,9 @@ import android.text.TextUtils;
import java.util.Map;
import ru.noties.markwon.MarkwonConfiguration;
import ru.noties.markwon.html.api.HtmlTag;
import ru.noties.markwon.html.HtmlTag;
import ru.noties.markwon.html.impl.CssInlineStyleParser;
import ru.noties.markwon.renderer.ImageSize;
import ru.noties.markwon.renderer.html2.CssInlineStyleParser;
public class ImageHandler extends SimpleTagHandler {

@ -1,4 +1,4 @@
package ru.noties.markwon.renderer.html2.tag;
package ru.noties.markwon.html.impl.tag;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
@ -7,9 +7,9 @@ import android.text.TextUtils;
import java.util.Map;
import ru.noties.markwon.html.impl.CssInlineStyleParser;
import ru.noties.markwon.html.impl.CssProperty;
import ru.noties.markwon.renderer.ImageSize;
import ru.noties.markwon.renderer.html2.CssInlineStyleParser;
import ru.noties.markwon.renderer.html2.CssProperty;
class ImageSizeParserImpl implements ImageHandler.ImageSizeParser {

@ -1,11 +1,11 @@
package ru.noties.markwon.renderer.html2.tag;
package ru.noties.markwon.html.impl.tag;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.text.TextUtils;
import ru.noties.markwon.MarkwonConfiguration;
import ru.noties.markwon.html.api.HtmlTag;
import ru.noties.markwon.html.HtmlTag;
public class LinkHandler extends SimpleTagHandler {
@Nullable

@ -1,10 +1,11 @@
package ru.noties.markwon.renderer.html2.tag;
package ru.noties.markwon.html.impl.tag;
import android.support.annotation.NonNull;
import ru.noties.markwon.MarkwonConfiguration;
import ru.noties.markwon.SpannableBuilder;
import ru.noties.markwon.html.api.HtmlTag;
import ru.noties.markwon.html.HtmlTag;
import ru.noties.markwon.html.TagHandler;
public class ListHandler extends TagHandler {

@ -1,11 +1,12 @@
package ru.noties.markwon.renderer.html2.tag;
package ru.noties.markwon.html.impl.tag;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import ru.noties.markwon.SpannableBuilder;
import ru.noties.markwon.MarkwonConfiguration;
import ru.noties.markwon.html.api.HtmlTag;
import ru.noties.markwon.SpannableBuilder;
import ru.noties.markwon.html.HtmlTag;
import ru.noties.markwon.html.TagHandler;
public abstract class SimpleTagHandler extends TagHandler {

@ -1,10 +1,11 @@
package ru.noties.markwon.renderer.html2.tag;
package ru.noties.markwon.html.impl.tag;
import android.support.annotation.NonNull;
import ru.noties.markwon.MarkwonConfiguration;
import ru.noties.markwon.SpannableBuilder;
import ru.noties.markwon.html.api.HtmlTag;
import ru.noties.markwon.html.HtmlTag;
import ru.noties.markwon.html.TagHandler;
public class StrikeHandler extends TagHandler {

@ -1,10 +1,10 @@
package ru.noties.markwon.renderer.html2.tag;
package ru.noties.markwon.html.impl.tag;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import ru.noties.markwon.MarkwonConfiguration;
import ru.noties.markwon.html.api.HtmlTag;
import ru.noties.markwon.html.HtmlTag;
public class StrongEmphasisHandler extends SimpleTagHandler {
@Nullable

@ -1,15 +1,16 @@
package ru.noties.markwon.renderer.html2.tag;
package ru.noties.markwon.html.impl.tag;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import ru.noties.markwon.MarkwonConfiguration;
import ru.noties.markwon.html.api.HtmlTag;
import ru.noties.markwon.html.HtmlTag;
import ru.noties.markwon.html.impl.span.SubScriptSpan;
public class SubScriptHandler extends SimpleTagHandler {
@Nullable
@Override
public Object getSpans(@NonNull MarkwonConfiguration configuration, @NonNull HtmlTag tag) {
return configuration.factory().subScript(configuration.theme());
return new SubScriptSpan();
}
}

@ -1,15 +1,16 @@
package ru.noties.markwon.renderer.html2.tag;
package ru.noties.markwon.html.impl.tag;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import ru.noties.markwon.MarkwonConfiguration;
import ru.noties.markwon.html.api.HtmlTag;
import ru.noties.markwon.html.HtmlTag;
import ru.noties.markwon.html.impl.span.SuperScriptSpan;
public class SuperScriptHandler extends SimpleTagHandler {
@Nullable
@Override
public Object getSpans(@NonNull MarkwonConfiguration configuration, @NonNull HtmlTag tag) {
return configuration.factory().superScript(configuration.theme());
return new SuperScriptSpan();
}
}

@ -1,10 +1,12 @@
package ru.noties.markwon.renderer.html2.tag;
package ru.noties.markwon.html.impl.tag;
import android.support.annotation.NonNull;
import android.text.style.UnderlineSpan;
import ru.noties.markwon.SpannableBuilder;
import ru.noties.markwon.MarkwonConfiguration;
import ru.noties.markwon.html.api.HtmlTag;
import ru.noties.markwon.SpannableBuilder;
import ru.noties.markwon.html.HtmlTag;
import ru.noties.markwon.html.TagHandler;
public class UnderlineHandler extends TagHandler {
@ -23,7 +25,7 @@ public class UnderlineHandler extends TagHandler {
SpannableBuilder.setSpans(
builder,
configuration.factory().underline(),
new UnderlineSpan(),
tag.start(),
tag.end()
);

@ -0,0 +1,239 @@
package ru.noties.markwon.html.impl;
import android.support.annotation.NonNull;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.robolectric.RobolectricTestRunner;
import org.robolectric.annotation.Config;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import ix.Ix;
import ix.IxFunction;
import ru.noties.markwon.test.TestUtils;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull;
import static ru.noties.markwon.test.TestUtils.with;
@RunWith(RobolectricTestRunner.class)
@Config(manifest = Config.NONE)
public class CssInlineStyleParserTest {
private CssInlineStyleParser.Impl impl;
@Before
public void before() {
impl = new CssInlineStyleParser.Impl();
}
@Test
public void simple_single_pair() {
final String input = "key: value;";
final List<CssProperty> list = listProperties(input);
assertEquals(1, list.size());
with(list.get(0), new TestUtils.Action<CssProperty>() {
@Override
public void apply(@NonNull CssProperty cssProperty) {
assertEquals("key", cssProperty.key());
assertEquals("value", cssProperty.value());
}
});
}
@Test
public void simple_two_pairs() {
final String input = "key1: value1; key2: value2;";
final List<CssProperty> list = listProperties(input);
assertEquals(2, list.size());
with(list.get(0), new TestUtils.Action<CssProperty>() {
@Override
public void apply(@NonNull CssProperty cssProperty) {
assertEquals("key1", cssProperty.key());
assertEquals("value1", cssProperty.value());
}
});
with(list.get(1), new TestUtils.Action<CssProperty>() {
@Override
public void apply(@NonNull CssProperty cssProperty) {
assertEquals("key2", cssProperty.key());
assertEquals("value2", cssProperty.value());
}
});
}
@Test
public void one_pair_eof() {
final String input = "key: value";
final List<CssProperty> list = listProperties(input);
assertEquals(1, list.size());
with(list.get(0), new TestUtils.Action<CssProperty>() {
@Override
public void apply(@NonNull CssProperty cssProperty) {
assertEquals("key", cssProperty.key());
assertEquals("value", cssProperty.value());
}
});
}
@Test
public void one_pair_eof_whitespaces() {
final String input = "key: value \n\n\t";
final List<CssProperty> list = listProperties(input);
assertEquals(1, list.size());
with(list.get(0), new TestUtils.Action<CssProperty>() {
@Override
public void apply(@NonNull CssProperty cssProperty) {
assertEquals("key", cssProperty.key());
assertEquals("value", cssProperty.value());
}
});
}
@Test
public void white_spaces() {
final String input = "\n\n\n\t \t key1 \n\n\n\t : \n\n\n\n \t value1 \n\n\n\n ; \n key2\n : \n value2 \n ; ";
final List<CssProperty> list = listProperties(input);
assertEquals(2, list.size());
with(list.get(0), new TestUtils.Action<CssProperty>() {
@Override
public void apply(@NonNull CssProperty cssProperty) {
assertEquals("key1", cssProperty.key());
assertEquals("value1", cssProperty.value());
}
});
with(list.get(1), new TestUtils.Action<CssProperty>() {
@Override
public void apply(@NonNull CssProperty cssProperty) {
assertEquals("key2", cssProperty.key());
assertEquals("value2", cssProperty.value());
}
});
}
@Test
public void list_of_keys() {
final String input = "key1 key2 key3 key4";
final List<CssProperty> list = listProperties(input);
assertEquals(0, list.size());
}
@Test
public void list_of_keys_and_value() {
final String input = "key1 key2 key3 key4: value4";
final List<CssProperty> list = listProperties(input);
assertEquals(1, list.size());
with(list.get(0), new TestUtils.Action<CssProperty>() {
@Override
public void apply(@NonNull CssProperty cssProperty) {
assertEquals("key4", cssProperty.key());
assertEquals("value4", cssProperty.value());
}
});
}
@Test
public void list_of_keys_separated_by_semi_colon() {
final String input = "key1;key2;key3;key4;";
final List<CssProperty> list = listProperties(input);
assertEquals(0, list.size());
}
@Test
public void key_value_with_invalid_between() {
final String input = "key1: value1; key2 key3: value3;";
final List<CssProperty> list = listProperties(input);
assertEquals(2, list.size());
with(list.get(0), new TestUtils.Action<CssProperty>() {
@Override
public void apply(@NonNull CssProperty cssProperty) {
assertEquals("key1", cssProperty.key());
assertEquals("value1", cssProperty.value());
}
});
with(list.get(1), new TestUtils.Action<CssProperty>() {
@Override
public void apply(@NonNull CssProperty cssProperty) {
assertEquals("key3", cssProperty.key());
assertEquals("value3", cssProperty.value());
}
});
}
@Test
public void css_functions() {
final Map<String, String> map = new HashMap<String, String>() {{
put("attr", "\" (\" attr(href) \")\"");
put("calc", "calc(100% - 100px)");
put("cubic-bezier", "cubic-bezier(0.1, 0.7, 1.0, 0.1)");
put("hsl", "hsl(120,100%,50%)");
put("hsla", "hsla(120,100%,50%,0.3)");
put("linear-gradient", "linear-gradient(red, yellow, blue)");
put("radial-gradient", "radial-gradient(red, green, blue)");
put("repeating-linear-gradient", "repeating-linear-gradient(red, yellow 10%, green 20%)");
put("repeating-radial-gradient", "repeating-radial-gradient(red, yellow 10%, green 15%)");
put("rgb", "rgb(255,0,0)");
put("rgba", "rgba(255,0,0,0.3)");
put("var", "var(--some-variable)");
put("url", "url(\"url.gif\")");
}};
final StringBuilder builder = new StringBuilder();
for (Map.Entry<String, String> entry: map.entrySet()) {
builder.append(entry.getKey())
.append(':')
.append(entry.getValue())
.append(';');
}
for (CssProperty cssProperty: impl.parse(builder.toString())) {
final String value = map.remove(cssProperty.key());
assertNotNull(cssProperty.key(), value);
assertEquals(cssProperty.key(), value, cssProperty.value());
}
assertEquals(0, map.size());
}
@NonNull
private List<CssProperty> listProperties(@NonNull String input) {
return Ix.from(impl.parse(input))
.map(new IxFunction<CssProperty, CssProperty>() {
@Override
public CssProperty apply(CssProperty cssProperty) {
return cssProperty.mutate();
}
})
.toList();
}
}

@ -0,0 +1,186 @@
package ru.noties.markwon.html.impl.tag;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.robolectric.RobolectricTestRunner;
import org.robolectric.annotation.Config;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
import ru.noties.markwon.renderer.ImageSize;
import ru.noties.markwon.renderer.html2.CssInlineStyleParser;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertNull;
@RunWith(RobolectricTestRunner.class)
@Config(manifest = Config.NONE)
public class ImageSizeParserImplTest {
private static final float DELTA = 1e-7F;
private ImageSizeParserImpl impl;
@Before
public void before() {
impl = new ImageSizeParserImpl(CssInlineStyleParser.create());
}
@Test
public void nothing() {
assertNull(impl.parse(Collections.<String, String>emptyMap()));
}
@Test
public void width_height_from_style() {
final String style = "width: 123; height: 321";
assertImageSize(
new ImageSize(dimension(123, null), dimension(321, null)),
impl.parse(Collections.singletonMap("style", style))
);
}
@Test
public void style_has_higher_priority_width() {
// if property is found in styles, do not lookup raw attribute
final Map<String, String> attributes = new HashMap<String, String>() {{
put("style", "width: 43");
put("width", "991");
}};
assertImageSize(
new ImageSize(dimension(43, null), null),
impl.parse(attributes)
);
}
@Test
public void style_has_higher_priority_height() {
// if property is found in styles, do not lookup raw attribute
final Map<String, String> attributes = new HashMap<String, String>() {{
put("style", "height: 177");
put("height", "8");
}};
assertImageSize(
new ImageSize(null, dimension(177, null)),
impl.parse(attributes)
);
}
@Test
public void width_style_height_attributes() {
final Map<String, String> attributes = new HashMap<String, String>() {{
put("style", "width: 99");
put("height", "7");
}};
assertImageSize(
new ImageSize(dimension(99, null), dimension(7, null)),
impl.parse(attributes)
);
}
@Test
public void height_style_width_attributes() {
final Map<String, String> attributes = new HashMap<String, String>() {{
put("style", "height: 15");
put("width", "88");
}};
assertImageSize(
new ImageSize(dimension(88, null), dimension(15, null)),
impl.parse(attributes)
);
}
@Test
public void non_empty_styles_width_height_attributes() {
final Map<String, String> attributes = new HashMap<String, String>() {{
put("style", "key1: value1; width0: 123; height0: 99");
put("width", "40");
put("height", "77");
}};
assertImageSize(
new ImageSize(dimension(40, null), dimension(77, null)),
impl.parse(attributes)
);
}
@Test
public void dimension_units() {
final Map<String, ImageSize.Dimension> map = new HashMap<String, ImageSize.Dimension>() {{
put("100", dimension(100, null));
put("100%", dimension(100, "%"));
put("1%", dimension(1, "%"));
put("0.2em", dimension(0.2F, "em"));
put("155px", dimension(155, "px"));
put("67blah", dimension(67, "blah"));
put("-1", dimension(-1, null));
put("-0.01pt", dimension(-0.01F, "pt"));
}};
for (Map.Entry<String, ImageSize.Dimension> entry : map.entrySet()) {
assertDimension(entry.getKey(), entry.getValue(), impl.dimension(entry.getKey()));
}
}
@Test
public void bad_dimension() {
final String[] dimensions = {
"calc(5px + 10rem)",
"whataver6",
"165 165",
"!@#$%^&*(%"
};
for (String dimension : dimensions) {
assertNull(dimension, impl.dimension(dimension));
}
}
private static void assertImageSize(@Nullable ImageSize expected, @Nullable ImageSize actual) {
if (expected == null) {
assertNull(actual);
} else {
assertNotNull(actual);
assertDimension("width", expected.width, actual.width);
assertDimension("height", expected.height, actual.height);
}
}
private static void assertDimension(
@NonNull String name,
@Nullable ImageSize.Dimension expected,
@Nullable ImageSize.Dimension actual) {
if (expected == null) {
assertNull(name, actual);
} else {
assertNotNull(name, actual);
assertEquals(name, expected.value, actual.value, DELTA);
assertEquals(name, expected.unit, actual.unit);
}
}
@NonNull
private static ImageSize.Dimension dimension(float value, @Nullable String unit) {
return new ImageSize.Dimension(value, unit);
}
}

@ -1,37 +0,0 @@
apply plugin: 'com.android.library'
android {
compileSdkVersion config['compile-sdk']
buildToolsVersion config['build-tools']
defaultConfig {
minSdkVersion config['min-sdk']
targetSdkVersion config['target-sdk']
versionCode 1
versionName version
}
lintOptions {
// okio....
disable 'InvalidPackage'
}
}
dependencies {
api project(':markwon')
deps.with {
api it['android-svg']
api it['android-gif']
api it['okhttp']
}
deps['test'].with {
testImplementation it['junit']
testImplementation it['robolectric']
}
}
registerArtifact(this)

@ -1,3 +0,0 @@
POM_NAME=Markwon-Image-Loader
POM_ARTIFACT_ID=markwon-image-loader
POM_PACKAGING=aar

@ -1 +0,0 @@
<manifest package="ru.noties.markwon.il" />

@ -1,405 +0,0 @@
package ru.noties.markwon.il;
import android.content.res.Resources;
import android.graphics.drawable.Drawable;
import android.net.Uri;
import android.os.Handler;
import android.os.Looper;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import java.io.IOException;
import java.io.InputStream;
import java.lang.ref.WeakReference;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
import okhttp3.OkHttpClient;
import ru.noties.markwon.image.AsyncDrawable;
public class AsyncDrawableLoader implements AsyncDrawable.Loader {
@NonNull
public static AsyncDrawableLoader create() {
return builder().build();
}
@NonNull
public static AsyncDrawableLoader.Builder builder() {
return new Builder();
}
private final ExecutorService executorService;
private final Handler mainThread;
private final Drawable errorDrawable;
private final Map<String, SchemeHandler> schemeHandlers;
private final List<MediaDecoder> mediaDecoders;
private final Map<String, Future<?>> requests;
AsyncDrawableLoader(Builder builder) {
this.executorService = builder.executorService;
this.mainThread = new Handler(Looper.getMainLooper());
this.errorDrawable = builder.errorDrawable;
this.schemeHandlers = builder.schemeHandlers;
this.mediaDecoders = builder.mediaDecoders;
this.requests = new HashMap<>(3);
}
@Override
public void load(@NonNull String destination, @NonNull AsyncDrawable drawable) {
// if drawable is not a link -> show loading placeholder...
requests.put(destination, execute(destination, drawable));
}
@Override
public void cancel(@NonNull String destination) {
final Future<?> request = requests.remove(destination);
if (request != null) {
request.cancel(true);
}
for (SchemeHandler schemeHandler : schemeHandlers.values()) {
schemeHandler.cancel(destination);
}
}
private Future<?> execute(@NonNull final String destination, @NonNull AsyncDrawable drawable) {
final WeakReference<AsyncDrawable> reference = new WeakReference<AsyncDrawable>(drawable);
// todo: should we cancel pending request for the same destination?
// we _could_ but there is possibility that one resource is request in multiple places
// todo: error handing (simply applying errorDrawable is not a good solution
// as reason for an error is unclear (no scheme handler, no input data, error decoding, etc)
// todo: more efficient ImageMediaDecoder... BitmapFactory.decodeStream is a bit not optimal
// for big images for sure. We _could_ introduce internal Drawable that will check for
// image bounds (but we will need to cache inputStream in order to inspect and optimize
// input image...)
return executorService.submit(new Runnable() {
@Override
public void run() {
final ImageItem item;
final Uri uri = Uri.parse(destination);
final SchemeHandler schemeHandler = schemeHandlers.get(uri.getScheme());
if (schemeHandler != null) {
item = schemeHandler.handle(destination, uri);
} else {
item = null;
}
final InputStream inputStream = item != null
? item.inputStream()
: null;
Drawable result = null;
if (inputStream != null) {
try {
final String fileName = item.fileName();
final MediaDecoder mediaDecoder = fileName != null
? mediaDecoderFromFile(fileName)
: mediaDecoderFromContentType(item.contentType());
if (mediaDecoder != null) {
result = mediaDecoder.decode(inputStream);
}
} finally {
try {
inputStream.close();
} catch (IOException e) {
// ignored
}
}
}
// if result is null, we assume it's an error
if (result == null) {
result = errorDrawable;
}
if (result != null) {
final Drawable out = result;
mainThread.post(new Runnable() {
@Override
public void run() {
final AsyncDrawable asyncDrawable = reference.get();
if (asyncDrawable != null && asyncDrawable.isAttached()) {
asyncDrawable.setResult(out);
}
}
});
}
requests.remove(destination);
}
});
}
@Nullable
private MediaDecoder mediaDecoderFromFile(@NonNull String fileName) {
MediaDecoder out = null;
for (MediaDecoder mediaDecoder : mediaDecoders) {
if (mediaDecoder.canDecodeByFileName(fileName)) {
out = mediaDecoder;
break;
}
}
return out;
}
@Nullable
private MediaDecoder mediaDecoderFromContentType(@Nullable String contentType) {
MediaDecoder out = null;
for (MediaDecoder mediaDecoder : mediaDecoders) {
if (mediaDecoder.canDecodeByContentType(contentType)) {
out = mediaDecoder;
break;
}
}
return out;
}
// todo: as now we have different layers of abstraction (for scheme handling and media decoding)
// we no longer should add dependencies implicitly, it would be way better to allow adding
// multiple artifacts (file, data, network, svg, gif)... at least, maybe we can extract API
// for this module (without implementations), but keep _all-in_ (fat) artifact with all of these.
public static class Builder {
/**
* @deprecated 2.0.0 add {@link NetworkSchemeHandler} directly
*/
@Deprecated
private OkHttpClient client;
/**
* @deprecated 2.0.0 construct {@link MediaDecoder} and {@link SchemeHandler} appropriately
*/
@Deprecated
private Resources resources;
private ExecutorService executorService;
private Drawable errorDrawable;
// @since 1.1.0
private final List<MediaDecoder> mediaDecoders = new ArrayList<>(3);
// @since 2.0.0
private final Map<String, SchemeHandler> schemeHandlers = new HashMap<>(3);
/**
* @deprecated 2.0.0 add {@link NetworkSchemeHandler} directly
*/
@NonNull
@Deprecated
public Builder client(@NonNull OkHttpClient client) {
this.client = client;
return this;
}
/**
* Supplied resources argument will be used to open files from assets directory
* and to create default {@link MediaDecoder}\'s which require resources instance
*
* @return self
*/
@NonNull
public Builder resources(@NonNull Resources resources) {
this.resources = resources;
return this;
}
@NonNull
public Builder executorService(@NonNull ExecutorService executorService) {
this.executorService = executorService;
return this;
}
@NonNull
public Builder errorDrawable(@NonNull Drawable errorDrawable) {
this.errorDrawable = errorDrawable;
return this;
}
/**
* @since 2.0.0
*/
@SuppressWarnings("UnusedReturnValue")
@NonNull
public Builder addSchemeHandler(@NonNull SchemeHandler schemeHandler) {
SchemeHandler previous;
for (String scheme : schemeHandler.schemes()) {
previous = schemeHandlers.put(scheme, schemeHandler);
if (previous != null) {
throw new IllegalStateException(String.format("Multiple scheme handlers handle " +
"the same scheme: `%s`, %s %s", scheme, previous, schemeHandler));
}
}
return this;
}
/**
* @see #addMediaDecoder(MediaDecoder)
* @see #addMediaDecoders(MediaDecoder...)
* @see #addMediaDecoders(Iterable)
* @since 1.1.0
* @deprecated 2.0.0
*/
@Deprecated
@NonNull
public Builder mediaDecoders(@NonNull List<MediaDecoder> mediaDecoders) {
// previously it was clearing before adding
for (MediaDecoder mediaDecoder : mediaDecoders) {
this.mediaDecoders.add(requireNonNull(mediaDecoder));
}
return this;
}
/**
* @see #addMediaDecoder(MediaDecoder)
* @see #addMediaDecoders(MediaDecoder...)
* @see #addMediaDecoders(Iterable)
* @since 1.1.0
* @deprecated 2.0.0
*/
@NonNull
@Deprecated
public Builder mediaDecoders(MediaDecoder... mediaDecoders) {
// previously it was clearing before adding
final int length = mediaDecoders != null
? mediaDecoders.length
: 0;
if (length > 0) {
for (int i = 0; i < length; i++) {
this.mediaDecoders.add(requireNonNull(mediaDecoders[i]));
}
}
return this;
}
/**
* @see SvgMediaDecoder
* @see GifMediaDecoder
* @see ImageMediaDecoder
* @since 2.0.0
*/
@NonNull
public Builder addMediaDecoder(@NonNull MediaDecoder mediaDecoder) {
mediaDecoders.add(mediaDecoder);
return this;
}
/**
* @see SvgMediaDecoder
* @see GifMediaDecoder
* @see ImageMediaDecoder
* @since 2.0.0
*/
@NonNull
public Builder addMediaDecoders(@NonNull Iterable<MediaDecoder> mediaDecoders) {
for (MediaDecoder mediaDecoder : mediaDecoders) {
this.mediaDecoders.add(requireNonNull(mediaDecoder));
}
return this;
}
/**
* @see SvgMediaDecoder
* @see GifMediaDecoder
* @see ImageMediaDecoder
* @since 2.0.0
*/
@NonNull
public Builder addMediaDecoders(MediaDecoder... mediaDecoders) {
final int length = mediaDecoders != null
? mediaDecoders.length
: 0;
if (length > 0) {
for (int i = 0; i < length; i++) {
this.mediaDecoders.add(requireNonNull(mediaDecoders[i]));
}
}
return this;
}
@NonNull
public AsyncDrawableLoader build() {
// I think we should deprecate this...
if (resources == null) {
resources = Resources.getSystem();
}
if (executorService == null) {
// @since 2.0.0 we are using newCachedThreadPool instead
// of `okHttpClient.dispatcher().executorService()`
executorService = Executors.newCachedThreadPool();
}
// @since 2.0.0
// put default scheme handlers (to mimic previous behavior)
// remove in 3.0.0 with plugins
if (schemeHandlers.size() == 0) {
if (client == null) {
client = new OkHttpClient();
}
addSchemeHandler(NetworkSchemeHandler.create(client));
addSchemeHandler(FileSchemeHandler.createWithAssets(resources.getAssets()));
addSchemeHandler(DataUriSchemeHandler.create());
}
// add default media decoders if not specified
// remove in 3.0.0 with plugins
if (mediaDecoders.size() == 0) {
mediaDecoders.add(SvgMediaDecoder.create(resources));
mediaDecoders.add(GifMediaDecoder.create(true));
mediaDecoders.add(ImageMediaDecoder.create(resources));
}
return new AsyncDrawableLoader(this);
}
}
// @since 2.0.0
@NonNull
private static <T> T requireNonNull(@Nullable T t) {
if (t == null) {
throw new NullPointerException();
}
return t;
}
}

@ -1,60 +0,0 @@
package ru.noties.markwon.il;
import android.support.annotation.Nullable;
public class DataUri {
private final String contentType;
private final boolean base64;
private final String data;
public DataUri(@Nullable String contentType, boolean base64, @Nullable String data) {
this.contentType = contentType;
this.base64 = base64;
this.data = data;
}
@Nullable
public String contentType() {
return contentType;
}
public boolean base64() {
return base64;
}
@Nullable
public String data() {
return data;
}
@Override
public String toString() {
return "DataUri{" +
"contentType='" + contentType + '\'' +
", base64=" + base64 +
", data='" + data + '\'' +
'}';
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
DataUri dataUri = (DataUri) o;
if (base64 != dataUri.base64) return false;
if (contentType != null ? !contentType.equals(dataUri.contentType) : dataUri.contentType != null)
return false;
return data != null ? data.equals(dataUri.data) : dataUri.data == null;
}
@Override
public int hashCode() {
int result = contentType != null ? contentType.hashCode() : 0;
result = 31 * result + (base64 ? 1 : 0);
result = 31 * result + (data != null ? data.hashCode() : 0);
return result;
}
}

@ -1,41 +0,0 @@
package ru.noties.markwon.il;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.text.TextUtils;
import android.util.Base64;
public abstract class DataUriDecoder {
@Nullable
public abstract byte[] decode(@NonNull DataUri dataUri);
@NonNull
public static DataUriDecoder create() {
return new Impl();
}
static class Impl extends DataUriDecoder {
@Nullable
@Override
public byte[] decode(@NonNull DataUri dataUri) {
final String data = dataUri.data();
if (!TextUtils.isEmpty(data)) {
try {
if (dataUri.base64()) {
return Base64.decode(data.getBytes("UTF-8"), Base64.DEFAULT);
} else {
return data.getBytes("UTF-8");
}
} catch (Throwable t) {
return null;
}
} else {
return null;
}
}
}
}

@ -1,79 +0,0 @@
package ru.noties.markwon.il;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
public abstract class DataUriParser {
@Nullable
public abstract DataUri parse(@NonNull String input);
@NonNull
public static DataUriParser create() {
return new Impl();
}
static class Impl extends DataUriParser {
@Nullable
@Override
public DataUri parse(@NonNull String input) {
final int index = input.indexOf(',');
// we expect exactly one comma
if (index < 0) {
return null;
}
final String contentType;
final boolean base64;
if (index > 0) {
final String part = input.substring(0, index);
final String[] parts = part.split(";");
final int length = parts.length;
if (length > 0) {
// if one: either content-type or base64
if (length == 1) {
final String value = parts[0];
if ("base64".equals(value)) {
contentType = null;
base64 = true;
} else {
contentType = value.indexOf('/') > -1
? value
: null;
base64 = false;
}
} else {
contentType = parts[0].indexOf('/') > -1
? parts[0]
: null;
base64 = "base64".equals(parts[length - 1]);
}
} else {
contentType = null;
base64 = false;
}
} else {
contentType = null;
base64 = false;
}
final String data;
if (index < input.length()) {
final String value = input.substring(index + 1, input.length()).replaceAll("\n", "");
if (value.length() == 0) {
data = null;
} else {
data = value;
}
} else {
data = null;
}
return new DataUri(contentType, base64, data);
}
}
}

@ -1,74 +0,0 @@
package ru.noties.markwon.il;
import android.net.Uri;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import java.io.ByteArrayInputStream;
import java.util.Collection;
import java.util.Collections;
/**
* @since 2.0.0
*/
public class DataUriSchemeHandler extends SchemeHandler {
@NonNull
public static DataUriSchemeHandler create() {
return new DataUriSchemeHandler(DataUriParser.create(), DataUriDecoder.create());
}
private static final String START = "data:";
private final DataUriParser uriParser;
private final DataUriDecoder uriDecoder;
@SuppressWarnings("WeakerAccess")
DataUriSchemeHandler(@NonNull DataUriParser uriParser, @NonNull DataUriDecoder uriDecoder) {
this.uriParser = uriParser;
this.uriDecoder = uriDecoder;
}
@Nullable
@Override
public ImageItem handle(@NonNull String raw, @NonNull Uri uri) {
if (!raw.startsWith(START)) {
return null;
}
String part = raw.substring(START.length());
// this part is added to support `data://` with which this functionality was released
if (part.startsWith("//")) {
part = part.substring(2);
}
final DataUri dataUri = uriParser.parse(part);
if (dataUri == null) {
return null;
}
final byte[] bytes = uriDecoder.decode(dataUri);
if (bytes == null) {
return null;
}
return new ImageItem(
dataUri.contentType(),
new ByteArrayInputStream(bytes),
null
);
}
@Override
public void cancel(@NonNull String raw) {
// no op
}
@NonNull
@Override
public Collection<String> schemes() {
return Collections.singleton("data");
}
}

@ -1,13 +0,0 @@
package ru.noties.markwon.il;
import android.graphics.drawable.Drawable;
import android.support.annotation.NonNull;
abstract class DrawableUtils {
static void intrinsicBounds(@NonNull Drawable drawable) {
drawable.setBounds(0, 0, drawable.getIntrinsicWidth(), drawable.getIntrinsicHeight());
}
private DrawableUtils() {}
}

@ -1,109 +0,0 @@
package ru.noties.markwon.il;
import android.content.res.AssetManager;
import android.net.Uri;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import java.io.BufferedInputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
/**
* @since 2.0.0
*/
public class FileSchemeHandler extends SchemeHandler {
@NonNull
public static FileSchemeHandler createWithAssets(@NonNull AssetManager assetManager) {
return new FileSchemeHandler(assetManager);
}
@NonNull
public static FileSchemeHandler create() {
return new FileSchemeHandler(null);
}
private static final String FILE_ANDROID_ASSETS = "android_asset";
@Nullable
private final AssetManager assetManager;
@SuppressWarnings("WeakerAccess")
FileSchemeHandler(@Nullable AssetManager assetManager) {
this.assetManager = assetManager;
}
@Nullable
@Override
public ImageItem handle(@NonNull String raw, @NonNull Uri uri) {
final List<String> segments = uri.getPathSegments();
if (segments == null
|| segments.size() == 0) {
// pointing to file & having no path segments is no use
return null;
}
final ImageItem out;
InputStream inputStream = null;
final boolean assets = FILE_ANDROID_ASSETS.equals(segments.get(0));
final String fileName = uri.getLastPathSegment();
if (assets) {
// no handling of assets here if we have no assetsManager
if (assetManager != null) {
final StringBuilder path = new StringBuilder();
for (int i = 1, size = segments.size(); i < size; i++) {
if (i != 1) {
path.append('/');
}
path.append(segments.get(i));
}
// load assets
try {
inputStream = assetManager.open(path.toString());
} catch (IOException e) {
e.printStackTrace();
}
}
} else {
try {
inputStream = new BufferedInputStream(new FileInputStream(new File(uri.getPath())));
} catch (FileNotFoundException e) {
e.printStackTrace();
}
}
if (inputStream != null) {
out = new ImageItem(fileName, inputStream, fileName);
} else {
out = null;
}
return out;
}
@Override
public void cancel(@NonNull String raw) {
// no op
}
@NonNull
@Override
public Collection<String> schemes() {
return Collections.singleton("file");
}
}

@ -1,91 +0,0 @@
package ru.noties.markwon.il;
import android.graphics.drawable.Drawable;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import pl.droidsonroids.gif.GifDrawable;
/**
* @since 1.1.0
*/
@SuppressWarnings("WeakerAccess")
public class GifMediaDecoder extends MediaDecoder {
protected static final String CONTENT_TYPE_GIF = "image/gif";
protected static final String FILE_EXTENSION_GIF = ".gif";
@NonNull
public static GifMediaDecoder create(boolean autoPlayGif) {
return new GifMediaDecoder(autoPlayGif);
}
private final boolean autoPlayGif;
protected GifMediaDecoder(boolean autoPlayGif) {
this.autoPlayGif = autoPlayGif;
}
@Override
public boolean canDecodeByContentType(@Nullable String contentType) {
return CONTENT_TYPE_GIF.equals(contentType);
}
@Override
public boolean canDecodeByFileName(@NonNull String fileName) {
return fileName.endsWith(FILE_EXTENSION_GIF);
}
@Nullable
@Override
public Drawable decode(@NonNull InputStream inputStream) {
Drawable out = null;
final byte[] bytes = readBytes(inputStream);
if (bytes != null) {
try {
out = newGifDrawable(bytes);
DrawableUtils.intrinsicBounds(out);
if (!autoPlayGif) {
((GifDrawable) out).pause();
}
} catch (IOException e) {
e.printStackTrace();
}
}
return out;
}
@NonNull
protected Drawable newGifDrawable(@NonNull byte[] bytes) throws IOException {
return new GifDrawable(bytes);
}
@Nullable
protected static byte[] readBytes(@NonNull InputStream stream) {
byte[] out = null;
try {
final ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
final int length = 1024 * 8;
final byte[] buffer = new byte[length];
int read;
while ((read = stream.read(buffer, 0, length)) != -1) {
outputStream.write(buffer, 0, read);
}
out = outputStream.toByteArray();
} catch (IOException e) {
e.printStackTrace();
}
return out;
}
}

@ -1,39 +0,0 @@
package ru.noties.markwon.il;
import android.support.annotation.Nullable;
import java.io.InputStream;
/**
* @since 2.0.0
*/
public class ImageItem {
private final String contentType;
private final InputStream inputStream;
private final String fileName;
public ImageItem(
@Nullable String contentType,
@Nullable InputStream inputStream,
@Nullable String fileName) {
this.contentType = contentType;
this.inputStream = inputStream;
this.fileName = fileName;
}
@Nullable
public String contentType() {
return contentType;
}
@Nullable
public InputStream inputStream() {
return inputStream;
}
@Nullable
public String fileName() {
return fileName;
}
}

@ -1,60 +0,0 @@
package ru.noties.markwon.il;
import android.content.res.Resources;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.graphics.drawable.BitmapDrawable;
import android.graphics.drawable.Drawable;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import java.io.InputStream;
/**
* This class can be used as the last {@link MediaDecoder} to _try_ to handle all rest cases.
* Here we just assume that supplied InputStream is of image type and try to decode it.
*
* @since 1.1.0
*/
public class ImageMediaDecoder extends MediaDecoder {
@NonNull
public static ImageMediaDecoder create(@NonNull Resources resources) {
return new ImageMediaDecoder(resources);
}
private final Resources resources;
@SuppressWarnings("WeakerAccess")
ImageMediaDecoder(Resources resources) {
this.resources = resources;
}
@Override
public boolean canDecodeByContentType(@Nullable String contentType) {
return true;
}
@Override
public boolean canDecodeByFileName(@NonNull String fileName) {
return true;
}
@Nullable
@Override
public Drawable decode(@NonNull InputStream inputStream) {
final Drawable out;
// absolutely not optimal... thing
final Bitmap bitmap = BitmapFactory.decodeStream(inputStream);
if (bitmap != null) {
out = new BitmapDrawable(resources, bitmap);
DrawableUtils.intrinsicBounds(out);
} else {
out = null;
}
return out;
}
}

@ -1,20 +0,0 @@
package ru.noties.markwon.il;
import android.graphics.drawable.Drawable;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import java.io.InputStream;
/**
* @since 1.1.0
*/
public abstract class MediaDecoder {
public abstract boolean canDecodeByContentType(@Nullable String contentType);
public abstract boolean canDecodeByFileName(@NonNull String fileName);
@Nullable
public abstract Drawable decode(@NonNull InputStream inputStream);
}

@ -1,89 +0,0 @@
package ru.noties.markwon.il;
import android.net.Uri;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import java.io.IOException;
import java.io.InputStream;
import java.util.Arrays;
import java.util.Collection;
import java.util.List;
import okhttp3.Call;
import okhttp3.OkHttpClient;
import okhttp3.Request;
import okhttp3.Response;
import okhttp3.ResponseBody;
/**
* @since 2.0.0
*/
public class NetworkSchemeHandler extends SchemeHandler {
@NonNull
public static NetworkSchemeHandler create(@NonNull OkHttpClient client) {
return new NetworkSchemeHandler(client);
}
private static final String HEADER_CONTENT_TYPE = "Content-Type";
private final OkHttpClient client;
@SuppressWarnings("WeakerAccess")
NetworkSchemeHandler(@NonNull OkHttpClient client) {
this.client = client;
}
@Nullable
@Override
public ImageItem handle(@NonNull String raw, @NonNull Uri uri) {
ImageItem out = null;
final Request request = new Request.Builder()
.url(raw)
.tag(raw)
.build();
Response response = null;
try {
response = client.newCall(request).execute();
} catch (IOException e) {
e.printStackTrace();
}
if (response != null) {
final ResponseBody body = response.body();
if (body != null) {
final InputStream inputStream = body.byteStream();
if (inputStream != null) {
final String contentType = response.header(HEADER_CONTENT_TYPE);
out = new ImageItem(contentType, inputStream, null);
}
}
}
return out;
}
@Override
public void cancel(@NonNull String raw) {
final List<Call> calls = client.dispatcher().queuedCalls();
if (calls != null) {
for (Call call : calls) {
if (!call.isCanceled()) {
if (raw.equals(call.request().tag())) {
call.cancel();
}
}
}
}
}
@NonNull
@Override
public Collection<String> schemes() {
return Arrays.asList("http", "https");
}
}

@ -1,25 +0,0 @@
package ru.noties.markwon.il;
import android.net.Uri;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import java.util.Collection;
/**
* @since 2.0.0
*/
public abstract class SchemeHandler {
@Nullable
public abstract ImageItem handle(@NonNull String raw, @NonNull Uri uri);
public abstract void cancel(@NonNull String raw);
/**
* Will be called only once during initialization, should return schemes that are
* handled by this handler
*/
@NonNull
public abstract Collection<String> schemes();
}

@ -1,81 +0,0 @@
package ru.noties.markwon.il;
import android.content.res.Resources;
import android.graphics.Bitmap;
import android.graphics.Canvas;
import android.graphics.drawable.BitmapDrawable;
import android.graphics.drawable.Drawable;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import com.caverock.androidsvg.SVG;
import com.caverock.androidsvg.SVGParseException;
import java.io.InputStream;
/**
* @since 1.1.0
*/
public class SvgMediaDecoder extends MediaDecoder {
private static final String CONTENT_TYPE_SVG = "image/svg+xml";
private static final String FILE_EXTENSION_SVG = ".svg";
@NonNull
public static SvgMediaDecoder create(@NonNull Resources resources) {
return new SvgMediaDecoder(resources);
}
private final Resources resources;
@SuppressWarnings("WeakerAccess")
SvgMediaDecoder(Resources resources) {
this.resources = resources;
}
@Override
public boolean canDecodeByContentType(@Nullable String contentType) {
return contentType != null && contentType.startsWith(CONTENT_TYPE_SVG);
}
@Override
public boolean canDecodeByFileName(@NonNull String fileName) {
return fileName.endsWith(FILE_EXTENSION_SVG);
}
@Nullable
@Override
public Drawable decode(@NonNull InputStream inputStream) {
final Drawable out;
SVG svg = null;
try {
svg = SVG.getFromInputStream(inputStream);
} catch (SVGParseException e) {
e.printStackTrace();
}
if (svg == null) {
out = null;
} else {
final float w = svg.getDocumentWidth();
final float h = svg.getDocumentHeight();
final float density = resources.getDisplayMetrics().density;
final int width = (int) (w * density + .5F);
final int height = (int) (h * density + .5F);
final Bitmap bitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_4444);
final Canvas canvas = new Canvas(bitmap);
canvas.scale(density, density);
svg.renderToCanvas(canvas);
out = new BitmapDrawable(resources, bitmap);
DrawableUtils.intrinsicBounds(out);
}
return out;
}
}

@ -1,119 +0,0 @@
package ru.noties.markwon.il;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.robolectric.RobolectricTestRunner;
import org.robolectric.annotation.Config;
import java.util.LinkedHashMap;
import java.util.Map;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNull;
@RunWith(RobolectricTestRunner.class)
@Config(manifest = Config.NONE)
public class DataUriParserTest {
private DataUriParser.Impl impl;
@Before
public void before() {
impl = new DataUriParser.Impl();
}
@Test
public void test() {
final Map<String, DataUri> data = new LinkedHashMap<String, DataUri>() {{
put(",", new DataUri(null, false, null));
put("image/svg+xml;base64,!@#$%^&*(", new DataUri("image/svg+xml", true, "!@#$%^&*("));
put("text/vnd-example+xyz;foo=bar;base64,R0lGODdh", new DataUri("text/vnd-example+xyz", true, "R0lGODdh"));
put("text/plain;charset=UTF-8;page=21,the%20data:1234,5678", new DataUri("text/plain", false, "the%20data:1234,5678"));
}};
for (Map.Entry<String, DataUri> entry : data.entrySet()) {
assertEquals(entry.getKey(), entry.getValue(), impl.parse(entry.getKey()));
}
}
@Test
public void data_new_lines_are_ignored() {
final String input = "image/png;base64,iVBORw0KGgoAAA\n" +
"ANSUhEUgAAAAUAAAAFCAYAAACNbyblAAAAHElEQVQI12P4\n" +
"//8/w38GIAXDIBKE0DHxgljNBAAO9TXL0Y4OHwAAAABJRU\n" +
"5ErkJggg==";
assertEquals(
new DataUri("image/png", true, "iVBORw0KGgoAAAANSUhEUgAAAAUAAAAFCAYAAACNbyblAAAAHElEQVQI12P4//8/w38GIAXDIBKE0DHxgljNBAAO9TXL0Y4OHwAAAABJRU5ErkJggg=="),
impl.parse(input)
);
}
@Test
public void no_comma_returns_null() {
final String[] inputs = {
"",
"what-ever",
";;;;;;;",
"some crazy data"
};
for (String input : inputs) {
assertNull(input, impl.parse(input));
}
}
@Test
public void two_commas() {
final String input = ",,"; // <- second one would be considered data...
assertEquals(
input,
new DataUri(null, false, ","),
impl.parse(input)
);
}
@Test
public void more_commas() {
final String input = "first,second,third"; // <- first is just a value (will be ignored)
assertEquals(
input,
new DataUri(null, false, "second,third"),
impl.parse(input)
);
}
@Test
public void base64_no_content_type() {
final String input = ";base64,12345";
assertEquals(
input,
new DataUri(null, true, "12345"),
impl.parse(input)
);
}
@Test
public void not_base64_no_content_type() {
final String input = ",qweRTY";
assertEquals(
input,
new DataUri(null, false, "qweRTY"),
impl.parse(input)
);
}
@Test
public void content_type_data_no_base64() {
final String input = "image/png,aSdFg";
assertEquals(
input,
new DataUri("image/png", false, "aSdFg"),
impl.parse(input)
);
}
}

@ -1,112 +0,0 @@
package ru.noties.markwon.il;
import android.net.Uri;
import android.support.annotation.NonNull;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.robolectric.RobolectricTestRunner;
import org.robolectric.annotation.Config;
import java.io.InputStream;
import java.util.HashMap;
import java.util.Map;
import java.util.Scanner;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertNull;
@RunWith(RobolectricTestRunner.class)
@Config(manifest = Config.NONE)
public class DataUriSchemeHandlerTest {
private DataUriSchemeHandler handler;
@Before
public void before() {
handler = DataUriSchemeHandler.create();
}
@Test
public void scheme_specific_part_is_empty() {
assertNull(handler.handle("data:", Uri.parse("data:")));
}
@Test
public void data_uri_is_empty() {
assertNull(handler.handle("data://whatever", Uri.parse("data://whatever")));
}
@Test
public void no_data() {
assertNull(handler.handle("data://,", Uri.parse("data://,")));
}
@Test
public void correct() {
final class Item {
final String contentType;
final String data;
Item(String contentType, String data) {
this.contentType = contentType;
this.data = data;
}
}
final Map<String, Item> expected = new HashMap<String, Item>() {{
put("data://text/plain;,123", new Item("text/plain", "123"));
put("data://image/svg+xml;base64,MTIz", new Item("image/svg+xml", "123"));
}};
for (Map.Entry<String, Item> entry : expected.entrySet()) {
final ImageItem item = handler.handle(entry.getKey(), Uri.parse(entry.getKey()));
assertNotNull(entry.getKey(), item);
assertEquals(entry.getKey(), entry.getValue().contentType, item.contentType());
assertEquals(entry.getKey(), entry.getValue().data, readStream(item.inputStream()));
}
}
@Test
public void correct_real() {
final class Item {
final String contentType;
final String data;
Item(String contentType, String data) {
this.contentType = contentType;
this.data = data;
}
}
final Map<String, Item> expected = new HashMap<String, Item>() {{
put("data:text/plain;,123", new Item("text/plain", "123"));
put("data:image/svg+xml;base64,MTIz", new Item("image/svg+xml", "123"));
}};
for (Map.Entry<String, Item> entry : expected.entrySet()) {
final ImageItem item = handler.handle(entry.getKey(), Uri.parse(entry.getKey()));
assertNotNull(entry.getKey(), item);
assertEquals(entry.getKey(), entry.getValue().contentType, item.contentType());
assertEquals(entry.getKey(), entry.getValue().data, readStream(item.inputStream()));
}
}
@NonNull
private static String readStream(@NonNull InputStream stream) {
try {
final Scanner scanner = new Scanner(stream, "UTF-8").useDelimiter("\\A");
return scanner.hasNext()
? scanner.next()
: "";
} catch (Throwable t) {
throw new RuntimeException(t);
}
}
}

@ -16,6 +16,10 @@ public class Prism4jThemeDarkula extends Prism4jThemeBase {
return new Prism4jThemeDarkula(0xFF2d2d2d);
}
/**
* @param background color
* @since 3.0.0
*/
@NonNull
public static Prism4jThemeDarkula create(@ColorInt int background) {
return new Prism4jThemeDarkula(background);

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

@ -3,12 +3,12 @@ package ru.noties.markwon;
import android.content.Context;
import android.support.annotation.NonNull;
import ru.noties.markwon.html.api.MarkwonHtmlParser;
import ru.noties.markwon.html.MarkwonHtmlParser;
import ru.noties.markwon.html.MarkwonHtmlRenderer;
import ru.noties.markwon.image.AsyncDrawableLoader;
import ru.noties.markwon.image.AsyncDrawableLoaderNoOp;
import ru.noties.markwon.renderer.ImageSizeResolver;
import ru.noties.markwon.renderer.ImageSizeResolverDef;
import ru.noties.markwon.renderer.html2.MarkwonHtmlRenderer;
import ru.noties.markwon.spans.LinkSpan;
import ru.noties.markwon.spans.MarkwonTheme;
@ -37,7 +37,6 @@ public class MarkwonConfiguration {
private final UrlProcessor urlProcessor;
private final ImageSizeResolver imageSizeResolver;
private final SpannableFactory factory; // @since 1.1.0
private final boolean softBreakAddsNewLine; // @since 1.1.1
private final MarkwonHtmlParser htmlParser; // @since 2.0.0
private final MarkwonHtmlRenderer htmlRenderer; // @since 2.0.0
private final boolean htmlAllowNonClosedTags; // @since 2.0.0
@ -50,7 +49,6 @@ public class MarkwonConfiguration {
this.urlProcessor = builder.urlProcessor;
this.imageSizeResolver = builder.imageSizeResolver;
this.factory = builder.factory;
this.softBreakAddsNewLine = builder.softBreakAddsNewLine;
this.htmlParser = builder.htmlParser;
this.htmlRenderer = builder.htmlRenderer;
this.htmlAllowNonClosedTags = builder.htmlAllowNonClosedTags;
@ -99,15 +97,6 @@ public class MarkwonConfiguration {
return factory;
}
/**
* @return a flag indicating if soft break should be treated as a hard
* break and thus adding a new line instead of adding a white space
* @since 1.1.1
*/
public boolean softBreakAddsNewLine() {
return softBreakAddsNewLine;
}
/**
* @since 2.0.0
*/
@ -143,7 +132,6 @@ public class MarkwonConfiguration {
private UrlProcessor urlProcessor;
private ImageSizeResolver imageSizeResolver;
private SpannableFactory factory; // @since 1.1.0
private boolean softBreakAddsNewLine; // @since 1.1.1
private MarkwonHtmlParser htmlParser; // @since 2.0.0
private MarkwonHtmlRenderer htmlRenderer; // @since 2.0.0
private boolean htmlAllowNonClosedTags; // @since 2.0.0
@ -161,7 +149,6 @@ public class MarkwonConfiguration {
this.urlProcessor = configuration.urlProcessor;
this.imageSizeResolver = configuration.imageSizeResolver;
this.factory = configuration.factory;
this.softBreakAddsNewLine = configuration.softBreakAddsNewLine;
this.htmlParser = configuration.htmlParser;
this.htmlRenderer = configuration.htmlRenderer;
this.htmlAllowNonClosedTags = configuration.htmlAllowNonClosedTags;
@ -203,19 +190,6 @@ public class MarkwonConfiguration {
return this;
}
/**
* @param softBreakAddsNewLine a flag indicating if soft break should be treated as a hard
* break and thus adding a new line instead of adding a white space
* @return self
* @see <a href="https://spec.commonmark.org/0.28/#soft-line-breaks" > spec </a >
* @since 1.1.1
*/
@NonNull
public Builder softBreakAddsNewLine(boolean softBreakAddsNewLine) {
this.softBreakAddsNewLine = softBreakAddsNewLine;
return this;
}
/**
* @since 2.0.0
*/
@ -276,17 +250,12 @@ public class MarkwonConfiguration {
// @since 2.0.0
if (htmlParser == null) {
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();
}
htmlParser = MarkwonHtmlParser.noOp();
}
// @since 2.0.0
if (htmlRenderer == null) {
htmlRenderer = MarkwonHtmlRenderer.create();
htmlRenderer = MarkwonHtmlRenderer.noOp();
}
return new MarkwonConfiguration(this);

@ -63,16 +63,4 @@ public interface SpannableFactory {
@NonNull MarkwonTheme theme,
@NonNull String destination,
@NonNull LinkSpan.Resolver resolver);
// Currently used by HTML parser
@Nullable
Object superScript(@NonNull MarkwonTheme theme);
// Currently used by HTML parser
@Nullable
Object subScript(@NonNull MarkwonTheme theme);
// Currently used by HTML parser
@Nullable
Object underline();
}

@ -118,21 +118,4 @@ public class SpannableFactoryDef implements SpannableFactory {
public Object link(@NonNull MarkwonTheme theme, @NonNull String destination, @NonNull LinkSpan.Resolver resolver) {
return new LinkSpan(theme, destination, resolver);
}
@Nullable
@Override
public Object superScript(@NonNull MarkwonTheme theme) {
return new SuperScriptSpan(theme);
}
@Override
public Object subScript(@NonNull MarkwonTheme theme) {
return new SubScriptSpan(theme);
}
@Nullable
@Override
public Object underline() {
return new UnderlineSpan();
}
}

@ -1,4 +1,4 @@
package ru.noties.markwon.html.api;
package ru.noties.markwon.html;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;

@ -1,4 +1,4 @@
package ru.noties.markwon.html.api;
package ru.noties.markwon.html;
import android.support.annotation.NonNull;
@ -34,7 +34,7 @@ public abstract class MarkwonHtmlParser {
* If you wish to keep them open (do not force close at the end of a
* document pass here {@link HtmlTag#NO_END}. Later non-closed tags
* can be detected by calling {@link HtmlTag#isClosed()}
* @param action {@link FlushAction} to be called with resulting tags ({@link ru.noties.markwon.html.api.HtmlTag.Inline})
* @param action {@link FlushAction} to be called with resulting tags ({@link HtmlTag.Inline})
*/
public abstract void flushInlineTags(
int documentLength,
@ -49,7 +49,7 @@ public abstract class MarkwonHtmlParser {
* If you wish to keep them open (do not force close at the end of a
* document pass here {@link HtmlTag#NO_END}. Later non-closed tags
* can be detected by calling {@link HtmlTag#isClosed()}
* @param action {@link FlushAction} to be called with resulting tags ({@link ru.noties.markwon.html.api.HtmlTag.Block})
* @param action {@link FlushAction} to be called with resulting tags ({@link HtmlTag.Block})
*/
public abstract void flushBlockTags(
int documentLength,

@ -1,4 +1,4 @@
package ru.noties.markwon.html.api;
package ru.noties.markwon.html;
import android.support.annotation.NonNull;

@ -0,0 +1,30 @@
package ru.noties.markwon.html;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import ru.noties.markwon.MarkwonConfiguration;
import ru.noties.markwon.SpannableBuilder;
/**
* @since 2.0.0
*/
public abstract class MarkwonHtmlRenderer {
/**
* @since 3.0.0
*/
@NonNull
public static MarkwonHtmlRenderer noOp() {
return new MarkwonHtmlRendererNoOp();
}
public abstract void render(
@NonNull MarkwonConfiguration configuration,
@NonNull SpannableBuilder builder,
@NonNull MarkwonHtmlParser parser
);
@Nullable
public abstract TagHandler tagHandler(@NonNull String tagName);
}

@ -0,0 +1,21 @@
package ru.noties.markwon.html;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import ru.noties.markwon.MarkwonConfiguration;
import ru.noties.markwon.SpannableBuilder;
class MarkwonHtmlRendererNoOp extends MarkwonHtmlRenderer {
@Override
public void render(@NonNull MarkwonConfiguration configuration, @NonNull SpannableBuilder builder, @NonNull MarkwonHtmlParser parser) {
}
@Nullable
@Override
public TagHandler tagHandler(@NonNull String tagName) {
return null;
}
}

@ -1,10 +1,9 @@
package ru.noties.markwon.renderer.html2.tag;
package ru.noties.markwon.html;
import android.support.annotation.NonNull;
import ru.noties.markwon.MarkwonConfiguration;
import ru.noties.markwon.SpannableBuilder;
import ru.noties.markwon.html.api.HtmlTag;
public abstract class TagHandler {

@ -1,101 +0,0 @@
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.MarkwonConfiguration;
import ru.noties.markwon.html.api.MarkwonHtmlParser;
import ru.noties.markwon.renderer.html2.tag.BlockquoteHandler;
import ru.noties.markwon.renderer.html2.tag.EmphasisHandler;
import ru.noties.markwon.renderer.html2.tag.HeadingHandler;
import ru.noties.markwon.renderer.html2.tag.ImageHandler;
import ru.noties.markwon.renderer.html2.tag.LinkHandler;
import ru.noties.markwon.renderer.html2.tag.ListHandler;
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 MarkwonConfiguration configuration,
@NonNull SpannableBuilder builder,
@NonNull MarkwonHtmlParser parser
);
@Nullable
public abstract TagHandler tagHandler(@NonNull String tagName);
@NonNull
public static MarkwonHtmlRenderer create() {
return builderWithDefaults().build();
}
@NonNull
public static Builder builderWithDefaults() {
final EmphasisHandler emphasisHandler = new EmphasisHandler();
final StrongEmphasisHandler strongEmphasisHandler = new StrongEmphasisHandler();
final StrikeHandler strikeHandler = new StrikeHandler();
final UnderlineHandler underlineHandler = new UnderlineHandler();
final ListHandler listHandler = new ListHandler();
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", underlineHandler)
.handler("ins", underlineHandler)
.handler("del", strikeHandler)
.handler("s", strikeHandler)
.handler("strike", strikeHandler)
.handler("a", new LinkHandler())
.handler("ul", listHandler)
.handler("ol", listHandler)
.handler("img", ImageHandler.create())
.handler("blockquote", new BlockquoteHandler())
.handler("h1", new HeadingHandler(1))
.handler("h2", new HeadingHandler(2))
.handler("h3", new HeadingHandler(3))
.handler("h4", new HeadingHandler(4))
.handler("h5", new HeadingHandler(5))
.handler("h6", new HeadingHandler(6));
}
@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));
}
}
}

@ -1,88 +0,0 @@
package ru.noties.markwon.renderer.html2;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import java.util.List;
import java.util.Map;
import ru.noties.markwon.SpannableBuilder;
import ru.noties.markwon.MarkwonConfiguration;
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 MarkwonConfiguration configuration,
@NonNull final SpannableBuilder builder,
@NonNull MarkwonHtmlParser parser) {
final int end;
if (!configuration.htmlAllowNonClosedTags()) {
end = HtmlTag.NO_END;
} else {
end = builder.length();
}
parser.flushInlineTags(end, new MarkwonHtmlParser.FlushAction<HtmlTag.Inline>() {
@Override
public void apply(@NonNull List<HtmlTag.Inline> tags) {
TagHandler handler;
for (HtmlTag.Inline inline : tags) {
// if tag is not closed -> do not render
if (!inline.isClosed()) {
continue;
}
handler = tagHandler(inline.name());
if (handler != null) {
handler.handle(configuration, builder, inline);
}
}
}
});
parser.flushBlockTags(end, new MarkwonHtmlParser.FlushAction<HtmlTag.Block>() {
@Override
public void apply(@NonNull List<HtmlTag.Block> tags) {
TagHandler handler;
for (HtmlTag.Block block : tags) {
if (!block.isClosed()) {
continue;
}
handler = tagHandler(block.name());
if (handler != null) {
handler.handle(configuration, builder, block);
} 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);
}
}

@ -7,7 +7,7 @@ import ru.noties.markwon.SpannableFactory;
import ru.noties.markwon.SyntaxHighlight;
import ru.noties.markwon.UrlProcessor;
import ru.noties.markwon.html.api.MarkwonHtmlParser;
import ru.noties.markwon.renderer.html2.MarkwonHtmlRenderer;
import ru.noties.markwon.html.MarkwonHtmlRenderer;
import ru.noties.markwon.image.AsyncDrawable;
import ru.noties.markwon.spans.LinkSpan;
import ru.noties.markwon.spans.MarkwonTheme;

@ -1,3 +1,3 @@
rootProject.name = 'MarkwonProject'
include ':app', ':markwon', ':markwon-view', ':sample-custom-extension', ':sample-latex-math', ':markwon-image-svg', ':markwon-image-gif',
':markwon-syntax-highlight', ':markwon-html-parser-api', ':markwon-html-parser-impl'
':markwon-syntax-highlight', ':markwon-html'