Inline html handling (no images)

This commit is contained in:
Dimitry Ivanov 2017-05-16 18:48:46 +03:00
parent e0c10c658b
commit bf18b87420
10 changed files with 268 additions and 61 deletions

View File

@ -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();

View File

@ -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);
} }
} }

View File

@ -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(
length,
new AsyncDrawableSpan(
configuration.theme(), configuration.theme(),
new AsyncDrawable( new AsyncDrawable(
image.getDestination(), image.getDestination(),
configuration.asyncDrawableLoader() configuration.asyncDrawableLoader()
), ),
AsyncDrawableSpan.ALIGN_BOTTOM, AsyncDrawableSpan.ALIGN_BOTTOM,
link) 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));

View File

@ -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();
}
}

View File

@ -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();
}
}

View File

@ -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) {

View File

@ -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();
}
}

View File

@ -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);
}
}

View File

@ -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);
}
}

View File

@ -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();
}
}