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"; + private final SpannableConfiguration configuration; private final SpannableStringBuilder builder; + private final Deque 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 // , , 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 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 (``) + 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 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(); + } +}