diff --git a/library/src/main/java/ru/noties/markwon/SpanFactoryDef.java b/library/src/main/java/ru/noties/markwon/SpanFactoryDef.java new file mode 100644 index 00000000..ba4a185a --- /dev/null +++ b/library/src/main/java/ru/noties/markwon/SpanFactoryDef.java @@ -0,0 +1,127 @@ +package ru.noties.markwon; + +import android.support.annotation.NonNull; +import android.text.style.StrikethroughSpan; + +import ru.noties.markwon.renderer.ImageSizeResolver; +import ru.noties.markwon.spans.AsyncDrawable; +import ru.noties.markwon.spans.AsyncDrawableSpan; +import ru.noties.markwon.spans.BlockQuoteSpan; +import ru.noties.markwon.spans.BulletListItemSpan; +import ru.noties.markwon.spans.CodeSpan; +import ru.noties.markwon.spans.EmphasisSpan; +import ru.noties.markwon.spans.HeadingSpan; +import ru.noties.markwon.spans.LinkSpan; +import ru.noties.markwon.spans.OrderedListItemSpan; +import ru.noties.markwon.spans.SpanFactory; +import ru.noties.markwon.spans.SpannableTheme; +import ru.noties.markwon.spans.StrongEmphasisSpan; +import ru.noties.markwon.spans.TaskListSpan; +import ru.noties.markwon.spans.ThematicBreakSpan; + +public class SpanFactoryDef implements SpanFactory { + @NonNull + private final SpannableTheme theme; + + @NonNull + private final LinkSpan.Resolver linkResolver; + + @NonNull + private final AsyncDrawable.Loader drawableLoader; + + @NonNull + private final ImageSizeResolver imageSizeResolver; + + public SpanFactoryDef(@NonNull SpannableTheme theme, + @NonNull LinkSpan.Resolver linkResolver, + @NonNull AsyncDrawable.Loader drawableLoader, + @NonNull ImageSizeResolver imageSizeResolver) { + this.theme = theme; + this.linkResolver = linkResolver; + this.drawableLoader = drawableLoader; + this.imageSizeResolver = imageSizeResolver; + } + + @NonNull + @Override + public Object createBlockQuote() { + return new BlockQuoteSpan(theme); + } + + @NonNull + @Override + public Object createBulletListItem(int level) { + return new BulletListItemSpan(theme, level); + } + + @NonNull + @Override + public Object createCode(boolean multiline) { + return new CodeSpan(theme, multiline); + } + + @NonNull + @Override + public Object createEmphasis() { + return new EmphasisSpan(); + } + + @NonNull + @Override + public Object createHeading(int level) { + return new HeadingSpan(theme, level); + } + + @NonNull + @Override + public Object createImage(@NonNull String destination, boolean link) { + return new AsyncDrawableSpan( + theme, + new AsyncDrawable( + destination, + drawableLoader, + imageSizeResolver, + null + ), + AsyncDrawableSpan.ALIGN_BOTTOM, + link + ); + } + + @NonNull + @Override + public Object createLink(@NonNull String destination) { + return new LinkSpan(theme, destination, linkResolver); + } + + @NonNull + @Override + public Object createOrderedListItem(int order) { + // todo| in order to provide real RTL experience there must be a way to provide this string + return new OrderedListItemSpan(theme, String.valueOf(order) + "." + '\u00a0'); + } + + @NonNull + @Override + public Object createStrikethrough() { + return new StrikethroughSpan(); + } + + @NonNull + @Override + public Object createStrongEmphasis() { + return new StrongEmphasisSpan(); + } + + @NonNull + @Override + public Object createTaskList(int indent, boolean done) { + return new TaskListSpan(theme, indent, done); + } + + @NonNull + @Override + public Object createThematicBreak() { + return new ThematicBreakSpan(theme); + } +} diff --git a/library/src/main/java/ru/noties/markwon/SpannableConfiguration.java b/library/src/main/java/ru/noties/markwon/SpannableConfiguration.java index 3aa18cb1..8dbe3304 100644 --- a/library/src/main/java/ru/noties/markwon/SpannableConfiguration.java +++ b/library/src/main/java/ru/noties/markwon/SpannableConfiguration.java @@ -8,6 +8,7 @@ import ru.noties.markwon.renderer.ImageSizeResolverDef; import ru.noties.markwon.renderer.html.SpannableHtmlParser; import ru.noties.markwon.spans.AsyncDrawable; import ru.noties.markwon.spans.LinkSpan; +import ru.noties.markwon.spans.SpanFactory; import ru.noties.markwon.spans.SpannableTheme; @SuppressWarnings("WeakerAccess") @@ -31,6 +32,7 @@ public class SpannableConfiguration { private final UrlProcessor urlProcessor; private final SpannableHtmlParser htmlParser; private final ImageSizeResolver imageSizeResolver; + private final SpanFactory spanFactory; private SpannableConfiguration(@NonNull Builder builder) { this.theme = builder.theme; @@ -40,6 +42,7 @@ public class SpannableConfiguration { this.urlProcessor = builder.urlProcessor; this.htmlParser = builder.htmlParser; this.imageSizeResolver = builder.imageSizeResolver; + this.spanFactory = builder.spanFactory; } @NonNull @@ -77,6 +80,11 @@ public class SpannableConfiguration { return imageSizeResolver; } + @NonNull + public SpanFactory spanFactory() { + return spanFactory; + } + @SuppressWarnings("unused") public static class Builder { @@ -88,6 +96,7 @@ public class SpannableConfiguration { private UrlProcessor urlProcessor; private SpannableHtmlParser htmlParser; private ImageSizeResolver imageSizeResolver; + private SpanFactory spanFactory; Builder(@NonNull Context context) { this.context = context; @@ -138,6 +147,12 @@ public class SpannableConfiguration { return this; } + @NonNull + public Builder spanFactory(@NonNull SpanFactory spanFactory) { + this.spanFactory = spanFactory; + return this; + } + @NonNull public SpannableConfiguration build() { @@ -165,6 +180,10 @@ public class SpannableConfiguration { imageSizeResolver = new ImageSizeResolverDef(); } + if (spanFactory == null) { + spanFactory = new SpanFactoryDef(theme, linkResolver, asyncDrawableLoader, imageSizeResolver); + } + if (htmlParser == null) { htmlParser = SpannableHtmlParser.create(theme, asyncDrawableLoader, urlProcessor, linkResolver, imageSizeResolver); } diff --git a/library/src/main/java/ru/noties/markwon/renderer/SpannableMarkdownVisitor.java b/library/src/main/java/ru/noties/markwon/renderer/SpannableMarkdownVisitor.java index b6e1f123..8ee7a4b3 100644 --- a/library/src/main/java/ru/noties/markwon/renderer/SpannableMarkdownVisitor.java +++ b/library/src/main/java/ru/noties/markwon/renderer/SpannableMarkdownVisitor.java @@ -5,7 +5,6 @@ import android.support.annotation.Nullable; import android.text.SpannableStringBuilder; import android.text.Spanned; import android.text.TextUtils; -import android.text.style.StrikethroughSpan; import org.commonmark.ext.gfm.strikethrough.Strikethrough; import org.commonmark.ext.gfm.tables.TableBody; @@ -44,19 +43,8 @@ import java.util.List; import ru.noties.markwon.ReverseSpannableStringBuilder; import ru.noties.markwon.SpannableConfiguration; 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; -import ru.noties.markwon.spans.BulletListItemSpan; -import ru.noties.markwon.spans.CodeSpan; -import ru.noties.markwon.spans.EmphasisSpan; -import ru.noties.markwon.spans.HeadingSpan; -import ru.noties.markwon.spans.LinkSpan; -import ru.noties.markwon.spans.OrderedListItemSpan; -import ru.noties.markwon.spans.StrongEmphasisSpan; +import ru.noties.markwon.spans.SpanFactory; import ru.noties.markwon.spans.TableRowSpan; -import ru.noties.markwon.spans.TaskListSpan; -import ru.noties.markwon.spans.ThematicBreakSpan; import ru.noties.markwon.tasklist.TaskListBlock; import ru.noties.markwon.tasklist.TaskListItem; @@ -64,6 +52,7 @@ import ru.noties.markwon.tasklist.TaskListItem; public class SpannableMarkdownVisitor extends AbstractVisitor { private final SpannableConfiguration configuration; + private final SpanFactory spanFactory; private final SpannableStringBuilder builder; private final Deque htmlInlineItems; @@ -79,6 +68,7 @@ public class SpannableMarkdownVisitor extends AbstractVisitor { @NonNull SpannableStringBuilder builder ) { this.configuration = configuration; + this.spanFactory = configuration.spanFactory(); this.builder = builder; this.htmlInlineItems = new ArrayDeque<>(2); } @@ -92,14 +82,14 @@ public class SpannableMarkdownVisitor extends AbstractVisitor { public void visit(StrongEmphasis strongEmphasis) { final int length = builder.length(); visitChildren(strongEmphasis); - setSpan(length, new StrongEmphasisSpan()); + setSpan(length, spanFactory.createStrongEmphasis()); } @Override public void visit(Emphasis emphasis) { final int length = builder.length(); visitChildren(emphasis); - setSpan(length, new EmphasisSpan()); + setSpan(length, spanFactory.createEmphasis()); } @Override @@ -116,7 +106,7 @@ public class SpannableMarkdownVisitor extends AbstractVisitor { visitChildren(blockQuote); - setSpan(length, new BlockQuoteSpan(configuration.theme())); + setSpan(length, spanFactory.createBlockQuote()); blockQuoteIndent -= 1; @@ -137,10 +127,7 @@ public class SpannableMarkdownVisitor extends AbstractVisitor { builder.append(code.getLiteral()); builder.append('\u00a0'); - setSpan(length, new CodeSpan( - configuration.theme(), - false - )); + setSpan(length, spanFactory.createCode(false)); } @Override @@ -175,10 +162,7 @@ public class SpannableMarkdownVisitor extends AbstractVisitor { ); builder.append('\u00a0').append('\n'); - setSpan(length, new CodeSpan( - configuration.theme(), - true - )); + setSpan(length, spanFactory.createCode(true)); newLine(); builder.append('\n'); @@ -218,11 +202,7 @@ public class SpannableMarkdownVisitor extends AbstractVisitor { visitChildren(listItem); - // todo| in order to provide real RTL experience there must be a way to provide this string - setSpan(length, new OrderedListItemSpan( - configuration.theme(), - String.valueOf(start) + "." + '\u00a0' - )); + setSpan(length, spanFactory.createOrderedListItem(start)); // after we have visited the children increment start number final OrderedList orderedList = (OrderedList) parent; @@ -232,10 +212,7 @@ public class SpannableMarkdownVisitor extends AbstractVisitor { visitChildren(listItem); - setSpan(length, new BulletListItemSpan( - configuration.theme(), - listLevel - 1 - )); + setSpan(length, spanFactory.createBulletListItem(listLevel - 1)); } blockQuoteIndent -= 1; @@ -251,7 +228,7 @@ public class SpannableMarkdownVisitor extends AbstractVisitor { final int length = builder.length(); builder.append(' '); // without space it won't render - setSpan(length, new ThematicBreakSpan(configuration.theme())); + setSpan(length, spanFactory.createThematicBreak()); newLine(); builder.append('\n'); @@ -264,7 +241,7 @@ public class SpannableMarkdownVisitor extends AbstractVisitor { final int length = builder.length(); visitChildren(heading); - setSpan(length, new HeadingSpan(configuration.theme(), heading.getLevel())); + setSpan(length, spanFactory.createHeading(heading.getLevel())); newLine(); @@ -306,7 +283,7 @@ public class SpannableMarkdownVisitor extends AbstractVisitor { final int length = builder.length(); visitChildren(customNode); - setSpan(length, new StrikethroughSpan()); + setSpan(length, spanFactory.createStrikethrough()); } else if (customNode instanceof TaskListItem) { @@ -320,11 +297,7 @@ public class SpannableMarkdownVisitor extends AbstractVisitor { visitChildren(customNode); - setSpan(length, new TaskListSpan( - configuration.theme(), - blockQuoteIndent, - listItem.done() - )); + setSpan(length, spanFactory.createTaskList(blockQuoteIndent, listItem.done())); newLine(); @@ -448,20 +421,7 @@ public class SpannableMarkdownVisitor extends AbstractVisitor { final boolean link = parent != null && parent instanceof Link; final String destination = configuration.urlProcessor().process(image.getDestination()); - setSpan( - length, - new AsyncDrawableSpan( - configuration.theme(), - new AsyncDrawable( - destination, - configuration.asyncDrawableLoader(), - configuration.imageSizeResolver(), - null - ), - AsyncDrawableSpan.ALIGN_BOTTOM, - link - ) - ); + setSpan(length, spanFactory.createImage(destination, link)); // todo, maybe, if image is not inside a link, we should make it clickable, so // user can open it in external viewer? @@ -520,11 +480,17 @@ public class SpannableMarkdownVisitor extends AbstractVisitor { final int length = builder.length(); visitChildren(link); final String destination = configuration.urlProcessor().process(link.getDestination()); - setSpan(length, new LinkSpan(configuration.theme(), destination, configuration.linkResolver())); + setSpan(length, spanFactory.createLink(destination)); } - private void setSpan(int start, @NonNull Object span) { - builder.setSpan(span, start, builder.length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); + private void setSpan(int start, @NonNull Object spans) { + if (spans instanceof Object[]) { + for (final Object span : (Object[]) spans) { + builder.setSpan(span, start, builder.length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); + } + } else { + builder.setSpan(spans, start, builder.length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); + } } private void newLine() { diff --git a/library/src/main/java/ru/noties/markwon/spans/SpanFactory.java b/library/src/main/java/ru/noties/markwon/spans/SpanFactory.java new file mode 100644 index 00000000..5a605763 --- /dev/null +++ b/library/src/main/java/ru/noties/markwon/spans/SpanFactory.java @@ -0,0 +1,42 @@ +package ru.noties.markwon.spans; + +import android.support.annotation.NonNull; + +public interface SpanFactory { + @NonNull + Object createBlockQuote(); + + @NonNull + Object createBulletListItem(int level); + + @NonNull + Object createCode(boolean multiline); + + @NonNull + Object createEmphasis(); + + @NonNull + Object createHeading(int level); + + @NonNull + Object createImage(@NonNull String destination, boolean link); + + @NonNull + Object createLink(@NonNull String destination); + + @NonNull + Object createOrderedListItem(int order); + + @NonNull + Object createStrongEmphasis(); + + @NonNull + Object createStrikethrough(); + + @NonNull + Object createTaskList(int indent, boolean done); + + @NonNull + Object createThematicBreak(); + +}