Inline html handling (no images)
This commit is contained in:
		
							parent
							
								
									e0c10c658b
								
							
						
					
					
						commit
						bf18b87420
					
				| @ -47,20 +47,6 @@ public class MainActivity extends Activity { | |||||||
| 
 | 
 | ||||||
|         final TextView textView = (TextView) findViewById(R.id.activity_main); |         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) |         final Picasso picasso = new Picasso.Builder(this) | ||||||
|                 .listener(new Picasso.Listener() { |                 .listener(new Picasso.Listener() { | ||||||
| @ -78,7 +64,8 @@ public class MainActivity extends Activity { | |||||||
|                 Scanner scanner = null; |                 Scanner scanner = null; | ||||||
|                 String md = null; |                 String md = null; | ||||||
|                 try { |                 try { | ||||||
|                     stream = getAssets().open("scrollable.md"); | //                    stream = getAssets().open("scrollable.md"); | ||||||
|  |                     stream = getAssets().open("test.md"); | ||||||
|                     scanner = new Scanner(stream).useDelimiter("\\A"); |                     scanner = new Scanner(stream).useDelimiter("\\A"); | ||||||
|                     if (scanner.hasNext()) { |                     if (scanner.hasNext()) { | ||||||
|                         md = scanner.next(); |                         md = scanner.next(); | ||||||
|  | |||||||
| @ -3,6 +3,7 @@ package ru.noties.markwon.renderer; | |||||||
| import android.content.Context; | import android.content.Context; | ||||||
| import android.support.annotation.NonNull; | import android.support.annotation.NonNull; | ||||||
| 
 | 
 | ||||||
|  | import ru.noties.markwon.renderer.html.SpannableHtmlParser; | ||||||
| import ru.noties.markwon.spans.AsyncDrawable; | import ru.noties.markwon.spans.AsyncDrawable; | ||||||
| import ru.noties.markwon.spans.LinkSpan; | import ru.noties.markwon.spans.LinkSpan; | ||||||
| import ru.noties.markwon.spans.SpannableTheme; | import ru.noties.markwon.spans.SpannableTheme; | ||||||
| @ -22,12 +23,14 @@ public class SpannableConfiguration { | |||||||
|     private final AsyncDrawable.Loader asyncDrawableLoader; |     private final AsyncDrawable.Loader asyncDrawableLoader; | ||||||
|     private final SyntaxHighlight syntaxHighlight; |     private final SyntaxHighlight syntaxHighlight; | ||||||
|     private final LinkSpan.Resolver linkResolver; |     private final LinkSpan.Resolver linkResolver; | ||||||
|  |     private final SpannableHtmlParser htmlParser; | ||||||
| 
 | 
 | ||||||
|     private SpannableConfiguration(Builder builder) { |     private SpannableConfiguration(Builder builder) { | ||||||
|         this.theme = builder.theme; |         this.theme = builder.theme; | ||||||
|         this.asyncDrawableLoader = builder.asyncDrawableLoader; |         this.asyncDrawableLoader = builder.asyncDrawableLoader; | ||||||
|         this.syntaxHighlight = builder.syntaxHighlight; |         this.syntaxHighlight = builder.syntaxHighlight; | ||||||
|         this.linkResolver = builder.linkResolver; |         this.linkResolver = builder.linkResolver; | ||||||
|  |         this.htmlParser = builder.htmlParser; | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     public SpannableTheme theme() { |     public SpannableTheme theme() { | ||||||
| @ -46,6 +49,10 @@ public class SpannableConfiguration { | |||||||
|         return linkResolver; |         return linkResolver; | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|  |     public SpannableHtmlParser htmlParser() { | ||||||
|  |         return htmlParser; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|     public static class Builder { |     public static class Builder { | ||||||
| 
 | 
 | ||||||
|         private final Context context; |         private final Context context; | ||||||
| @ -53,6 +60,7 @@ public class SpannableConfiguration { | |||||||
|         private AsyncDrawable.Loader asyncDrawableLoader; |         private AsyncDrawable.Loader asyncDrawableLoader; | ||||||
|         private SyntaxHighlight syntaxHighlight; |         private SyntaxHighlight syntaxHighlight; | ||||||
|         private LinkSpan.Resolver linkResolver; |         private LinkSpan.Resolver linkResolver; | ||||||
|  |         private SpannableHtmlParser htmlParser; | ||||||
| 
 | 
 | ||||||
|         public Builder(Context context) { |         public Builder(Context context) { | ||||||
|             this.context = context; |             this.context = context; | ||||||
| @ -78,6 +86,11 @@ public class SpannableConfiguration { | |||||||
|             return this; |             return this; | ||||||
|         } |         } | ||||||
| 
 | 
 | ||||||
|  |         public Builder htmlParser(SpannableHtmlParser htmlParser) { | ||||||
|  |             this.htmlParser = htmlParser; | ||||||
|  |             return this; | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|         public SpannableConfiguration build() { |         public SpannableConfiguration build() { | ||||||
|             if (theme == null) { |             if (theme == null) { | ||||||
|                 theme = SpannableTheme.create(context); |                 theme = SpannableTheme.create(context); | ||||||
| @ -91,6 +104,9 @@ public class SpannableConfiguration { | |||||||
|             if (linkResolver == null) { |             if (linkResolver == null) { | ||||||
|                 linkResolver = new LinkResolverDef(); |                 linkResolver = new LinkResolverDef(); | ||||||
|             } |             } | ||||||
|  |             if (htmlParser == null) { | ||||||
|  |                 htmlParser = SpannableHtmlParser.create(theme); | ||||||
|  |             } | ||||||
|             return new SpannableConfiguration(this); |             return new SpannableConfiguration(this); | ||||||
|         } |         } | ||||||
|     } |     } | ||||||
|  | |||||||
| @ -4,7 +4,6 @@ import android.support.annotation.NonNull; | |||||||
| import android.text.SpannableStringBuilder; | import android.text.SpannableStringBuilder; | ||||||
| import android.text.Spanned; | import android.text.Spanned; | ||||||
| import android.text.style.StrikethroughSpan; | import android.text.style.StrikethroughSpan; | ||||||
| import android.text.style.URLSpan; |  | ||||||
| 
 | 
 | ||||||
| import org.commonmark.ext.gfm.strikethrough.Strikethrough; | import org.commonmark.ext.gfm.strikethrough.Strikethrough; | ||||||
| import org.commonmark.node.AbstractVisitor; | import org.commonmark.node.AbstractVisitor; | ||||||
| @ -17,6 +16,7 @@ import org.commonmark.node.FencedCodeBlock; | |||||||
| import org.commonmark.node.HardLineBreak; | import org.commonmark.node.HardLineBreak; | ||||||
| import org.commonmark.node.Heading; | import org.commonmark.node.Heading; | ||||||
| import org.commonmark.node.HtmlBlock; | import org.commonmark.node.HtmlBlock; | ||||||
|  | import org.commonmark.node.HtmlInline; | ||||||
| import org.commonmark.node.Image; | import org.commonmark.node.Image; | ||||||
| import org.commonmark.node.Link; | import org.commonmark.node.Link; | ||||||
| import org.commonmark.node.ListBlock; | import org.commonmark.node.ListBlock; | ||||||
| @ -29,7 +29,11 @@ import org.commonmark.node.StrongEmphasis; | |||||||
| import org.commonmark.node.Text; | import org.commonmark.node.Text; | ||||||
| import org.commonmark.node.ThematicBreak; | import org.commonmark.node.ThematicBreak; | ||||||
| 
 | 
 | ||||||
|  | import java.util.ArrayDeque; | ||||||
|  | import java.util.Deque; | ||||||
|  | 
 | ||||||
| import ru.noties.debug.Debug; | import ru.noties.debug.Debug; | ||||||
|  | import ru.noties.markwon.renderer.html.SpannableHtmlParser; | ||||||
| import ru.noties.markwon.spans.AsyncDrawable; | import ru.noties.markwon.spans.AsyncDrawable; | ||||||
| import ru.noties.markwon.spans.AsyncDrawableSpan; | import ru.noties.markwon.spans.AsyncDrawableSpan; | ||||||
| import ru.noties.markwon.spans.BlockQuoteSpan; | 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.StrongEmphasisSpan; | ||||||
| import ru.noties.markwon.spans.ThematicBreakSpan; | import ru.noties.markwon.spans.ThematicBreakSpan; | ||||||
| 
 | 
 | ||||||
|  | // please do not reuse between different texts (due to the html handling) | ||||||
| public class SpannableMarkdownVisitor extends AbstractVisitor { | public class SpannableMarkdownVisitor extends AbstractVisitor { | ||||||
| 
 | 
 | ||||||
|  |     private static final String HTML_CONTENT = "<%1$s>%2$s</%1$s>"; | ||||||
|  | 
 | ||||||
|     private final SpannableConfiguration configuration; |     private final SpannableConfiguration configuration; | ||||||
|     private final SpannableStringBuilder builder; |     private final SpannableStringBuilder builder; | ||||||
|  |     private final Deque<HtmlInlineItem> htmlInlineItems; | ||||||
| 
 | 
 | ||||||
|     private int blockQuoteIndent; |     private int blockQuoteIndent; | ||||||
|     private int listLevel; |     private int listLevel; | ||||||
| @ -56,17 +64,16 @@ public class SpannableMarkdownVisitor extends AbstractVisitor { | |||||||
|     ) { |     ) { | ||||||
|         this.configuration = configuration; |         this.configuration = configuration; | ||||||
|         this.builder = builder; |         this.builder = builder; | ||||||
|  |         this.htmlInlineItems = new ArrayDeque<>(2); | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     @Override |     @Override | ||||||
|     public void visit(Text text) { |     public void visit(Text text) { | ||||||
| //        Debug.i(text); |  | ||||||
|         builder.append(text.getLiteral()); |         builder.append(text.getLiteral()); | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     @Override |     @Override | ||||||
|     public void visit(StrongEmphasis strongEmphasis) { |     public void visit(StrongEmphasis strongEmphasis) { | ||||||
| //        Debug.i(strongEmphasis); |  | ||||||
|         final int length = builder.length(); |         final int length = builder.length(); | ||||||
|         visitChildren(strongEmphasis); |         visitChildren(strongEmphasis); | ||||||
|         setSpan(length, new StrongEmphasisSpan()); |         setSpan(length, new StrongEmphasisSpan()); | ||||||
| @ -74,7 +81,6 @@ public class SpannableMarkdownVisitor extends AbstractVisitor { | |||||||
| 
 | 
 | ||||||
|     @Override |     @Override | ||||||
|     public void visit(Emphasis emphasis) { |     public void visit(Emphasis emphasis) { | ||||||
| //        Debug.i(emphasis); |  | ||||||
|         final int length = builder.length(); |         final int length = builder.length(); | ||||||
|         visitChildren(emphasis); |         visitChildren(emphasis); | ||||||
|         setSpan(length, new EmphasisSpan()); |         setSpan(length, new EmphasisSpan()); | ||||||
| @ -83,8 +89,6 @@ public class SpannableMarkdownVisitor extends AbstractVisitor { | |||||||
|     @Override |     @Override | ||||||
|     public void visit(BlockQuote blockQuote) { |     public void visit(BlockQuote blockQuote) { | ||||||
| 
 | 
 | ||||||
| //        Debug.i(blockQuote); |  | ||||||
| 
 |  | ||||||
|         newLine(); |         newLine(); | ||||||
|         if (blockQuoteIndent != 0) { |         if (blockQuoteIndent != 0) { | ||||||
|             builder.append('\n'); |             builder.append('\n'); | ||||||
| @ -112,8 +116,6 @@ public class SpannableMarkdownVisitor extends AbstractVisitor { | |||||||
|     @Override |     @Override | ||||||
|     public void visit(Code code) { |     public void visit(Code code) { | ||||||
| 
 | 
 | ||||||
| //        Debug.i(code); |  | ||||||
| 
 |  | ||||||
|         final int length = builder.length(); |         final int length = builder.length(); | ||||||
| 
 | 
 | ||||||
|         // NB, in order to provide a _padding_ feeling code is wrapped inside two unbreakable spaces |         // 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) { |     private void visitList(Node node) { | ||||||
| //        Debug.i(node); |  | ||||||
|         newLine(); |         newLine(); | ||||||
|         visitChildren(node); |         visitChildren(node); | ||||||
|         newLine(); |         newLine(); | ||||||
| @ -175,15 +176,11 @@ public class SpannableMarkdownVisitor extends AbstractVisitor { | |||||||
|     @Override |     @Override | ||||||
|     public void visit(ListItem listItem) { |     public void visit(ListItem listItem) { | ||||||
| 
 | 
 | ||||||
| //        Debug.i(listItem); |  | ||||||
| 
 |  | ||||||
|         final int length = builder.length(); |         final int length = builder.length(); | ||||||
| 
 | 
 | ||||||
|         blockQuoteIndent += 1; |         blockQuoteIndent += 1; | ||||||
|         listLevel += 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(); |         final Node parent = listItem.getParent(); | ||||||
|         if (parent instanceof OrderedList) { |         if (parent instanceof OrderedList) { | ||||||
| 
 | 
 | ||||||
| @ -223,8 +220,6 @@ public class SpannableMarkdownVisitor extends AbstractVisitor { | |||||||
|     @Override |     @Override | ||||||
|     public void visit(ThematicBreak thematicBreak) { |     public void visit(ThematicBreak thematicBreak) { | ||||||
| 
 | 
 | ||||||
| //        Debug.i(thematicBreak); |  | ||||||
| 
 |  | ||||||
|         newLine(); |         newLine(); | ||||||
| 
 | 
 | ||||||
|         final int length = builder.length(); |         final int length = builder.length(); | ||||||
| @ -238,8 +233,6 @@ public class SpannableMarkdownVisitor extends AbstractVisitor { | |||||||
|     @Override |     @Override | ||||||
|     public void visit(Heading heading) { |     public void visit(Heading heading) { | ||||||
| 
 | 
 | ||||||
| //        Debug.i(heading); |  | ||||||
| 
 |  | ||||||
|         newLine(); |         newLine(); | ||||||
| 
 | 
 | ||||||
|         final int length = builder.length(); |         final int length = builder.length(); | ||||||
| @ -258,21 +251,16 @@ public class SpannableMarkdownVisitor extends AbstractVisitor { | |||||||
| 
 | 
 | ||||||
|     @Override |     @Override | ||||||
|     public void visit(SoftLineBreak softLineBreak) { |     public void visit(SoftLineBreak softLineBreak) { | ||||||
|         Debug.i(softLineBreak); |  | ||||||
|         newLine(); |         newLine(); | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     @Override |     @Override | ||||||
|     public void visit(HardLineBreak hardLineBreak) { |     public void visit(HardLineBreak hardLineBreak) { | ||||||
|         Debug.i(hardLineBreak); |  | ||||||
|         newLine(); |         newLine(); | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     @Override |     @Override | ||||||
|     public void visit(CustomNode customNode) { |     public void visit(CustomNode customNode) { | ||||||
| 
 |  | ||||||
| //        Debug.i(customNode); |  | ||||||
| 
 |  | ||||||
|         if (customNode instanceof Strikethrough) { |         if (customNode instanceof Strikethrough) { | ||||||
|             final int length = builder.length(); |             final int length = builder.length(); | ||||||
|             visitChildren(customNode); |             visitChildren(customNode); | ||||||
| @ -287,8 +275,6 @@ public class SpannableMarkdownVisitor extends AbstractVisitor { | |||||||
| 
 | 
 | ||||||
|         final boolean inTightList = isInTightList(paragraph); |         final boolean inTightList = isInTightList(paragraph); | ||||||
| 
 | 
 | ||||||
| //        Debug.i(paragraph, inTightList, listLevel); |  | ||||||
| 
 |  | ||||||
|         if (!inTightList) { |         if (!inTightList) { | ||||||
|             newLine(); |             newLine(); | ||||||
|         } |         } | ||||||
| @ -311,8 +297,6 @@ public class SpannableMarkdownVisitor extends AbstractVisitor { | |||||||
| 
 | 
 | ||||||
|         visitChildren(image); |         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 |         // we must check if anything _was_ added, as we need at least one char to render | ||||||
|         if (length == builder.length()) { |         if (length == builder.length()) { | ||||||
|             builder.append(' '); // breakable space |             builder.append(' '); // breakable space | ||||||
| @ -321,14 +305,17 @@ public class SpannableMarkdownVisitor extends AbstractVisitor { | |||||||
|         final Node parent = image.getParent(); |         final Node parent = image.getParent(); | ||||||
|         final boolean link = parent != null && parent instanceof Link; |         final boolean link = parent != null && parent instanceof Link; | ||||||
| 
 | 
 | ||||||
|         setSpan(length, new AsyncDrawableSpan( |         setSpan( | ||||||
|                 configuration.theme(), |                 length, | ||||||
|                 new AsyncDrawable( |                 new AsyncDrawableSpan( | ||||||
|                         image.getDestination(), |                         configuration.theme(), | ||||||
|                         configuration.asyncDrawableLoader() |                         new AsyncDrawable( | ||||||
|                 ), |                                 image.getDestination(), | ||||||
|                 AsyncDrawableSpan.ALIGN_BOTTOM, |                                 configuration.asyncDrawableLoader() | ||||||
|                 link) |                         ), | ||||||
|  |                         AsyncDrawableSpan.ALIGN_BOTTOM, | ||||||
|  |                         link | ||||||
|  |                 ) | ||||||
|         ); |         ); | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
| @ -339,6 +326,46 @@ public class SpannableMarkdownVisitor extends AbstractVisitor { | |||||||
|         super.visit(htmlBlock); |         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 |     @Override | ||||||
|     public void visit(Link link) { |     public void visit(Link link) { | ||||||
|         final int length = builder.length(); |         final int length = builder.length(); | ||||||
| @ -369,6 +396,15 @@ public class SpannableMarkdownVisitor extends AbstractVisitor { | |||||||
|         return false; |         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) { | //    private static String dump(Node node) { | ||||||
| //        final StringBuilder builder = new StringBuilder(); | //        final StringBuilder builder = new StringBuilder(); | ||||||
| //        node.accept(new DumpVisitor(builder)); | //        node.accept(new DumpVisitor(builder)); | ||||||
|  | |||||||
| @ -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(); | ||||||
|  |     } | ||||||
|  | } | ||||||
| @ -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(); | ||||||
|  |     } | ||||||
|  | } | ||||||
| @ -1,4 +1,4 @@ | |||||||
| package ru.noties.markwon.renderer; | package ru.noties.markwon.renderer.html; | ||||||
| 
 | 
 | ||||||
| import android.annotation.TargetApi; | import android.annotation.TargetApi; | ||||||
| import android.os.Build; | import android.os.Build; | ||||||
| @ -10,11 +10,44 @@ import android.text.Spanned; | |||||||
| import java.util.HashMap; | import java.util.HashMap; | ||||||
| import java.util.Map; | import java.util.Map; | ||||||
| 
 | 
 | ||||||
|  | import ru.noties.markwon.spans.SpannableTheme; | ||||||
|  | 
 | ||||||
| @SuppressWarnings("WeakerAccess") | @SuppressWarnings("WeakerAccess") | ||||||
| public class SpannableHtmlParser { | public class SpannableHtmlParser { | ||||||
| 
 | 
 | ||||||
|     // we need to handle images independently (in order to parse alt, width, height, etc) |     // 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 |     // for simple tags without arguments | ||||||
|     // <b>, <i>, etc |     // <b>, <i>, etc | ||||||
|     public interface SpanProvider { |     public interface SpanProvider { | ||||||
| @ -25,15 +58,6 @@ public class SpannableHtmlParser { | |||||||
|         Object[] getSpans(@NonNull String html); |         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 Map<String, SpanProvider> customTags; | ||||||
|     private final HtmlParser parser; |     private final HtmlParser parser; | ||||||
| 
 | 
 | ||||||
| @ -42,6 +66,47 @@ public class SpannableHtmlParser { | |||||||
|         this.parser = builder.parser; |         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 { |     public static class Builder { | ||||||
| 
 | 
 | ||||||
|         private final Map<String, SpanProvider> customTags = new HashMap<>(3); |         private final Map<String, SpanProvider> customTags = new HashMap<>(3); | ||||||
| @ -52,20 +117,46 @@ public class SpannableHtmlParser { | |||||||
|             return this; |             return this; | ||||||
|         } |         } | ||||||
| 
 | 
 | ||||||
|         public Builder setParser(@NonNull HtmlParser parser) { |         public Builder parser(@NonNull HtmlParser parser) { | ||||||
|             this.parser = parser; |             this.parser = parser; | ||||||
|             return this; |             return this; | ||||||
|         } |         } | ||||||
| 
 | 
 | ||||||
|         public SpannableHtmlParser build() { |         public SpannableHtmlParser build() { | ||||||
|             if (parser == null) { |             if (parser == null) { | ||||||
|                 // todo, images.... |  | ||||||
|                 parser = DefaultHtmlParser.create(null, null); |                 parser = DefaultHtmlParser.create(null, null); | ||||||
|             } |             } | ||||||
|             return new SpannableHtmlParser(this); |             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 abstract class DefaultHtmlParser implements HtmlParser { | ||||||
| 
 | 
 | ||||||
|         public static DefaultHtmlParser create(@Nullable Html.ImageGetter imageGetter, @Nullable Html.TagHandler tagHandler) { |         public static DefaultHtmlParser create(@Nullable Html.ImageGetter imageGetter, @Nullable Html.TagHandler tagHandler) { | ||||||
| @ -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(); | ||||||
|  |     } | ||||||
|  | } | ||||||
| @ -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); | ||||||
|  |     } | ||||||
|  | } | ||||||
| @ -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); | ||||||
|  |     } | ||||||
|  | } | ||||||
| @ -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(); | ||||||
|  |     } | ||||||
|  | } | ||||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user
	 Dimitry Ivanov
						Dimitry Ivanov