diff --git a/markwon/src/main/java/ru/noties/markwon/SpannableBuilder.java b/markwon/src/main/java/ru/noties/markwon/SpannableBuilder.java index 715c3c92..07e2bb85 100644 --- a/markwon/src/main/java/ru/noties/markwon/SpannableBuilder.java +++ b/markwon/src/main/java/ru/noties/markwon/SpannableBuilder.java @@ -163,11 +163,46 @@ public class SpannableBuilder implements Appendable, CharSequence { */ @Override public CharSequence subSequence(int start, int end) { - // todo: NB, we do not copy spans here... we should I think - // the thing to deal with: implement own `getSpans` method to mimic _native_ SpannableStringBuilder - // behaviour. For example originally it will return all spans that at least _overlap_ with specified - // range... which can be confusing - return builder.subSequence(start, end); + + final CharSequence out; + + // @since 2.0.1 we copy spans to resulting subSequence + final List spans = getSpans(start, end); + if (spans.isEmpty()) { + out = builder.subSequence(start, end); + } else { + + // we should not be SpannableStringBuilderReversed here + final SpannableStringBuilder builder = new SpannableStringBuilder(this.builder.subSequence(start, end)); + + final int length = builder.length(); + + int s; + int e; + + for (Span span : spans) { + + // we should limit start/end to resulting subSequence length + // + // for example, originally it was 5-7 and range 5-7 requested + // span should have 0-2 + // + // if a span was fully including resulting subSequence it's start and + // end must be within 0..length bounds + s = Math.max(0, span.start - start); + e = Math.max(length, s + (span.end - span.start)); + + builder.setSpan( + span.what, + s, + e, + span.flags + ); + } + out = builder; + } + + return out; } /** @@ -263,7 +298,7 @@ public class SpannableBuilder implements Appendable, CharSequence { /** * 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 + * CharSequence and makes the thing more explicit * * @since 2.0.0 */ @@ -338,10 +373,10 @@ public class SpannableBuilder implements Appendable, CharSequence { */ public static class Span { - final Object what; - int start; - int end; - final int flags; + public final Object what; + public int start; + public int end; + public final int flags; Span(@NonNull Object what, int start, int end, int flags) { this.what = what; @@ -355,7 +390,6 @@ public class SpannableBuilder implements Appendable, CharSequence { * @since 2.0.1 made inner class of {@link SpannableBuilder}, initially added in 1.0.1 */ static class SpannableStringBuilderReversed extends SpannableStringBuilder { - SpannableStringBuilderReversed(CharSequence text) { super(text); } diff --git a/markwon/src/test/java/ru/noties/markwon/SpannableBuilderTest.java b/markwon/src/test/java/ru/noties/markwon/SpannableBuilderTest.java index d94e17dc..a1daf218 100644 --- a/markwon/src/test/java/ru/noties/markwon/SpannableBuilderTest.java +++ b/markwon/src/test/java/ru/noties/markwon/SpannableBuilderTest.java @@ -1,6 +1,8 @@ package ru.noties.markwon; import android.support.annotation.NonNull; +import android.text.SpannableStringBuilder; +import android.text.Spanned; import org.junit.Before; import org.junit.Test; @@ -18,6 +20,7 @@ import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertTrue; import static ru.noties.markwon.SpannableBuilder.isPositionValid; +import static ru.noties.markwon.SpannableBuilder.setSpans; @RunWith(RobolectricTestRunner.class) @Config(manifest = Config.NONE) @@ -65,11 +68,6 @@ public class SpannableBuilderTest { } } -// @Test -// public void set_spans_position_invalid() { -// // will be silently ignored -// } - @Test public void get_spans() { @@ -165,6 +163,173 @@ public class SpannableBuilderTest { .toList(); } + @Test + public void set_spans_position_invalid() { + // if supplied position is invalid, no spans should be added + + builder.append('0'); + + assertTrue(builder.getSpans(0, builder.length()).isEmpty()); + + setSpans(builder, new Object(), -1, -1); + + assertTrue(builder.getSpans(0, builder.length()).isEmpty()); + } + + @Test + public void set_spans_single() { + // single span as `spans` argument correctly added + + builder.append('0'); + + assertTrue(builder.getSpans(0, builder.length()).isEmpty()); + + final Object span = new Object(); + setSpans(builder, span, 0, 1); + + final List spans = builder.getSpans(0, builder.length()); + assertEquals(1, spans.size()); + assertEquals(span, spans.get(0).what); + } + + @Test + public void set_spans_array_detected() { + // if supplied `spans` argument is an array -> it should be expanded + + builder.append('0'); + + assertTrue(builder.getSpans(0, builder.length()).isEmpty()); + + final Object[] spans = { + new Object(), + new Object(), + new Object() + }; + + setSpans(builder, spans, 0, 1); + + final List actual = builder.getSpans(0, builder.length()); + assertEquals(spans.length, actual.size()); + + for (int i = 0, length = spans.length; i < length; i++) { + assertEquals(spans[i], actual.get(i).what); + } + } + + @Test + public void set_spans_array_of_arrays() { + // if array of arrays is supplied -> it won't be expanded to single elements + + builder.append('0'); + + assertTrue(builder.getSpans(0, builder.length()).isEmpty()); + + final Object[] spans = { + new Object[]{ + new Object(), new Object() + }, + new Object[]{ + new Object(), new Object(), new Object() + } + }; + + setSpans(builder, spans, 0, 1); + + final List actual = builder.getSpans(0, builder.length()); + assertEquals(2, actual.size()); + + for (int i = 0, length = spans.length; i < length; i++) { + assertEquals(spans[i], actual.get(i).what); + } + } + + @Test + public void set_spans_null() { + // if `spans` argument is null, then nothing will be added + + builder.append('0'); + + assertTrue(builder.getSpans(0, builder.length()).isEmpty()); + + setSpans(builder, null, 0, builder.length()); + + assertTrue(builder.getSpans(0, builder.length()).isEmpty()); + } + + @Test + public void spans_reversed() { + // resulting SpannableStringBuilder should have spans reversed + + final Object[] spans = { + 0, + 1, + 2 + }; + + for (Object span : spans) { + builder.append(span.toString(), span); + } + + final SpannableStringBuilder spannableStringBuilder = builder.spannableStringBuilder(); + final Object[] actual = spannableStringBuilder.getSpans(0, builder.length(), Object.class); + + for (int start = 0, length = spans.length, end = length - 1; start < length; start++, end--) { + assertEquals(spans[start], actual[end]); + } + } + + @Test + public void append_spanned_normal() { + // #append is called with regular Spanned content -> spans should be added in reverse + + final SpannableStringBuilder ssb = new SpannableStringBuilder(); + for (int i = 0; i < 3; i++) { + ssb.append(String.valueOf(i)); + ssb.setSpan(i, i, i + 1, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); + } + + assertTrue(builder.getSpans(0, builder.length()).isEmpty()); + + builder.append(ssb); + + assertEquals("012", builder.toString()); + + // this one would return normal order as spans are reversed here +// final List spans = builder.getSpans(0, builder.length()); + + final SpannableStringBuilder spannableStringBuilder = builder.spannableStringBuilder(); + final Object[] spans = spannableStringBuilder.getSpans(0, builder.length(), Object.class); + assertEquals(3, spans.length); + + for (int i = 0, length = spans.length; i < length; i++) { + assertEquals(length - 1 - i, spans[i]); + } + } + + @Test + public void append_spanned_reversed() { + // #append is called with reversed spanned content -> spans should be added as-are + + final SpannableBuilder spannableBuilder = new SpannableBuilder(); + for (int i = 0; i < 3; i++) { + spannableBuilder.append(String.valueOf(i), i); + } + + assertTrue(builder.getSpans(0, builder.length()).isEmpty()); + + builder.append(spannableBuilder.spannableStringBuilder()); + + final SpannableStringBuilder spannableStringBuilder = builder.spannableStringBuilder(); + final Object[] spans = spannableStringBuilder.getSpans(0, spannableStringBuilder.length(), Object.class); + assertEquals(3, spans.length); + + for (int i = 0, length = spans.length; i < length; i++) { + // in the end order should be as we expect in order to properly render it + // (no matter if reversed is used or not) + assertEquals(length - 1 - i, spans[i]); + } + } + private static class Position { @NonNull