From bf18b874204291d52638e0928cb5890a4dfd7931 Mon Sep 17 00:00:00 2001
From: Dimitry Ivanov <mail@dimitryivanov.ru>
Date: Tue, 16 May 2017 18:48:46 +0300
Subject: [PATCH] Inline html handling (no images)

---
 .../java/ru/noties/markwon/MainActivity.java  |  17 +--
 .../renderer/SpannableConfiguration.java      |  16 +++
 .../renderer/SpannableMarkdownVisitor.java    | 104 ++++++++++------
 .../markwon/renderer/html/BoldProvider.java   |  10 ++
 .../renderer/html/ItalicsProvider.java        |  10 ++
 .../{ => html}/SpannableHtmlParser.java       | 115 ++++++++++++++++--
 .../markwon/renderer/html/StrikeProvider.java |  10 ++
 .../renderer/html/SubScriptProvider.java      |  18 +++
 .../renderer/html/SuperScriptProvider.java    |  18 +++
 .../renderer/html/UnderlineProvider.java      |  11 ++
 10 files changed, 268 insertions(+), 61 deletions(-)
 create mode 100644 library-renderer/src/main/java/ru/noties/markwon/renderer/html/BoldProvider.java
 create mode 100644 library-renderer/src/main/java/ru/noties/markwon/renderer/html/ItalicsProvider.java
 rename library-renderer/src/main/java/ru/noties/markwon/renderer/{ => html}/SpannableHtmlParser.java (55%)
 create mode 100644 library-renderer/src/main/java/ru/noties/markwon/renderer/html/StrikeProvider.java
 create mode 100644 library-renderer/src/main/java/ru/noties/markwon/renderer/html/SubScriptProvider.java
 create mode 100644 library-renderer/src/main/java/ru/noties/markwon/renderer/html/SuperScriptProvider.java
 create mode 100644 library-renderer/src/main/java/ru/noties/markwon/renderer/html/UnderlineProvider.java

diff --git a/app/src/main/java/ru/noties/markwon/MainActivity.java b/app/src/main/java/ru/noties/markwon/MainActivity.java
index 8c22023a..d4b7cbad 100644
--- a/app/src/main/java/ru/noties/markwon/MainActivity.java
+++ b/app/src/main/java/ru/noties/markwon/MainActivity.java
@@ -47,20 +47,6 @@ public class MainActivity extends Activity {
 
         final TextView textView = (TextView) findViewById(R.id.activity_main);
 
-//        final Drawable drawable = getDrawable(R.mipmap.ic_launcher);
-////        drawable.setBounds(0, 0, 16, 16);
-//        final SpannableStringBuilder builder = new SpannableStringBuilder();
-//        for (int i = 0; i < 10; i++) {
-//            builder.append("text here and icon: \u00a0");
-//            //noinspection WrongConstant
-//            builder.setSpan(new AsyncDrawableSpan(drawable, i % 3), builder.length() - 1, builder.length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
-//            builder.append('\n');
-//        }
-//        textView.setText(builder);
-//
-//        if (true) {
-//            return;
-//        }
 
         final Picasso picasso = new Picasso.Builder(this)
                 .listener(new Picasso.Listener() {
@@ -78,7 +64,8 @@ public class MainActivity extends Activity {
                 Scanner scanner = null;
                 String md = null;
                 try {
-                    stream = getAssets().open("scrollable.md");
+//                    stream = getAssets().open("scrollable.md");
+                    stream = getAssets().open("test.md");
                     scanner = new Scanner(stream).useDelimiter("\\A");
                     if (scanner.hasNext()) {
                         md = scanner.next();
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
index 32652faa..24f8206e 100644
--- a/library-renderer/src/main/java/ru/noties/markwon/renderer/SpannableConfiguration.java
+++ b/library-renderer/src/main/java/ru/noties/markwon/renderer/SpannableConfiguration.java
@@ -3,6 +3,7 @@ package ru.noties.markwon.renderer;
 import android.content.Context;
 import android.support.annotation.NonNull;
 
+import ru.noties.markwon.renderer.html.SpannableHtmlParser;
 import ru.noties.markwon.spans.AsyncDrawable;
 import ru.noties.markwon.spans.LinkSpan;
 import ru.noties.markwon.spans.SpannableTheme;
@@ -22,12 +23,14 @@ public class SpannableConfiguration {
     private final AsyncDrawable.Loader asyncDrawableLoader;
     private final SyntaxHighlight syntaxHighlight;
     private final LinkSpan.Resolver linkResolver;
+    private final SpannableHtmlParser htmlParser;
 
     private SpannableConfiguration(Builder builder) {
         this.theme = builder.theme;
         this.asyncDrawableLoader = builder.asyncDrawableLoader;
         this.syntaxHighlight = builder.syntaxHighlight;
         this.linkResolver = builder.linkResolver;
+        this.htmlParser = builder.htmlParser;
     }
 
     public SpannableTheme theme() {
@@ -46,6 +49,10 @@ public class SpannableConfiguration {
         return linkResolver;
     }
 
+    public SpannableHtmlParser htmlParser() {
+        return htmlParser;
+    }
+
     public static class Builder {
 
         private final Context context;
@@ -53,6 +60,7 @@ public class SpannableConfiguration {
         private AsyncDrawable.Loader asyncDrawableLoader;
         private SyntaxHighlight syntaxHighlight;
         private LinkSpan.Resolver linkResolver;
+        private SpannableHtmlParser htmlParser;
 
         public Builder(Context context) {
             this.context = context;
@@ -78,6 +86,11 @@ public class SpannableConfiguration {
             return this;
         }
 
+        public Builder htmlParser(SpannableHtmlParser htmlParser) {
+            this.htmlParser = htmlParser;
+            return this;
+        }
+
         public SpannableConfiguration build() {
             if (theme == null) {
                 theme = SpannableTheme.create(context);
@@ -91,6 +104,9 @@ public class SpannableConfiguration {
             if (linkResolver == null) {
                 linkResolver = new LinkResolverDef();
             }
+            if (htmlParser == null) {
+                htmlParser = SpannableHtmlParser.create(theme);
+            }
             return new SpannableConfiguration(this);
         }
     }
diff --git a/library-renderer/src/main/java/ru/noties/markwon/renderer/SpannableMarkdownVisitor.java b/library-renderer/src/main/java/ru/noties/markwon/renderer/SpannableMarkdownVisitor.java
index d13fcbca..60c49bf7 100644
--- a/library-renderer/src/main/java/ru/noties/markwon/renderer/SpannableMarkdownVisitor.java
+++ b/library-renderer/src/main/java/ru/noties/markwon/renderer/SpannableMarkdownVisitor.java
@@ -4,7 +4,6 @@ import android.support.annotation.NonNull;
 import android.text.SpannableStringBuilder;
 import android.text.Spanned;
 import android.text.style.StrikethroughSpan;
-import android.text.style.URLSpan;
 
 import org.commonmark.ext.gfm.strikethrough.Strikethrough;
 import org.commonmark.node.AbstractVisitor;
@@ -17,6 +16,7 @@ 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.Link;
 import org.commonmark.node.ListBlock;
@@ -29,7 +29,11 @@ import org.commonmark.node.StrongEmphasis;
 import org.commonmark.node.Text;
 import org.commonmark.node.ThematicBreak;
 
+import java.util.ArrayDeque;
+import java.util.Deque;
+
 import ru.noties.debug.Debug;
+import ru.noties.markwon.renderer.html.SpannableHtmlParser;
 import ru.noties.markwon.spans.AsyncDrawable;
 import ru.noties.markwon.spans.AsyncDrawableSpan;
 import ru.noties.markwon.spans.BlockQuoteSpan;
@@ -42,10 +46,14 @@ import ru.noties.markwon.spans.OrderedListItemSpan;
 import ru.noties.markwon.spans.StrongEmphasisSpan;
 import ru.noties.markwon.spans.ThematicBreakSpan;
 
+// please do not reuse between different texts (due to the html handling)
 public class SpannableMarkdownVisitor extends AbstractVisitor {
 
+    private static final String HTML_CONTENT = "<%1$s>%2$s</%1$s>";
+
     private final SpannableConfiguration configuration;
     private final SpannableStringBuilder builder;
+    private final Deque<HtmlInlineItem> htmlInlineItems;
 
     private int blockQuoteIndent;
     private int listLevel;
@@ -56,17 +64,16 @@ public class SpannableMarkdownVisitor extends AbstractVisitor {
     ) {
         this.configuration = configuration;
         this.builder = builder;
+        this.htmlInlineItems = new ArrayDeque<>(2);
     }
 
     @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());
@@ -74,7 +81,6 @@ public class SpannableMarkdownVisitor extends AbstractVisitor {
 
     @Override
     public void visit(Emphasis emphasis) {
-//        Debug.i(emphasis);
         final int length = builder.length();
         visitChildren(emphasis);
         setSpan(length, new EmphasisSpan());
@@ -83,8 +89,6 @@ public class SpannableMarkdownVisitor extends AbstractVisitor {
     @Override
     public void visit(BlockQuote blockQuote) {
 
-//        Debug.i(blockQuote);
-
         newLine();
         if (blockQuoteIndent != 0) {
             builder.append('\n');
@@ -112,8 +116,6 @@ public class SpannableMarkdownVisitor extends AbstractVisitor {
     @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
@@ -163,7 +165,6 @@ public class SpannableMarkdownVisitor extends AbstractVisitor {
     }
 
     private void visitList(Node node) {
-//        Debug.i(node);
         newLine();
         visitChildren(node);
         newLine();
@@ -175,15 +176,11 @@ public class SpannableMarkdownVisitor extends AbstractVisitor {
     @Override
     public void visit(ListItem listItem) {
 
-//        Debug.i(listItem);
-
         final int length = builder.length();
 
         blockQuoteIndent += 1;
         listLevel += 1;
 
-        // todo, can be a bullet list & ordered list (with leading numbers... looks like we need to `draw` numbers...
-
         final Node parent = listItem.getParent();
         if (parent instanceof OrderedList) {
 
@@ -223,8 +220,6 @@ public class SpannableMarkdownVisitor extends AbstractVisitor {
     @Override
     public void visit(ThematicBreak thematicBreak) {
 
-//        Debug.i(thematicBreak);
-
         newLine();
 
         final int length = builder.length();
@@ -238,8 +233,6 @@ public class SpannableMarkdownVisitor extends AbstractVisitor {
     @Override
     public void visit(Heading heading) {
 
-//        Debug.i(heading);
-
         newLine();
 
         final int length = builder.length();
@@ -258,21 +251,16 @@ public class SpannableMarkdownVisitor extends AbstractVisitor {
 
     @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);
@@ -287,8 +275,6 @@ public class SpannableMarkdownVisitor extends AbstractVisitor {
 
         final boolean inTightList = isInTightList(paragraph);
 
-//        Debug.i(paragraph, inTightList, listLevel);
-
         if (!inTightList) {
             newLine();
         }
@@ -311,8 +297,6 @@ public class SpannableMarkdownVisitor extends AbstractVisitor {
 
         visitChildren(image);
 
-        // if image has no link, create it (to open in external app)
-
         // we must check if anything _was_ added, as we need at least one char to render
         if (length == builder.length()) {
             builder.append(' '); // breakable space
@@ -321,14 +305,17 @@ public class SpannableMarkdownVisitor extends AbstractVisitor {
         final Node parent = image.getParent();
         final boolean link = parent != null && parent instanceof Link;
 
-        setSpan(length, new AsyncDrawableSpan(
-                configuration.theme(),
-                new AsyncDrawable(
-                        image.getDestination(),
-                        configuration.asyncDrawableLoader()
-                ),
-                AsyncDrawableSpan.ALIGN_BOTTOM,
-                link)
+        setSpan(
+                length,
+                new AsyncDrawableSpan(
+                        configuration.theme(),
+                        new AsyncDrawable(
+                                image.getDestination(),
+                                configuration.asyncDrawableLoader()
+                        ),
+                        AsyncDrawableSpan.ALIGN_BOTTOM,
+                        link
+                )
         );
     }
 
@@ -339,6 +326,46 @@ public class SpannableMarkdownVisitor extends AbstractVisitor {
         super.visit(htmlBlock);
     }
 
+    @Override
+    public void visit(HtmlInline htmlInline) {
+        final SpannableHtmlParser htmlParser = configuration.htmlParser();
+        final SpannableHtmlParser.Tag tag = htmlParser.parseTag(htmlInline.getLiteral());
+        if (tag != null) {
+            if (tag.opening()) {
+                // push in stack
+                htmlInlineItems.push(new HtmlInlineItem(tag.name(), builder.length()));
+                visitChildren(htmlInline);
+            } else {
+                // pop last item
+                if (htmlInlineItems.size() > 0) {
+                    final HtmlInlineItem item = htmlInlineItems.pop();
+                    final int start = item.start;
+                    final Object span = htmlParser.handleTag(item.tag);
+                    if (span != null) {
+                        setSpan(start, span);
+                    } else {
+                        final String content = builder.subSequence(start, builder.length()).toString();
+                        final String html = String.format(HTML_CONTENT, item.tag, content);
+                        final Object[] spans = htmlParser.htmlSpans(html);
+                        final int length = spans != null
+                                ? spans.length
+                                : 0;
+                        for (int i = 0; i < length; i++) {
+                            setSpan(start, spans[i]);
+                        }
+                    }
+                } else {
+                    throw new IllegalStateException("Unexpected closing html tag: " + tag.name()
+                            + ", at position: " + builder.length());
+                }
+            }
+        } else {
+            // let's add what we have
+            builder.append(htmlInline.getLiteral());
+            visitChildren(htmlInline);
+        }
+    }
+
     @Override
     public void visit(Link link) {
         final int length = builder.length();
@@ -369,6 +396,15 @@ public class SpannableMarkdownVisitor extends AbstractVisitor {
         return false;
     }
 
+    private static class HtmlInlineItem {
+        final String tag;
+        final int start;
+        HtmlInlineItem(String tag, int start) {
+            this.tag = tag;
+            this.start = start;
+        }
+    }
+
 //    private static String dump(Node node) {
 //        final StringBuilder builder = new StringBuilder();
 //        node.accept(new DumpVisitor(builder));
diff --git a/library-renderer/src/main/java/ru/noties/markwon/renderer/html/BoldProvider.java b/library-renderer/src/main/java/ru/noties/markwon/renderer/html/BoldProvider.java
new file mode 100644
index 00000000..be89a777
--- /dev/null
+++ b/library-renderer/src/main/java/ru/noties/markwon/renderer/html/BoldProvider.java
@@ -0,0 +1,10 @@
+package ru.noties.markwon.renderer.html;
+
+import ru.noties.markwon.spans.StrongEmphasisSpan;
+
+class BoldProvider implements SpannableHtmlParser.SpanProvider {
+    @Override
+    public Object provide() {
+        return new StrongEmphasisSpan();
+    }
+}
diff --git a/library-renderer/src/main/java/ru/noties/markwon/renderer/html/ItalicsProvider.java b/library-renderer/src/main/java/ru/noties/markwon/renderer/html/ItalicsProvider.java
new file mode 100644
index 00000000..e4a47bae
--- /dev/null
+++ b/library-renderer/src/main/java/ru/noties/markwon/renderer/html/ItalicsProvider.java
@@ -0,0 +1,10 @@
+package ru.noties.markwon.renderer.html;
+
+import ru.noties.markwon.spans.EmphasisSpan;
+
+class ItalicsProvider implements SpannableHtmlParser.SpanProvider {
+    @Override
+    public Object provide() {
+        return new EmphasisSpan();
+    }
+}
diff --git a/library-renderer/src/main/java/ru/noties/markwon/renderer/SpannableHtmlParser.java b/library-renderer/src/main/java/ru/noties/markwon/renderer/html/SpannableHtmlParser.java
similarity index 55%
rename from library-renderer/src/main/java/ru/noties/markwon/renderer/SpannableHtmlParser.java
rename to library-renderer/src/main/java/ru/noties/markwon/renderer/html/SpannableHtmlParser.java
index afcb93b3..99286b52 100644
--- a/library-renderer/src/main/java/ru/noties/markwon/renderer/SpannableHtmlParser.java
+++ b/library-renderer/src/main/java/ru/noties/markwon/renderer/html/SpannableHtmlParser.java
@@ -1,4 +1,4 @@
-package ru.noties.markwon.renderer;
+package ru.noties.markwon.renderer.html;
 
 import android.annotation.TargetApi;
 import android.os.Build;
@@ -10,11 +10,44 @@ import android.text.Spanned;
 import java.util.HashMap;
 import java.util.Map;
 
+import ru.noties.markwon.spans.SpannableTheme;
+
 @SuppressWarnings("WeakerAccess")
 public class SpannableHtmlParser {
 
     // we need to handle images independently (in order to parse alt, width, height, etc)
 
+    // creates default parser
+    public static SpannableHtmlParser create(@NonNull SpannableTheme theme) {
+        return builderWithDefaults(theme)
+                .build();
+    }
+
+    public static Builder builder() {
+        return new Builder();
+    }
+
+    public static Builder builderWithDefaults(@NonNull SpannableTheme theme) {
+
+        final BoldProvider boldProvider = new BoldProvider();
+        final ItalicsProvider italicsProvider = new ItalicsProvider();
+        final StrikeProvider strikeProvider = new StrikeProvider();
+
+        return new Builder()
+                .customTag("b", boldProvider)
+                .customTag("strong", boldProvider)
+                .customTag("i", italicsProvider)
+                .customTag("em", italicsProvider)
+                .customTag("cite", italicsProvider)
+                .customTag("dfn", italicsProvider)
+                .customTag("sup", new SuperScriptProvider(theme))
+                .customTag("sub", new SubScriptProvider(theme))
+                .customTag("u", new UnderlineProvider())
+                .customTag("del", strikeProvider)
+                .customTag("s", strikeProvider)
+                .customTag("strike", strikeProvider);
+    }
+
     // for simple tags without arguments
     // <b>, <i>, etc
     public interface SpanProvider {
@@ -25,15 +58,6 @@ public class SpannableHtmlParser {
         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;
 
@@ -42,6 +66,47 @@ public class SpannableHtmlParser {
         this.parser = builder.parser;
     }
 
+    @Nullable
+    public Tag parseTag(String html) {
+
+        final Tag tag;
+
+        final int length = html != null
+                ? html.length()
+                : 0;
+
+        // absolutely minimum (`<i>`)
+        if (length < 3) {
+            tag = null;
+        } else {
+            final boolean closing = '<' == html.charAt(0) && '/' == html.charAt(1);
+            final String name = closing
+                    ? html.substring(2, length - 1)
+                    : html.substring(1, length - 1);
+            tag = new Tag(name, !closing);
+        }
+
+        return tag;
+    }
+
+    @Nullable
+    public Object handleTag(String tag) {
+        final Object out;
+        final SpanProvider provider = customTags.get(tag);
+        if (provider != null) {
+            out = provider.provide();
+        } else {
+            out = null;
+        }
+        return out;
+    }
+
+    @Nullable
+    public Object[] htmlSpans(String html) {
+        // todo, additional handling of: image & link
+        return parser.getSpans(html);
+    }
+
     public static class Builder {
 
         private final Map<String, SpanProvider> customTags = new HashMap<>(3);
@@ -52,20 +117,46 @@ public class SpannableHtmlParser {
             return this;
         }
 
-        public Builder setParser(@NonNull HtmlParser parser) {
+        public Builder parser(@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 class Tag {
+
+        private final String name;
+        private final boolean opening;
+
+        public Tag(String name, boolean opening) {
+            this.name = name;
+            this.opening = opening;
+        }
+
+        public String name() {
+            return name;
+        }
+
+        public boolean opening() {
+            return opening;
+        }
+
+        @Override
+        public String toString() {
+            return "Tag{" +
+                    "name='" + name + '\'' +
+                    ", opening=" + opening +
+                    '}';
+        }
+    }
+
     public static abstract class DefaultHtmlParser implements HtmlParser {
 
         public static DefaultHtmlParser create(@Nullable Html.ImageGetter imageGetter, @Nullable Html.TagHandler tagHandler) {
diff --git a/library-renderer/src/main/java/ru/noties/markwon/renderer/html/StrikeProvider.java b/library-renderer/src/main/java/ru/noties/markwon/renderer/html/StrikeProvider.java
new file mode 100644
index 00000000..d4690440
--- /dev/null
+++ b/library-renderer/src/main/java/ru/noties/markwon/renderer/html/StrikeProvider.java
@@ -0,0 +1,10 @@
+package ru.noties.markwon.renderer.html;
+
+import android.text.style.StrikethroughSpan;
+
+class StrikeProvider implements SpannableHtmlParser.SpanProvider {
+    @Override
+    public Object provide() {
+        return new StrikethroughSpan();
+    }
+}
diff --git a/library-renderer/src/main/java/ru/noties/markwon/renderer/html/SubScriptProvider.java b/library-renderer/src/main/java/ru/noties/markwon/renderer/html/SubScriptProvider.java
new file mode 100644
index 00000000..13299404
--- /dev/null
+++ b/library-renderer/src/main/java/ru/noties/markwon/renderer/html/SubScriptProvider.java
@@ -0,0 +1,18 @@
+package ru.noties.markwon.renderer.html;
+
+import ru.noties.markwon.spans.SpannableTheme;
+import ru.noties.markwon.spans.SubScriptSpan;
+
+class SubScriptProvider implements SpannableHtmlParser.SpanProvider {
+
+    private final SpannableTheme theme;
+
+    public SubScriptProvider(SpannableTheme theme) {
+        this.theme = theme;
+    }
+
+    @Override
+    public Object provide() {
+        return new SubScriptSpan(theme);
+    }
+}
diff --git a/library-renderer/src/main/java/ru/noties/markwon/renderer/html/SuperScriptProvider.java b/library-renderer/src/main/java/ru/noties/markwon/renderer/html/SuperScriptProvider.java
new file mode 100644
index 00000000..91202571
--- /dev/null
+++ b/library-renderer/src/main/java/ru/noties/markwon/renderer/html/SuperScriptProvider.java
@@ -0,0 +1,18 @@
+package ru.noties.markwon.renderer.html;
+
+import ru.noties.markwon.spans.SpannableTheme;
+import ru.noties.markwon.spans.SuperScriptSpan;
+
+class SuperScriptProvider implements SpannableHtmlParser.SpanProvider {
+
+    private final SpannableTheme theme;
+
+    SuperScriptProvider(SpannableTheme theme) {
+        this.theme = theme;
+    }
+
+    @Override
+    public Object provide() {
+        return new SuperScriptSpan(theme);
+    }
+}
diff --git a/library-renderer/src/main/java/ru/noties/markwon/renderer/html/UnderlineProvider.java b/library-renderer/src/main/java/ru/noties/markwon/renderer/html/UnderlineProvider.java
new file mode 100644
index 00000000..1d917471
--- /dev/null
+++ b/library-renderer/src/main/java/ru/noties/markwon/renderer/html/UnderlineProvider.java
@@ -0,0 +1,11 @@
+package ru.noties.markwon.renderer.html;
+
+import android.text.style.UnderlineSpan;
+
+class UnderlineProvider implements SpannableHtmlParser.SpanProvider {
+
+    @Override
+    public Object provide() {
+        return new UnderlineSpan();
+    }
+}