diff --git a/README.md b/README.md
index 382ec771..bb5433a6 100644
--- a/README.md
+++ b/README.md
@@ -107,6 +107,28 @@ Lorem ipsum dolor sit amet
 Lorem ipsum dolor sit amet
 ```
 
+### H.T.M.L.
+<b>O</b><i>K<s>A</s><sup>42<sup>43<sub><b>42</b></sub></sup></sup><u>Y</u></i>
+
+<img src="h" /> <img src="h">
+<img src="h" alt="alt text">
+
+<hr>
+
+<hr />
+
+<h1>Hello</h1>
+
+<h2>Hello</h2>
+
+<h3>Hello</h3>
+
+<h4>Hello</h4>
+
+<h5>Hello</h5>
+
+<h6>Hello</h6>
+
 
 [1]: https://github.com
 [github]: https://github.com
diff --git a/app/build.gradle b/app/build.gradle
index 608dece2..2e9465b9 100644
--- a/app/build.gradle
+++ b/app/build.gradle
@@ -15,12 +15,14 @@ android {
 }
 
 dependencies {
+
     compile project(':library-renderer')
+    compile project(':library-image-loader')
+
     compile 'ru.noties:debug:3.0.0@jar'
-    compile 'com.squareup.picasso:picasso:2.5.2'
-    compile 'com.caverock:androidsvg:1.2.1'
-    compile 'pl.droidsonroids.gif:android-gif-drawable:1.2.7'
-    compile 'com.squareup.okhttp3:okhttp:3.8.0'
+
+    compile OK_HTTP
+
     compile 'com.google.dagger:dagger:2.10'
     annotationProcessor 'com.google.dagger:dagger-compiler:2.10'
 }
diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index bea26fe9..1cf83a05 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -3,6 +3,7 @@
     package="ru.noties.markwon">
 
     <uses-permission android:name="android.permission.INTERNET" />
+    <uses-permission android:name="android.permission.ACCESS_ALL_DOWNLOADS" />
 
     <application
         android:name=".App"
@@ -38,19 +39,19 @@
                     android:scheme="https" />
 
                 <!--<data-->
-                    <!--android:host="*"-->
-                    <!--android:scheme="http"-->
-                    <!--android:mimeType="text/markdown"/>-->
+                <!--android:host="*"-->
+                <!--android:scheme="http"-->
+                <!--android:mimeType="text/markdown"/>-->
 
                 <!--<data-->
-                    <!--android:host="*"-->
-                    <!--android:scheme="file"-->
-                    <!--android:mimeType="text/markdown"/>-->
+                <!--android:host="*"-->
+                <!--android:scheme="file"-->
+                <!--android:mimeType="text/markdown"/>-->
 
                 <!--<data-->
-                    <!--android:host="*"-->
-                    <!--android:scheme="https"-->
-                    <!--android:mimeType="text/markdown"/>-->
+                <!--android:host="*"-->
+                <!--android:scheme="https"-->
+                <!--android:mimeType="text/markdown"/>-->
 
                 <data android:pathPattern=".*\\.markdown" />
                 <data android:pathPattern=".*\\.mdown" />
diff --git a/app/src/main/java/ru/noties/markwon/App.java b/app/src/main/java/ru/noties/markwon/App.java
index 6b185d78..5c9b3157 100644
--- a/app/src/main/java/ru/noties/markwon/App.java
+++ b/app/src/main/java/ru/noties/markwon/App.java
@@ -4,6 +4,9 @@ import android.app.Application;
 import android.content.Context;
 import android.support.annotation.NonNull;
 
+import ru.noties.debug.AndroidLogDebugOutput;
+import ru.noties.debug.Debug;
+
 public class App extends Application {
 
     private AppComponent component;
@@ -12,6 +15,8 @@ public class App extends Application {
     public void onCreate() {
         super.onCreate();
 
+        Debug.init(new AndroidLogDebugOutput(BuildConfig.DEBUG));
+
         component = DaggerAppComponent.builder()
                 .appModule(new AppModule(this))
                 .build();
diff --git a/app/src/main/java/ru/noties/markwon/AppModule.java b/app/src/main/java/ru/noties/markwon/AppModule.java
index 9a8f5a86..7d04a4db 100644
--- a/app/src/main/java/ru/noties/markwon/AppModule.java
+++ b/app/src/main/java/ru/noties/markwon/AppModule.java
@@ -5,8 +5,6 @@ import android.content.res.Resources;
 import android.os.Handler;
 import android.os.Looper;
 
-import com.squareup.picasso.Picasso;
-
 import java.util.concurrent.ExecutorService;
 import java.util.concurrent.Executors;
 
@@ -14,7 +12,10 @@ import javax.inject.Singleton;
 
 import dagger.Module;
 import dagger.Provides;
+import okhttp3.Cache;
 import okhttp3.OkHttpClient;
+import ru.noties.markwon.il.AsyncDrawableLoader;
+import ru.noties.markwon.spans.AsyncDrawable;
 
 @Module
 class AppModule {
@@ -38,7 +39,10 @@ class AppModule {
     @Provides
     @Singleton
     OkHttpClient client() {
-        return new OkHttpClient();
+        return new OkHttpClient.Builder()
+                .cache(new Cache(app.getCacheDir(), 1024L * 20))
+                .followRedirects(true)
+                .build();
     }
 
     @Singleton
@@ -60,7 +64,14 @@ class AppModule {
     }
 
     @Provides
-    Picasso picasso(Context context) {
-        return Picasso.with(context);
+    AsyncDrawable.Loader asyncDrawableLoader(
+            OkHttpClient client,
+            ExecutorService executorService,
+            Resources resources) {
+        return AsyncDrawableLoader.builder()
+                .client(client)
+                .executorService(executorService)
+                .resources(resources)
+                .build();
     }
 }
diff --git a/app/src/main/java/ru/noties/markwon/AsyncDrawableLoader.java b/app/src/main/java/ru/noties/markwon/AsyncDrawableLoader.java
deleted file mode 100644
index d349e498..00000000
--- a/app/src/main/java/ru/noties/markwon/AsyncDrawableLoader.java
+++ /dev/null
@@ -1,155 +0,0 @@
-package ru.noties.markwon;
-
-import android.content.res.Resources;
-import android.graphics.Bitmap;
-import android.graphics.Canvas;
-import android.graphics.drawable.BitmapDrawable;
-import android.graphics.drawable.ColorDrawable;
-import android.graphics.drawable.Drawable;
-import android.support.annotation.NonNull;
-
-import com.caverock.androidsvg.SVG;
-import com.squareup.picasso.Picasso;
-
-import java.io.ByteArrayOutputStream;
-import java.io.InputStream;
-import java.net.HttpURLConnection;
-import java.net.URL;
-import java.util.HashMap;
-import java.util.Map;
-import java.util.concurrent.CopyOnWriteArrayList;
-import java.util.concurrent.ExecutorService;
-import java.util.concurrent.Future;
-
-import javax.inject.Inject;
-
-import pl.droidsonroids.gif.GifDrawable;
-import ru.noties.debug.Debug;
-import ru.noties.markwon.spans.AsyncDrawable;
-
-@ActivityScope
-public class AsyncDrawableLoader implements AsyncDrawable.Loader {
-
-    @Inject
-    Resources resources;
-
-    @Inject
-    Picasso picasso;
-
-    @Inject
-    ExecutorService executorService;
-
-    private final Map<String, Future<?>> requests = new HashMap<>(3);
-    private final CopyOnWriteArrayList<AsyncDrawableTarget> targets = new CopyOnWriteArrayList<>();
-
-    // sh*t..
-    @Inject
-    public AsyncDrawableLoader() {
-    }
-
-    @Override
-    public void load(@NonNull String destination, @NonNull AsyncDrawable drawable) {
-
-        if (destination.endsWith(".svg")) {
-            // load svg
-            requests.put(destination, loadSvg(destination, drawable));
-        } else if (destination.endsWith(".gif")) {
-            requests.put(destination, loadGif(destination, drawable));
-        } else {
-
-            final Drawable error = new ColorDrawable(0xFFff0000);
-            final Drawable placeholder = new ColorDrawable(0xFF00ff00);
-            error.setBounds(0, 0, 100, 100);
-            placeholder.setBounds(0, 0, 50, 50);
-
-            final AsyncDrawableTarget target = new AsyncDrawableTarget(resources, drawable, new AsyncDrawableTarget.DoneListener() {
-                @Override
-                public void onLoadingDone(AsyncDrawableTarget target) {
-                    targets.remove(target);
-                }
-            });
-
-            targets.add(target);
-
-            picasso
-                    .load(destination)
-                    .tag(destination)
-                    .placeholder(placeholder)
-                    .error(error)
-                    .into(target);
-        }
-    }
-
-    @Override
-    public void cancel(@NonNull String destination) {
-        Debug.i("destination: %s", destination);
-        picasso.cancelTag(destination);
-
-        final Future<?> future = requests.remove(destination);
-        if (future != null) {
-            future.cancel(true);
-        }
-    }
-
-    private Future<?> loadSvg(final String destination, final AsyncDrawable asyncDrawable) {
-        return executorService.submit(new Runnable() {
-            @Override
-            public void run() {
-                try {
-
-                    final URL url = new URL(destination);
-                    final HttpURLConnection connection = (HttpURLConnection) url.openConnection();
-                    final InputStream inputStream = connection.getInputStream();
-
-                    final SVG svg = SVG.getFromInputStream(inputStream);
-                    final float w = svg.getDocumentWidth();
-                    final float h = svg.getDocumentHeight();
-                    Debug.i("w: %s, h: %s", w, h);
-
-                    final float density = resources.getDisplayMetrics().density;
-                    Debug.i(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);
-
-                    final Drawable drawable = new BitmapDrawable(resources, bitmap);
-                    drawable.setBounds(0, 0, drawable.getIntrinsicWidth(), drawable.getIntrinsicHeight());
-                    asyncDrawable.setResult(drawable);
-
-                } catch (Throwable t) {
-                    Debug.e(t);
-                }
-            }
-        });
-    }
-
-    private Future<?> loadGif(final String destination, final AsyncDrawable asyncDrawable) {
-        return executorService.submit(new Runnable() {
-            @Override
-            public void run() {
-                try {
-
-                    final URL url = new URL(destination);
-                    final HttpURLConnection connection = (HttpURLConnection) url.openConnection();
-                    final InputStream inputStream = connection.getInputStream();
-
-                    final ByteArrayOutputStream baos = new ByteArrayOutputStream();
-                    final byte[] buffer = new byte[1024 * 8];
-                    int read;
-                    while ((read = inputStream.read(buffer, 0, buffer.length)) != -1) {
-                        baos.write(buffer, 0, read);
-                    }
-                    final GifDrawable drawable = new GifDrawable(baos.toByteArray());
-                    drawable.setBounds(0, 0, drawable.getIntrinsicWidth(), drawable.getIntrinsicHeight());
-                    asyncDrawable.setResult(drawable);
-                } catch (Throwable t) {
-                    Debug.e(t);
-                }
-            }
-        });
-    }
-}
diff --git a/app/src/main/java/ru/noties/markwon/AsyncDrawableTarget.java b/app/src/main/java/ru/noties/markwon/AsyncDrawableTarget.java
deleted file mode 100644
index 1de0eaa1..00000000
--- a/app/src/main/java/ru/noties/markwon/AsyncDrawableTarget.java
+++ /dev/null
@@ -1,81 +0,0 @@
-package ru.noties.markwon;
-
-import android.content.res.Resources;
-import android.graphics.Bitmap;
-import android.graphics.drawable.BitmapDrawable;
-import android.graphics.drawable.Drawable;
-
-import com.squareup.picasso.Picasso;
-import com.squareup.picasso.Target;
-
-import ru.noties.markwon.spans.AsyncDrawable;
-
-public class AsyncDrawableTarget implements Target {
-
-    interface DoneListener {
-        void onLoadingDone(AsyncDrawableTarget target);
-    }
-
-    private final Resources resources;
-    private final AsyncDrawable asyncDrawable;
-    private final DoneListener listener;
-
-    public AsyncDrawableTarget(Resources resources, AsyncDrawable asyncDrawable, DoneListener listener) {
-        this.resources = resources;
-        this.asyncDrawable = asyncDrawable;
-        this.listener = listener;
-    }
-
-    @Override
-    public void onBitmapLoaded(Bitmap bitmap, Picasso.LoadedFrom from) {
-        if (bitmap != null) {
-            final Drawable drawable = new BitmapDrawable(resources, bitmap);
-            drawable.setBounds(0, 0, drawable.getIntrinsicWidth(), drawable.getIntrinsicHeight());
-            asyncDrawable.setResult(drawable);
-        }
-        notifyDone();
-    }
-
-    @Override
-    public void onBitmapFailed(Drawable errorDrawable) {
-        if (errorDrawable != null) {
-            asyncDrawable.setResult(errorDrawable);
-        }
-        notifyDone();
-    }
-
-    @Override
-    public void onPrepareLoad(Drawable placeHolderDrawable) {
-        if (placeHolderDrawable != null) {
-            asyncDrawable.setResult(placeHolderDrawable);
-        }
-    }
-
-    private void notifyDone() {
-        if (listener != null) {
-            listener.onLoadingDone(this);
-        }
-    }
-//
-//    private void attach() {
-//
-//        // amazing stuff here, in order to keep this target alive (picasso stores target in a WeakReference)
-//        // we need to do this
-//
-//        //noinspection unchecked
-//        List<AsyncDrawableTarget> list = (List<AsyncDrawableTarget>) view.getTag(R.id.amazing);
-//        if (list == null) {
-//            list = new ArrayList<>(2);
-//            view.setTag(R.id.amazing, list);
-//        }
-//        list.add(this);
-//    }
-//
-//    private void detach() {
-//        //noinspection unchecked
-//        final List<AsyncDrawableTarget> list = (List<AsyncDrawableTarget>) view.getTag(R.id.amazing);
-//        if (list != null) {
-//            list.remove(this);
-//        }
-//    }
-}
diff --git a/app/src/main/java/ru/noties/markwon/CollectionUtils.java b/app/src/main/java/ru/noties/markwon/CollectionUtils.java
deleted file mode 100644
index 7e3cad92..00000000
--- a/app/src/main/java/ru/noties/markwon/CollectionUtils.java
+++ /dev/null
@@ -1,12 +0,0 @@
-package ru.noties.markwon;
-
-import java.util.Collection;
-
-public abstract class CollectionUtils {
-
-    public static boolean isEmpty(Collection<?> collection) {
-        return collection == null || collection.size() == 0;
-    }
-
-    private CollectionUtils() {}
-}
diff --git a/app/src/main/java/ru/noties/markwon/MainActivity.java b/app/src/main/java/ru/noties/markwon/MainActivity.java
index 30f1995d..ba311cbd 100644
--- a/app/src/main/java/ru/noties/markwon/MainActivity.java
+++ b/app/src/main/java/ru/noties/markwon/MainActivity.java
@@ -9,15 +9,10 @@ import android.widget.TextView;
 
 import javax.inject.Inject;
 
-import ru.noties.debug.AndroidLogDebugOutput;
 import ru.noties.debug.Debug;
 
 public class MainActivity extends Activity {
 
-    static {
-        Debug.init(new AndroidLogDebugOutput(true));
-    }
-
     @Inject
     MarkdownLoader markdownLoader;
 
@@ -112,6 +107,16 @@ public class MainActivity extends Activity {
                 : null;
     }
 
+    @Override
+    protected void onRestoreInstanceState(Bundle savedInstanceState) {
+        try {
+            super.onRestoreInstanceState(savedInstanceState);
+        } catch (Throwable t) {
+            // amazing stuff, we need this because on JB it will crash otherwise with NPE
+            Debug.e(t);
+        }
+    }
+
     @Override
     protected void onDestroy() {
         super.onDestroy();
diff --git a/app/src/main/java/ru/noties/markwon/MarkdownLoader.java b/app/src/main/java/ru/noties/markwon/MarkdownLoader.java
index 0e186765..02bb902d 100644
--- a/app/src/main/java/ru/noties/markwon/MarkdownLoader.java
+++ b/app/src/main/java/ru/noties/markwon/MarkdownLoader.java
@@ -72,13 +72,20 @@ public class MarkdownLoader {
         }
     }
 
+    private boolean isCancelled() {
+        return task == null || task.isCancelled();
+    }
+
     private void deliver(@NonNull final OnMarkdownTextLoaded loaded, final String text) {
-        if (task != null
-                && !task.isCancelled()) {
+        if (!isCancelled()) {
             handler.post(new Runnable() {
                 @Override
                 public void run() {
-                    loaded.apply(text);
+                    // as this call is async, we need to check again if we are cancelled
+                    if (!isCancelled()) {
+                        loaded.apply(text);
+                        task = null;
+                    }
                 }
             });
         }
diff --git a/app/src/main/java/ru/noties/markwon/MarkdownRenderer.java b/app/src/main/java/ru/noties/markwon/MarkdownRenderer.java
index 002719e1..1d0c4dc5 100644
--- a/app/src/main/java/ru/noties/markwon/MarkdownRenderer.java
+++ b/app/src/main/java/ru/noties/markwon/MarkdownRenderer.java
@@ -17,6 +17,7 @@ import java.util.concurrent.Future;
 import javax.inject.Inject;
 
 import ru.noties.markwon.renderer.SpannableRenderer;
+import ru.noties.markwon.spans.AsyncDrawable;
 
 @ActivityScope
 public class MarkdownRenderer {
@@ -26,7 +27,7 @@ public class MarkdownRenderer {
     }
 
     @Inject
-    AsyncDrawableLoader loader;
+    AsyncDrawable.Loader loader;
 
     @Inject
     ExecutorService service;
@@ -62,22 +63,19 @@ public class MarkdownRenderer {
                         .urlProcessor(urlProcessor)
                         .build();
 
-                final Parser parser = Parser.builder()
-                        .extensions(Collections.singleton(StrikethroughExtension.create()))
-                        .build();
+                final CharSequence text = Markwon.markdown(configuration, markdown);
 
-                final Node node = parser.parse(markdown);
-                final SpannableRenderer renderer = new SpannableRenderer();
-                final CharSequence text = renderer.render(configuration, node);
-
-//                final CharSequence text = Markwon.markdown(configuration, markdown);
-                handler.post(new Runnable() {
-                    @Override
-                    public void run() {
-                        listener.onMarkdownReady(text);
-                    }
-                });
-                task = null;
+                if (!isCancelled()) {
+                    handler.post(new Runnable() {
+                        @Override
+                        public void run() {
+                            if (!isCancelled()) {
+                                listener.onMarkdownReady(text);
+                                task = null;
+                            }
+                        }
+                    });
+                }
             }
         });
     }
@@ -88,4 +86,8 @@ public class MarkdownRenderer {
             task = null;
         }
     }
+
+    private boolean isCancelled() {
+        return task == null || task.isCancelled();
+    }
 }
diff --git a/app/src/main/java/ru/noties/markwon/Themes.java b/app/src/main/java/ru/noties/markwon/Themes.java
index 05e51747..d6a9ae8b 100644
--- a/app/src/main/java/ru/noties/markwon/Themes.java
+++ b/app/src/main/java/ru/noties/markwon/Themes.java
@@ -22,7 +22,7 @@ public class Themes {
 
     public void apply(@NonNull Context context) {
         final boolean dark = preferences.getBoolean(KEY_THEME_DARK, false);
-        // we have only 2 themes and Light one is default, so no need to apply it
+        // we have only 2 themes and Light one is default
         final int theme;
         if (dark) {
             theme = R.style.AppThemeBaseDark;
diff --git a/app/src/main/java/ru/noties/markwon/Views.java b/app/src/main/java/ru/noties/markwon/Views.java
index 57c283ca..b9f692d0 100644
--- a/app/src/main/java/ru/noties/markwon/Views.java
+++ b/app/src/main/java/ru/noties/markwon/Views.java
@@ -6,10 +6,12 @@ import android.support.annotation.IntDef;
 import android.support.annotation.NonNull;
 import android.view.View;
 
+@SuppressWarnings("WeakerAccess")
 public abstract class Views {
 
     @IntDef({View.INVISIBLE, View.GONE})
-    @interface NotVisible {}
+    @interface NotVisible {
+    }
 
     public static <V extends View> V findView(@NonNull View view, @IdRes int id) {
         //noinspection unchecked
@@ -32,5 +34,6 @@ public abstract class Views {
         view.setVisibility(visibility);
     }
 
-    private Views() {}
+    private Views() {
+    }
 }
diff --git a/app/src/main/res/values/styles.xml b/app/src/main/res/values/styles.xml
index b8632f76..d8703dd5 100644
--- a/app/src/main/res/values/styles.xml
+++ b/app/src/main/res/values/styles.xml
@@ -6,7 +6,7 @@
         <item name="ic_app_bar_theme">@drawable/ic_app_bar_theme_light</item>
     </style>
 
-    <style name="AppThemeBaseLight" parent="android:Theme.Holo.Light">
+    <style name="AppThemeBaseLight" parent="android:Theme.Holo.Light.NoActionBar">
         <item name="ic_app_bar_theme">@drawable/ic_app_bar_theme_dark</item>
     </style>
 
diff --git a/build.gradle b/build.gradle
index 9b51f6fc..ece44916 100644
--- a/build.gradle
+++ b/build.gradle
@@ -32,4 +32,8 @@ ext {
     final def commonMarkVersion = '0.9.0'
     COMMON_MARK = "com.atlassian.commonmark:commonmark:$commonMarkVersion"
     COMMON_MARK_STRIKETHROUGHT = "com.atlassian.commonmark:commonmark-ext-gfm-strikethrough:$commonMarkVersion"
+
+    ANDROID_SVG = 'com.caverock:androidsvg:1.2.1'
+    ANDROID_GIF = 'pl.droidsonroids.gif:android-gif-drawable:1.2.7'
+    OK_HTTP = 'com.squareup.okhttp3:okhttp:3.8.0'
 }
diff --git a/library-image-loader/build.gradle b/library-image-loader/build.gradle
new file mode 100644
index 00000000..3fbfe21a
--- /dev/null
+++ b/library-image-loader/build.gradle
@@ -0,0 +1,25 @@
+apply plugin: 'com.android.library'
+
+android {
+
+    compileSdkVersion TARGET_SDK
+    buildToolsVersion BUILD_TOOLS
+
+    defaultConfig {
+        minSdkVersion MIN_SDK
+        targetSdkVersion TARGET_SDK
+        versionCode 1
+        versionName version
+    }
+}
+
+dependencies {
+
+    compile project(':library-renderer')
+    compile ANDROID_SVG
+    compile ANDROID_GIF
+    compile OK_HTTP
+
+    // todo, debugging only
+    compile 'ru.noties:debug:3.0.0@jar'
+}
\ No newline at end of file
diff --git a/library-image-loader/src/main/AndroidManifest.xml b/library-image-loader/src/main/AndroidManifest.xml
new file mode 100644
index 00000000..35da8e8a
--- /dev/null
+++ b/library-image-loader/src/main/AndroidManifest.xml
@@ -0,0 +1 @@
+<manifest package="ru.noties.markwon.il" />
diff --git a/library-image-loader/src/main/java/ru/noties/markwon/il/AsyncDrawableLoader.java b/library-image-loader/src/main/java/ru/noties/markwon/il/AsyncDrawableLoader.java
new file mode 100644
index 00000000..9b0feae0
--- /dev/null
+++ b/library-image-loader/src/main/java/ru/noties/markwon/il/AsyncDrawableLoader.java
@@ -0,0 +1,298 @@
+package ru.noties.markwon.il;
+
+import android.content.res.Resources;
+import android.graphics.Bitmap;
+import android.graphics.BitmapFactory;
+import android.graphics.Canvas;
+import android.graphics.drawable.BitmapDrawable;
+import android.graphics.drawable.Drawable;
+import android.os.Handler;
+import android.os.Looper;
+import android.support.annotation.NonNull;
+import android.text.TextUtils;
+
+import com.caverock.androidsvg.SVG;
+import com.caverock.androidsvg.SVGParseException;
+
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.lang.ref.WeakReference;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Future;
+
+import okhttp3.Call;
+import okhttp3.OkHttpClient;
+import okhttp3.Request;
+import okhttp3.Response;
+import okhttp3.ResponseBody;
+import pl.droidsonroids.gif.GifDrawable;
+import ru.noties.debug.Debug;
+import ru.noties.markwon.spans.AsyncDrawable;
+
+public class AsyncDrawableLoader implements AsyncDrawable.Loader {
+
+    public static AsyncDrawableLoader create() {
+        return builder().build();
+    }
+
+    public static AsyncDrawableLoader.Builder builder() {
+        return new Builder();
+    }
+
+    private static final String HEADER_CONTENT_TYPE = "Content-Type";
+    private static final String CONTENT_TYPE_SVG = "image/svg+xml";
+    private static final String CONTENT_TYPE_GIF = "image/gif";
+
+    private final OkHttpClient client;
+    private final Resources resources;
+    private final ExecutorService executorService;
+    private final Handler mainThread;
+    private final Drawable errorDrawable;
+
+    private final Map<String, Future<?>> requests;
+
+    AsyncDrawableLoader(Builder builder) {
+        this.client = builder.client;
+        this.resources = builder.resources;
+        this.executorService = builder.executorService;
+        this.mainThread = new Handler(Looper.getMainLooper());
+        this.errorDrawable = builder.errorDrawable;
+        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);
+        }
+
+        final List<Call> calls = client.dispatcher().queuedCalls();
+        if (calls != null) {
+            for (Call call : calls) {
+                if (!call.isCanceled()) {
+                    if (destination.equals(call.request().tag())) {
+                        call.cancel();
+                    }
+                }
+            }
+        }
+    }
+
+    private Future<?> execute(@NonNull final String destination, @NonNull AsyncDrawable drawable) {
+        final WeakReference<AsyncDrawable> reference = new WeakReference<AsyncDrawable>(drawable);
+        // todo, if not a link -> show placeholder
+        return executorService.submit(new Runnable() {
+            @Override
+            public void run() {
+
+                final Request request = new Request.Builder()
+                        .url(destination)
+                        .tag(destination)
+                        .build();
+
+                Response response = null;
+                try {
+                    response = client.newCall(request).execute();
+                } catch (IOException e) {
+                    e.printStackTrace();
+                }
+
+                Debug.i(destination, response);
+
+                Drawable result = null;
+
+                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);
+                            try {
+                                // svg can have `image/svg+xml;charset=...`
+                                if (CONTENT_TYPE_SVG.equals(contentType)
+                                        || (!TextUtils.isEmpty(contentType) && contentType.startsWith(CONTENT_TYPE_SVG))) {
+                                    // handle SVG
+                                    result = handleSvg(inputStream);
+                                } else if (CONTENT_TYPE_GIF.equals(contentType)) {
+                                    // handle gif
+                                    result = handleGif(inputStream);
+                                } else {
+                                    result = handleSimple(inputStream);
+                                    // just try to decode whatever it is
+                                }
+                            } finally {
+                                try {
+                                    inputStream.close();
+                                } catch (IOException e) {
+                                    // no op
+                                }
+                            }
+                        }
+                    }
+                }
+
+                // 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);
+            }
+        });
+    }
+
+    private Drawable handleSvg(InputStream stream) {
+
+        final Drawable out;
+
+        SVG svg = null;
+        try {
+            svg = SVG.getFromInputStream(stream);
+        } 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;
+    }
+
+    private Drawable handleGif(InputStream stream) {
+
+        Drawable out = null;
+
+        final byte[] bytes = readBytes(stream);
+        if (bytes != null) {
+            try {
+                out = new GifDrawable(bytes);
+                DrawableUtils.intrinsicBounds(out);
+            } catch (IOException e) {
+                e.printStackTrace();
+            }
+        }
+
+        return out;
+    }
+
+    private Drawable handleSimple(InputStream stream) {
+
+        final Drawable out;
+
+        final Bitmap bitmap = BitmapFactory.decodeStream(stream);
+        if (bitmap != null) {
+            out = new BitmapDrawable(resources, bitmap);
+            DrawableUtils.intrinsicBounds(out);
+        } else {
+            out = null;
+        }
+
+        return out;
+    }
+
+    private static byte[] readBytes(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;
+    }
+
+    public static class Builder {
+
+        private OkHttpClient client;
+        private Resources resources;
+        private ExecutorService executorService;
+        private Drawable errorDrawable;
+
+        public Builder client(@NonNull OkHttpClient client) {
+            this.client = client;
+            return this;
+        }
+
+        public Builder resources(@NonNull Resources resources) {
+            this.resources = resources;
+            return this;
+        }
+
+        public Builder executorService(ExecutorService executorService) {
+            this.executorService = executorService;
+            return this;
+        }
+
+        public Builder errorDrawable(Drawable errorDrawable) {
+            this.errorDrawable = errorDrawable;
+            return this;
+        }
+
+        public AsyncDrawableLoader build() {
+            if (client == null) {
+                client = new OkHttpClient();
+            }
+            if (resources == null) {
+                resources = Resources.getSystem();
+            }
+            if (executorService == null) {
+                // we will use executor from okHttp
+                executorService = client.dispatcher().executorService();
+            }
+            return new AsyncDrawableLoader(this);
+        }
+    }
+}
diff --git a/library-image-loader/src/main/java/ru/noties/markwon/il/DrawableUtils.java b/library-image-loader/src/main/java/ru/noties/markwon/il/DrawableUtils.java
new file mode 100644
index 00000000..f2aef636
--- /dev/null
+++ b/library-image-loader/src/main/java/ru/noties/markwon/il/DrawableUtils.java
@@ -0,0 +1,13 @@
+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() {}
+}
diff --git a/library-renderer/src/main/java/ru/noties/markwon/DrawablesScheduler.java b/library-renderer/src/main/java/ru/noties/markwon/DrawablesScheduler.java
index 7defb361..bdb50024 100644
--- a/library-renderer/src/main/java/ru/noties/markwon/DrawablesScheduler.java
+++ b/library-renderer/src/main/java/ru/noties/markwon/DrawablesScheduler.java
@@ -14,7 +14,6 @@ import java.util.ArrayList;
 import java.util.Collections;
 import java.util.List;
 
-import ru.noties.debug.Debug;
 import ru.noties.markwon.spans.AsyncDrawable;
 import ru.noties.markwon.spans.AsyncDrawableSpan;
 
@@ -22,7 +21,7 @@ abstract class DrawablesScheduler {
 
     static void schedule(@NonNull final TextView textView) {
 
-        final List<Pair> list = extract(textView, true);
+        final List<AsyncDrawable> list = extract(textView);
         if (list.size() > 0) {
             textView.addOnAttachStateChangeListener(new View.OnAttachStateChangeListener() {
                 @Override
@@ -37,22 +36,22 @@ abstract class DrawablesScheduler {
                 }
             });
 
-            for (Pair pair : list) {
-                pair.drawable.setCallback2(new DrawableCallbackImpl(textView, pair.coordinatesProvider, pair.drawable.getBounds()));
+            for (AsyncDrawable drawable : list) {
+                drawable.setCallback2(new DrawableCallbackImpl(textView, drawable.getBounds()));
             }
         }
     }
 
     // must be called when text manually changed in TextView
     static void unschedule(@NonNull TextView view) {
-        for (Pair pair : extract(view, false)) {
-            pair.drawable.setCallback2(null);
+        for (AsyncDrawable drawable : extract(view)) {
+            drawable.setCallback2(null);
         }
     }
 
-    private static List<Pair> extract(@NonNull TextView view, boolean coordinates) {
+    private static List<AsyncDrawable> extract(@NonNull TextView view) {
 
-        final List<Pair> list;
+        final List<AsyncDrawable> list;
 
         final CharSequence cs = view.getText();
         final int length = cs != null
@@ -74,18 +73,14 @@ abstract class DrawablesScheduler {
                     if (span instanceof AsyncDrawableSpan) {
 
                         final AsyncDrawableSpan asyncDrawableSpan = (AsyncDrawableSpan) span;
-                        final CoordinatesProvider provider = coordinates
-                                ? new AsyncDrawableSpanCoordinatesProvider(asyncDrawableSpan)
-                                : null;
-
-                        list.add(new Pair(asyncDrawableSpan.getDrawable(), provider));
+                        list.add(asyncDrawableSpan.getDrawable());
                     } else if (span instanceof DynamicDrawableSpan) {
                         // it's really not optimal thing because it stores Drawable in WeakReference...
                         // which is why it will be most likely already de-referenced...
                         final Drawable d = ((DynamicDrawableSpan) span).getDrawable();
                         if (d != null
                                 && d instanceof AsyncDrawable) {
-                            list.add(new Pair((AsyncDrawable) d, null));
+                            list.add((AsyncDrawable) d);
                         }
                     }
                 }
@@ -101,21 +96,13 @@ abstract class DrawablesScheduler {
     private DrawablesScheduler() {
     }
 
-    private interface CoordinatesProvider {
-        int getX();
-
-        int getY();
-    }
-
     private static class DrawableCallbackImpl implements Drawable.Callback {
 
         private final TextView view;
-        private final CoordinatesProvider coordinatesProvider;
         private Rect previousBounds;
 
-        DrawableCallbackImpl(TextView view, CoordinatesProvider provider, Rect initialBounds) {
+        DrawableCallbackImpl(TextView view, Rect initialBounds) {
             this.view = view;
-            this.coordinatesProvider = provider;
             this.previousBounds = new Rect(initialBounds);
         }
 
@@ -134,7 +121,7 @@ abstract class DrawablesScheduler {
 
             final Rect rect = who.getBounds();
 
-            // okay... teh thing is IF we do not change bounds size, normal invalidate would do
+            // okay... the thing is IF we do not change bounds size, normal invalidate would do
             // but if the size has changed, then we need to update the whole layout...
 
             if (!previousBounds.equals(rect)) {
@@ -143,29 +130,7 @@ abstract class DrawablesScheduler {
                 previousBounds = new Rect(rect);
             } else {
 
-                // if bounds are the same then simple invalidate would do
-
-                if (coordinatesProvider != null) {
-                    final int x = coordinatesProvider.getX();
-                    final int y = coordinatesProvider.getY();
-                    view.postInvalidate(
-                            x + rect.left,
-                            y + rect.top,
-                            x + rect.right,
-                            y + rect.bottom
-                    );
-                    Debug.i(x + rect.left,
-                            y + rect.top,
-                            x + rect.right,
-                            y + rect.bottom);
-                } else {
-                    Debug.i();
-                    // else all we can do is request full re-draw... maybe system is smart enough not re-draw what is not on screen?
-                    view.postInvalidate();
-//                 we do not need to invalidate if, for example, a gif is playing somewhere out of current viewPort...
-//                 but i do not see...
-                }
-//                view.postInvalidate();
+                view.postInvalidate();
             }
         }
 
@@ -180,33 +145,4 @@ abstract class DrawablesScheduler {
             view.removeCallbacks(what);
         }
     }
-
-    private static class AsyncDrawableSpanCoordinatesProvider implements CoordinatesProvider {
-
-        private final AsyncDrawableSpan span;
-
-        private AsyncDrawableSpanCoordinatesProvider(AsyncDrawableSpan span) {
-            this.span = span;
-        }
-
-        @Override
-        public int getX() {
-            return span.lastKnownDrawX();
-        }
-
-        @Override
-        public int getY() {
-            return span.lastKnownDrawY();
-        }
-    }
-
-    private static class Pair {
-        final AsyncDrawable drawable;
-        final CoordinatesProvider coordinatesProvider;
-
-        Pair(AsyncDrawable drawable, CoordinatesProvider coordinatesProvider) {
-            this.drawable = drawable;
-            this.coordinatesProvider = coordinatesProvider;
-        }
-    }
 }
diff --git a/library-renderer/src/main/java/ru/noties/markwon/LinkResolverDef.java b/library-renderer/src/main/java/ru/noties/markwon/LinkResolverDef.java
index 2ca4de56..109af717 100644
--- a/library-renderer/src/main/java/ru/noties/markwon/LinkResolverDef.java
+++ b/library-renderer/src/main/java/ru/noties/markwon/LinkResolverDef.java
@@ -11,7 +11,7 @@ import android.view.View;
 
 import ru.noties.markwon.spans.LinkSpan;
 
-class LinkResolverDef implements LinkSpan.Resolver {
+public class LinkResolverDef implements LinkSpan.Resolver {
     @Override
     public void resolve(View view, @NonNull String link) {
         final Uri uri = Uri.parse(link);
diff --git a/library-renderer/src/main/java/ru/noties/markwon/SpannableConfiguration.java b/library-renderer/src/main/java/ru/noties/markwon/SpannableConfiguration.java
index 024c8fa0..4952b599 100644
--- a/library-renderer/src/main/java/ru/noties/markwon/SpannableConfiguration.java
+++ b/library-renderer/src/main/java/ru/noties/markwon/SpannableConfiguration.java
@@ -121,7 +121,7 @@ public class SpannableConfiguration {
                 urlProcessor = new UrlProcessorNoOp();
             }
             if (htmlParser == null) {
-                htmlParser = SpannableHtmlParser.create(theme, asyncDrawableLoader, urlProcessor);
+                htmlParser = SpannableHtmlParser.create(theme, asyncDrawableLoader, urlProcessor, linkResolver);
             }
             return new SpannableConfiguration(this);
         }
diff --git a/library-renderer/src/main/java/ru/noties/markwon/UrlProcessorRelativeToAbsolute.java b/library-renderer/src/main/java/ru/noties/markwon/UrlProcessorRelativeToAbsolute.java
index bae6fcb6..fcfd4cab 100644
--- a/library-renderer/src/main/java/ru/noties/markwon/UrlProcessorRelativeToAbsolute.java
+++ b/library-renderer/src/main/java/ru/noties/markwon/UrlProcessorRelativeToAbsolute.java
@@ -6,6 +6,7 @@ import android.support.annotation.Nullable;
 import java.net.MalformedURLException;
 import java.net.URL;
 
+@SuppressWarnings("WeakerAccess")
 public class UrlProcessorRelativeToAbsolute implements UrlProcessor {
 
     private final URL base;
diff --git a/library-renderer/src/main/java/ru/noties/markwon/renderer/SpannableMarkdownVisitor.java b/library-renderer/src/main/java/ru/noties/markwon/renderer/SpannableMarkdownVisitor.java
index 656f1087..70fef6a6 100644
--- a/library-renderer/src/main/java/ru/noties/markwon/renderer/SpannableMarkdownVisitor.java
+++ b/library-renderer/src/main/java/ru/noties/markwon/renderer/SpannableMarkdownVisitor.java
@@ -33,7 +33,6 @@ import org.commonmark.node.ThematicBreak;
 import java.util.ArrayDeque;
 import java.util.Deque;
 
-import ru.noties.debug.Debug;
 import ru.noties.markwon.SpannableConfiguration;
 import ru.noties.markwon.renderer.html.SpannableHtmlParser;
 import ru.noties.markwon.spans.AsyncDrawable;
@@ -51,8 +50,6 @@ import ru.noties.markwon.spans.ThematicBreakSpan;
 @SuppressWarnings("WeakerAccess")
 public class SpannableMarkdownVisitor extends AbstractVisitor {
 
-    private static final String HTML_CONTENT = "<%1$s>%2$s</%3$s>";
-
     private final SpannableConfiguration configuration;
     private final SpannableStringBuilder builder;
     private final Deque<HtmlInlineItem> htmlInlineItems;
@@ -302,7 +299,7 @@ public class SpannableMarkdownVisitor extends AbstractVisitor {
 
         // we must check if anything _was_ added, as we need at least one char to render
         if (length == builder.length()) {
-            builder.append(' '); // breakable space
+            builder.append('\uFFFC');
         }
 
         final Node parent = image.getParent();
@@ -321,17 +318,23 @@ public class SpannableMarkdownVisitor extends AbstractVisitor {
                         link
                 )
         );
+
+        // todo, maybe, if image is not inside a link, we should make it clickable, so
+        // user can open it in external viewer?
     }
 
     @Override
     public void visit(HtmlBlock htmlBlock) {
         // http://spec.commonmark.org/0.18/#html-blocks
-        Debug.i(htmlBlock, htmlBlock.getLiteral());
-        super.visit(htmlBlock);
+        final Spanned spanned = configuration.htmlParser().getSpanned(null, htmlBlock.getLiteral());
+        if (!TextUtils.isEmpty(spanned)) {
+            builder.append(spanned);
+        }
     }
 
     @Override
     public void visit(HtmlInline htmlInline) {
+
         final SpannableHtmlParser htmlParser = configuration.htmlParser();
         final SpannableHtmlParser.Tag tag = htmlParser.parseTag(htmlInline.getLiteral());
 
@@ -340,37 +343,25 @@ public class SpannableMarkdownVisitor extends AbstractVisitor {
             final boolean voidTag = tag.voidTag();
             if (!voidTag && tag.opening()) {
                 // push in stack
-                htmlInlineItems.push(new HtmlInlineItem(tag.name(), builder.length()));
+                htmlInlineItems.push(new HtmlInlineItem(tag, builder.length()));
                 visitChildren(htmlInline);
             } else {
 
                 if (!voidTag) {
                     if (htmlInlineItems.size() > 0) {
                         final HtmlInlineItem item = htmlInlineItems.pop();
-                        final Object span = htmlParser.handleTag(item.tag);
-                        final int start = item.start;
+                        final Object span = htmlParser.getSpanForTag(item.tag);
                         if (span != null) {
                             setSpan(item.start, span);
-                        } else {
-                            final String content = builder.subSequence(start, builder.length()).toString();
-                            final String html = String.format(HTML_CONTENT, item.tag, content, tag.name());
-                            final Object[] spans = htmlParser.htmlSpans(html);
-                            final int length = spans != null
-                                    ? spans.length
-                                    : 0;
-                            for (int i = 0; i < length; i++) {
-                                setSpan(start, spans[i]);
-                            }
                         }
                     }
                 } else {
-                    final String content = htmlInline.getLiteral();
-                    if (!TextUtils.isEmpty(content)) {
-                        final Spanned html = htmlParser.html(content);
-                        if (!TextUtils.isEmpty(html)) {
-                            builder.append(html);
-                        }
+
+                    final Spanned html = htmlParser.getSpanned(tag, htmlInline.getLiteral());
+                    if (!TextUtils.isEmpty(html)) {
+                        builder.append(html);
                     }
+
                 }
             }
         } else {
@@ -412,10 +403,11 @@ public class SpannableMarkdownVisitor extends AbstractVisitor {
     }
 
     private static class HtmlInlineItem {
-        final String tag;
+
+        final SpannableHtmlParser.Tag tag;
         final int start;
 
-        HtmlInlineItem(String tag, int start) {
+        HtmlInlineItem(SpannableHtmlParser.Tag tag, int start) {
             this.tag = tag;
             this.start = start;
         }
diff --git a/library-renderer/src/main/java/ru/noties/markwon/renderer/html/BoldProvider.java b/library-renderer/src/main/java/ru/noties/markwon/renderer/html/BoldProvider.java
index be89a777..7613b8b4 100644
--- a/library-renderer/src/main/java/ru/noties/markwon/renderer/html/BoldProvider.java
+++ b/library-renderer/src/main/java/ru/noties/markwon/renderer/html/BoldProvider.java
@@ -1,10 +1,13 @@
 package ru.noties.markwon.renderer.html;
 
+import android.support.annotation.NonNull;
+
 import ru.noties.markwon.spans.StrongEmphasisSpan;
 
 class BoldProvider implements SpannableHtmlParser.SpanProvider {
+
     @Override
-    public Object provide() {
+    public Object provide(@NonNull SpannableHtmlParser.Tag tag) {
         return new StrongEmphasisSpan();
     }
 }
diff --git a/library-renderer/src/main/java/ru/noties/markwon/renderer/html/HtmlImageGetter.java b/library-renderer/src/main/java/ru/noties/markwon/renderer/html/HtmlImageGetter.java
deleted file mode 100644
index 1469a4f1..00000000
--- a/library-renderer/src/main/java/ru/noties/markwon/renderer/html/HtmlImageGetter.java
+++ /dev/null
@@ -1,31 +0,0 @@
-package ru.noties.markwon.renderer.html;
-
-import android.graphics.drawable.Drawable;
-import android.support.annotation.NonNull;
-import android.support.annotation.Nullable;
-import android.text.Html;
-
-import ru.noties.markwon.UrlProcessor;
-import ru.noties.markwon.spans.AsyncDrawable;
-
-class HtmlImageGetter implements Html.ImageGetter {
-
-    private final AsyncDrawable.Loader loader;
-    private final UrlProcessor urlProcessor;
-
-    HtmlImageGetter(@NonNull AsyncDrawable.Loader loader, @Nullable UrlProcessor urlProcessor) {
-        this.loader = loader;
-        this.urlProcessor = urlProcessor;
-    }
-
-    @Override
-    public Drawable getDrawable(String source) {
-        final String destination;
-        if (urlProcessor == null) {
-            destination = source;
-        } else {
-            destination = urlProcessor.process(source);
-        }
-        return new AsyncDrawable(destination, loader);
-    }
-}
diff --git a/library-renderer/src/main/java/ru/noties/markwon/renderer/html/ImageProviderImpl.java b/library-renderer/src/main/java/ru/noties/markwon/renderer/html/ImageProviderImpl.java
new file mode 100644
index 00000000..3e45caee
--- /dev/null
+++ b/library-renderer/src/main/java/ru/noties/markwon/renderer/html/ImageProviderImpl.java
@@ -0,0 +1,63 @@
+package ru.noties.markwon.renderer.html;
+
+import android.support.annotation.NonNull;
+import android.text.SpannableString;
+import android.text.Spanned;
+import android.text.TextUtils;
+
+import java.util.Map;
+
+import ru.noties.markwon.UrlProcessor;
+import ru.noties.markwon.spans.AsyncDrawable;
+import ru.noties.markwon.spans.AsyncDrawableSpan;
+import ru.noties.markwon.spans.SpannableTheme;
+
+class ImageProviderImpl implements SpannableHtmlParser.ImageProvider {
+
+    private final SpannableTheme theme;
+    private final AsyncDrawable.Loader loader;
+    private final UrlProcessor urlProcessor;
+
+    ImageProviderImpl(
+            @NonNull SpannableTheme theme,
+            @NonNull AsyncDrawable.Loader loader,
+            @NonNull UrlProcessor urlProcessor) {
+        this.theme = theme;
+        this.loader = loader;
+        this.urlProcessor = urlProcessor;
+    }
+
+    @Override
+    public Spanned provide(@NonNull SpannableHtmlParser.Tag tag) {
+
+        final Spanned spanned;
+
+        final Map<String, String> attributes = tag.attributes();
+        final String src = attributes.get("src");
+        final String alt = attributes.get("alt");
+
+        if (!TextUtils.isEmpty(src)) {
+
+            final String destination = urlProcessor.process(src);
+
+            final String replacement;
+            if (!TextUtils.isEmpty(alt)) {
+                replacement = alt;
+            } else {
+                replacement = "\uFFFC";
+            }
+
+            final AsyncDrawable drawable = new AsyncDrawable(destination, loader);
+            final AsyncDrawableSpan span = new AsyncDrawableSpan(theme, drawable);
+
+            final SpannableString string = new SpannableString(replacement);
+            string.setSpan(span, 0, string.length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
+
+            spanned = string;
+        } else {
+            spanned = null;
+        }
+
+        return spanned;
+    }
+}
diff --git a/library-renderer/src/main/java/ru/noties/markwon/renderer/html/ItalicsProvider.java b/library-renderer/src/main/java/ru/noties/markwon/renderer/html/ItalicsProvider.java
index e4a47bae..3fd7f068 100644
--- a/library-renderer/src/main/java/ru/noties/markwon/renderer/html/ItalicsProvider.java
+++ b/library-renderer/src/main/java/ru/noties/markwon/renderer/html/ItalicsProvider.java
@@ -1,10 +1,13 @@
 package ru.noties.markwon.renderer.html;
 
+import android.support.annotation.NonNull;
+
 import ru.noties.markwon.spans.EmphasisSpan;
 
 class ItalicsProvider implements SpannableHtmlParser.SpanProvider {
+
     @Override
-    public Object provide() {
+    public Object provide(@NonNull SpannableHtmlParser.Tag tag) {
         return new EmphasisSpan();
     }
 }
diff --git a/library-renderer/src/main/java/ru/noties/markwon/renderer/html/LinkProvider.java b/library-renderer/src/main/java/ru/noties/markwon/renderer/html/LinkProvider.java
new file mode 100644
index 00000000..b85456f6
--- /dev/null
+++ b/library-renderer/src/main/java/ru/noties/markwon/renderer/html/LinkProvider.java
@@ -0,0 +1,45 @@
+package ru.noties.markwon.renderer.html;
+
+import android.support.annotation.NonNull;
+import android.text.TextUtils;
+
+import java.util.Map;
+
+import ru.noties.markwon.UrlProcessor;
+import ru.noties.markwon.spans.LinkSpan;
+import ru.noties.markwon.spans.SpannableTheme;
+
+class LinkProvider implements SpannableHtmlParser.SpanProvider {
+
+    private final SpannableTheme theme;
+    private final UrlProcessor urlProcessor;
+    private final LinkSpan.Resolver resolver;
+
+    LinkProvider(
+            @NonNull SpannableTheme theme,
+            @NonNull UrlProcessor urlProcessor,
+            @NonNull LinkSpan.Resolver resolver) {
+        this.theme = theme;
+        this.urlProcessor = urlProcessor;
+        this.resolver = resolver;
+    }
+
+    @Override
+    public Object provide(@NonNull SpannableHtmlParser.Tag tag) {
+
+        final Object span;
+
+        final Map<String, String> attributes = tag.attributes();
+        final String href = attributes.get("href");
+        if (!TextUtils.isEmpty(href)) {
+
+            final String destination = urlProcessor.process(href);
+            span = new LinkSpan(theme, destination, resolver);
+
+        } else {
+            span = null;
+        }
+
+        return span;
+    }
+}
diff --git a/library-renderer/src/main/java/ru/noties/markwon/renderer/html/SpannableHtmlParser.java b/library-renderer/src/main/java/ru/noties/markwon/renderer/html/SpannableHtmlParser.java
index 6a6e577a..74df59a1 100644
--- a/library-renderer/src/main/java/ru/noties/markwon/renderer/html/SpannableHtmlParser.java
+++ b/library-renderer/src/main/java/ru/noties/markwon/renderer/html/SpannableHtmlParser.java
@@ -7,37 +7,35 @@ import android.support.annotation.Nullable;
 import android.text.Html;
 import android.text.Spanned;
 
-import java.util.Collections;
 import java.util.HashMap;
-import java.util.HashSet;
 import java.util.Map;
-import java.util.Set;
 
-import ru.noties.debug.Debug;
+import ru.noties.markwon.LinkResolverDef;
 import ru.noties.markwon.UrlProcessor;
+import ru.noties.markwon.UrlProcessorNoOp;
 import ru.noties.markwon.spans.AsyncDrawable;
+import ru.noties.markwon.spans.LinkSpan;
 import ru.noties.markwon.spans.SpannableTheme;
 
 @SuppressWarnings("WeakerAccess")
 public class SpannableHtmlParser {
 
-    // we need to handle images independently (in order to parse alt, width, height, etc)
-
     // creates default parser
     public static SpannableHtmlParser create(
             @NonNull SpannableTheme theme,
             @NonNull AsyncDrawable.Loader loader
     ) {
-        return builderWithDefaults(theme, loader, null)
+        return builderWithDefaults(theme, loader, null, null)
                 .build();
     }
 
     public static SpannableHtmlParser create(
             @NonNull SpannableTheme theme,
             @NonNull AsyncDrawable.Loader loader,
-            @NonNull UrlProcessor urlProcessor
+            @NonNull UrlProcessor urlProcessor,
+            @NonNull LinkSpan.Resolver resolver
     ) {
-        return builderWithDefaults(theme, loader, urlProcessor)
+        return builderWithDefaults(theme, loader, urlProcessor, resolver)
                 .build();
     }
 
@@ -45,151 +43,133 @@ public class SpannableHtmlParser {
         return new Builder();
     }
 
+    public static Builder builderWithDefaults(@NonNull SpannableTheme theme) {
+        return builderWithDefaults(theme, null, null, null);
+    }
+
     public static Builder builderWithDefaults(
             @NonNull SpannableTheme theme,
             @Nullable AsyncDrawable.Loader asyncDrawableLoader,
-            @Nullable UrlProcessor urlProcessor
+            @Nullable UrlProcessor urlProcessor,
+            @Nullable LinkSpan.Resolver resolver
     ) {
 
+        if (urlProcessor == null) {
+            urlProcessor = new UrlProcessorNoOp();
+        }
+
+        if (resolver == null) {
+            resolver = new LinkResolverDef();
+        }
+
         final BoldProvider boldProvider = new BoldProvider();
         final ItalicsProvider italicsProvider = new ItalicsProvider();
         final StrikeProvider strikeProvider = new StrikeProvider();
 
-        final HtmlParser parser;
+        final ImageProvider imageProvider;
         if (asyncDrawableLoader != null) {
-            parser = DefaultHtmlParser.create(new HtmlImageGetter(asyncDrawableLoader, urlProcessor), null);
+            imageProvider = new ImageProviderImpl(theme, asyncDrawableLoader, urlProcessor);
         } else {
-            parser = DefaultHtmlParser.create(null, null);
+            imageProvider = null;
         }
 
         return new Builder()
-                .customTag("b", boldProvider)
-                .customTag("strong", boldProvider)
-                .customTag("i", italicsProvider)
-                .customTag("em", italicsProvider)
-                .customTag("cite", italicsProvider)
-                .customTag("dfn", italicsProvider)
-                .customTag("sup", new SuperScriptProvider(theme))
-                .customTag("sub", new SubScriptProvider(theme))
-                .customTag("u", new UnderlineProvider())
-                .customTag("del", strikeProvider)
-                .customTag("s", strikeProvider)
-                .customTag("strike", strikeProvider)
-                .parser(parser);
+                .simpleTag("b", boldProvider)
+                .simpleTag("strong", boldProvider)
+                .simpleTag("i", italicsProvider)
+                .simpleTag("em", italicsProvider)
+                .simpleTag("cite", italicsProvider)
+                .simpleTag("dfn", italicsProvider)
+                .simpleTag("sup", new SuperScriptProvider(theme))
+                .simpleTag("sub", new SubScriptProvider(theme))
+                .simpleTag("u", new UnderlineProvider())
+                .simpleTag("del", strikeProvider)
+                .simpleTag("s", strikeProvider)
+                .simpleTag("strike", strikeProvider)
+                .simpleTag("a", new LinkProvider(theme, urlProcessor, resolver))
+                .imageProvider(imageProvider);
     }
 
     // for simple tags without arguments
     // <b>, <i>, etc
     public interface SpanProvider {
-        Object provide();
+        Object provide(@NonNull Tag tag);
+    }
+
+    public interface ImageProvider {
+        Spanned provide(@NonNull Tag tag);
     }
 
     public interface HtmlParser {
-        Object[] getSpans(@NonNull String html);
+
+        // returns span for a simple content
+        Object getSpan(@NonNull String html);
 
         Spanned parse(@NonNull String html);
     }
 
-    private static final String LINK_START = "<a ";
-
-    private final Map<String, SpanProvider> customTags;
-    private final Set<String> voidTags;
+    private final Map<String, SpanProvider> simpleTags;
+    private final ImageProvider imageProvider;
     private final HtmlParser parser;
+    private final TagParser tagParser;
 
     private SpannableHtmlParser(Builder builder) {
-        this.customTags = builder.customTags;
-        this.voidTags = voidTags();
+        this.simpleTags = builder.simpleTags;
+        this.imageProvider = builder.imageProvider;
         this.parser = builder.parser;
+        this.tagParser = new TagParser();
     }
 
     @Nullable
     public Tag parseTag(String html) {
-
-        final Tag tag;
-
-        final int length = html != null
-                ? html.length()
-                : 0;
-
-        // absolutely minimum (`<i>`)
-        if (length < 3) {
-            tag = null;
-        } else {
-            // okay, we will consider a tag a void one if it's in our void list tag
-            final boolean closing = '<' == html.charAt(0) && '/' == html.charAt(1);
-            final boolean voidTag;
-            if (closing) {
-                voidTag = false;
-            } else {
-                int firstNonChar = -1;
-                for (int i = 1; i < length; i++) {
-                    if (!Character.isLetterOrDigit(html.charAt(i))) {
-                        firstNonChar = i;
-                        break;
-                    }
-                }
-                if (firstNonChar > 1) {
-                    final String name = html.substring(1, firstNonChar);
-                    voidTag = voidTags.contains(name);
-                } else {
-                    voidTag = false;
-                }
-            }
-
-            // todo, we do not strip to void tag name, so it can be possibly ended with `/`
-            final String name = closing
-                    ? html.substring(2, length - 1)
-                    : html.substring(1, length - 1);
-
-            tag = new Tag(name, !closing, voidTag);
-        }
-
-        return tag;
+        return tagParser.parse(html);
     }
 
     @Nullable
-    public Object handleTag(String tag) {
+    public Object getSpanForTag(@NonNull Tag tag) {
+
+        // check if we have specific handler for tag.name
+
         final Object out;
-        final SpanProvider provider = customTags.get(tag);
+
+        final SpanProvider provider = simpleTags.get(tag.name);
         if (provider != null) {
-            out = provider.provide();
+            out = provider.provide(tag);
         } else {
-            out = null;
+            // let's prepare mock content & extract spans from it
+            // actual content doesn't matter, here it's just `abc`
+            final String mock = tag.raw + "abc" + "</" + tag.name + ">";
+            out = parser.getSpan(mock);
         }
+
         return out;
     }
 
-    @Nullable
-    public Object[] htmlSpans(String html) {
-        // todo, additional handling of: image & link
-        Debug.i("html: %s", html);
-        return parser.getSpans(html);
-    }
-
-    // this is called when we encounter `void` tag
-    // `img` is a void tag
-    public Spanned html(String html) {
-        Debug.i("html: %s", html);
-        return parser.parse(html);
-    }
-
-    private static Set<String> voidTags() {
-        final String[] tags = {
-                "area", "base", "br", "col", "embed", "hr", "img", "input",
-                "keygen", "link", "meta", "param", "source", "track", "wbr"
-        };
-        final Set<String> set = new HashSet<>(tags.length);
-        Collections.addAll(set, tags);
-        return set;
+    // if tag is NULL, then it's HtmlBlock... else just a void tag
+    public Spanned getSpanned(@Nullable Tag tag, String html) {
+        final Spanned spanned;
+        if (tag != null && "img".equals(tag.name) && imageProvider != null) {
+            spanned = imageProvider.provide(tag);
+        } else {
+            spanned = parser.parse(html);
+        }
+        return spanned;
     }
 
     public static class Builder {
 
-        private final Map<String, SpanProvider> customTags = new HashMap<>(3);
+        private final Map<String, SpanProvider> simpleTags = new HashMap<>(3);
+
+        private ImageProvider imageProvider;
         private HtmlParser parser;
 
-        public Builder customTag(@NonNull String tag, @NonNull SpanProvider provider) {
-            customTags.put(tag, provider);
+        public Builder simpleTag(@NonNull String tag, @NonNull SpanProvider provider) {
+            simpleTags.put(tag, provider);
+            return this;
+        }
+
+        public Builder imageProvider(ImageProvider imageProvider) {
+            this.imageProvider = imageProvider;
             return this;
         }
 
@@ -200,7 +180,7 @@ public class SpannableHtmlParser {
 
         public SpannableHtmlParser build() {
             if (parser == null) {
-                parser = DefaultHtmlParser.create(null, null);
+                parser = DefaultHtmlParser.create();
             }
             return new SpannableHtmlParser(this);
         }
@@ -208,20 +188,34 @@ public class SpannableHtmlParser {
 
     public static class Tag {
 
+        private final String raw;
         private final String name;
+        private final Map<String, String> attributes;
+
         private final boolean opening;
         private final boolean voidTag;
 
-        public Tag(String name, boolean opening, boolean voidTag) {
+        public Tag(String raw, String name, @NonNull Map<String, String> attributes, boolean opening, boolean voidTag) {
+            this.raw = raw;
             this.name = name;
+            this.attributes = attributes;
             this.opening = opening;
             this.voidTag = voidTag;
         }
 
+        public String raw() {
+            return raw;
+        }
+
         public String name() {
             return name;
         }
 
+        @NonNull
+        public Map<String, String> attributes() {
+            return attributes;
+        }
+
         public boolean opening() {
             return opening;
         }
@@ -233,7 +227,9 @@ public class SpannableHtmlParser {
         @Override
         public String toString() {
             return "Tag{" +
-                    "name='" + name + '\'' +
+                    "raw='" + raw + '\'' +
+                    ", name='" + name + '\'' +
+                    ", attributes=" + attributes +
                     ", opening=" + opening +
                     ", voidTag=" + voidTag +
                     '}';
@@ -242,25 +238,20 @@ public class SpannableHtmlParser {
 
     public static abstract class DefaultHtmlParser implements HtmlParser {
 
-        public static DefaultHtmlParser create(@Nullable Html.ImageGetter imageGetter, @Nullable Html.TagHandler tagHandler) {
+        public static DefaultHtmlParser create() {
             final DefaultHtmlParser parser;
             if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
-                parser = new Parser24(imageGetter, tagHandler);
+                parser = new Parser24();
             } else {
-                parser = new ParserPre24(imageGetter, tagHandler);
+                parser = new ParserPre24();
             }
             return parser;
         }
 
-        final Html.ImageGetter imageGetter;
-        final Html.TagHandler tagHandler;
+        Object getSpan(Spanned spanned) {
 
-        DefaultHtmlParser(Html.ImageGetter imageGetter, Html.TagHandler tagHandler) {
-            this.imageGetter = imageGetter;
-            this.tagHandler = tagHandler;
-        }
+            final Object out;
 
-        Object[] getSpans(Spanned spanned) {
             final Object[] spans;
             final int length = spanned != null ? spanned.length() : 0;
             if (length == 0) {
@@ -268,42 +259,42 @@ public class SpannableHtmlParser {
             } else {
                 spans = spanned.getSpans(0, length, Object.class);
             }
-            return spans;
+
+            if (spans != null
+                    && spans.length > 0) {
+                out = spans[0];
+            } else {
+                out = null;
+            }
+
+            return out;
         }
 
         @SuppressWarnings("deprecation")
         private static class ParserPre24 extends DefaultHtmlParser {
 
-            ParserPre24(Html.ImageGetter imageGetter, Html.TagHandler tagHandler) {
-                super(imageGetter, tagHandler);
-            }
-
             @Override
-            public Object[] getSpans(@NonNull String html) {
-                return getSpans(parse(html));
+            public Object getSpan(@NonNull String html) {
+                return getSpan(parse(html));
             }
 
             @Override
             public Spanned parse(@NonNull String html) {
-                return Html.fromHtml(html, imageGetter, tagHandler);
+                return Html.fromHtml(html, null, null);
             }
         }
 
         @TargetApi(Build.VERSION_CODES.N)
         private static class Parser24 extends DefaultHtmlParser {
 
-            Parser24(Html.ImageGetter imageGetter, Html.TagHandler tagHandler) {
-                super(imageGetter, tagHandler);
-            }
-
             @Override
-            public Object[] getSpans(@NonNull String html) {
-                return getSpans(parse(html));
+            public Object getSpan(@NonNull String html) {
+                return getSpan(parse(html));
             }
 
             @Override
             public Spanned parse(@NonNull String html) {
-                return Html.fromHtml(html, Html.FROM_HTML_MODE_COMPACT, imageGetter, tagHandler);
+                return Html.fromHtml(html, Html.FROM_HTML_MODE_COMPACT, null, null);
             }
         }
     }
diff --git a/library-renderer/src/main/java/ru/noties/markwon/renderer/html/StrikeProvider.java b/library-renderer/src/main/java/ru/noties/markwon/renderer/html/StrikeProvider.java
index d4690440..df01f9e6 100644
--- a/library-renderer/src/main/java/ru/noties/markwon/renderer/html/StrikeProvider.java
+++ b/library-renderer/src/main/java/ru/noties/markwon/renderer/html/StrikeProvider.java
@@ -1,10 +1,11 @@
 package ru.noties.markwon.renderer.html;
 
+import android.support.annotation.NonNull;
 import android.text.style.StrikethroughSpan;
 
 class StrikeProvider implements SpannableHtmlParser.SpanProvider {
     @Override
-    public Object provide() {
+    public Object provide(@NonNull SpannableHtmlParser.Tag tag) {
         return new StrikethroughSpan();
     }
 }
diff --git a/library-renderer/src/main/java/ru/noties/markwon/renderer/html/SubScriptProvider.java b/library-renderer/src/main/java/ru/noties/markwon/renderer/html/SubScriptProvider.java
index 13299404..920ff01e 100644
--- a/library-renderer/src/main/java/ru/noties/markwon/renderer/html/SubScriptProvider.java
+++ b/library-renderer/src/main/java/ru/noties/markwon/renderer/html/SubScriptProvider.java
@@ -1,5 +1,7 @@
 package ru.noties.markwon.renderer.html;
 
+import android.support.annotation.NonNull;
+
 import ru.noties.markwon.spans.SpannableTheme;
 import ru.noties.markwon.spans.SubScriptSpan;
 
@@ -12,7 +14,7 @@ class SubScriptProvider implements SpannableHtmlParser.SpanProvider {
     }
 
     @Override
-    public Object provide() {
+    public Object provide(@NonNull SpannableHtmlParser.Tag tag) {
         return new SubScriptSpan(theme);
     }
 }
diff --git a/library-renderer/src/main/java/ru/noties/markwon/renderer/html/SuperScriptProvider.java b/library-renderer/src/main/java/ru/noties/markwon/renderer/html/SuperScriptProvider.java
index 91202571..4faf9078 100644
--- a/library-renderer/src/main/java/ru/noties/markwon/renderer/html/SuperScriptProvider.java
+++ b/library-renderer/src/main/java/ru/noties/markwon/renderer/html/SuperScriptProvider.java
@@ -1,5 +1,7 @@
 package ru.noties.markwon.renderer.html;
 
+import android.support.annotation.NonNull;
+
 import ru.noties.markwon.spans.SpannableTheme;
 import ru.noties.markwon.spans.SuperScriptSpan;
 
@@ -12,7 +14,7 @@ class SuperScriptProvider implements SpannableHtmlParser.SpanProvider {
     }
 
     @Override
-    public Object provide() {
+    public Object provide(@NonNull SpannableHtmlParser.Tag tag) {
         return new SuperScriptSpan(theme);
     }
 }
diff --git a/library-renderer/src/main/java/ru/noties/markwon/renderer/html/TagParser.java b/library-renderer/src/main/java/ru/noties/markwon/renderer/html/TagParser.java
new file mode 100644
index 00000000..8d9b57d9
--- /dev/null
+++ b/library-renderer/src/main/java/ru/noties/markwon/renderer/html/TagParser.java
@@ -0,0 +1,155 @@
+package ru.noties.markwon.renderer.html;
+
+import android.support.annotation.Nullable;
+
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.Map;
+import java.util.Set;
+
+class TagParser {
+
+
+    private static final Set<String> VOID_TAGS;
+    static {
+        final String[] tags = {
+                "area", "base", "br", "col", "embed", "hr", "img", "input",
+                "keygen", "link", "meta", "param", "source", "track", "wbr"
+        };
+        final Set<String> set = new HashSet<>(tags.length);
+        Collections.addAll(set, tags);
+        VOID_TAGS = Collections.unmodifiableSet(set);
+    }
+
+
+    TagParser() {
+    }
+
+    @Nullable
+    SpannableHtmlParser.Tag parse(String html) {
+
+        final SpannableHtmlParser.Tag tag;
+
+        final int length = html != null
+                ? html.length()
+                : 0;
+
+        // absolutely minimum (`<i>`)
+        if (length < 3) {
+            tag = null;
+        } else {
+
+//            // okay, we will consider a tag a void one if it's in our void list tag
+
+            final boolean closing = '<' == html.charAt(0) && '/' == html.charAt(1);
+            final boolean voidTag;
+
+            Map<String, String> attributes = null;
+
+            final StringBuilder builder = new StringBuilder();
+
+            String name = null;
+            String pendingAttribute = null;
+
+            char c;
+            char valueDelimiter = '\0';
+
+            for (int i = 0; i < length; i++) {
+
+                c = html.charAt(i);
+
+                // no more handling
+                if ('>' == c
+                        || '\\' == c) {
+                    break;
+                }
+
+                if (name == null) {
+                    if (Character.isSpaceChar(c)) {
+                        //noinspection StatementWithEmptyBody
+                        if (builder.length() == 0) {
+                            // ignore it, we must wait until we have tagName
+                        } else {
+
+                            name = builder.toString();
+
+                            // clear buffer
+                            builder.setLength(0);
+                        }
+                    } else {
+                        if (Character.isLetterOrDigit(c)) {
+                            builder.append(c);
+                        } /*else {
+                        // we allow non-letter-digit only if builder.length == 0
+                        // if we have already started
+                    }*/
+                    }
+                } else if (pendingAttribute == null) {
+                    // we start checking for attribute
+                    // ignore non-letter-digits before
+                    if (Character.isLetterOrDigit(c)) {
+                        builder.append(c);
+                    } else /*if ('=' == c)*/ {
+
+                        // attribute name is finished (only if we have already added something)
+                        // else it's trailing chars that we are not interested in
+                        if (builder.length() > 0) {
+                            pendingAttribute = builder.toString();
+                            builder.setLength(0);
+                        }
+                    }
+                } else {
+                    // first char that we will meet will be the delimiter
+                    if (valueDelimiter == '\0') {
+                        valueDelimiter = c;
+                    } else {
+                        if (c == valueDelimiter) {
+                            if (attributes == null) {
+                                attributes = new HashMap<>(3);
+                            }
+                            attributes.put(pendingAttribute, builder.toString());
+                            pendingAttribute = null;
+                            valueDelimiter = '\0';
+                            builder.setLength(0);
+                        } else {
+                            builder.append(c);
+                        }
+                    }
+                }
+            }
+
+            if (builder.length() > 0) {
+                if (name == null) {
+                    name = builder.toString();
+                } else if (pendingAttribute != null) {
+                    if (attributes == null) {
+                        attributes = new HashMap<>(3);
+                    }
+                    attributes.put(pendingAttribute, builder.toString());
+                }
+            }
+
+            // in case of wrong parsing
+            if (name == null) {
+                tag = null;
+            } else {
+
+                voidTag = !closing && VOID_TAGS.contains(name);
+
+                final Map<String, String> attributesMap;
+                if (attributes == null
+                        || attributes.size() == 0) {
+                    //noinspection unchecked
+                    attributesMap = Collections.EMPTY_MAP;
+                } else {
+                    attributesMap = Collections.unmodifiableMap(attributes);
+                }
+
+                tag = new SpannableHtmlParser.Tag(html, name, attributesMap, !closing, voidTag);
+            }
+        }
+
+        return tag;
+    }
+}
diff --git a/library-renderer/src/main/java/ru/noties/markwon/renderer/html/UnderlineProvider.java b/library-renderer/src/main/java/ru/noties/markwon/renderer/html/UnderlineProvider.java
index 1d917471..38acc73c 100644
--- a/library-renderer/src/main/java/ru/noties/markwon/renderer/html/UnderlineProvider.java
+++ b/library-renderer/src/main/java/ru/noties/markwon/renderer/html/UnderlineProvider.java
@@ -1,11 +1,12 @@
 package ru.noties.markwon.renderer.html;
 
+import android.support.annotation.NonNull;
 import android.text.style.UnderlineSpan;
 
 class UnderlineProvider implements SpannableHtmlParser.SpanProvider {
 
     @Override
-    public Object provide() {
+    public Object provide(@NonNull SpannableHtmlParser.Tag tag) {
         return new UnderlineSpan();
     }
 }
diff --git a/settings.gradle b/settings.gradle
index 43603efd..fb1c7062 100644
--- a/settings.gradle
+++ b/settings.gradle
@@ -1 +1 @@
-include ':app', ':library-renderer'
+include ':app', ':library-renderer', ':library-image-loader'