From 02fc054ad9765c62ec00218c369b561d47a8ab9d Mon Sep 17 00:00:00 2001 From: Cyrus Bakhtiari-Haftlang Date: Mon, 1 Oct 2018 11:04:57 +0200 Subject: [PATCH] Replaced the implementation of SpannableBuilder SpannableBuilder will now extend SpannableStringBuilder. The major diff- erence will be that the span order will be reverted upon a call of `getSpans()`. Another addition will be support for setting multiple multiple spans by array. The reverse order implementation is credited Uncodin/bypass --- .../ru/noties/markwon/SpannableBuilder.java | 291 +++--------------- .../markwon/SpannableStringBuilderImpl.java | 13 - .../ru/noties/markwon/SpannedReversed.java | 9 - .../renderer/SpannableMarkdownVisitor.java | 24 +- .../markwon/renderer/SpannableRenderer.java | 4 +- .../renderer/html2/tag/BlockquoteHandler.java | 3 +- .../renderer/html2/tag/ListHandler.java | 2 +- .../renderer/html2/tag/SimpleTagHandler.java | 5 +- .../renderer/html2/tag/StrikeHandler.java | 3 +- .../renderer/html2/tag/UnderlineHandler.java | 3 +- .../visitor/SpannableMarkdownVisitorTest.java | 8 +- .../markwon/sample/extension/IconVisitor.java | 10 +- .../sample/extension/MainActivity.java | 2 +- .../sample/jlatexmath/MainActivity.java | 6 +- 14 files changed, 81 insertions(+), 302 deletions(-) delete mode 100644 markwon/src/main/java/ru/noties/markwon/SpannableStringBuilderImpl.java delete mode 100644 markwon/src/main/java/ru/noties/markwon/SpannedReversed.java diff --git a/markwon/src/main/java/ru/noties/markwon/SpannableBuilder.java b/markwon/src/main/java/ru/noties/markwon/SpannableBuilder.java index 9e3ec713..20992e7f 100644 --- a/markwon/src/main/java/ru/noties/markwon/SpannableBuilder.java +++ b/markwon/src/main/java/ru/noties/markwon/SpannableBuilder.java @@ -5,41 +5,52 @@ 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 2.0.0 implements Appendable and CharSequence - * - * @since 1.0.1 */ -@SuppressWarnings({"WeakerAccess", "unused"}) -public class SpannableBuilder implements Appendable, CharSequence { +public class SpannableBuilder extends SpannableStringBuilder { + + public SpannableBuilder() { + super(); + } + + public SpannableBuilder(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; + } /** - * @since 2.0.0 + * Convenience for allowing {@link Nullable} and {@link NonNull} spans, as well as + * {@link NonNull} array of spans. {@link Spanned#SPAN_EXCLUSIVE_EXCLUSIVE} will be applied as + * flag. + * + * @param spans A span object, an array of span objects or null + * @param start Start index (inclusive) + * @param end End index (exclusive) */ - public static void setSpans(@NonNull SpannableBuilder builder, @Nullable Object spans, int start, int end) { + public void setSpans(@Nullable Object spans, int start, int end) { if (spans != null) { - // setting a span for an invalid position can lead to silent fail (no exception, // but execution is stopped) - if (!isPositionValid(builder.length(), start, end)) { + if (!isPositionValid(length(), start, end)) { return; } if (spans.getClass().isArray()) { for (Object o : ((Object[]) spans)) { - builder.setSpan(o, start, end, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); + setSpan(o, start, end, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); } } else { - builder.setSpan(spans, start, end, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); + setSpan(spans, start, end, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); } } } @@ -50,242 +61,20 @@ public class SpannableBuilder implements Appendable, CharSequence { && end <= length; } - - private final StringBuilder 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 StringBuilder(cs); - 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 - @Override - public SpannableBuilder append(char c) { - builder.append(c); - return this; - } - - @NonNull - @Override - public SpannableBuilder append(@NonNull CharSequence cs) { - - copySpans(length(), cs); - - builder.append(cs); - - return this; - } - - /** - * @since 2.0.0 to follow Appendable interface - */ - @NonNull - @Override - public SpannableBuilder append(CharSequence csq, int start, int end) { - - final CharSequence cs = csq.subSequence(start, end); - copySpans(length(), cs); - - builder.append(cs); - - 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; - } - - @Override - public int length() { - return builder.length(); - } - - @Override - public char charAt(int index) { - return builder.charAt(index); - } - - /** - * @since 2.0.0 to follow CharSequence interface - */ - @Override - public CharSequence subSequence(int start, int end) { - return builder.subSequence(start, end); - } - - 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(); - } + private static void reverse(Object[] arr) { + if (arr == null) { + return; } - builder.replace(start, end, ""); - - return impl; - } - - @Override - @NonNull - public String toString() { - return builder.toString(); - } - - @NonNull - public CharSequence text() { - // @since 2.0.0 redirects this call to `#spannableStringBuilder()` - return spannableStringBuilder(); - } - - /** - * Simple method to create a SpannableStringBuilder, which is created anyway. Unlike {@link #text()} - * method which returns the same SpannableStringBuilder there is no need to cast the resulting - * CharSequence - * - * @since 2.0.0 - */ - @NonNull - public SpannableStringBuilder spannableStringBuilder() { - - // 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); - } - - 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); - final int length = spans != null - ? spans.length - : 0; - - if (length > 0) { - if (reverse) { - Object o; - for (int i = length - 1; i >= 0; i--) { - o = spans[i]; - setSpan( - o, - index + spanned.getSpanStart(o), - index + spanned.getSpanEnd(o), - spanned.getSpanFlags(o) - ); - } - } else { - Object o; - for (int i = 0; i < length; i++) { - o = spans[i]; - setSpan( - o, - index + spanned.getSpanStart(o), - index + spanned.getSpanEnd(o), - spanned.getSpanFlags(o) - ); - } - } - } + 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++; } } - - 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; - } - } -} +} \ No newline at end of file diff --git a/markwon/src/main/java/ru/noties/markwon/SpannableStringBuilderImpl.java b/markwon/src/main/java/ru/noties/markwon/SpannableStringBuilderImpl.java deleted file mode 100644 index 7b29440b..00000000 --- a/markwon/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/markwon/src/main/java/ru/noties/markwon/SpannedReversed.java b/markwon/src/main/java/ru/noties/markwon/SpannedReversed.java deleted file mode 100644 index 3fd7f566..00000000 --- a/markwon/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/markwon/src/main/java/ru/noties/markwon/renderer/SpannableMarkdownVisitor.java b/markwon/src/main/java/ru/noties/markwon/renderer/SpannableMarkdownVisitor.java index 967784e5..81596d01 100644 --- a/markwon/src/main/java/ru/noties/markwon/renderer/SpannableMarkdownVisitor.java +++ b/markwon/src/main/java/ru/noties/markwon/renderer/SpannableMarkdownVisitor.java @@ -415,7 +415,7 @@ public class SpannableMarkdownVisitor extends AbstractVisitor { pendingTableRow.add(new TableRowSpan.Cell( tableCellAlignment(cell.getAlignment()), - builder.removeFromEnd(length) + removeFromEnd(length) )); tableRowIsHeader = cell.isHeader(); @@ -508,12 +508,13 @@ public class SpannableMarkdownVisitor extends AbstractVisitor { } private void setSpan(int start, @Nullable Object span) { - SpannableBuilder.setSpans(builder, span, start, builder.length()); + builder.setSpans(span, start, builder.length()); } private void newLine() { - if (builder.length() > 0 - && '\n' != builder.lastChar()) { + final int length = builder.length(); + + if (length > 0 && '\n' != builder.charAt(length - 1)) { builder.append('\n'); } } @@ -530,6 +531,21 @@ public class SpannableMarkdownVisitor extends AbstractVisitor { return false; } + @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 = 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 SpannableBuilder impl = new SpannableBuilder(builder, start, end); + builder.delete(start, end); + + return impl; + } + + @TableRowSpan.Alignment private static int tableCellAlignment(TableCell.Alignment alignment) { final int out; diff --git a/markwon/src/main/java/ru/noties/markwon/renderer/SpannableRenderer.java b/markwon/src/main/java/ru/noties/markwon/renderer/SpannableRenderer.java index 32d620dd..ad5441bf 100644 --- a/markwon/src/main/java/ru/noties/markwon/renderer/SpannableRenderer.java +++ b/markwon/src/main/java/ru/noties/markwon/renderer/SpannableRenderer.java @@ -10,9 +10,9 @@ import ru.noties.markwon.SpannableConfiguration; public class SpannableRenderer { @NonNull - public CharSequence render(@NonNull SpannableConfiguration configuration, @NonNull Node node) { + public SpannableBuilder render(@NonNull SpannableConfiguration configuration, @NonNull Node node) { final SpannableBuilder builder = new SpannableBuilder(); node.accept(new SpannableMarkdownVisitor(configuration, builder)); - return builder.text(); + return builder; } } diff --git a/markwon/src/main/java/ru/noties/markwon/renderer/html2/tag/BlockquoteHandler.java b/markwon/src/main/java/ru/noties/markwon/renderer/html2/tag/BlockquoteHandler.java index 99ddf153..4af87f67 100644 --- a/markwon/src/main/java/ru/noties/markwon/renderer/html2/tag/BlockquoteHandler.java +++ b/markwon/src/main/java/ru/noties/markwon/renderer/html2/tag/BlockquoteHandler.java @@ -18,8 +18,7 @@ public class BlockquoteHandler extends TagHandler { visitChildren(configuration, builder, tag.getAsBlock()); } - SpannableBuilder.setSpans( - builder, + builder.setSpans( configuration.factory().blockQuote(configuration.theme()), tag.start(), tag.end() diff --git a/markwon/src/main/java/ru/noties/markwon/renderer/html2/tag/ListHandler.java b/markwon/src/main/java/ru/noties/markwon/renderer/html2/tag/ListHandler.java index fca098e7..35d61800 100644 --- a/markwon/src/main/java/ru/noties/markwon/renderer/html2/tag/ListHandler.java +++ b/markwon/src/main/java/ru/noties/markwon/renderer/html2/tag/ListHandler.java @@ -48,7 +48,7 @@ public class ListHandler extends TagHandler { bulletLevel ); } - SpannableBuilder.setSpans(builder, spans, child.start(), child.end()); + builder.setSpans(spans, child.start(), child.end()); } } } diff --git a/markwon/src/main/java/ru/noties/markwon/renderer/html2/tag/SimpleTagHandler.java b/markwon/src/main/java/ru/noties/markwon/renderer/html2/tag/SimpleTagHandler.java index e5940cc7..32dfce08 100644 --- a/markwon/src/main/java/ru/noties/markwon/renderer/html2/tag/SimpleTagHandler.java +++ b/markwon/src/main/java/ru/noties/markwon/renderer/html2/tag/SimpleTagHandler.java @@ -14,9 +14,6 @@ public abstract class SimpleTagHandler extends TagHandler { @Override public void handle(@NonNull SpannableConfiguration configuration, @NonNull SpannableBuilder builder, @NonNull HtmlTag tag) { - final Object spans = getSpans(configuration, tag); - if (spans != null) { - SpannableBuilder.setSpans(builder, spans, tag.start(), tag.end()); - } + builder.setSpans(getSpans(configuration, tag), tag.start(), tag.end()); } } diff --git a/markwon/src/main/java/ru/noties/markwon/renderer/html2/tag/StrikeHandler.java b/markwon/src/main/java/ru/noties/markwon/renderer/html2/tag/StrikeHandler.java index 965ddfea..cf9a1b4b 100644 --- a/markwon/src/main/java/ru/noties/markwon/renderer/html2/tag/StrikeHandler.java +++ b/markwon/src/main/java/ru/noties/markwon/renderer/html2/tag/StrikeHandler.java @@ -18,8 +18,7 @@ public class StrikeHandler extends TagHandler { visitChildren(configuration, builder, tag.getAsBlock()); } - SpannableBuilder.setSpans( - builder, + builder.setSpans( configuration.factory().strikethrough(), tag.start(), tag.end() diff --git a/markwon/src/main/java/ru/noties/markwon/renderer/html2/tag/UnderlineHandler.java b/markwon/src/main/java/ru/noties/markwon/renderer/html2/tag/UnderlineHandler.java index ff870ef6..3bd637a1 100644 --- a/markwon/src/main/java/ru/noties/markwon/renderer/html2/tag/UnderlineHandler.java +++ b/markwon/src/main/java/ru/noties/markwon/renderer/html2/tag/UnderlineHandler.java @@ -21,8 +21,7 @@ public class UnderlineHandler extends TagHandler { visitChildren(configuration, builder, tag.getAsBlock()); } - SpannableBuilder.setSpans( - builder, + builder.setSpans( configuration.factory().underline(), tag.start(), tag.end() diff --git a/markwon/src/test/java/ru/noties/markwon/renderer/visitor/SpannableMarkdownVisitorTest.java b/markwon/src/test/java/ru/noties/markwon/renderer/visitor/SpannableMarkdownVisitorTest.java index 047a0584..b6a2c485 100644 --- a/markwon/src/test/java/ru/noties/markwon/renderer/visitor/SpannableMarkdownVisitorTest.java +++ b/markwon/src/test/java/ru/noties/markwon/renderer/visitor/SpannableMarkdownVisitorTest.java @@ -1,7 +1,6 @@ package ru.noties.markwon.renderer.visitor; import android.support.annotation.NonNull; -import android.text.SpannableStringBuilder; import org.commonmark.node.Node; import org.junit.Test; @@ -50,20 +49,19 @@ public class SpannableMarkdownVisitorTest { final Node node = Markwon.createParser().parse(data.input()); node.accept(visitor); - final SpannableStringBuilder stringBuilder = builder.spannableStringBuilder(); final TestValidator validator = TestValidator.create(file); int index = 0; for (TestNode testNode : data.output()) { - index = validator.validate(stringBuilder, index, testNode); + index = validator.validate(builder, index, testNode); } // assert that the whole thing is processed - assertEquals("`" + stringBuilder + "`", stringBuilder.length(), index); + assertEquals("`" + builder + "`", builder.length(), index); - final Object[] spans = stringBuilder.getSpans(0, stringBuilder.length(), Object.class); + final Object[] spans = builder.getSpans(0, builder.length(), Object.class); final int length = spans != null ? spans.length : 0; 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..06bf7df2 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,6 +1,7 @@ package ru.noties.markwon.sample.extension; import android.support.annotation.NonNull; +import android.text.Spanned; import android.text.TextUtils; import android.widget.TextView; @@ -51,9 +52,14 @@ 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 19a69704..49c72ca8 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 @@ -70,6 +70,6 @@ public class MainActivity extends Activity { node.accept(visitor); // apply - textView.setText(builder.text()); + textView.setText(builder); } } diff --git a/sample-latex-math/src/main/java/ru/noties/markwon/sample/jlatexmath/MainActivity.java b/sample-latex-math/src/main/java/ru/noties/markwon/sample/jlatexmath/MainActivity.java index dee84ee4..776ee850 100644 --- a/sample-latex-math/src/main/java/ru/noties/markwon/sample/jlatexmath/MainActivity.java +++ b/sample-latex-math/src/main/java/ru/noties/markwon/sample/jlatexmath/MainActivity.java @@ -8,7 +8,6 @@ import org.commonmark.node.CustomBlock; import org.commonmark.node.Node; import org.commonmark.parser.Parser; -import ru.noties.jlatexmath.JLatexMathAndroid; import ru.noties.markwon.Markwon; import ru.noties.markwon.SpannableBuilder; import ru.noties.markwon.SpannableConfiguration; @@ -83,8 +82,7 @@ public class MainActivity extends Activity { final int length = builder.length(); builder.append(latex); - SpannableBuilder.setSpans( - builder, + builder.setSpans( configuration.factory().image( configuration.theme(), JLatexMathMedia.makeDestination(latex), @@ -100,6 +98,6 @@ public class MainActivity extends Activity { }; node.accept(visitor); - Markwon.setText(textView, builder.text()); + Markwon.setText(textView, builder); } }