Move all html entities to markwon-html module
This commit is contained in:
		
							parent
							
								
									2efd12f020
								
							
						
					
					
						commit
						27ed17aaff
					
				| @ -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' | ||||
|  | ||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user
	 Dimitry Ivanov
						Dimitry Ivanov