diff --git a/app/build.gradle b/app/build.gradle
index fbb16176..6003452c 100644
--- a/app/build.gradle
+++ b/app/build.gradle
@@ -1,32 +1,20 @@
 apply plugin: 'com.android.application'
 
 android {
-    compileSdkVersion 25
-    buildToolsVersion "25.0.2"
+
+    compileSdkVersion TARGET_SDK
+    buildToolsVersion BUILD_TOOLS
+
     defaultConfig {
         applicationId "ru.noties.markwon"
-        minSdkVersion 15
-        targetSdkVersion 25
+        minSdkVersion MIN_SDK
+        targetSdkVersion TARGET_SDK
         versionCode 1
-        versionName "1.0"
-        testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
-    }
-    buildTypes {
-        release {
-            minifyEnabled false
-            proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
-        }
+        versionName version
     }
 }
 
 dependencies {
-    compile fileTree(dir: 'libs', include: ['*.jar'])
-    androidTestCompile('com.android.support.test.espresso:espresso-core:2.2.2', {
-        exclude group: 'com.android.support', module: 'support-annotations'
-    })
-    compile 'com.android.support:appcompat-v7:25.3.1'
-    compile 'com.atlassian.commonmark:commonmark:0.9.0'
-    compile 'com.atlassian.commonmark:commonmark-ext-gfm-strikethrough:0.9.0'
+    compile project(':library-renderer')
     compile 'ru.noties:debug:3.0.0@jar'
-    testCompile 'junit:junit:4.12'
 }
diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index 815ca533..8bb5b207 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -1,20 +1,22 @@
 <?xml version="1.0" encoding="utf-8"?>
-<manifest xmlns:android="http://schemas.android.com/apk/res/android"
-          package="ru.noties.markwon">
+<manifest
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    package="ru.noties.markwon">
 
     <application
-            android:allowBackup="true"
-            android:icon="@mipmap/ic_launcher"
-            android:label="@string/app_name"
-            android:supportsRtl="true"
-            android:theme="@style/AppTheme">
+        android:allowBackup="true"
+        android:icon="@mipmap/ic_launcher"
+        android:label="@string/app_name"
+        android:supportsRtl="true"
+        android:theme="@style/AppTheme">
+
         <activity android:name=".MainActivity">
             <intent-filter>
-                <action android:name="android.intent.action.MAIN"/>
-
-                <category android:name="android.intent.category.LAUNCHER"/>
+                <action android:name="android.intent.action.MAIN" />
+                <category android:name="android.intent.category.LAUNCHER" />
             </intent-filter>
         </activity>
+
     </application>
 
 </manifest>
\ No newline at end of file
diff --git a/app/src/main/assets/test.md b/app/src/main/assets/test.md
index 8adf2d45..55aaab47 100644
--- a/app/src/main/assets/test.md
+++ b/app/src/main/assets/test.md
@@ -8,7 +8,13 @@
 >> Second Quote
 >>> Third one, yuhuu!
 
-`can a code have **markdown?**` so go it doesn't
+`can a code have **markdown?**` so good it doesn't
+
+<h1>Yo!
+Omg
+
+ddffdg
+</h1>
 
 
 ## Unordered list
@@ -17,6 +23,9 @@
 * second
 * * second first
 * * second __second__ jks8feif fdsuif yuweru sdfoisdfu wutwe iower wtew ruweir weoir wutywr wer woeirwr wieyriow eryowe rwyeor oweryower o
+* * * hm, is it actually a thing?
+* * * * and this?!
+* * * * * omg
 * third `and some code`
 
 
@@ -59,5 +68,4 @@ To compare<sub>~~13~~</sub>
 
 <font color="#FF0000">RED</font>
 
-**PS** additional text to check if this view scrolls gracefully, sofihweo fwfw fuwf weyf pwefiowef twe weuifphw efwepfuwoefh wfypiwe fuwoef wiefg wtefw uf ywfyw fweouf wpfyw fwfe# Hello!
-
+**PS** additional text to check if this view scrolls gracefully, sofihweo fwfw fuwf weyf pwefiowef twe weuifphw efwepfuwoefh wfypiwe fuwoef wiefg wtefw uf ywfyw fweouf wpfyw fwfe#
\ No newline at end of file
diff --git a/app/src/main/java/ru/noties/markwon/MainActivity.java b/app/src/main/java/ru/noties/markwon/MainActivity.java
index 473b8607..4397f6bf 100644
--- a/app/src/main/java/ru/noties/markwon/MainActivity.java
+++ b/app/src/main/java/ru/noties/markwon/MainActivity.java
@@ -1,26 +1,26 @@
 package ru.noties.markwon;
 
-import android.graphics.drawable.Drawable;
+import android.app.Activity;
 import android.os.Bundle;
 import android.os.SystemClock;
-import android.support.v7.app.AppCompatActivity;
 import android.text.method.LinkMovementMethod;
-import android.text.style.StrikethroughSpan;
 import android.widget.TextView;
 
 import org.commonmark.ext.gfm.strikethrough.StrikethroughExtension;
 import org.commonmark.node.Node;
 import org.commonmark.parser.Parser;
-import ru.noties.debug.AndroidLogDebugOutput;
-import ru.noties.debug.Debug;
-import ru.noties.markwon.spans.DrawableSpanUtils;
 
 import java.io.IOException;
 import java.io.InputStream;
 import java.util.Arrays;
 import java.util.Scanner;
 
-public class MainActivity extends AppCompatActivity {
+import ru.noties.debug.AndroidLogDebugOutput;
+import ru.noties.debug.Debug;
+import ru.noties.markwon.renderer.*;
+import ru.noties.markwon.spans.DrawableSpanUtils;
+
+public class MainActivity extends Activity {
 
     static {
         Debug.init(new AndroidLogDebugOutput(true));
@@ -62,21 +62,28 @@ public class MainActivity extends AppCompatActivity {
                             .extensions(Arrays.asList(StrikethroughExtension.create()))
                             .build();
                     final Node node = parser.parse(md);
-                    final CharSequence text = new SpannableRenderer()._render(node/*, new Runnable() {
-                        @Override
-                        public void run() {
-                            textView.setText(textView.getText());
-                            final Drawable drawable = null;
-                            drawable.setCallback(textView);
-                        }
-                    }*/);
+
+                    final CharSequence text = new ru.noties.markwon.renderer.SpannableRenderer().render(
+                            SpannableConfiguration.create(MainActivity.this),
+                            node
+                    );
+
+//                    final CharSequence text = new SpannableRenderer()._render(node/*, new Runnable() {
+//                        @Override
+//                        public void run() {
+//                            textView.setText(textView.getText());
+//                            final Drawable drawable = null;
+//                            drawable.setCallback(textView);
+//                        }
+//                    }*/);
                     final long end = SystemClock.uptimeMillis();
                     Debug.i("Rendered: %d ms, length: %d", end - start, text.length());
+//                    Debug.i(text);
                     textView.post(new Runnable() {
                         @Override
                         public void run() {
                             // NB! LinkMovementMethod forces frequent updates...
-//                            textView.setMovementMethod(LinkMovementMethod.getInstance());
+                            textView.setMovementMethod(LinkMovementMethod.getInstance());
                             textView.setText(text);
                             DrawableSpanUtils.scheduleDrawables(textView);
                         }
diff --git a/app/src/main/java/ru/noties/markwon/Markwon.java b/app/src/main/java/ru/noties/markwon/Markwon.java
index 285a4714..2a76c84c 100644
--- a/app/src/main/java/ru/noties/markwon/Markwon.java
+++ b/app/src/main/java/ru/noties/markwon/Markwon.java
@@ -1,7 +1,7 @@
-package ru.noties.markwon;
-
-public class Markwon {
-
-    // todo, annotation processor to PRE_COMPILE markdown!! no... multiple lnguages and you are out, forget about it
-    // view for debugging (to view in preview) x3!
-}
+//package ru.noties.markwon;
+//
+//public class Markwon {
+//
+//    // todo, annotation processor to PRE_COMPILE markdown!! no... multiple lnguages and you are out, forget about it
+//    // view for debugging (to view in preview) x3!
+//}
diff --git a/app/src/main/java/ru/noties/markwon/SpannableRenderer.java b/app/src/main/java/ru/noties/markwon/SpannableRenderer.java
index 5db373da..e75d7071 100644
--- a/app/src/main/java/ru/noties/markwon/SpannableRenderer.java
+++ b/app/src/main/java/ru/noties/markwon/SpannableRenderer.java
@@ -1,442 +1,444 @@
-package ru.noties.markwon;
-
-import android.graphics.Canvas;
-import android.graphics.ColorFilter;
-import android.graphics.drawable.Drawable;
-import android.os.Handler;
-import android.os.Looper;
-import android.support.annotation.IntRange;
-import android.support.annotation.NonNull;
-import android.support.annotation.Nullable;
-import android.text.Html;
-import android.text.SpannableStringBuilder;
-import android.text.Spanned;
-import android.text.style.AbsoluteSizeSpan;
-import android.text.style.StrikethroughSpan;
-import android.text.style.URLSpan;
-
-import org.commonmark.ext.gfm.strikethrough.Strikethrough;
-import org.commonmark.node.AbstractVisitor;
-import org.commonmark.node.BlockQuote;
-import org.commonmark.node.BulletList;
-import org.commonmark.node.Code;
-import org.commonmark.node.CustomBlock;
-import org.commonmark.node.CustomNode;
-import org.commonmark.node.Document;
-import org.commonmark.node.Emphasis;
-import org.commonmark.node.FencedCodeBlock;
-import org.commonmark.node.HardLineBreak;
-import org.commonmark.node.Heading;
-import org.commonmark.node.HtmlBlock;
-import org.commonmark.node.HtmlInline;
-import org.commonmark.node.Image;
-import org.commonmark.node.IndentedCodeBlock;
-import org.commonmark.node.Link;
-import org.commonmark.node.ListItem;
-import org.commonmark.node.Node;
-import org.commonmark.node.OrderedList;
-import org.commonmark.node.Paragraph;
-import org.commonmark.node.SoftLineBreak;
-import org.commonmark.node.StrongEmphasis;
-import org.commonmark.node.Text;
-import org.commonmark.node.ThematicBreak;
-import org.commonmark.renderer.Renderer;
-
-import java.util.ArrayDeque;
-import java.util.Arrays;
-import java.util.Deque;
-
-import ru.noties.debug.Debug;
-import ru.noties.markwon.spans.BlockQuoteSpan;
-import ru.noties.markwon.spans.CodeSpan;
-import ru.noties.markwon.spans.DrawableSpan;
-import ru.noties.markwon.spans.EmphasisSpan;
-import ru.noties.markwon.spans.ListItemSpan;
-import ru.noties.markwon.spans.StrongEmphasisSpan;
-import ru.noties.markwon.spans.SubSpan;
-import ru.noties.markwon.spans.SupSpan;
-import ru.noties.markwon.spans.ThematicBreakSpan;
-
-public class SpannableRenderer implements Renderer {
-
-    // todo, util to extract all drawables and attach to textView (gif, animations, lazyLoading, etc)
-
-    @Override
-    public void render(Node node, Appendable output) {
-
-    }
-
-    @Override
-    public String render(Node node) {
-        // hm.. doesn't make sense to render to string
-        throw null;
-    }
-
-    public CharSequence _render(Node node) {
-        final SpannableStringBuilder builder = new SpannableStringBuilder();
-        node.accept(new SpannableNodeRenderer(builder));
-        return builder;
-    }
-
-    private static class SpannableNodeRenderer extends AbstractVisitor {
-
-//        private static final float[] HEADING_SIZES = {
-//                1.5f, 1.4f, 1.3f, 1.2f, 1.1f, 1f,
-//        };
-
-        private final SpannableStringBuilder builder;
-
-        private int blockQuoteIndent;
-        private int listLevel;
-
-        SpannableNodeRenderer(SpannableStringBuilder builder) {
-            this.builder = builder;
-        }
-
-        @Override
-        public void visit(HardLineBreak hardLineBreak) {
-            Debug.i(hardLineBreak);
-        }
-
-        @Override
-        public void visit(Text text) {
-            Debug.i(text);
-            builder.append(text.getLiteral());
-        }
-
-        @Override
-        public void visit(StrongEmphasis strongEmphasis) {
-            final int length = builder.length();
-            visitChildren(strongEmphasis);
-            builder.setSpan(new StrongEmphasisSpan(), length, builder.length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
-        }
-
-        @Override
-        public void visit(Emphasis emphasis) {
-            final int length = builder.length();
-            visitChildren(emphasis);
-            builder.setSpan(new EmphasisSpan(), length, builder.length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
-        }
-
-        @Override
-        public void visit(IndentedCodeBlock indentedCodeBlock) {
-            Debug.i(indentedCodeBlock);
-        }
-
-        @Override
-        public void visit(BlockQuote blockQuote) {
-            builder.append('\n');
-            final int length = builder.length();
-            blockQuoteIndent += 1;
-            visitChildren(blockQuote);
-            builder.setSpan(new BlockQuoteSpan(blockQuoteIndent), length, builder.length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
-            blockQuoteIndent -= 1;
-        }
-
-        @Override
-        public void visit(Code code) {
-            final int length = builder.length();
-            builder.append(code.getLiteral());
-//            builder.setSpan(new ForegroundColorSpan(0xff00ff00), length, builder.length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
-            builder.setSpan(new CodeSpan(false, length, builder.length()), length, builder.length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
-        }
-
-        @Override
-        public void visit(BulletList bulletList) {
-            Debug.i(bulletList, bulletList.getBulletMarker());
-            visitChildren(bulletList);
-        }
-
-        @Override
-        public void visit(ListItem listItem) {
-            Debug.i(listItem);
+//package ru.noties.markwon;
+//
+//import android.graphics.Canvas;
+//import android.graphics.ColorFilter;
+//import android.graphics.drawable.Drawable;
+//import android.os.Handler;
+//import android.os.Looper;
+//import android.support.annotation.IntRange;
+//import android.support.annotation.NonNull;
+//import android.support.annotation.Nullable;
+//import android.text.Html;
+//import android.text.SpannableStringBuilder;
+//import android.text.Spanned;
+//import android.text.style.AbsoluteSizeSpan;
+//import android.text.style.StrikethroughSpan;
+//import android.text.style.URLSpan;
+//
+//import org.commonmark.ext.gfm.strikethrough.Strikethrough;
+//import org.commonmark.node.AbstractVisitor;
+//import org.commonmark.node.BlockQuote;
+//import org.commonmark.node.BulletList;
+//import org.commonmark.node.Code;
+//import org.commonmark.node.CustomBlock;
+//import org.commonmark.node.CustomNode;
+//import org.commonmark.node.Document;
+//import org.commonmark.node.Emphasis;
+//import org.commonmark.node.FencedCodeBlock;
+//import org.commonmark.node.HardLineBreak;
+//import org.commonmark.node.Heading;
+//import org.commonmark.node.HtmlBlock;
+//import org.commonmark.node.HtmlInline;
+//import org.commonmark.node.Image;
+//import org.commonmark.node.IndentedCodeBlock;
+//import org.commonmark.node.Link;
+//import org.commonmark.node.ListItem;
+//import org.commonmark.node.Node;
+//import org.commonmark.node.OrderedList;
+//import org.commonmark.node.Paragraph;
+//import org.commonmark.node.SoftLineBreak;
+//import org.commonmark.node.StrongEmphasis;
+//import org.commonmark.node.Text;
+//import org.commonmark.node.ThematicBreak;
+//import org.commonmark.renderer.Renderer;
+//
+//import java.util.ArrayDeque;
+//import java.util.Arrays;
+//import java.util.Deque;
+//
+//import ru.noties.debug.Debug;
+//import ru.noties.markwon.spans.BlockQuoteSpan;
+//import ru.noties.markwon.spans.CodeSpan;
+//import ru.noties.markwon.spans.DrawableSpan;
+//import ru.noties.markwon.spans.EmphasisSpan;
+//import ru.noties.markwon.spans.BulletListItemSpan;
+//import ru.noties.markwon.spans.StrongEmphasisSpan;
+//import ru.noties.markwon.spans.SubSpan;
+//import ru.noties.markwon.spans.SupSpan;
+//import ru.noties.markwon.spans.ThematicBreakSpan;
+//
+//public class SpannableRenderer implements Renderer {
+//
+//    // todo, util to extract all drawables and attach to textView (gif, animations, lazyLoading, etc)
+//
+//    @Override
+//    public void render(Node node, Appendable output) {
+//
+//    }
+//
+//    @Override
+//    public String render(Node node) {
+//        // hm.. doesn't make sense to render to string
+//        throw null;
+//    }
+//
+//    public CharSequence _render(Node node) {
+//        final SpannableStringBuilder builder = new SpannableStringBuilder();
+//        node.accept(new SpannableNodeRenderer(builder));
+//        return builder;
+//    }
+//
+//    private static class SpannableNodeRenderer extends AbstractVisitor {
+//
+////        private static final float[] HEADING_SIZES = {
+////                1.5f, 1.4f, 1.3f, 1.2f, 1.1f, 1f,
+////        };
+//
+//        private final SpannableStringBuilder builder;
+//
+//        private int blockQuoteIndent;
+//        private int listLevel;
+//
+//        SpannableNodeRenderer(SpannableStringBuilder builder) {
+//            this.builder = builder;
+//        }
+//
+//        @Override
+//        public void visit(HardLineBreak hardLineBreak) {
+//            // todo
+//            Debug.i(hardLineBreak);
+//        }
+//
+//        @Override
+//        public void visit(Text text) {
+//            builder.append(text.getLiteral());
+//        }
+//
+//        @Override
+//        public void visit(StrongEmphasis strongEmphasis) {
+//            final int length = builder.length();
+//            visitChildren(strongEmphasis);
+//            builder.setSpan(new StrongEmphasisSpan(), length, builder.length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
+//        }
+//
+//        @Override
+//        public void visit(Emphasis emphasis) {
+//            final int length = builder.length();
+//            visitChildren(emphasis);
+//            builder.setSpan(new EmphasisSpan(), length, builder.length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
+//        }
+//
+//        @Override
+//        public void visit(IndentedCodeBlock indentedCodeBlock) {
+//            // todo
+//            Debug.i(indentedCodeBlock);
+//        }
+//
+//        @Override
+//        public void visit(BlockQuote blockQuote) {
 //            builder.append('\n');
-            if (builder.charAt(builder.length() - 1) != '\n') {
-                builder.append('\n');
-            }
-            final int length = builder.length();
-            blockQuoteIndent += 1;
-            listLevel += 1;
-            visitChildren(listItem);
-//            builder.setSpan(new BulletSpan(4, 0xff0000ff), length, builder.length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
-            builder.setSpan(new ListItemSpan(blockQuoteIndent, listLevel > 1, length), length, builder.length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
-            blockQuoteIndent -= 1;
-            listLevel -= 1;
-        }
-
-        @Override
-        public void visit(ThematicBreak thematicBreak) {
-            final int length = builder.length();
-            builder.append('\n')
-                    .append(' '); // without space it won't render
-            builder.setSpan(new ThematicBreakSpan(), length, builder.length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
-            builder.append('\n');
-        }
-
-        @Override
-        public void visit(OrderedList orderedList) {
-            Debug.i(orderedList, orderedList.getDelimiter(), orderedList.getStartNumber());
-            // todo, ordering numbers
-            super.visit(orderedList);
-        }
-
-        @Override
-        public void visit(SoftLineBreak softLineBreak) {
-            Debug.i(softLineBreak);
-        }
-
-        @Override
-        public void visit(Heading heading) {
-            Debug.i(heading);
-            if (builder.length() != 0 && builder.charAt(builder.length() - 1) != '\n') {
-                builder.append('\n');
-            }
-            final int length = builder.length();
-            visitChildren(heading);
-            final int max = 120;
-            final int one = 20; // total is 6
-            final int size = max - ((heading.getLevel() - 1) * one);
-            builder.setSpan(new AbsoluteSizeSpan(size), length, builder.length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
-            builder.append('\n');
-        }
-
-        @Override
-        public void visit(FencedCodeBlock fencedCodeBlock) {
-            builder.append('\n');
-            final int length = builder.length();
-            builder.append(fencedCodeBlock.getLiteral());
-            builder.setSpan(new CodeSpan(true, length, builder.length() - 1), length, builder.length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
-        }
-
-        @Override
-        public void visit(Paragraph paragraph) {
-            Debug.i(paragraph);
-            if (listLevel == 0
-                    && blockQuoteIndent == 0) {
-                builder.append('\n')
-                        .append('\n');
-            }
-            visitChildren(paragraph);
-
-            if (listLevel == 0
-                    && blockQuoteIndent == 0) {
-                builder.append('\n')
-                        .append('\n');
-            }
-        }
-
-//        private int htmlStart = -1;
-        private final Deque<HtmlInlineItem> htmlStack = new ArrayDeque<>();
-
-        private static class HtmlInlineItem {
-
-            final int start;
-            final String tag;
-
-            private HtmlInlineItem(int start, String tag) {
-                this.start = start;
-                this.tag = tag;
-            }
-        }
-
-        @Override
-        public void visit(HtmlInline htmlInline) {
-
-//            Debug.i(htmlInline, htmlStart);
-//            Debug.i(htmlInline.getLiteral(), htmlInline.toString());
-
-            // okay, it's seems that we desperately need to understand if it's opening tag or closing
-
-            final HtmlTag tag = parseTag(htmlInline.getLiteral());
-
-            Debug.i(htmlInline.getLiteral(), tag);
-
-            if (tag != null) {
-                Debug.i("tag: %s, closing: %s", tag.tag, tag.closing);
-                if (!tag.closing) {
-                    htmlStack.push(new HtmlInlineItem(builder.length(), tag.tag));
-                    visitChildren(htmlInline);
-                } else {
-                    final HtmlInlineItem item = htmlStack.pop();
-                    final int start = item.start;
-                    final int end = builder.length();
-                    // here, additionally, we can render some tags ourselves (sup/sub)
-                    if ("sup".equals(item.tag)) {
-                         builder.setSpan(new SupSpan(), start, end, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
-                    } else if("sub".equals(item.tag)) {
-                        builder.setSpan(new SubSpan(), start, end, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
-                    } else if("del".equals(item.tag)) {
-                        // weird, but `Html` class does not return a spannable for `<del>o</del>`
-                        // seems like a bug
-                        builder.setSpan(new StrikethroughSpan(), start, end, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
-                    } else {
-                        final String html = "<" + item.tag + ">" + (builder.subSequence(start, end).toString()) + "</" + item.tag + ">";
-                        final Spanned spanned = Html.fromHtml(html);
-                        final Object[] spans = spanned.getSpans(0, spanned.length(), Object.class);
-
-                        Debug.i("html: %s, start: %d, end: %d, spans: %s", html, start, end, Arrays.toString(spans));
-
-                        if (spans != null
-                                && spans.length > 0) {
-                            for (Object span: spans) {
-                                Debug.i(span);
-                                builder.setSpan(span, start, end, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
-                            }
-                        }
-                    }
-                }
-            } else {
-                super.visit(htmlInline);
-            }
-        }
-
-        private static class HtmlTag {
-            final String tag;
-            final boolean closing;
-            HtmlTag(String tag, boolean closing) {
-                this.tag = tag;
-                this.closing = closing;
-            }
-            @Override
-            public String toString() {
-                return "HtmlTag{" +
-                        "tag='" + tag + '\'' +
-                        ", closing=" + closing +
-                        '}';
-            }
-        }
-
-        private static HtmlTag parseTag(String in) {
-
-            final HtmlTag out;
-
-            final int length = in != null
-                    ? in.length()
-                    : 0;
-
-            Debug.i(in, length);
-
-            if (length == 0 || length < 3) {
-                out = null;
-            } else {
-
-                final boolean closing = '<' == in.charAt(0) && '/' == in.charAt(1);
-                final String tag = closing
-                        ? in.substring(2, in.length() - 1)
-                        : in.substring(1, in.length() - 1);
-                out = new HtmlTag(tag, closing);
-            }
-
-            return out;
-        }
-
-        @Override
-        public void visit(HtmlBlock htmlBlock) {
-            // interestring thing... what is it also?
-            Debug.i(htmlBlock);
-        }
-
-        @Override
-        public void visit(CustomBlock customBlock) {
-            // not supported, what is it anyway?
-            Debug.i(customBlock);
-        }
-
-        @Override
-        public void visit(Document document) {
-            // the whole document, no need to do anything
-            Debug.i(document);
-            super.visit(document);
-        }
-
-        @Override
-        public void visit(Link link) {
-            Debug.i(link);
-            final int length = builder.length();
-            visitChildren(link);
-            builder.setSpan(new URLSpan(link.getDestination()), length, builder.length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
-        }
-
-        @Override
-        public void visit(Image image) {
-            // not supported... maybe for now?
-            Debug.i(image);
-            super.visit(image);
-
-            final int length = builder.length();
-            final TestDrawable drawable = new TestDrawable();
-            final DrawableSpan span = new DrawableSpan(drawable);
-            builder.append("  ");
-            builder.setSpan(span, length, builder.length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
-        }
-
-        @Override
-        public void visit(CustomNode customNode) {
-
-            Debug.i(customNode);
-
-            if (customNode instanceof Strikethrough) {
-                final int length = builder.length();
-                visitChildren(customNode);
-                builder.setSpan(new StrikethroughSpan(), length, builder.length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
-            } else {
-                super.visit(customNode);
-            }
-        }
-    }
-
-
-    private static class TestDrawable extends Drawable {
-
-        private final Handler handler = new Handler(Looper.getMainLooper());
-        private boolean called;
-
-        TestDrawable() {
-            setBounds(0, 0, 50, 50);
-        }
-
-        @Override
-        public void draw(@NonNull final Canvas canvas) {
-            canvas.clipRect(getBounds());
-            if (!called) {
-                canvas.drawColor(0xFF00ff00);
-                handler.removeCallbacksAndMessages(null);
-                handler.postDelayed(new Runnable() {
-                    @Override
-                    public void run() {
-                        called = true;
-                        setBounds(0, 0, 400, 400);
-                        invalidateSelf();
-                    }
-                }, 2000L);
-            } else {
-                canvas.drawColor(0xFFff0000);
-            }
-        }
-
-        @Override
-        public void setAlpha(@IntRange(from = 0, to = 255) int alpha) {
-
-        }
-
-        @Override
-        public void setColorFilter(@Nullable ColorFilter colorFilter) {
-
-        }
-
-        @Override
-        public int getOpacity() {
-            return 0;
-        }
-
-        @Override
-        public int getIntrinsicWidth() {
-            return called ? 400 : 50;
-        }
-
-        @Override
-        public int getIntrinsicHeight() {
-            return called ? 400 : 50;
-        }
-    }
-}
+//            final int length = builder.length();
+//            blockQuoteIndent += 1;
+//            visitChildren(blockQuote);
+//            builder.setSpan(new BlockQuoteSpan(blockQuoteIndent), length, builder.length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
+//            blockQuoteIndent -= 1;
+//        }
+//
+//        @Override
+//        public void visit(Code code) {
+//            final int length = builder.length();
+//            builder.append(code.getLiteral());
+////            builder.setSpan(new ForegroundColorSpan(0xff00ff00), length, builder.length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
+//            builder.setSpan(new CodeSpan(false, length, builder.length()), length, builder.length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
+//        }
+//
+//        @Override
+//        public void visit(BulletList bulletList) {
+//            Debug.i(bulletList, bulletList.getBulletMarker());
+//            visitChildren(bulletList);
+//        }
+//
+//        @Override
+//        public void visit(ListItem listItem) {
+//            Debug.i(listItem);
+////            builder.append('\n');
+//            if (builder.charAt(builder.length() - 1) != '\n') {
+//                builder.append('\n');
+//            }
+//            final int length = builder.length();
+//            blockQuoteIndent += 1;
+//            listLevel += 1;
+//            visitChildren(listItem);
+////            builder.setSpan(new BulletSpan(4, 0xff0000ff), length, builder.length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
+//            builder.setSpan(new BulletListItemSpan(blockQuoteIndent, listLevel > 1, length), length, builder.length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
+//            blockQuoteIndent -= 1;
+//            listLevel -= 1;
+//        }
+//
+//        @Override
+//        public void visit(ThematicBreak thematicBreak) {
+//            final int length = builder.length();
+//            builder.append('\n')
+//                    .append(' '); // without space it won't render
+//            builder.setSpan(new ThematicBreakSpan(), length, builder.length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
+//            builder.append('\n');
+//        }
+//
+//        @Override
+//        public void visit(OrderedList orderedList) {
+//            Debug.i(orderedList, orderedList.getDelimiter(), orderedList.getStartNumber());
+//            // todo, ordering numbers
+//            super.visit(orderedList);
+//        }
+//
+//        @Override
+//        public void visit(SoftLineBreak softLineBreak) {
+//            Debug.i(softLineBreak);
+//        }
+//
+//        @Override
+//        public void visit(Heading heading) {
+//            Debug.i(heading);
+//            if (builder.length() != 0 && builder.charAt(builder.length() - 1) != '\n') {
+//                builder.append('\n');
+//            }
+//            final int length = builder.length();
+//            visitChildren(heading);
+//            final int max = 120;
+//            final int one = 20; // total is 6
+//            final int size = max - ((heading.getLevel() - 1) * one);
+//            builder.setSpan(new AbsoluteSizeSpan(size), length, builder.length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
+//            builder.append('\n');
+//        }
+//
+//        @Override
+//        public void visit(FencedCodeBlock fencedCodeBlock) {
+//            builder.append('\n');
+//            final int length = builder.length();
+//            builder.append(fencedCodeBlock.getLiteral());
+//            builder.setSpan(new CodeSpan(true, length, builder.length() - 1), length, builder.length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
+//        }
+//
+//        @Override
+//        public void visit(Paragraph paragraph) {
+//            Debug.i(paragraph);
+//            if (listLevel == 0
+//                    && blockQuoteIndent == 0) {
+//                builder.append('\n')
+//                        .append('\n');
+//            }
+//            visitChildren(paragraph);
+//
+//            if (listLevel == 0
+//                    && blockQuoteIndent == 0) {
+//                builder.append('\n')
+//                        .append('\n');
+//            }
+//        }
+//
+////        private int htmlStart = -1;
+//        private final Deque<HtmlInlineItem> htmlStack = new ArrayDeque<>();
+//
+//        private static class HtmlInlineItem {
+//
+//            final int start;
+//            final String tag;
+//
+//            private HtmlInlineItem(int start, String tag) {
+//                this.start = start;
+//                this.tag = tag;
+//            }
+//        }
+//
+//        @Override
+//        public void visit(HtmlInline htmlInline) {
+//
+////            Debug.i(htmlInline, htmlStart);
+////            Debug.i(htmlInline.getLiteral(), htmlInline.toString());
+//
+//            // okay, it's seems that we desperately need to understand if it's opening tag or closing
+//
+//            final HtmlTag tag = parseTag(htmlInline.getLiteral());
+//
+//            Debug.i(htmlInline.getLiteral(), tag);
+//
+//            if (tag != null) {
+//                Debug.i("tag: %s, closing: %s", tag.tag, tag.closing);
+//                if (!tag.closing) {
+//                    htmlStack.push(new HtmlInlineItem(builder.length(), tag.tag));
+//                    visitChildren(htmlInline);
+//                } else {
+//                    final HtmlInlineItem item = htmlStack.pop();
+//                    final int start = item.start;
+//                    final int end = builder.length();
+//                    // here, additionally, we can render some tags ourselves (sup/sub)
+//                    if ("sup".equals(item.tag)) {
+//                         builder.setSpan(new SupSpan(), start, end, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
+//                    } else if("sub".equals(item.tag)) {
+//                        builder.setSpan(new SubSpan(), start, end, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
+//                    } else if("del".equals(item.tag)) {
+//                        // weird, but `Html` class does not return a spannable for `<del>o</del>`
+//                        // seems like a bug
+//                        builder.setSpan(new StrikethroughSpan(), start, end, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
+//                    } else {
+//                        final String html = "<" + item.tag + ">" + (builder.subSequence(start, end).toString()) + "</" + item.tag + ">";
+//                        final Spanned spanned = Html.fromHtml(html);
+//                        final Object[] spans = spanned.getSpans(0, spanned.length(), Object.class);
+//
+//                        Debug.i("html: %s, start: %d, end: %d, spans: %s", html, start, end, Arrays.toString(spans));
+//
+//                        if (spans != null
+//                                && spans.length > 0) {
+//                            for (Object span: spans) {
+//                                Debug.i(span);
+//                                builder.setSpan(span, start, end, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
+//                            }
+//                        }
+//                    }
+//                }
+//            } else {
+//                super.visit(htmlInline);
+//            }
+//        }
+//
+//        private static class HtmlTag {
+//            final String tag;
+//            final boolean closing;
+//            HtmlTag(String tag, boolean closing) {
+//                this.tag = tag;
+//                this.closing = closing;
+//            }
+//            @Override
+//            public String toString() {
+//                return "HtmlTag{" +
+//                        "tag='" + tag + '\'' +
+//                        ", closing=" + closing +
+//                        '}';
+//            }
+//        }
+//
+//        private static HtmlTag parseTag(String in) {
+//
+//            final HtmlTag out;
+//
+//            final int length = in != null
+//                    ? in.length()
+//                    : 0;
+//
+//            Debug.i(in, length);
+//
+//            if (length == 0 || length < 3) {
+//                out = null;
+//            } else {
+//
+//                final boolean closing = '<' == in.charAt(0) && '/' == in.charAt(1);
+//                final String tag = closing
+//                        ? in.substring(2, in.length() - 1)
+//                        : in.substring(1, in.length() - 1);
+//                out = new HtmlTag(tag, closing);
+//            }
+//
+//            return out;
+//        }
+//
+//        @Override
+//        public void visit(HtmlBlock htmlBlock) {
+//            // interestring thing... what is it also?
+//            Debug.i(htmlBlock);
+//        }
+//
+//        @Override
+//        public void visit(CustomBlock customBlock) {
+//            // not supported, what is it anyway?
+//            Debug.i(customBlock);
+//        }
+//
+//        @Override
+//        public void visit(Document document) {
+//            // the whole document, no need to do anything
+//            Debug.i(document);
+//            super.visit(document);
+//        }
+//
+//        @Override
+//        public void visit(Link link) {
+//            Debug.i(link);
+//            final int length = builder.length();
+//            visitChildren(link);
+//            builder.setSpan(new URLSpan(link.getDestination()), length, builder.length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
+//        }
+//
+//        @Override
+//        public void visit(Image image) {
+//            // not supported... maybe for now?
+//            Debug.i(image);
+//            final int length = builder.length();
+//            super.visit(image);
+//
+////            final int length = builder.length();
+//            final TestDrawable drawable = new TestDrawable();
+//            final DrawableSpan span = new DrawableSpan(drawable);
+//            builder.append("  ");
+//            builder.setSpan(span, length, builder.length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
+//        }
+//
+//        @Override
+//        public void visit(CustomNode customNode) {
+//
+//            Debug.i(customNode);
+//
+//            if (customNode instanceof Strikethrough) {
+//                final int length = builder.length();
+//                visitChildren(customNode);
+//                builder.setSpan(new StrikethroughSpan(), length, builder.length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
+//            } else {
+//                super.visit(customNode);
+//            }
+//        }
+//    }
+//
+//
+//    private static class TestDrawable extends Drawable {
+//
+//        private final Handler handler = new Handler(Looper.getMainLooper());
+//        private boolean called;
+//
+//        TestDrawable() {
+//            setBounds(0, 0, 50, 50);
+//        }
+//
+//        @Override
+//        public void draw(@NonNull final Canvas canvas) {
+//            canvas.clipRect(getBounds());
+//            if (!called) {
+//                canvas.drawColor(0xFF00ff00);
+//                handler.removeCallbacksAndMessages(null);
+//                handler.postDelayed(new Runnable() {
+//                    @Override
+//                    public void run() {
+//                        called = true;
+//                        setBounds(0, 0, 400, 400);
+//                        invalidateSelf();
+//                    }
+//                }, 2000L);
+//            } else {
+//                canvas.drawColor(0xFFff0000);
+//            }
+//        }
+//
+//        @Override
+//        public void setAlpha(@IntRange(from = 0, to = 255) int alpha) {
+//
+//        }
+//
+//        @Override
+//        public void setColorFilter(@Nullable ColorFilter colorFilter) {
+//
+//        }
+//
+//        @Override
+//        public int getOpacity() {
+//            return 0;
+//        }
+//
+//        @Override
+//        public int getIntrinsicWidth() {
+//            return called ? 400 : 50;
+//        }
+//
+//        @Override
+//        public int getIntrinsicHeight() {
+//            return called ? 400 : 50;
+//        }
+//    }
+//}
diff --git a/app/src/main/java/ru/noties/markwon/spans/BlockQuoteSpan.java b/app/src/main/java/ru/noties/markwon/spans2/BlockQuoteSpan.java
similarity index 97%
rename from app/src/main/java/ru/noties/markwon/spans/BlockQuoteSpan.java
rename to app/src/main/java/ru/noties/markwon/spans2/BlockQuoteSpan.java
index bfe76eec..68937347 100644
--- a/app/src/main/java/ru/noties/markwon/spans/BlockQuoteSpan.java
+++ b/app/src/main/java/ru/noties/markwon/spans2/BlockQuoteSpan.java
@@ -1,4 +1,4 @@
-package ru.noties.markwon.spans;
+package ru.noties.markwon.spans2;
 
 import android.graphics.Canvas;
 import android.graphics.Paint;
diff --git a/app/src/main/java/ru/noties/markwon/spans/CodeSpan.java b/app/src/main/java/ru/noties/markwon/spans2/CodeSpan.java
similarity index 65%
rename from app/src/main/java/ru/noties/markwon/spans/CodeSpan.java
rename to app/src/main/java/ru/noties/markwon/spans2/CodeSpan.java
index 2c22af23..485592b3 100644
--- a/app/src/main/java/ru/noties/markwon/spans/CodeSpan.java
+++ b/app/src/main/java/ru/noties/markwon/spans2/CodeSpan.java
@@ -1,4 +1,4 @@
-package ru.noties.markwon.spans;
+package ru.noties.markwon.spans2;
 
 import android.graphics.Canvas;
 import android.graphics.Paint;
@@ -6,12 +6,6 @@ import android.graphics.Rect;
 import android.support.annotation.IntRange;
 import android.support.annotation.NonNull;
 import android.support.annotation.Nullable;
-import android.text.Layout;
-import android.text.StaticLayout;
-import android.text.TextPaint;
-import android.text.style.LeadingMarginSpan;
-import android.text.style.LineHeightSpan;
-import android.text.style.MetricAffectingSpan;
 import android.text.style.ReplacementSpan;
 
 import ru.noties.debug.Debug;
@@ -24,10 +18,16 @@ public class CodeSpan extends ReplacementSpan/* implements LeadingMarginSpan*/ {
     private final int start;
     private final int end;
 
+    private final Rect rect = new Rect();
+    private final Rect borderRect = new Rect();
+    private final Paint paint = new Paint();
+
     public CodeSpan(boolean multiline, int start, int end) {
         this.multiline = multiline;
         this.start = start;
         this.end = end;
+
+        paint.setStyle(Paint.Style.FILL);
     }
 
     @Override
@@ -74,33 +74,104 @@ public class CodeSpan extends ReplacementSpan/* implements LeadingMarginSpan*/ {
             @NonNull Paint paint
     ) {
 
-        Debug.i("text: %s, x: %s, top: %s, y: %s, bottom: %s", text.subSequence(start, end), x, top, y, bottom);
-
-        final CharSequence cs = text.subSequence(start, end);
-
-        final int width = 32 + (int) (paint.measureText(cs, 0, cs.length()) + .5F);
-
         final int left = (int) (x + .5F);
-        final int right = multiline
-                ? canvas.getWidth()
-                : left + width;
 
-        final Rect rect = new Rect(
-                left,
-                top,
-                right,
-                bottom
-        );
+        final int right;
+        if (multiline) {
+            right = canvas.getWidth();
+        } else {
+            final int width = (16 * 2) + (int) (paint.measureText(text, start, end) + .5F);
+            right = left + width;
+        }
 
-        final Paint p = new Paint();
-        p.setStyle(Paint.Style.FILL);
-        p.setColor(0x80ff0000);
-        canvas.drawRect(rect, p);
+        rect.set(left, top, right, bottom);
 
+
+        // okay, draw background first
+        drawBackground(canvas);
+
+        // then, if any, draw borders
+        drawBorders(canvas, this.start == start, this.end == end);
+
+        // draw text
         // y center position
         final int b = bottom - ((bottom - top) / 2) - (int) ((paint.descent() + paint.ascent()) / 2);
-        p.setColor(0xFF000000);
-        canvas.drawText(cs, 0, cs.length(), x + 16, b, paint);
+//        if (config.textColor != 0) {
+//            // we will use Paint object that is used to draw all the text (textSize, textColor, typeface, etc)
+//            paint.setColor(config.textColor);
+//        }
+        canvas.drawText(text, start, end, x + 16, b, paint);
+
+
+//        Debug.i("text: %s, x: %s, top: %s, y: %s, bottom: %s", text.subSequence(start, end), x, top, y, bottom);
+//
+//        final CharSequence cs = text.subSequence(start, end);
+//
+//        final int width = 32 + (int) (paint.measureText(cs, 0, cs.length()) + .5F);
+//
+//        final int left = (int) (x + .5F);
+//        final int right = multiline
+//                ? canvas.getWidth()
+//                : left + width;
+//
+//        final Rect rect = new Rect(
+//                left,
+//                top,
+//                right,
+//                bottom
+//        );
+//
+//        final Paint p = new Paint();
+//        p.setStyle(Paint.Style.FILL);
+//        p.setColor(0x80ff0000);
+//        canvas.drawRect(rect, p);
+//
+//        // y center position
+//        final int b = bottom - ((bottom - top) / 2) - (int) ((paint.descent() + paint.ascent()) / 2);
+//        p.setColor(0xFF000000);
+//        canvas.drawText(cs, 0, cs.length(), x + 16, b, paint);
+    }
+
+    private void drawBackground(Canvas canvas) {
+//        final int color = config.backgroundColor;
+//        if (color != 0) {
+            paint.setColor(0x40ff0000);
+            canvas.drawRect(rect, paint);
+//        }
+    }
+
+    private void drawBorders(Canvas canvas, boolean top, boolean bottom) {
+
+        final int color = 0xFFff0000;
+        final int width = 4;
+//        if (color == 0
+//                || width == 0) {
+//            return;
+//        }
+
+        paint.setColor(color);
+
+        // left and right are always drawn
+
+        // LEFT
+        borderRect.set(rect.left, rect.top, rect.left + width, rect.bottom);
+        canvas.drawRect(borderRect, paint);
+
+        // RIGHT
+        borderRect.set(rect.right - width, rect.top, rect.right, rect.bottom);
+        canvas.drawRect(borderRect, paint);
+
+        // TOP
+        if (top) {
+            borderRect.set(rect.left, rect.top, rect.right, rect.top + width);
+            canvas.drawRect(borderRect, paint);
+        }
+
+        // BOTTOM
+        if (bottom) {
+            borderRect.set(rect.left, rect.bottom - width, rect.right, rect.bottom);
+            canvas.drawRect(borderRect, paint);
+        }
     }
 
 
diff --git a/app/src/main/java/ru/noties/markwon/spans2/DrawableSpan.java b/app/src/main/java/ru/noties/markwon/spans2/DrawableSpan.java
new file mode 100644
index 00000000..5da2fbce
--- /dev/null
+++ b/app/src/main/java/ru/noties/markwon/spans2/DrawableSpan.java
@@ -0,0 +1,97 @@
+package ru.noties.markwon.spans2;
+
+import android.graphics.Canvas;
+import android.graphics.Paint;
+import android.graphics.Rect;
+import android.graphics.drawable.Drawable;
+import android.support.annotation.IntDef;
+import android.support.annotation.IntRange;
+import android.support.annotation.NonNull;
+import android.support.annotation.Nullable;
+import android.text.style.ReplacementSpan;
+
+public class DrawableSpan extends ReplacementSpan {
+
+    @IntDef({ ALIGN_BOTTOM, ALIGN_BASELINE, ALIGN_CENTER })
+    @interface Alignment {}
+
+    public static final int ALIGN_BOTTOM = 0;
+    public static final int ALIGN_BASELINE = 1;
+    public static final int ALIGN_CENTER = 2;
+
+    private final Drawable drawable;
+    private final int alignment;
+
+    public DrawableSpan(@NonNull Drawable drawable) {
+        this(drawable, ALIGN_BOTTOM);
+    }
+
+    public DrawableSpan(@NonNull Drawable drawable, @Alignment int alignment) {
+        this.drawable = drawable;
+        this.alignment = alignment;
+
+        // additionally set intrinsic bounds if empty
+        final Rect rect = drawable.getBounds();
+        if (rect.isEmpty()) {
+            drawable.setBounds(0, 0, drawable.getIntrinsicWidth(), drawable.getIntrinsicHeight());
+        }
+    }
+
+    @Override
+    public int getSize(
+            @NonNull Paint paint,
+            CharSequence text,
+            @IntRange(from = 0) int start,
+            @IntRange(from = 0) int end,
+            @Nullable Paint.FontMetricsInt fm) {
+
+        final Rect rect = drawable.getBounds();
+
+        if (fm != null) {
+            fm.ascent = -rect.bottom;
+            fm.descent = 0;
+
+            fm.top = fm.ascent;
+            fm.bottom = 0;
+        }
+
+        return rect.right;
+    }
+
+    @Override
+    public void draw(
+            @NonNull Canvas canvas,
+            CharSequence text,
+            @IntRange(from = 0) int start,
+            @IntRange(from = 0) int end,
+            float x,
+            int top,
+            int y,
+            int bottom,
+            @NonNull Paint paint) {
+
+        final Drawable drawable = this.drawable;
+
+        final int b = bottom - drawable.getBounds().bottom;
+
+        final int save = canvas.save();
+        try {
+            final int translationY;
+            if (ALIGN_CENTER == alignment) {
+                translationY = (int) (b / 2.F + .5F);
+            } else if (ALIGN_BASELINE == alignment) {
+                translationY = b - paint.getFontMetricsInt().descent;
+            } else {
+                translationY = b;
+            }
+            canvas.translate(x, translationY);
+            drawable.draw(canvas);
+        } finally {
+            canvas.restoreToCount(save);
+        }
+    }
+
+    public Drawable getDrawable() {
+        return drawable;
+    }
+}
diff --git a/app/src/main/java/ru/noties/markwon/spans/DrawableSpanUtils.java b/app/src/main/java/ru/noties/markwon/spans2/DrawableSpanUtils.java
similarity index 99%
rename from app/src/main/java/ru/noties/markwon/spans/DrawableSpanUtils.java
rename to app/src/main/java/ru/noties/markwon/spans2/DrawableSpanUtils.java
index 7528c8ec..8b3fe20b 100644
--- a/app/src/main/java/ru/noties/markwon/spans/DrawableSpanUtils.java
+++ b/app/src/main/java/ru/noties/markwon/spans2/DrawableSpanUtils.java
@@ -1,4 +1,4 @@
-package ru.noties.markwon.spans;
+package ru.noties.markwon.spans2;
 
 import android.graphics.Rect;
 import android.graphics.drawable.Drawable;
diff --git a/app/src/main/java/ru/noties/markwon/spans2/EmphasisSpan.java b/app/src/main/java/ru/noties/markwon/spans2/EmphasisSpan.java
new file mode 100644
index 00000000..bcd8804b
--- /dev/null
+++ b/app/src/main/java/ru/noties/markwon/spans2/EmphasisSpan.java
@@ -0,0 +1,17 @@
+package ru.noties.markwon.spans2;
+
+import android.text.TextPaint;
+import android.text.style.MetricAffectingSpan;
+
+public class EmphasisSpan extends MetricAffectingSpan {
+
+    @Override
+    public void updateMeasureState(TextPaint p) {
+        p.setTextSkewX(-0.25f);
+    }
+
+    @Override
+    public void updateDrawState(TextPaint tp) {
+        tp.setTextSkewX(-0.25f);
+    }
+}
diff --git a/app/src/main/java/ru/noties/markwon/spans/ListItemSpan.java b/app/src/main/java/ru/noties/markwon/spans2/ListItemSpan.java
similarity index 97%
rename from app/src/main/java/ru/noties/markwon/spans/ListItemSpan.java
rename to app/src/main/java/ru/noties/markwon/spans2/ListItemSpan.java
index 065a1b2f..5b81952d 100644
--- a/app/src/main/java/ru/noties/markwon/spans/ListItemSpan.java
+++ b/app/src/main/java/ru/noties/markwon/spans2/ListItemSpan.java
@@ -1,4 +1,4 @@
-package ru.noties.markwon.spans;
+package ru.noties.markwon.spans2;
 
 import android.graphics.Canvas;
 import android.graphics.Paint;
diff --git a/app/src/main/java/ru/noties/markwon/spans2/StrongEmphasisSpan.java b/app/src/main/java/ru/noties/markwon/spans2/StrongEmphasisSpan.java
new file mode 100644
index 00000000..0ec3e8b7
--- /dev/null
+++ b/app/src/main/java/ru/noties/markwon/spans2/StrongEmphasisSpan.java
@@ -0,0 +1,17 @@
+package ru.noties.markwon.spans2;
+
+import android.text.TextPaint;
+import android.text.style.MetricAffectingSpan;
+
+public class StrongEmphasisSpan extends MetricAffectingSpan {
+
+    @Override
+    public void updateMeasureState(TextPaint p) {
+        p.setFakeBoldText(true);
+    }
+
+    @Override
+    public void updateDrawState(TextPaint tp) {
+        tp.setFakeBoldText(true);
+    }
+}
diff --git a/app/src/main/java/ru/noties/markwon/spans2/SubSpan.java b/app/src/main/java/ru/noties/markwon/spans2/SubSpan.java
new file mode 100644
index 00000000..991219c6
--- /dev/null
+++ b/app/src/main/java/ru/noties/markwon/spans2/SubSpan.java
@@ -0,0 +1,19 @@
+package ru.noties.markwon.spans2;
+
+import android.text.TextPaint;
+import android.text.style.MetricAffectingSpan;
+
+public class SubSpan extends MetricAffectingSpan {
+
+    @Override
+    public void updateDrawState(TextPaint tp) {
+        tp.setTextSize(tp.getTextSize() * .75F);
+        tp.baselineShift -= (int) (tp.ascent() / 2);
+    }
+
+    @Override
+    public void updateMeasureState(TextPaint tp) {
+        tp.setTextSize(tp.getTextSize() * .75F);
+        tp.baselineShift -= (int) (tp.ascent() / 2);
+    }
+}
diff --git a/app/src/main/java/ru/noties/markwon/spans2/SupSpan.java b/app/src/main/java/ru/noties/markwon/spans2/SupSpan.java
new file mode 100644
index 00000000..044efc70
--- /dev/null
+++ b/app/src/main/java/ru/noties/markwon/spans2/SupSpan.java
@@ -0,0 +1,19 @@
+package ru.noties.markwon.spans2;
+
+import android.text.TextPaint;
+import android.text.style.MetricAffectingSpan;
+
+public class SupSpan extends MetricAffectingSpan {
+
+    @Override
+    public void updateDrawState(TextPaint tp) {
+        tp.setTextSize(tp.getTextSize() * .75F);
+        tp.baselineShift += (int) (tp.ascent() / 2);
+    }
+
+    @Override
+    public void updateMeasureState(TextPaint tp) {
+        tp.setTextSize(tp.getTextSize() * .75F);
+        tp.baselineShift += (int) (tp.ascent() / 2);
+    }
+}
diff --git a/app/src/main/java/ru/noties/markwon/spans2/ThematicBreakSpan.java b/app/src/main/java/ru/noties/markwon/spans2/ThematicBreakSpan.java
new file mode 100644
index 00000000..731c9628
--- /dev/null
+++ b/app/src/main/java/ru/noties/markwon/spans2/ThematicBreakSpan.java
@@ -0,0 +1,25 @@
+package ru.noties.markwon.spans2;
+
+import android.graphics.Canvas;
+import android.graphics.Paint;
+import android.graphics.Rect;
+import android.text.Layout;
+import android.text.style.LeadingMarginSpan;
+
+public class ThematicBreakSpan implements LeadingMarginSpan {
+
+    @Override
+    public int getLeadingMargin(boolean first) {
+        return 1;
+    }
+
+    @Override
+    public void drawLeadingMargin(Canvas c, Paint p, int x, int dir, int top, int baseline, int bottom, CharSequence text, int start, int end, boolean first, Layout layout) {
+        final int middle = (bottom - top) / 2;
+        final Rect rect = new Rect(0, top + middle - 2, c.getWidth(), top + middle + 2);
+        final Paint paint = new Paint();
+        paint.setStyle(Paint.Style.FILL);
+        paint.setColor(0x80000000);
+        c.drawRect(rect, paint);
+    }
+}
diff --git a/app/src/main/res/layout/activity_main.xml b/app/src/main/res/layout/activity_main.xml
index 5d4636b2..b80f26ff 100644
--- a/app/src/main/res/layout/activity_main.xml
+++ b/app/src/main/res/layout/activity_main.xml
@@ -1,17 +1,19 @@
 <?xml version="1.0" encoding="utf-8"?>
 <ScrollView
-        xmlns:android="http://schemas.android.com/apk/res/android"
-        xmlns:tools="http://schemas.android.com/tools"
-android:layout_width="match_parent"
-android:layout_height="match_parent">
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:tools="http://schemas.android.com/tools"
+    android:layout_width="match_parent"
+    android:layout_height="match_parent">
 
-    <TextView
-            xmlns:android="http://schemas.android.com/apk/res/android"
-            android:id="@+id/activity_main"
-            android:layout_width="match_parent"
-            android:layout_height="wrap_content"
-            android:layout_margin="16dip"
-            tools:context="ru.noties.markwon.MainActivity"
-            tools:text="yo\nman"/>
+    <TextView xmlns:android="http://schemas.android.com/apk/res/android"
+        android:id="@+id/activity_main"
+        android:layout_width="match_parent"
+        android:layout_height="wrap_content"
+        android:layout_margin="16dip"
+        android:textSize="18sp"
+        android:lineSpacingExtra="2dip"
+        android:textColor="@color/colorPrimaryDark"
+        tools:context="ru.noties.markwon.MainActivity"
+        tools:text="yo\nman" />
 
 </ScrollView>
diff --git a/app/src/main/res/values-v21/styles.xml b/app/src/main/res/values-v21/styles.xml
new file mode 100644
index 00000000..4e1e75f3
--- /dev/null
+++ b/app/src/main/res/values-v21/styles.xml
@@ -0,0 +1,10 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+
+    <style name="AppThemeBase" parent="android:Theme.Material.Light">
+        <item name="android:colorAccent">@color/colorAccent</item>
+        <item name="android:colorPrimary">@color/colorPrimary</item>
+        <item name="android:colorPrimaryDark">@color/colorPrimaryDark</item>
+    </style>
+
+</resources>
\ No newline at end of file
diff --git a/app/src/main/res/values-w820dp/dimens.xml b/app/src/main/res/values-w820dp/dimens.xml
deleted file mode 100644
index 63fc8164..00000000
--- a/app/src/main/res/values-w820dp/dimens.xml
+++ /dev/null
@@ -1,6 +0,0 @@
-<resources>
-    <!-- Example customization of dimensions originally defined in res/values/dimens.xml
-         (such as screen margins) for screens with more than 820dp of available width. This
-         would include 7" and 10" devices in landscape (~960dp and ~1280dp respectively). -->
-    <dimen name="activity_horizontal_margin">64dp</dimen>
-</resources>
diff --git a/app/src/main/res/values/styles.xml b/app/src/main/res/values/styles.xml
index 5885930d..5b267e56 100644
--- a/app/src/main/res/values/styles.xml
+++ b/app/src/main/res/values/styles.xml
@@ -1,11 +1,8 @@
 <resources>
 
+    <style name="AppThemeBase" parent="android:Theme.Holo.Light"/>
+
     <!-- Base application theme. -->
-    <style name="AppTheme" parent="Theme.AppCompat.Light.DarkActionBar">
-        <!-- Customize your theme here. -->
-        <item name="colorPrimary">@color/colorPrimary</item>
-        <item name="colorPrimaryDark">@color/colorPrimaryDark</item>
-        <item name="colorAccent">@color/colorAccent</item>
-    </style>
+    <style name="AppTheme" parent="AppThemeBase"/>
 
 </resources>
diff --git a/build.gradle b/build.gradle
index a83e0d6a..43ca8a81 100644
--- a/build.gradle
+++ b/build.gradle
@@ -1,14 +1,9 @@
-// Top-level build file where you can add configuration options common to all sub-projects/modules.
-
 buildscript {
     repositories {
         jcenter()
     }
     dependencies {
         classpath 'com.android.tools.build:gradle:2.3.1'
-
-        // NOTE: Do not place your application dependencies here; they belong
-        // in the individual module build.gradle files
     }
 }
 
@@ -33,4 +28,8 @@ ext {
     // Dependencies
     final def supportVersion = '25.3.1'
     SUPPORT_ANNOTATIONS = "com.android.support:support-annotations:$supportVersion"
+
+    final def commonMarkVersion = '0.9.0'
+    COMMON_MARK = "com.atlassian.commonmark:commonmark:$commonMarkVersion"
+    COMMON_MARK_STRIKETHROUGHT = "com.atlassian.commonmark:commonmark-ext-gfm-strikethrough:$commonMarkVersion"
 }
diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties
index ac15c9df..38657fe7 100644
--- a/gradle/wrapper/gradle-wrapper.properties
+++ b/gradle/wrapper/gradle-wrapper.properties
@@ -3,4 +3,4 @@ distributionBase=GRADLE_USER_HOME
 distributionPath=wrapper/dists
 zipStoreBase=GRADLE_USER_HOME
 zipStorePath=wrapper/dists
-distributionUrl=https\://services.gradle.org/distributions/gradle-3.3-all.zip
+distributionUrl=https\://services.gradle.org/distributions/gradle-3.5-all.zip
diff --git a/library-renderer/build.gradle b/library-renderer/build.gradle
new file mode 100644
index 00000000..faf18fd8
--- /dev/null
+++ b/library-renderer/build.gradle
@@ -0,0 +1,26 @@
+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-spans')
+
+    compile SUPPORT_ANNOTATIONS
+    compile COMMON_MARK
+    compile COMMON_MARK_STRIKETHROUGHT
+
+    // todo, debugging only
+    compile 'ru.noties:debug:3.0.0@jar'
+}
diff --git a/library-renderer/src/main/AndroidManifest.xml b/library-renderer/src/main/AndroidManifest.xml
new file mode 100644
index 00000000..c3f0b404
--- /dev/null
+++ b/library-renderer/src/main/AndroidManifest.xml
@@ -0,0 +1 @@
+<manifest package="ru.noties.markwon.renderer" />
diff --git a/library-renderer/src/main/java/ru/noties/markwon/renderer/SpannableConfiguration.java b/library-renderer/src/main/java/ru/noties/markwon/renderer/SpannableConfiguration.java
new file mode 100644
index 00000000..079c3d5c
--- /dev/null
+++ b/library-renderer/src/main/java/ru/noties/markwon/renderer/SpannableConfiguration.java
@@ -0,0 +1,109 @@
+package ru.noties.markwon.renderer;
+
+import android.content.Context;
+import android.support.annotation.NonNull;
+
+import ru.noties.markwon.spans.BlockQuoteSpan;
+import ru.noties.markwon.spans.BulletListItemSpan;
+import ru.noties.markwon.spans.CodeSpan;
+import ru.noties.markwon.spans.HeadingSpan;
+
+public class SpannableConfiguration {
+
+    // creates default configuration
+    public static SpannableConfiguration create(@NonNull Context context) {
+        return new Builder(context).build();
+    }
+
+    public static Builder builder(@NonNull Context context) {
+        return new Builder(context);
+    }
+
+    private final BlockQuoteSpan.Config blockQuoteConfig;
+    private final CodeSpan.Config codeConfig;
+    private final BulletListItemSpan.Config bulletListConfig;
+    private final HeadingSpan.Config headingConfig;
+
+    private SpannableConfiguration(Builder builder) {
+        this.blockQuoteConfig = builder.blockQuoteConfig;
+        this.codeConfig = builder.codeConfig;
+        this.bulletListConfig = builder.bulletListConfig;
+        this.headingConfig = builder.headingConfig;
+    }
+
+    public BlockQuoteSpan.Config getBlockQuoteConfig() {
+        return blockQuoteConfig;
+    }
+
+    public CodeSpan.Config getCodeConfig() {
+        return codeConfig;
+    }
+
+    public BulletListItemSpan.Config getBulletListConfig() {
+        return bulletListConfig;
+    }
+
+    public HeadingSpan.Config getHeadingConfig() {
+        return headingConfig;
+    }
+
+    public static class Builder {
+
+        private final Context context;
+        private BlockQuoteSpan.Config blockQuoteConfig;
+        private CodeSpan.Config codeConfig;
+        private BulletListItemSpan.Config bulletListConfig;
+        private HeadingSpan.Config headingConfig;
+
+        public Builder(Context context) {
+            this.context = context;
+        }
+
+        public Builder setBlockQuoteConfig(@NonNull BlockQuoteSpan.Config blockQuoteConfig) {
+            this.blockQuoteConfig = blockQuoteConfig;
+            return this;
+        }
+
+        public Builder setCodeConfig(@NonNull CodeSpan.Config codeConfig) {
+            this.codeConfig = codeConfig;
+            return this;
+        }
+
+        public Builder setBulletListConfig(@NonNull BulletListItemSpan.Config bulletListConfig) {
+            this.bulletListConfig = bulletListConfig;
+            return this;
+        }
+
+        public Builder setHeadingConfig(@NonNull HeadingSpan.Config headingConfig) {
+            this.headingConfig = headingConfig;
+            return this;
+        }
+
+        // todo, change to something more reliable
+        public SpannableConfiguration build() {
+            if (blockQuoteConfig == null) {
+                blockQuoteConfig = new BlockQuoteSpan.Config(
+                        px(16),
+                        px(4),
+                        0xFFcccccc
+                );
+            }
+            if (codeConfig == null) {
+                codeConfig = CodeSpan.Config.builder()
+                        .setMultilineMargin(px(8))
+                        .build();
+            }
+            if (bulletListConfig == null) {
+                bulletListConfig = new BulletListItemSpan.Config(0, px(16), px(1));
+            }
+            if (headingConfig == null) {
+                headingConfig = new HeadingSpan.Config(px(1), 0);
+            }
+            return new SpannableConfiguration(this);
+        }
+
+        private int px(int dp) {
+            return (int) (context.getResources().getDisplayMetrics().density * dp + .5F);
+        }
+    }
+}
diff --git a/library-renderer/src/main/java/ru/noties/markwon/renderer/SpannableHtmlParser.java b/library-renderer/src/main/java/ru/noties/markwon/renderer/SpannableHtmlParser.java
new file mode 100644
index 00000000..afcb93b3
--- /dev/null
+++ b/library-renderer/src/main/java/ru/noties/markwon/renderer/SpannableHtmlParser.java
@@ -0,0 +1,126 @@
+package ru.noties.markwon.renderer;
+
+import android.annotation.TargetApi;
+import android.os.Build;
+import android.support.annotation.NonNull;
+import android.support.annotation.Nullable;
+import android.text.Html;
+import android.text.Spanned;
+
+import java.util.HashMap;
+import java.util.Map;
+
+@SuppressWarnings("WeakerAccess")
+public class SpannableHtmlParser {
+
+    // we need to handle images independently (in order to parse alt, width, height, etc)
+
+    // for simple tags without arguments
+    // <b>, <i>, etc
+    public interface SpanProvider {
+        Object provide();
+    }
+
+    public interface HtmlParser {
+        Object[] getSpans(@NonNull String html);
+    }
+
+    // creates default parser
+    public static SpannableHtmlParser create() {
+        return null;
+    }
+
+    public static Builder builder() {
+        return new Builder();
+    }
+
+    private final Map<String, SpanProvider> customTags;
+    private final HtmlParser parser;
+
+    private SpannableHtmlParser(Builder builder) {
+        this.customTags = builder.customTags;
+        this.parser = builder.parser;
+    }
+
+    public static class Builder {
+
+        private final Map<String, SpanProvider> customTags = new HashMap<>(3);
+        private HtmlParser parser;
+
+        public Builder customTag(@NonNull String tag, @NonNull SpanProvider provider) {
+            customTags.put(tag, provider);
+            return this;
+        }
+
+        public Builder setParser(@NonNull HtmlParser parser) {
+            this.parser = parser;
+            return this;
+        }
+
+        public SpannableHtmlParser build() {
+            if (parser == null) {
+                // todo, images....
+                parser = DefaultHtmlParser.create(null, null);
+            }
+            return new SpannableHtmlParser(this);
+        }
+    }
+
+    public static abstract class DefaultHtmlParser implements HtmlParser {
+
+        public static DefaultHtmlParser create(@Nullable Html.ImageGetter imageGetter, @Nullable Html.TagHandler tagHandler) {
+            final DefaultHtmlParser parser;
+            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
+                parser = new Parser24(imageGetter, tagHandler);
+            } else {
+                parser = new ParserPre24(imageGetter, tagHandler);
+            }
+            return parser;
+        }
+
+        final Html.ImageGetter imageGetter;
+        final Html.TagHandler tagHandler;
+
+        DefaultHtmlParser(Html.ImageGetter imageGetter, Html.TagHandler tagHandler) {
+            this.imageGetter = imageGetter;
+            this.tagHandler = tagHandler;
+        }
+
+        Object[] getSpans(Spanned spanned) {
+            final Object[] spans;
+            final int length = spanned != null ? spanned.length() : 0;
+            if (length == 0) {
+                spans = null;
+            } else {
+                spans = spanned.getSpans(0, length, Object.class);
+            }
+            return spans;
+        }
+
+        @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(Html.fromHtml(html, imageGetter, tagHandler));
+            }
+        }
+
+        @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(Html.fromHtml(html, Html.FROM_HTML_MODE_COMPACT, imageGetter, tagHandler));
+            }
+        }
+    }
+}
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
new file mode 100644
index 00000000..a9a01bf3
--- /dev/null
+++ b/library-renderer/src/main/java/ru/noties/markwon/renderer/SpannableMarkdownVisitor.java
@@ -0,0 +1,310 @@
+package ru.noties.markwon.renderer;
+
+import android.support.annotation.NonNull;
+import android.text.SpannableStringBuilder;
+import android.text.Spanned;
+import android.text.style.StrikethroughSpan;
+
+import org.commonmark.ext.gfm.strikethrough.Strikethrough;
+import org.commonmark.node.AbstractVisitor;
+import org.commonmark.node.BlockQuote;
+import org.commonmark.node.BulletList;
+import org.commonmark.node.Code;
+import org.commonmark.node.CustomNode;
+import org.commonmark.node.Emphasis;
+import org.commonmark.node.FencedCodeBlock;
+import org.commonmark.node.HardLineBreak;
+import org.commonmark.node.Heading;
+import org.commonmark.node.HtmlBlock;
+import org.commonmark.node.Image;
+import org.commonmark.node.ListBlock;
+import org.commonmark.node.ListItem;
+import org.commonmark.node.Node;
+import org.commonmark.node.OrderedList;
+import org.commonmark.node.Paragraph;
+import org.commonmark.node.SoftLineBreak;
+import org.commonmark.node.StrongEmphasis;
+import org.commonmark.node.Text;
+import org.commonmark.node.ThematicBreak;
+
+import ru.noties.debug.Debug;
+import ru.noties.markwon.spans.BlockQuoteSpan;
+import ru.noties.markwon.spans.BulletListItemSpan;
+import ru.noties.markwon.spans.CodeSpan;
+import ru.noties.markwon.spans.EmphasisSpan;
+import ru.noties.markwon.spans.HeadingSpan;
+import ru.noties.markwon.spans.StrongEmphasisSpan;
+import ru.noties.markwon.spans.ThematicBreakSpan;
+
+public class SpannableMarkdownVisitor extends AbstractVisitor {
+
+    // http://spec.commonmark.org/0.18/#html-blocks
+
+    private final SpannableConfiguration configuration;
+    private final SpannableStringBuilder builder;
+
+    private int blockQuoteIndent;
+    private int listLevel;
+
+    public SpannableMarkdownVisitor(
+            @NonNull SpannableConfiguration configuration,
+            @NonNull SpannableStringBuilder builder
+    ) {
+        this.configuration = configuration;
+        this.builder = builder;
+    }
+
+    @Override
+    public void visit(Text text) {
+        Debug.i(text);
+        builder.append(text.getLiteral());
+    }
+
+    @Override
+    public void visit(StrongEmphasis strongEmphasis) {
+        Debug.i(strongEmphasis);
+        final int length = builder.length();
+        visitChildren(strongEmphasis);
+        setSpan(length, new StrongEmphasisSpan());
+    }
+
+    @Override
+    public void visit(Emphasis emphasis) {
+        Debug.i(emphasis);
+        final int length = builder.length();
+        visitChildren(emphasis);
+        setSpan(length, new EmphasisSpan());
+    }
+
+    @Override
+    public void visit(BlockQuote blockQuote) {
+
+        newLine();
+
+        final int length = builder.length();
+
+        blockQuoteIndent += 1;
+
+        visitChildren(blockQuote);
+
+        setSpan(length, new BlockQuoteSpan(
+                configuration.getBlockQuoteConfig(),
+                blockQuoteIndent
+        ));
+
+        blockQuoteIndent -= 1;
+
+        newLine();
+    }
+
+    @Override
+    public void visit(Code code) {
+
+        Debug.i(code);
+
+        final int length = builder.length();
+
+        // NB, in order to provide a _padding_ feeling code is wrapped inside two unbreakable spaces
+        // unfortunately we cannot use this for multiline code as we cannot control there a new line break will be inserted
+        builder.append('\u00a0');
+        builder.append(code.getLiteral());
+        builder.append('\u00a0');
+
+        setSpan(length, new CodeSpan(
+                configuration.getCodeConfig(),
+                false
+        ));
+    }
+
+    @Override
+    public void visit(FencedCodeBlock fencedCodeBlock) {
+
+        Debug.i(fencedCodeBlock);
+
+        newLine();
+
+        final int length = builder.length();
+
+        builder.append(fencedCodeBlock.getLiteral());
+        setSpan(length, new CodeSpan(
+                configuration.getCodeConfig(),
+                true
+        ));
+
+        newLine();
+    }
+
+    @Override
+    public void visit(Image image) {
+
+        Debug.i(image);
+
+        final int length = builder.length();
+
+        visitChildren(image);
+
+        if (length == builder.length()) {
+            // nothing is added, and we need at least one symbol
+            builder.append(' ');
+        }
+
+
+////            final int length = builder.length();
+//        final TestDrawable drawable = new TestDrawable();
+//        final DrawableSpan span = new DrawableSpan(drawable);
+//        builder.append("  ");
+//        builder.setSpan(span, length, builder.length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
+    }
+
+    @Override
+    public void visit(BulletList bulletList) {
+        Debug.i(bulletList);
+        newLine();
+        visitChildren(bulletList);
+        newLine();
+    }
+
+    @Override
+    public void visit(ListItem listItem) {
+
+        Debug.i(listItem);
+
+        final int length = builder.length();
+        blockQuoteIndent += 1;
+        listLevel += 1;
+        visitChildren(listItem);
+        // todo, can be a bullet list & ordered list (with leading numbers... looks like we need to `draw` numbers...
+        setSpan(length, new BulletListItemSpan(
+                configuration.getBulletListConfig(),
+                blockQuoteIndent,
+                listLevel - 1,
+                length
+        ));
+//        builder.setSpan(new BulletListItemSpan(blockQuoteIndent, listLevel > 1, length), length, builder.length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
+        blockQuoteIndent -= 1;
+        listLevel -= 1;
+
+        newLine();
+    }
+
+    @Override
+    public void visit(ThematicBreak thematicBreak) {
+
+        Debug.i(thematicBreak);
+
+        newLine();
+
+        // todo, new lines...
+        final int length = builder.length();
+        builder.append(' '); // without space it won't render
+        builder.setSpan(new ThematicBreakSpan(), length, builder.length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
+
+        newLine();
+    }
+
+    @Override
+    public void visit(OrderedList orderedList) {
+
+        Debug.i(orderedList);
+
+        newLine();
+
+//        Debug.i(orderedList, orderedList.getDelimiter(), orderedList.getStartNumber());
+        // todo, ordering numbers
+        super.visit(orderedList);
+
+        newLine();
+    }
+
+    @Override
+    public void visit(Heading heading) {
+
+        Debug.i(heading);
+
+        newLine();
+
+        final int length = builder.length();
+        visitChildren(heading);
+        setSpan(length, new HeadingSpan(
+                configuration.getHeadingConfig(),
+                heading.getLevel(),
+                builder.length())
+        );
+
+        newLine();
+    }
+
+    @Override
+    public void visit(SoftLineBreak softLineBreak) {
+        Debug.i(softLineBreak);
+        newLine();
+    }
+
+    @Override
+    public void visit(HardLineBreak hardLineBreak) {
+        Debug.i(hardLineBreak);
+        newLine();
+    }
+
+    @Override
+    public void visit(CustomNode customNode) {
+
+        Debug.i(customNode);
+
+        if (customNode instanceof Strikethrough) {
+            final int length = builder.length();
+            visitChildren(customNode);
+            setSpan(length, new StrikethroughSpan());
+        } else {
+            super.visit(customNode);
+        }
+    }
+
+    @Override
+    public void visit(Paragraph paragraph) {
+
+        final boolean inTightList = isInTightList(paragraph);
+
+        Debug.i(paragraph, inTightList);
+
+        if (!inTightList) {
+            newLine();
+        }
+
+        visitChildren(paragraph);
+
+        if (!inTightList) {
+            newLine();
+        }
+    }
+
+    @Override
+    public void visit(HtmlBlock htmlBlock) {
+        // http://spec.commonmark.org/0.18/#html-blocks
+        Debug.i(htmlBlock, htmlBlock.getLiteral());
+        super.visit(htmlBlock);
+    }
+
+    private void setSpan(int start, @NonNull Object span) {
+        builder.setSpan(span, start, builder.length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
+    }
+
+    private void newLine() {
+        if (builder.length() > 0
+                && '\n' != builder.charAt(builder.length() - 1)) {
+            builder.append('\n');
+        }
+    }
+
+    private boolean isInTightList(Paragraph paragraph) {
+        final Node parent = paragraph.getParent();
+        if (parent != null) {
+            final Node gramps = parent.getParent();
+            if (gramps != null && gramps instanceof ListBlock) {
+                ListBlock list = (ListBlock) gramps;
+                return list.isTight();
+            }
+        }
+        return false;
+    }
+}
diff --git a/library-renderer/src/main/java/ru/noties/markwon/renderer/SpannableRenderer.java b/library-renderer/src/main/java/ru/noties/markwon/renderer/SpannableRenderer.java
new file mode 100644
index 00000000..accb401f
--- /dev/null
+++ b/library-renderer/src/main/java/ru/noties/markwon/renderer/SpannableRenderer.java
@@ -0,0 +1,28 @@
+package ru.noties.markwon.renderer;
+
+import android.support.annotation.NonNull;
+import android.support.annotation.Nullable;
+import android.text.SpannableStringBuilder;
+
+import org.commonmark.node.Node;
+
+// please note that this class does not implement Renderer in order to return CharSequence (instead of String)
+public class SpannableRenderer {
+
+    // todo
+    // * LinkDrawableSpan, that draws link whilst image is still loading (it must be clickable...)
+    // * Common interface for images (in markdown & inline-html)
+    // * util method to properly copy markdown (with lists/links, etc)
+
+    public CharSequence render(@NonNull SpannableConfiguration configuration, @Nullable Node node) {
+        final CharSequence out;
+        if (node == null) {
+            out = null;
+        } else {
+            final SpannableStringBuilder builder = new SpannableStringBuilder();
+            node.accept(new SpannableMarkdownVisitor(configuration, builder));
+            out = builder;
+        }
+        return out;
+    }
+}
diff --git a/library-spans/src/androidTest/java/ru/noties/markwon/spans/ExampleInstrumentedTest.java b/library-spans/src/androidTest/java/ru/noties/markwon/spans/ExampleInstrumentedTest.java
deleted file mode 100644
index 6386d053..00000000
--- a/library-spans/src/androidTest/java/ru/noties/markwon/spans/ExampleInstrumentedTest.java
+++ /dev/null
@@ -1,26 +0,0 @@
-package ru.noties.markwon.spans;
-
-import android.content.Context;
-import android.support.test.InstrumentationRegistry;
-import android.support.test.runner.AndroidJUnit4;
-
-import org.junit.Test;
-import org.junit.runner.RunWith;
-
-import static org.junit.Assert.*;
-
-/**
- * Instrumentation test, which will execute on an Android device.
- *
- * @see <a href="http://d.android.com/tools/testing">Testing documentation</a>
- */
-@RunWith(AndroidJUnit4.class)
-public class ExampleInstrumentedTest {
-    @Test
-    public void useAppContext() throws Exception {
-        // Context of the app under test.
-        Context appContext = InstrumentationRegistry.getTargetContext();
-
-        assertEquals("ru.noties.markwon.spans.test", appContext.getPackageName());
-    }
-}
diff --git a/library-spans/src/main/AndroidManifest.xml b/library-spans/src/main/AndroidManifest.xml
index 6ba735e1..84de88b7 100644
--- a/library-spans/src/main/AndroidManifest.xml
+++ b/library-spans/src/main/AndroidManifest.xml
@@ -1,10 +1 @@
-<manifest xmlns:android="http://schemas.android.com/apk/res/android"
-
-    package="ru.noties.markwon.spans">
-
-    <application android:allowBackup="true" android:label="@string/app_name"
-        android:supportsRtl="true">
-
-    </application>
-
-</manifest>
+<manifest package="ru.noties.markwon.spans" />
diff --git a/library-spans/src/main/java/ru/noties/markwon/spans/BlockQuoteSpan.java b/library-spans/src/main/java/ru/noties/markwon/spans/BlockQuoteSpan.java
new file mode 100644
index 00000000..35f73e96
--- /dev/null
+++ b/library-spans/src/main/java/ru/noties/markwon/spans/BlockQuoteSpan.java
@@ -0,0 +1,73 @@
+package ru.noties.markwon.spans;
+
+import android.graphics.Canvas;
+import android.graphics.Paint;
+import android.graphics.Rect;
+import android.support.annotation.ColorInt;
+import android.support.annotation.IntRange;
+import android.support.annotation.NonNull;
+import android.text.Layout;
+import android.text.style.LeadingMarginSpan;
+
+public class BlockQuoteSpan implements LeadingMarginSpan {
+
+    @SuppressWarnings("WeakerAccess")
+    public static class Config {
+
+        final int totalWidth;
+        final int quoteWidth;
+        final int quoteColor; // by default textColor with 0.1 alpha
+
+        public Config(
+                @IntRange(from = 0) int totalWidth,
+                @IntRange(from = 0) int quoteWidth,
+                @ColorInt int quoteColor) {
+            this.totalWidth = totalWidth;
+            this.quoteWidth = quoteWidth;
+            this.quoteColor = quoteColor;
+        }
+    }
+
+    private final Config config;
+    private final Rect rect = new Rect();
+    private final Paint paint = new Paint();
+    private final int indent;
+
+    public BlockQuoteSpan(@NonNull Config config, int indent) {
+        this.config = config;
+        this.indent = indent;
+
+        paint.setStyle(Paint.Style.FILL);
+        paint.setColor(config.quoteColor);
+    }
+
+    @Override
+    public int getLeadingMargin(boolean first) {
+        return config.totalWidth;
+    }
+
+    @Override
+    public void drawLeadingMargin(
+            Canvas c,
+            Paint p,
+            int x,
+            int dir,
+            int top,
+            int baseline,
+            int bottom,
+            CharSequence text,
+            int start,
+            int end,
+            boolean first,
+            Layout layout) {
+
+        final int save = c.save();
+        try {
+            final int left = config.totalWidth * (indent - 1);
+            rect.set(left, top, left + config.quoteWidth, bottom);
+            c.drawRect(rect, paint);
+        } finally {
+            c.restoreToCount(save);
+        }
+    }
+}
diff --git a/library-spans/src/main/java/ru/noties/markwon/spans/BulletListItemSpan.java b/library-spans/src/main/java/ru/noties/markwon/spans/BulletListItemSpan.java
new file mode 100644
index 00000000..ba92f59d
--- /dev/null
+++ b/library-spans/src/main/java/ru/noties/markwon/spans/BulletListItemSpan.java
@@ -0,0 +1,135 @@
+package ru.noties.markwon.spans;
+
+import android.graphics.Canvas;
+import android.graphics.Paint;
+import android.graphics.Rect;
+import android.graphics.RectF;
+import android.support.annotation.ColorInt;
+import android.support.annotation.IntRange;
+import android.support.annotation.NonNull;
+import android.text.Layout;
+import android.text.style.LeadingMarginSpan;
+
+public class BulletListItemSpan implements LeadingMarginSpan {
+
+    // todo, there are 3 types of bullets: filled circle, stroke circle & filled rectangle
+    // also, there are ordered lists
+
+    public static class Config {
+
+        final int bulletColor; // by default uses text color
+        final int marginWidth;
+        final int bulletStrokeWidth;
+
+        // from 0 but it makes sense to provide something wider
+        public Config(@ColorInt int bulletColor, @IntRange(from = 0) int marginWidth, int bulletStrokeWidth) {
+            this.bulletColor = bulletColor;
+            this.marginWidth = marginWidth;
+            this.bulletStrokeWidth = bulletStrokeWidth;
+        }
+    }
+
+    private final Config config;
+    private final Paint paint = new Paint();
+
+    private RectF circle;
+    private Rect rectangle;
+
+    private final int blockIndent;
+    private final int level;
+    private final int start;
+
+    public BulletListItemSpan(
+            @NonNull Config config,
+            @IntRange(from = 0) int blockIndent,
+            @IntRange(from = 0) int level,
+            @IntRange(from = 0) int start) {
+        this.config = config;
+        this.blockIndent = blockIndent;
+        this.level = level;
+        this.start = start;
+    }
+
+    @Override
+    public int getLeadingMargin(boolean first) {
+        return config.marginWidth;
+    }
+
+    @Override
+    public void drawLeadingMargin(Canvas c, Paint p, int x, int dir, int top, int baseline, int bottom, CharSequence text, int start, int end, boolean first, Layout layout) {
+
+        // if there was a line break, we don't need to draw it
+        if (this.start != start) {
+            return;
+        }
+
+        final int color;
+        final float stroke;
+
+        if (config.bulletColor == 0) {
+            color = p.getColor();
+        } else {
+            color = config.bulletColor;
+        }
+
+        if (config.bulletStrokeWidth == 0) {
+            stroke = p.getStrokeWidth();
+        } else {
+            stroke = config.bulletStrokeWidth;
+        }
+
+        paint.setColor(color);
+        paint.setStrokeWidth(stroke);
+
+        final int save = c.save();
+        try {
+
+            // by default we use half of margin width, but if height is less than width, we calculate from it
+            final int width = config.marginWidth;
+            final int height = bottom - top;
+
+            final int side = Math.min(config.marginWidth, height) / 2;
+
+            final int marginLeft = (width - side) / 2;
+            final int marginTop = (height - side) / 2;
+
+            final int l = (width * (blockIndent - 1)) + marginLeft;
+            final int t = top + marginTop;
+            final int r = l + side;
+            final int b = t + side;
+
+            if (level == 0
+                    || level == 1) {
+
+                // ensure we have circle rectF
+                if (circle == null) {
+                    circle = new RectF();
+                }
+
+                circle.set(l, t, r, b);
+
+                final Paint.Style style = level == 0
+                        ? Paint.Style.FILL
+                        : Paint.Style.STROKE;
+                paint.setStyle(style);
+
+                c.drawOval(circle, paint);
+            } else {
+
+                // ensure rectangle
+                if (rectangle == null) {
+                    rectangle = new Rect();
+                }
+
+                rectangle.set(l, t, r, b);
+
+                paint.setStyle(Paint.Style.FILL);
+
+                c.drawRect(rectangle, paint);
+            }
+
+        } finally {
+            c.restoreToCount(save);
+        }
+    }
+}
diff --git a/library-spans/src/main/java/ru/noties/markwon/spans/CodeSpan.java b/library-spans/src/main/java/ru/noties/markwon/spans/CodeSpan.java
new file mode 100644
index 00000000..b36d51b2
--- /dev/null
+++ b/library-spans/src/main/java/ru/noties/markwon/spans/CodeSpan.java
@@ -0,0 +1,307 @@
+package ru.noties.markwon.spans;
+
+import android.graphics.Canvas;
+import android.graphics.Paint;
+import android.graphics.Rect;
+import android.graphics.Typeface;
+import android.support.annotation.ColorInt;
+import android.support.annotation.IntRange;
+import android.support.annotation.NonNull;
+import android.text.Layout;
+import android.text.TextPaint;
+import android.text.style.LeadingMarginSpan;
+import android.text.style.MetricAffectingSpan;
+
+public class CodeSpan extends MetricAffectingSpan implements LeadingMarginSpan {
+
+    // the thing is.. we cannot use replacementSpan, because it won't let us create multiline code..
+    // and we want new lines when we do not fit the width
+    // plus it complicates the copying
+
+    // replacement span is great because we can have additional paddings & can actually get a hold
+    // of Canvas to draw background, but it implies a lot of manual text handling
+
+    // also, we can reuse Rect instance as long as we apply our dimensions in each draw call
+
+    @SuppressWarnings("WeakerAccess")
+    public static class Config {
+
+        public static Builder builder() {
+            return new Builder();
+        }
+
+        final int textColor; // by default the same as main text
+        final int backgroundColor; // by default textColor with 0.1 alpha
+        final int multilineMargin; // by default 0
+        final int textSize; // by default the same as main text
+        final Typeface typeface; // by default Typeface.MONOSPACE
+
+        private Config(Builder builder) {
+            this.textColor = builder.textColor;
+            this.backgroundColor = builder.backgroundColor;
+            this.multilineMargin = builder.multilineMargin;
+            this.textSize = builder.textSize;
+            this.typeface = builder.typeface;
+        }
+
+        public static class Builder {
+
+            int textColor;
+            int backgroundColor;
+            int multilineMargin;
+            int textSize;
+            Typeface typeface;
+
+            public Builder setTextColor(@ColorInt int textColor) {
+                this.textColor = textColor;
+                return this;
+            }
+
+            public Builder setBackgroundColor(@ColorInt int backgroundColor) {
+                this.backgroundColor = backgroundColor;
+                return this;
+            }
+
+            public Builder setMultilineMargin(int multilineMargin) {
+                this.multilineMargin = multilineMargin;
+                return this;
+            }
+
+            public Builder setTextSize(@IntRange(from = 0) int textSize) {
+                this.textSize = textSize;
+                return this;
+            }
+
+            public Builder setTypeface(@NonNull Typeface typeface) {
+                this.typeface = typeface;
+                return this;
+            }
+
+            public Config build() {
+                if (typeface == null) {
+                    typeface = Typeface.MONOSPACE;
+                }
+                return new Config(this);
+            }
+        }
+    }
+
+    private final Config config;
+    private final Rect rect = new Rect();
+    private final Paint paint = new Paint();
+
+    private final boolean multiline;
+
+    public CodeSpan(@NonNull Config config, boolean multiline) {
+        this.config = config;
+        this.multiline = multiline;
+
+        paint.setStyle(Paint.Style.FILL);
+        paint.setTypeface(config.typeface);
+    }
+
+    @Override
+    public void updateMeasureState(TextPaint p) {
+        apply(p);
+    }
+
+    @Override
+    public void updateDrawState(TextPaint ds) {
+        apply(ds);
+        if (!multiline) {
+            final int color;
+            if (config.backgroundColor == 0) {
+                color = applyAlpha(ds.getColor(), 25);
+            } else {
+                color = config.backgroundColor;
+            }
+            ds.bgColor = color;
+        }
+    }
+
+    private void apply(TextPaint p) {
+        p.setTypeface(config.typeface);
+        if (config.textSize > 0) {
+            p.setTextSize(config.textSize);
+        }
+        if (config.textColor != 0) {
+            p.setColor(config.textColor);
+        }
+    }
+
+    @Override
+    public int getLeadingMargin(boolean first) {
+        return multiline ? config.multilineMargin : 0;
+    }
+
+    @Override
+    public void drawLeadingMargin(Canvas c, Paint p, int x, int dir, int top, int baseline, int bottom, CharSequence text, int start, int end, boolean first, Layout layout) {
+
+        if (multiline) {
+
+            final int color;
+            if (config.backgroundColor == 0) {
+                color = applyAlpha(p.getColor(), 25);
+            } else {
+                color = config.backgroundColor;
+            }
+            paint.setColor(color);
+
+            rect.set(x, top, c.getWidth(), bottom);
+
+            c.drawRect(rect, paint);
+        }
+
+//        paint.setTextSize(p.getTextSize());
+//
+//        final int left = (int) (x + .5F);
+//
+//        final int right;
+//        if (multiline) {
+//            right = c.getWidth();
+//        } else {
+//            final int width = (config.paddingHorizontal * 2) + (int) (paint.measureText(text, start, end) + .5F);
+//            right = left + width;
+//        }
+//
+//        rect.set(left, top, right, bottom);
+//
+//        // okay, draw background first
+//        drawBackground(c);
+
+        // then, if any, draw borders
+//        drawBorders(c, this.start == start, this.end == end);
+
+//        final int color;
+//        if (config.textColor == 0) {
+//            color = p.getColor();
+//        } else {
+//            color = config.textColor;
+//        }
+//        paint.setColor(color);
+//
+//        // draw text
+//        // y center position
+//        final int b = bottom - ((bottom - top) / 2) - (int) ((paint.descent() + paint.ascent()) / 2);
+//        canvas.drawText(text, start, end, x + config.paddingHorizontal, b, paint);
+    }
+
+    private static int applyAlpha(int color, int alpha) {
+        return (color & 0x00FFFFFF) | (alpha << 24);
+    }
+
+
+//    @Override
+//    public int getSize(
+//            @NonNull Paint p,
+//            CharSequence text,
+//            @IntRange(from = 0) int start,
+//            @IntRange(from = 0) int end,
+//            @Nullable Paint.FontMetricsInt fm
+//    ) {
+//
+//        paint.setTextSize(p.getTextSize());
+//
+//        final int width = (config.paddingHorizontal * 2) + (int) (paint.measureText(text, start, end) + .5F);
+//
+//        if (fm != null) {
+//            // we add a padding top & bottom
+//            final float ratio = .62F; // golden ratio, there is no much point of moving this to config... it seems a bit `specific`...
+//            fm.ascent = fm.ascent - (config.paddingVertical);
+//            fm.descent = (int) (-fm.ascent * ratio);
+//            fm.top = fm.ascent;
+//            fm.bottom = fm.descent;
+//        }
+//
+//        return width;
+//    }
+
+//    @Override
+//    public void draw(
+//            @NonNull Canvas canvas,
+//            CharSequence text,
+//            @IntRange(from = 0) int start,
+//            @IntRange(from = 0) int end,
+//            float x,
+//            int top,
+//            int y,
+//            int bottom,
+//            @NonNull Paint p
+//    ) {
+//
+//        paint.setTextSize(p.getTextSize());
+//
+//        final int left = (int) (x + .5F);
+//
+//        final int right;
+//        if (multiline) {
+//            right = canvas.getWidth();
+//        } else {
+//            final int width = (config.paddingHorizontal * 2) + (int) (paint.measureText(text, start, end) + .5F);
+//            right = left + width;
+//        }
+//
+//        rect.set(left, top, right, bottom);
+//
+//        // okay, draw background first
+//        drawBackground(canvas);
+//
+//        // then, if any, draw borders
+//        drawBorders(canvas, this.start == start, this.end == end);
+//
+//        final int color;
+//        if (config.textColor == 0) {
+//            color = p.getColor();
+//        } else {
+//            color = config.textColor;
+//        }
+//        paint.setColor(color);
+//
+//        // draw text
+//        // y center position
+//        final int b = bottom - ((bottom - top) / 2) - (int) ((paint.descent() + paint.ascent()) / 2);
+//        canvas.drawText(text, start, end, x + config.paddingHorizontal, b, paint);
+//    }
+
+//    private void drawBackground(Canvas canvas) {
+//        final int color = config.backgroundColor;
+//        if (color != 0) {
+//            paint.setColor(color);
+//            canvas.drawRect(rect, paint);
+//        }
+//    }
+//
+//    private void drawBorders(Canvas canvas, boolean top, boolean bottom) {
+//
+//        final int color = config.borderColor;
+//        final int width = config.borderWidth;
+//        if (color == 0
+//                || width == 0) {
+//            return;
+//        }
+//
+//        paint.setColor(color);
+//
+//        // left and right are always drawn
+//
+//        // LEFT
+//        borderRect.set(rect.left, rect.top, rect.left + width, rect.bottom);
+//        canvas.drawRect(borderRect, paint);
+//
+//        // RIGHT
+//        borderRect.set(rect.right - width, rect.top, rect.right, rect.bottom);
+//        canvas.drawRect(borderRect, paint);
+//
+//        // TOP
+//        if (top) {
+//            borderRect.set(rect.left, rect.top, rect.right, rect.top + width);
+//            canvas.drawRect(borderRect, paint);
+//        }
+//
+//        // BOTTOM
+//        if (bottom) {
+//            borderRect.set(rect.left, rect.bottom - width, rect.right, rect.bottom);
+//            canvas.drawRect(borderRect, paint);
+//        }
+//    }
+}
diff --git a/app/src/main/java/ru/noties/markwon/spans/DrawableSpan.java b/library-spans/src/main/java/ru/noties/markwon/spans/DrawableSpan.java
similarity index 98%
rename from app/src/main/java/ru/noties/markwon/spans/DrawableSpan.java
rename to library-spans/src/main/java/ru/noties/markwon/spans/DrawableSpan.java
index 79043eb9..8c065099 100644
--- a/app/src/main/java/ru/noties/markwon/spans/DrawableSpan.java
+++ b/library-spans/src/main/java/ru/noties/markwon/spans/DrawableSpan.java
@@ -10,6 +10,7 @@ import android.support.annotation.NonNull;
 import android.support.annotation.Nullable;
 import android.text.style.ReplacementSpan;
 
+@SuppressWarnings("WeakerAccess")
 public class DrawableSpan extends ReplacementSpan {
 
     @IntDef({ ALIGN_BOTTOM, ALIGN_BASELINE, ALIGN_CENTER })
diff --git a/library-spans/src/main/java/ru/noties/markwon/spans/DrawableSpanUtils.java b/library-spans/src/main/java/ru/noties/markwon/spans/DrawableSpanUtils.java
new file mode 100644
index 00000000..d182ae6d
--- /dev/null
+++ b/library-spans/src/main/java/ru/noties/markwon/spans/DrawableSpanUtils.java
@@ -0,0 +1,117 @@
+package ru.noties.markwon.spans;
+
+import android.graphics.Rect;
+import android.graphics.drawable.Drawable;
+import android.os.SystemClock;
+import android.support.annotation.NonNull;
+import android.text.Spanned;
+import android.view.View;
+import android.widget.TextView;
+
+import java.util.ArrayList;
+import java.util.List;
+
+public class DrawableSpanUtils {
+
+    // todo, add `unschedule` method (to be used when new text is set, so
+    // drawables are removed from callbacks)
+
+    // this method is not completely valid because DynamicDrawableSpan stores
+    // a drawable in weakReference & it could easily be freed, thus we might need
+    // to re-schedule a new one, but we have no means to do it
+    public static void scheduleDrawables(@NonNull final TextView textView) {
+
+        final CharSequence cs = textView.getText();
+        final int length = cs != null
+                ? cs.length()
+                : 0;
+        if (length == 0 || !(cs instanceof Spanned)) {
+            return;
+        }
+
+        final Object[] spans = ((Spanned) cs).getSpans(0, length, Object.class);
+        if (spans != null
+                && spans.length > 0) {
+
+            final List<Drawable> list = new ArrayList<>(2);
+
+            for (Object span: spans) {
+                if (span instanceof DrawableSpan) {
+                    list.add(((DrawableSpan) span).getDrawable());
+                }
+            }
+
+            if (list.size() > 0) {
+                textView.addOnAttachStateChangeListener(new View.OnAttachStateChangeListener() {
+                    @Override
+                    public void onViewAttachedToWindow(View v) {
+                        // can it happen that the same view first detached & them attached with all previous content? hm..
+                        // no op for now
+                    }
+
+                    @Override
+                    public void onViewDetachedFromWindow(View v) {
+                        // remove callbacks...
+                        textView.removeOnAttachStateChangeListener(this);
+                        for (Drawable drawable: list) {
+                            drawable.setCallback(null);
+                        }
+                    }
+                });
+
+                for (Drawable drawable: list) {
+                    drawable.setCallback(new DrawableCallbackImpl(textView, drawable.getBounds()));
+                }
+            }
+        }
+    }
+
+    private DrawableSpanUtils() {}
+
+    private static class DrawableCallbackImpl implements Drawable.Callback {
+
+        private final TextView view;
+        private Rect previousBounds;
+
+        DrawableCallbackImpl(TextView view, Rect initialBounds) {
+            this.view = view;
+            this.previousBounds = new Rect(initialBounds);
+        }
+
+        @Override
+        public void invalidateDrawable(@NonNull Drawable who) {
+
+            // okay... teh 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...
+
+            final Rect rect = who.getBounds();
+
+            if (!previousBounds.equals(rect)) {
+                // the only method that seems to work when bounds have changed
+                view.setText(view.getText());
+                previousBounds = new Rect(rect);
+            } else {
+                // if bounds are the same then simple invalidate would do
+                final int scrollX = view.getScrollX();
+                final int scrollY = view.getScrollY();
+                view.postInvalidate(
+                        scrollX + rect.left,
+                        scrollY + rect.top,
+                        scrollX + rect.right,
+                        scrollY + rect.bottom
+                );
+            }
+        }
+
+        @Override
+        public void scheduleDrawable(@NonNull Drawable who, @NonNull Runnable what, long when) {
+            final long delay = when - SystemClock.uptimeMillis();
+            view.postDelayed(what, delay);
+        }
+
+        @Override
+        public void unscheduleDrawable(@NonNull Drawable who, @NonNull Runnable what) {
+            view.removeCallbacks(what);
+        }
+    }
+}
diff --git a/app/src/main/java/ru/noties/markwon/spans/EmphasisSpan.java b/library-spans/src/main/java/ru/noties/markwon/spans/EmphasisSpan.java
similarity index 100%
rename from app/src/main/java/ru/noties/markwon/spans/EmphasisSpan.java
rename to library-spans/src/main/java/ru/noties/markwon/spans/EmphasisSpan.java
diff --git a/library-spans/src/main/java/ru/noties/markwon/spans/HeadingSpan.java b/library-spans/src/main/java/ru/noties/markwon/spans/HeadingSpan.java
new file mode 100644
index 00000000..1ac74fb6
--- /dev/null
+++ b/library-spans/src/main/java/ru/noties/markwon/spans/HeadingSpan.java
@@ -0,0 +1,96 @@
+package ru.noties.markwon.spans;
+
+import android.graphics.Canvas;
+import android.graphics.Paint;
+import android.graphics.Rect;
+import android.support.annotation.ColorInt;
+import android.support.annotation.IntRange;
+import android.support.annotation.NonNull;
+import android.text.Layout;
+import android.text.TextPaint;
+import android.text.style.LeadingMarginSpan;
+import android.text.style.MetricAffectingSpan;
+
+public class HeadingSpan extends MetricAffectingSpan implements LeadingMarginSpan {
+
+    // taken from html spec (most browsers render headings like that)
+    private static final float[] HEADING_SIZES = {
+            2.F, 1.5F, 1.17F, 1.F, .83F, .67F,
+    };
+
+    public static class Config {
+
+        final int breakHeight; // by default stroke width
+        final int breakColor; // by default -> textColor
+
+        public Config(@IntRange(from = 0) int breakHeight, @ColorInt int breakColor) {
+            this.breakHeight = breakHeight;
+            this.breakColor = breakColor;
+        }
+    }
+
+    private final Config config;
+    private final Rect rect = new Rect();
+    private final Paint paint = new Paint();
+    private final int level;
+    private final int end;
+
+    public HeadingSpan(@NonNull Config config, @IntRange(from = 1, to = 6) int level, @IntRange(from = 0) int end) {
+        this.config = config;
+        this.level = level - 1;
+        this.end = end;
+
+        paint.setStyle(Paint.Style.FILL);
+    }
+
+    @Override
+    public void updateMeasureState(TextPaint p) {
+        apply(p);
+    }
+
+    @Override
+    public void updateDrawState(TextPaint tp) {
+        apply(tp);
+    }
+
+    private void apply(TextPaint paint) {
+        paint.setTextSize(paint.getTextSize() * HEADING_SIZES[level]);
+        paint.setFakeBoldText(true);
+    }
+
+    @Override
+    public int getLeadingMargin(boolean first) {
+        // no margin actually, but we need to access Canvas to draw break
+        return 0;
+    }
+
+    @Override
+    public void drawLeadingMargin(Canvas c, Paint p, int x, int dir, int top, int baseline, int bottom, CharSequence text, int start, int end, boolean first, Layout layout) {
+
+        // if we are configured to draw underlines, draw them here
+
+        if (level == 0
+                || level == 1) {
+
+            if (this.end == end) {
+
+                final int color;
+                final int breakHeight;
+                if (config.breakColor == 0) {
+                    color = p.getColor();
+                } else {
+                    color = config.breakColor;
+                }
+                if (config.breakHeight == 0) {
+                    breakHeight = (int) (p.getStrokeWidth() + .5F);
+                } else {
+                    breakHeight = config.breakHeight;
+                }
+                paint.setColor(color);
+
+                rect.set(x, bottom - breakHeight, c.getWidth(), bottom);
+                c.drawRect(rect, paint);
+            }
+        }
+    }
+}
diff --git a/app/src/main/java/ru/noties/markwon/spans/StrongEmphasisSpan.java b/library-spans/src/main/java/ru/noties/markwon/spans/StrongEmphasisSpan.java
similarity index 100%
rename from app/src/main/java/ru/noties/markwon/spans/StrongEmphasisSpan.java
rename to library-spans/src/main/java/ru/noties/markwon/spans/StrongEmphasisSpan.java
diff --git a/app/src/main/java/ru/noties/markwon/spans/SubSpan.java b/library-spans/src/main/java/ru/noties/markwon/spans/SubSpan.java
similarity index 100%
rename from app/src/main/java/ru/noties/markwon/spans/SubSpan.java
rename to library-spans/src/main/java/ru/noties/markwon/spans/SubSpan.java
diff --git a/app/src/main/java/ru/noties/markwon/spans/SupSpan.java b/library-spans/src/main/java/ru/noties/markwon/spans/SupSpan.java
similarity index 100%
rename from app/src/main/java/ru/noties/markwon/spans/SupSpan.java
rename to library-spans/src/main/java/ru/noties/markwon/spans/SupSpan.java
diff --git a/app/src/main/java/ru/noties/markwon/spans/ThematicBreakSpan.java b/library-spans/src/main/java/ru/noties/markwon/spans/ThematicBreakSpan.java
similarity index 100%
rename from app/src/main/java/ru/noties/markwon/spans/ThematicBreakSpan.java
rename to library-spans/src/main/java/ru/noties/markwon/spans/ThematicBreakSpan.java
diff --git a/library-spans/src/main/res/values/strings.xml b/library-spans/src/main/res/values/strings.xml
deleted file mode 100644
index b54e5f5a..00000000
--- a/library-spans/src/main/res/values/strings.xml
+++ /dev/null
@@ -1,3 +0,0 @@
-<resources>
-    <string name="app_name">Spans</string>
-</resources>
diff --git a/library-spans/src/test/java/ru/noties/markwon/spans/ExampleUnitTest.java b/library-spans/src/test/java/ru/noties/markwon/spans/ExampleUnitTest.java
deleted file mode 100644
index 12a283a9..00000000
--- a/library-spans/src/test/java/ru/noties/markwon/spans/ExampleUnitTest.java
+++ /dev/null
@@ -1,17 +0,0 @@
-package ru.noties.markwon.spans;
-
-import org.junit.Test;
-
-import static org.junit.Assert.*;
-
-/**
- * Example local unit test, which will execute on the development machine (host).
- *
- * @see <a href="http://d.android.com/tools/testing">Testing documentation</a>
- */
-public class ExampleUnitTest {
-    @Test
-    public void addition_isCorrect() throws Exception {
-        assertEquals(4, 2 + 2);
-    }
-}
\ No newline at end of file
diff --git a/library-view/build.gradle b/library-view/build.gradle
new file mode 100644
index 00000000..fa1bfd9d
--- /dev/null
+++ b/library-view/build.gradle
@@ -0,0 +1,19 @@
+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 SUPPORT_ANNOTATIONS
+}
\ No newline at end of file
diff --git a/library-view/src/main/AndroidManifest.xml b/library-view/src/main/AndroidManifest.xml
new file mode 100644
index 00000000..f763152f
--- /dev/null
+++ b/library-view/src/main/AndroidManifest.xml
@@ -0,0 +1 @@
+<manifest package="ru.noties.markwon.view"/>
diff --git a/library-view/src/main/java/ru/noties/markwon/view/MarkdownTextView.java b/library-view/src/main/java/ru/noties/markwon/view/MarkdownTextView.java
new file mode 100644
index 00000000..df8f77b5
--- /dev/null
+++ b/library-view/src/main/java/ru/noties/markwon/view/MarkdownTextView.java
@@ -0,0 +1,59 @@
+package ru.noties.markwon.view;
+
+import android.annotation.TargetApi;
+import android.content.Context;
+import android.os.Build;
+import android.support.annotation.Nullable;
+import android.util.AttributeSet;
+import android.widget.TextView;
+
+import org.commonmark.ext.gfm.strikethrough.StrikethroughExtension;
+import org.commonmark.node.Node;
+import org.commonmark.parser.Parser;
+
+import java.util.Collections;
+
+import ru.noties.markwon.renderer.SpannableConfiguration;
+import ru.noties.markwon.renderer.SpannableRenderer;
+
+public class MarkdownTextView extends TextView {
+
+    private Parser parser;
+    private SpannableRenderer renderer;
+    private SpannableConfiguration configuration;
+
+    public MarkdownTextView(Context context) {
+        super(context);
+    }
+
+    public MarkdownTextView(Context context, @Nullable AttributeSet attrs) {
+        super(context, attrs);
+    }
+
+    public MarkdownTextView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
+        super(context, attrs, defStyleAttr);
+    }
+
+    @TargetApi(Build.VERSION_CODES.LOLLIPOP)
+    public MarkdownTextView(Context context, @Nullable AttributeSet attrs, int defStyleAttr, int defStyleRes) {
+        super(context, attrs, defStyleAttr, defStyleRes);
+    }
+
+    @Override
+    public void setText(CharSequence text, BufferType type) {
+        if (parser == null) {
+            parser = Parser.builder()
+                    .extensions(Collections.singletonList(StrikethroughExtension.create()))
+                    .build();
+        }
+        if (renderer == null) {
+            renderer = new SpannableRenderer();
+        }
+        if (configuration == null) {
+            configuration = SpannableConfiguration.create(getContext());
+        }
+        final Node node = parser.parse(text.toString());
+        final CharSequence cs = renderer.render(configuration, node);
+        super.setText(cs, type);
+    }
+}
diff --git a/settings.gradle b/settings.gradle
index 8d8cfdd7..dce89cfc 100644
--- a/settings.gradle
+++ b/settings.gradle
@@ -1 +1 @@
-include ':app', ':library-spans'
+include ':app', ':library-spans', ':library-renderer', ':library-view'