From 194218df2ac1b9240d16ef42bac9faad740ad330 Mon Sep 17 00:00:00 2001 From: Cyrus Bakhtiari-Haftlang Date: Thu, 12 Jul 2018 09:28:47 +0200 Subject: [PATCH 1/2] Replaced custom SpannableBuilder with ReverseSpannableStringBuilder Background: The usage and maintaining of SpannableBuilder is reduced by using a good-old-SpannableStringBuilder directly but making sure its spans are reversed. The implementation is credited Uncodin/bypass --- .../ReverseSpannableStringBuilder.java | 42 +++ .../ru/noties/markwon/SpannableBuilder.java | 245 ------------------ .../markwon/SpannableStringBuilderImpl.java | 13 - .../ru/noties/markwon/SpannedReversed.java | 9 - .../renderer/SpannableMarkdownVisitor.java | 28 +- .../markwon/renderer/SpannableRenderer.java | 7 +- .../markwon/sample/extension/IconVisitor.java | 10 +- .../sample/extension/MainActivity.java | 7 +- 8 files changed, 77 insertions(+), 284 deletions(-) create mode 100644 library/src/main/java/ru/noties/markwon/ReverseSpannableStringBuilder.java delete mode 100644 library/src/main/java/ru/noties/markwon/SpannableBuilder.java delete mode 100644 library/src/main/java/ru/noties/markwon/SpannableStringBuilderImpl.java delete mode 100644 library/src/main/java/ru/noties/markwon/SpannedReversed.java diff --git a/library/src/main/java/ru/noties/markwon/ReverseSpannableStringBuilder.java b/library/src/main/java/ru/noties/markwon/ReverseSpannableStringBuilder.java new file mode 100644 index 00000000..1bcf0b7a --- /dev/null +++ b/library/src/main/java/ru/noties/markwon/ReverseSpannableStringBuilder.java @@ -0,0 +1,42 @@ +package ru.noties.markwon; + +import android.text.SpannableStringBuilder; + +/** + * Copied as is from @see Uncodin/bypass + */ +public class ReverseSpannableStringBuilder extends SpannableStringBuilder { + + public ReverseSpannableStringBuilder() { + super(); + } + + public ReverseSpannableStringBuilder(CharSequence text, int start, int end) { + super(text, start, end); + } + + @Override + public T[] getSpans(int queryStart, int queryEnd, Class kind) { + T[] ret = super.getSpans(queryStart, queryEnd, kind); + reverse(ret); + return ret; + } + + private static void reverse(Object[] arr) { + if (arr == null) { + return; + } + + int i = 0; + int j = arr.length - 1; + Object tmp; + while (j > i) { + tmp = arr[j]; + arr[j] = arr[i]; + arr[i] = tmp; + j--; + i++; + } + } +} diff --git a/library/src/main/java/ru/noties/markwon/SpannableBuilder.java b/library/src/main/java/ru/noties/markwon/SpannableBuilder.java deleted file mode 100644 index 95570900..00000000 --- a/library/src/main/java/ru/noties/markwon/SpannableBuilder.java +++ /dev/null @@ -1,245 +0,0 @@ -package ru.noties.markwon; - -import android.support.annotation.NonNull; -import android.support.annotation.Nullable; -import android.text.SpannableStringBuilder; -import android.text.Spanned; - -import java.util.ArrayDeque; -import java.util.Deque; -import java.util.Iterator; - -/** - * This class is used to _revert_ order of applied spans. Original SpannableStringBuilder - * is using an array to store all the information about spans. So, a span that is added first - * will be drawn first, which leads to subtle bugs (spans receive wrong `x` values when - * requested to draw itself) - * - * @since 1.0.1 - */ -@SuppressWarnings({"WeakerAccess", "unused"}) -public class SpannableBuilder { - - // do not implement CharSequence (or any of Spanned interfaces) - - // we will be using SpannableStringBuilder anyway as a backing store - // as it has tight connection with system (implements some hidden methods, etc) - private final SpannableStringBuilder builder; - - // actually we might be just using ArrayList - private final Deque spans = new ArrayDeque<>(8); - - public SpannableBuilder() { - this(""); - } - - public SpannableBuilder(@NonNull CharSequence cs) { - this.builder = new SpannableStringBuilderImpl(cs.toString()); - copySpans(0, cs); - } - - /** - * Additional method that takes a String, which is proven to NOT contain any spans - * - * @param text String to append - * @return this instance - */ - @NonNull - public SpannableBuilder append(@NonNull String text) { - builder.append(text); - return this; - } - - @NonNull - public SpannableBuilder append(char c) { - builder.append(c); - return this; - } - - @NonNull - public SpannableBuilder append(@NonNull CharSequence cs) { - - copySpans(length(), cs); - - builder.append(cs.toString()); - - return this; - } - - @NonNull - public SpannableBuilder append(@NonNull CharSequence cs, @NonNull Object span) { - final int length = length(); - append(cs); - setSpan(span, length); - return this; - } - - @NonNull - public SpannableBuilder append(@NonNull CharSequence cs, @NonNull Object span, int flags) { - final int length = length(); - append(cs); - setSpan(span, length, length(), flags); - return this; - } - - @NonNull - public SpannableBuilder setSpan(@NonNull Object span, int start) { - return setSpan(span, start, length()); - } - - @NonNull - public SpannableBuilder setSpan(@NonNull Object span, int start, int end) { - return setSpan(span, start, end, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); - } - - @NonNull - public SpannableBuilder setSpan(@NonNull Object span, int start, int end, int flags) { - spans.push(new Span(span, start, end, flags)); - return this; - } - - public int length() { - return builder.length(); - } - - public char charAt(int index) { - return builder.charAt(index); - } - - public char lastChar() { - return builder.charAt(length() - 1); - } - - @NonNull - public CharSequence removeFromEnd(int start) { - - // this method is not intended to be used by clients - // it's a workaround to support tables - - final int end = length(); - - // as we do not expose builder and do no apply spans to it, we are safe to NOT to convert to String - final SpannableStringBuilderImpl impl = new SpannableStringBuilderImpl(builder.subSequence(start, end)); - - final Iterator iterator = spans.iterator(); - - Span span; - - while (iterator.hasNext() && ((span = iterator.next())) != null) { - if (span.start >= start && span.end <= end) { - impl.setSpan(span.what, span.start - start, span.end - start, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); - iterator.remove(); - } - } - - builder.replace(start, end, ""); - - return impl; - } - - @Override - @NonNull - public String toString() { - return builder.toString(); - } - - @NonNull - public CharSequence text() { - - // okay, in order to not allow external modification and keep our spans order - // we should not return our builder - // - // plus, if this method was called -> all spans would be applied, which potentially - // breaks the order that we intend to use - // so, we will defensively copy builder - - // as we do not expose builder and do no apply spans to it, we are safe to NOT to convert to String - final SpannableStringBuilderImpl impl = new SpannableStringBuilderImpl(builder); - - for (Span span : spans) { - impl.setSpan(span.what, span.start, span.end, span.flags); - } - - // now, let's remove trailing newLines (so small amounts of text are displayed correctly) - // @since 1.0.2 - - final int length = impl.length(); - if (length > 0) { - int amount = 0; - for (int i = length - 1; i >=0 ; i--) { - if (Character.isWhitespace(impl.charAt(i))) { - amount += 1; - } else { - break; - } - } - if (amount > 0) { - impl.replace(length - amount, length, ""); - } - } - - return impl; - } - - private void copySpans(final int index, @Nullable CharSequence cs) { - - // we must identify already reversed Spanned... - // and (!) iterate backwards when adding (to preserve order) - - if (cs instanceof Spanned) { - - final Spanned spanned = (Spanned) cs; - final boolean reverse = spanned instanceof SpannedReversed; - - final Object[] spans = spanned.getSpans(0, spanned.length(), Object.class); - - iterate(reverse, spans, new Action() { - @Override - public void apply(Object o) { - setSpan( - o, - index + spanned.getSpanStart(o), - index + spanned.getSpanEnd(o), - spanned.getSpanFlags(o) - ); - } - }); - } - } - - static class Span { - - final Object what; - int start; - int end; - final int flags; - - Span(@NonNull Object what, int start, int end, int flags) { - this.what = what; - this.start = start; - this.end = end; - this.flags = flags; - } - } - - private interface Action { - void apply(Object o); - } - - private static void iterate(boolean reverse, @Nullable Object[] array, @NonNull Action action) { - final int length = array != null - ? array.length - : 0; - if (length > 0) { - if (reverse) { - for (int i = length - 1; i >= 0; i--) { - action.apply(array[i]); - } - } else { - for (int i = 0; i < length; i++) { - action.apply(array[i]); - } - } - } - } -} diff --git a/library/src/main/java/ru/noties/markwon/SpannableStringBuilderImpl.java b/library/src/main/java/ru/noties/markwon/SpannableStringBuilderImpl.java deleted file mode 100644 index 7b29440b..00000000 --- a/library/src/main/java/ru/noties/markwon/SpannableStringBuilderImpl.java +++ /dev/null @@ -1,13 +0,0 @@ -package ru.noties.markwon; - -import android.text.SpannableStringBuilder; - -/** - * @since 1.0.1 - */ -class SpannableStringBuilderImpl extends SpannableStringBuilder implements SpannedReversed { - - SpannableStringBuilderImpl(CharSequence text) { - super(text); - } -} diff --git a/library/src/main/java/ru/noties/markwon/SpannedReversed.java b/library/src/main/java/ru/noties/markwon/SpannedReversed.java deleted file mode 100644 index 3fd7f566..00000000 --- a/library/src/main/java/ru/noties/markwon/SpannedReversed.java +++ /dev/null @@ -1,9 +0,0 @@ -package ru.noties.markwon; - -import android.text.Spanned; - -/** - * @since 1.0.1 - */ -interface SpannedReversed extends Spanned { -} 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 ceb22ef0..b6e1f123 100644 --- a/library/src/main/java/ru/noties/markwon/renderer/SpannableMarkdownVisitor.java +++ b/library/src/main/java/ru/noties/markwon/renderer/SpannableMarkdownVisitor.java @@ -2,6 +2,7 @@ package ru.noties.markwon.renderer; import android.support.annotation.NonNull; import android.support.annotation.Nullable; +import android.text.SpannableStringBuilder; import android.text.Spanned; import android.text.TextUtils; import android.text.style.StrikethroughSpan; @@ -40,7 +41,7 @@ import java.util.ArrayList; import java.util.Deque; import java.util.List; -import ru.noties.markwon.SpannableBuilder; +import ru.noties.markwon.ReverseSpannableStringBuilder; import ru.noties.markwon.SpannableConfiguration; import ru.noties.markwon.renderer.html.SpannableHtmlParser; import ru.noties.markwon.spans.AsyncDrawable; @@ -63,7 +64,7 @@ import ru.noties.markwon.tasklist.TaskListItem; public class SpannableMarkdownVisitor extends AbstractVisitor { private final SpannableConfiguration configuration; - private final SpannableBuilder builder; + private final SpannableStringBuilder builder; private final Deque htmlInlineItems; private int blockQuoteIndent; @@ -75,7 +76,7 @@ public class SpannableMarkdownVisitor extends AbstractVisitor { public SpannableMarkdownVisitor( @NonNull SpannableConfiguration configuration, - @NonNull SpannableBuilder builder + @NonNull SpannableStringBuilder builder ) { this.configuration = configuration; this.builder = builder; @@ -381,10 +382,9 @@ public class SpannableMarkdownVisitor extends AbstractVisitor { if (pendingTableRow == null) { pendingTableRow = new ArrayList<>(2); } - pendingTableRow.add(new TableRowSpan.Cell( tableCellAlignment(cell.getAlignment()), - builder.removeFromEnd(length) + removeFromEnd(length) )); tableRowIsHeader = cell.isHeader(); @@ -396,6 +396,22 @@ public class SpannableMarkdownVisitor extends AbstractVisitor { return handled; } + @NonNull + private CharSequence removeFromEnd(int start) { + + // this method is not intended to be used by clients + // it's a workaround to support tables + + final int end = builder.length(); + + // as we do not expose builder and do no apply spans to it, we are safe to NOT to convert to String + final SpannableStringBuilder impl = new ReverseSpannableStringBuilder(builder, start, end); + builder.delete(start, end); + + return impl; + } + + @Override public void visit(Paragraph paragraph) { @@ -513,7 +529,7 @@ public class SpannableMarkdownVisitor extends AbstractVisitor { private void newLine() { if (builder.length() > 0 - && '\n' != builder.lastChar()) { + && '\n' != builder.charAt(builder.length() - 1)) { builder.append('\n'); } } diff --git a/library/src/main/java/ru/noties/markwon/renderer/SpannableRenderer.java b/library/src/main/java/ru/noties/markwon/renderer/SpannableRenderer.java index 32d620dd..15acf2a3 100644 --- a/library/src/main/java/ru/noties/markwon/renderer/SpannableRenderer.java +++ b/library/src/main/java/ru/noties/markwon/renderer/SpannableRenderer.java @@ -1,18 +1,19 @@ package ru.noties.markwon.renderer; import android.support.annotation.NonNull; +import android.text.SpannableStringBuilder; import org.commonmark.node.Node; -import ru.noties.markwon.SpannableBuilder; +import ru.noties.markwon.ReverseSpannableStringBuilder; import ru.noties.markwon.SpannableConfiguration; public class SpannableRenderer { @NonNull public CharSequence render(@NonNull SpannableConfiguration configuration, @NonNull Node node) { - final SpannableBuilder builder = new SpannableBuilder(); + final SpannableStringBuilder builder = new ReverseSpannableStringBuilder(); node.accept(new SpannableMarkdownVisitor(configuration, builder)); - return builder.text(); + return builder; } } diff --git a/sample-custom-extension/src/main/java/ru/noties/markwon/sample/extension/IconVisitor.java b/sample-custom-extension/src/main/java/ru/noties/markwon/sample/extension/IconVisitor.java index d373ff75..e6c6b8c4 100644 --- a/sample-custom-extension/src/main/java/ru/noties/markwon/sample/extension/IconVisitor.java +++ b/sample-custom-extension/src/main/java/ru/noties/markwon/sample/extension/IconVisitor.java @@ -1,25 +1,25 @@ package ru.noties.markwon.sample.extension; import android.support.annotation.NonNull; +import android.text.SpannableStringBuilder; +import android.text.Spanned; import android.text.TextUtils; -import android.widget.TextView; import org.commonmark.node.CustomNode; -import ru.noties.markwon.SpannableBuilder; import ru.noties.markwon.SpannableConfiguration; import ru.noties.markwon.renderer.SpannableMarkdownVisitor; @SuppressWarnings("WeakerAccess") public class IconVisitor extends SpannableMarkdownVisitor { - private final SpannableBuilder builder; + private final SpannableStringBuilder builder; private final IconSpanProvider iconSpanProvider; public IconVisitor( @NonNull SpannableConfiguration configuration, - @NonNull SpannableBuilder builder, + @NonNull SpannableStringBuilder builder, @NonNull IconSpanProvider iconSpanProvider ) { super(configuration, builder); @@ -51,7 +51,7 @@ public class IconVisitor extends SpannableMarkdownVisitor { final int length = builder.length(); builder.append(name); - builder.setSpan(iconSpanProvider.provide(name, color, size), length); + builder.setSpan(iconSpanProvider.provide(name, color, size), length, length, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); builder.append(' '); return true; diff --git a/sample-custom-extension/src/main/java/ru/noties/markwon/sample/extension/MainActivity.java b/sample-custom-extension/src/main/java/ru/noties/markwon/sample/extension/MainActivity.java index a81f8eb0..64e994b3 100644 --- a/sample-custom-extension/src/main/java/ru/noties/markwon/sample/extension/MainActivity.java +++ b/sample-custom-extension/src/main/java/ru/noties/markwon/sample/extension/MainActivity.java @@ -2,6 +2,7 @@ package ru.noties.markwon.sample.extension; import android.app.Activity; import android.os.Bundle; +import android.text.SpannableStringBuilder; import android.widget.TextView; import org.commonmark.ext.gfm.strikethrough.StrikethroughExtension; @@ -11,7 +12,7 @@ import org.commonmark.parser.Parser; import java.util.Arrays; -import ru.noties.markwon.SpannableBuilder; +import ru.noties.markwon.ReverseSpannableStringBuilder; import ru.noties.markwon.SpannableConfiguration; import ru.noties.markwon.tasklist.TaskListExtension; @@ -43,7 +44,7 @@ public class MainActivity extends Activity { final Node node = parser.parse(markdown); - final SpannableBuilder builder = new SpannableBuilder(); + final SpannableStringBuilder builder = new ReverseSpannableStringBuilder(); // please note that here I am passing `0` as fallback it means that if markdown references // unknown icon, it will try to load fallback one and will fail with ResourceNotFound. It's @@ -61,6 +62,6 @@ public class MainActivity extends Activity { node.accept(visitor); // apply - textView.setText(builder.text()); + textView.setText(builder); } } From bef7fd235fb214424a3e156a9eaa76830a82ba9e Mon Sep 17 00:00:00 2001 From: Cyrus Bakhtiari-Haftlang Date: Thu, 12 Jul 2018 14:18:48 +0200 Subject: [PATCH 2/2] Extracted span generation into a separate factory --- .../ru/noties/markwon/SpanFactoryDef.java | 127 ++++++++++++++++++ .../markwon/SpannableConfiguration.java | 19 +++ .../renderer/SpannableMarkdownVisitor.java | 82 ++++------- .../ru/noties/markwon/spans/SpanFactory.java | 42 ++++++ 4 files changed, 212 insertions(+), 58 deletions(-) create mode 100644 library/src/main/java/ru/noties/markwon/SpanFactoryDef.java create mode 100644 library/src/main/java/ru/noties/markwon/spans/SpanFactory.java 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(); + +}