Add SpannableBuilder#getSpans method
This commit is contained in:
		
							parent
							
								
									cbf9a7b4a6
								
							
						
					
					
						commit
						7a1b76af66
					
				| @ -7,7 +7,6 @@ | ||||
| [](http://search.maven.org/#search|ga|1|g%3A%22ru.noties%22%20AND%20a%3A%22markwon-syntax-highlight%22) | ||||
| [](http://search.maven.org/#search|ga|1|g%3A%22ru.noties%22%20AND%20a%3A%22markwon-view%22) | ||||
| 
 | ||||
| [](https://travis-ci.org/noties/Markwon) | ||||
| [](https://travis-ci.org/noties/Markwon) | ||||
| 
 | ||||
| **Markwon** is a markdown library for Android. It parses markdown | ||||
|  | ||||
| @ -2,12 +2,16 @@ package ru.noties.markwon; | ||||
| 
 | ||||
| import android.support.annotation.NonNull; | ||||
| import android.support.annotation.Nullable; | ||||
| import android.support.annotation.VisibleForTesting; | ||||
| import android.text.SpannableStringBuilder; | ||||
| import android.text.Spanned; | ||||
| 
 | ||||
| import java.util.ArrayDeque; | ||||
| import java.util.ArrayList; | ||||
| import java.util.Collections; | ||||
| import java.util.Deque; | ||||
| import java.util.Iterator; | ||||
| import java.util.List; | ||||
| 
 | ||||
| /** | ||||
|  * This class is used to _revert_ order of applied spans. Original SpannableStringBuilder | ||||
| @ -44,7 +48,9 @@ public class SpannableBuilder implements Appendable, CharSequence { | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     private static boolean isPositionValid(int length, int start, int end) { | ||||
|     // @since 2.0.1 package-private visibility for testing | ||||
|     @VisibleForTesting | ||||
|     static boolean isPositionValid(int length, int start, int end) { | ||||
|         return end > start | ||||
|                 && start >= 0 | ||||
|                 && end <= length; | ||||
| @ -157,9 +163,60 @@ 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); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * This method will return all {@link Span} spans that <em>overlap</em> specified range, | ||||
|      * so if for example a 1..9 range is specified some spans might have 0..6 or 0..10 start/end ranges. | ||||
|      * NB spans are returned in reversed order (no in order that we store them internally) | ||||
|      * | ||||
|      * @since 2.0.1 | ||||
|      */ | ||||
|     @NonNull | ||||
|     public List<Span> getSpans(int start, int end) { | ||||
| 
 | ||||
|         final int length = length(); | ||||
| 
 | ||||
|         if (!isPositionValid(length, start, end)) { | ||||
|             // we might as well throw here | ||||
|             return Collections.emptyList(); | ||||
|         } | ||||
| 
 | ||||
|         // all requested | ||||
|         if (start == 0 | ||||
|                 && length == end) { | ||||
|             // but also copy (do not allow external modification) | ||||
|             final List<Span> list = new ArrayList<>(spans); | ||||
|             Collections.reverse(list); | ||||
|             return Collections.unmodifiableList(list); | ||||
|         } | ||||
| 
 | ||||
|         final List<Span> list = new ArrayList<>(0); | ||||
| 
 | ||||
|         final Iterator<Span> iterator = spans.descendingIterator(); | ||||
|         Span span; | ||||
| 
 | ||||
|         while (iterator.hasNext()) { | ||||
|             span = iterator.next(); | ||||
|             // we must execute 2 checks: if overlap with specified range or fully include it | ||||
|             // if span.start is >= range.start -> check if it's before range.end | ||||
|             // if span.end is <= end -> check if it's after range.start | ||||
|             if ( | ||||
|                     (span.start >= start && span.start < end) | ||||
|                             || (span.end <= end && span.end > start) | ||||
|                             || (span.start < start && span.end > end)) { | ||||
|                 list.add(span); | ||||
|             } | ||||
|         } | ||||
| 
 | ||||
|         return Collections.unmodifiableList(list); | ||||
|     } | ||||
| 
 | ||||
|     public char lastChar() { | ||||
|         return builder.charAt(length() - 1); | ||||
|     } | ||||
| @ -173,7 +230,7 @@ public class SpannableBuilder implements Appendable, CharSequence { | ||||
|         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 SpannableStringBuilderReversed impl = new SpannableStringBuilderReversed(builder.subSequence(start, end)); | ||||
| 
 | ||||
|         final Iterator<Span> iterator = spans.iterator(); | ||||
| 
 | ||||
| @ -222,13 +279,15 @@ public class SpannableBuilder implements Appendable, CharSequence { | ||||
| 
 | ||||
|         // 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); | ||||
|         final SpannableStringBuilderReversed reversed = new SpannableStringBuilderReversed(builder); | ||||
| 
 | ||||
|         // NB, as e are using Deque -> iteration will be started with last element | ||||
|         // so, spans will be appearing in the for loop in reverse order | ||||
|         for (Span span : spans) { | ||||
|             impl.setSpan(span.what, span.start, span.end, span.flags); | ||||
|             reversed.setSpan(span.what, span.start, span.end, span.flags); | ||||
|         } | ||||
| 
 | ||||
|         return impl; | ||||
|         return reversed; | ||||
|     } | ||||
| 
 | ||||
|     private void copySpans(final int index, @Nullable CharSequence cs) { | ||||
| @ -239,7 +298,7 @@ public class SpannableBuilder implements Appendable, CharSequence { | ||||
|         if (cs instanceof Spanned) { | ||||
| 
 | ||||
|             final Spanned spanned = (Spanned) cs; | ||||
|             final boolean reverse = spanned instanceof SpannedReversed; | ||||
|             final boolean reversed = spanned instanceof SpannableStringBuilderReversed; | ||||
| 
 | ||||
|             final Object[] spans = spanned.getSpans(0, spanned.length(), Object.class); | ||||
|             final int length = spans != null | ||||
| @ -247,7 +306,7 @@ public class SpannableBuilder implements Appendable, CharSequence { | ||||
|                     : 0; | ||||
| 
 | ||||
|             if (length > 0) { | ||||
|                 if (reverse) { | ||||
|                 if (reversed) { | ||||
|                     Object o; | ||||
|                     for (int i = length - 1; i >= 0; i--) { | ||||
|                         o = spans[i]; | ||||
| @ -274,7 +333,10 @@ public class SpannableBuilder implements Appendable, CharSequence { | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     static class Span { | ||||
|     /** | ||||
|      * @since 2.0.1 made public in order to be returned from `getSpans` method, initially added in 1.0.1 | ||||
|      */ | ||||
|     public static class Span { | ||||
| 
 | ||||
|         final Object what; | ||||
|         int start; | ||||
| @ -288,4 +350,14 @@ public class SpannableBuilder implements Appendable, CharSequence { | ||||
|             this.flags = flags; | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * @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); | ||||
|         } | ||||
|     } | ||||
| } | ||||
|  | ||||
| @ -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); | ||||
|     } | ||||
| } | ||||
| @ -1,9 +0,0 @@ | ||||
| package ru.noties.markwon; | ||||
| 
 | ||||
| import android.text.Spanned; | ||||
| 
 | ||||
| /** | ||||
|  * @since 1.0.1 | ||||
|  */ | ||||
| interface SpannedReversed extends Spanned { | ||||
| } | ||||
| @ -0,0 +1,194 @@ | ||||
| package ru.noties.markwon; | ||||
| 
 | ||||
| import android.support.annotation.NonNull; | ||||
| 
 | ||||
| import org.junit.Before; | ||||
| import org.junit.Test; | ||||
| import org.junit.runner.RunWith; | ||||
| import org.robolectric.RobolectricTestRunner; | ||||
| import org.robolectric.annotation.Config; | ||||
| 
 | ||||
| import java.util.Arrays; | ||||
| import java.util.List; | ||||
| 
 | ||||
| import ix.Ix; | ||||
| import ix.IxFunction; | ||||
| 
 | ||||
| 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; | ||||
| 
 | ||||
| @RunWith(RobolectricTestRunner.class) | ||||
| @Config(manifest = Config.NONE) | ||||
| public class SpannableBuilderTest { | ||||
| 
 | ||||
|     private SpannableBuilder builder; | ||||
| 
 | ||||
|     @Before | ||||
|     public void before() { | ||||
|         builder = new SpannableBuilder(); | ||||
|     } | ||||
| 
 | ||||
|     @Test | ||||
|     public void position_invalid() { | ||||
| 
 | ||||
|         final Position[] positions = { | ||||
|                 Position.of(0, 0, 0), | ||||
|                 Position.of(-1, -1, -1), | ||||
|                 Position.of(0, -1, 1), | ||||
|                 Position.of(1, 1, 1), | ||||
|                 Position.of(0, 0, 10), | ||||
|                 Position.of(10, 10, 0), | ||||
|                 Position.of(10, 5, 2), | ||||
|                 Position.of(5, 1, 1) | ||||
|         }; | ||||
| 
 | ||||
|         for (Position position : positions) { | ||||
|             assertFalse(position.toString(), isPositionValid(position.length, position.start, position.end)); | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     @Test | ||||
|     public void position_valid() { | ||||
| 
 | ||||
|         final Position[] positions = { | ||||
|                 Position.of(1, 0, 1), | ||||
|                 Position.of(2, 0, 1), | ||||
|                 Position.of(2, 1, 2), | ||||
|                 Position.of(10, 0, 10), | ||||
|                 Position.of(7, 6, 7) | ||||
|         }; | ||||
| 
 | ||||
|         for (Position position : positions) { | ||||
|             assertTrue(position.toString(), isPositionValid(position.length, position.start, position.end)); | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
| //    @Test | ||||
| //    public void set_spans_position_invalid() { | ||||
| //        // will be silently ignored | ||||
| //    } | ||||
| 
 | ||||
|     @Test | ||||
|     public void get_spans() { | ||||
| 
 | ||||
|         // all spans that overlap with specified range or spans that include it fully -> should be returned | ||||
| 
 | ||||
|         final int length = 10; | ||||
| 
 | ||||
|         for (int i = 0; i < length; i++) { | ||||
|             builder.append(String.valueOf(i)); | ||||
|         } | ||||
| 
 | ||||
|         for (int start = 0, end = length - 1; start < end; start++, end--) { | ||||
|             builder.setSpan("" + start + "-" + end, start, end); | ||||
|         } | ||||
| 
 | ||||
|         // all (simple check that spans that take range greater that supplied range are also returned) | ||||
|         final List<String> all = Arrays.asList("0-9", "1-8", "2-7", "3-6", "4-5"); | ||||
|         for (int start = 0, end = length - 1; start < end; start++, end--) { | ||||
|             assertEquals( | ||||
|                     "" + start + "-" + end, | ||||
|                     all, | ||||
|                     getSpans(start, end) | ||||
|             ); | ||||
|         } | ||||
| 
 | ||||
|         assertEquals( | ||||
|                 "1-3", | ||||
|                 Arrays.asList("0-9", "1-8", "2-7"), | ||||
|                 getSpans(1, 3) | ||||
|         ); | ||||
| 
 | ||||
|         assertEquals( | ||||
|                 "1-10", | ||||
|                 all, | ||||
|                 getSpans(1, 10) | ||||
|         ); | ||||
| 
 | ||||
|         assertEquals( | ||||
|                 "5-10", | ||||
|                 Arrays.asList("0-9", "1-8", "2-7", "3-6"), | ||||
|                 getSpans(5, 10) | ||||
|         ); | ||||
| 
 | ||||
|         assertEquals( | ||||
|                 "7-10", | ||||
|                 Arrays.asList("0-9", "1-8"), | ||||
|                 getSpans(7, 10) | ||||
|         ); | ||||
|     } | ||||
| 
 | ||||
|     @Test | ||||
|     public void get_spans_out_of_range() { | ||||
| 
 | ||||
|         // let's test that if span.start >= range.start -> it will be less than range.end | ||||
|         // if span.end <= end -> it will be greater than range.start | ||||
| 
 | ||||
|         for (int i = 0; i < 10; i++) { | ||||
|             builder.append(String.valueOf(i)); | ||||
|             builder.setSpan("" + i + "-" + (i + 1), i, i + 1); | ||||
|         } | ||||
| 
 | ||||
|         assertEquals(10, getSpans(0, 10).size()); | ||||
| 
 | ||||
|         // so | ||||
|         //  0-1 | ||||
|         //  1-2 | ||||
|         //  2-3 | ||||
|         //  etc | ||||
| 
 | ||||
|         //noinspection ArraysAsListWithZeroOrOneArgument | ||||
|         assertEquals( | ||||
|                 "0-1", | ||||
|                 Arrays.asList("0-1"), | ||||
|                 getSpans(0, 1) | ||||
|         ); | ||||
| 
 | ||||
|         assertEquals( | ||||
|                 "1-5", | ||||
|                 Arrays.asList("1-2", "2-3", "3-4", "4-5"), | ||||
|                 getSpans(1, 5) | ||||
|         ); | ||||
|     } | ||||
| 
 | ||||
|     @NonNull | ||||
|     private List<String> getSpans(int start, int end) { | ||||
|         return Ix.from(builder.getSpans(start, end)) | ||||
|                 .map(new IxFunction<SpannableBuilder.Span, String>() { | ||||
|                     @Override | ||||
|                     public String apply(SpannableBuilder.Span span) { | ||||
|                         return (String) span.what; | ||||
|                     } | ||||
|                 }) | ||||
|                 .toList(); | ||||
|     } | ||||
| 
 | ||||
|     private static class Position { | ||||
| 
 | ||||
|         @NonNull | ||||
|         static Position of(int length, int start, int end) { | ||||
|             return new Position(length, start, end); | ||||
|         } | ||||
| 
 | ||||
|         final int length; | ||||
|         final int start; | ||||
|         final int end; | ||||
| 
 | ||||
|         private Position(int length, int start, int end) { | ||||
|             this.length = length; | ||||
|             this.start = start; | ||||
|             this.end = end; | ||||
|         } | ||||
| 
 | ||||
|         @Override | ||||
|         public String toString() { | ||||
|             return "Position{" + | ||||
|                     "length=" + length + | ||||
|                     ", start=" + start + | ||||
|                     ", end=" + end + | ||||
|                     '}'; | ||||
|         } | ||||
|     } | ||||
| } | ||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user
	 Dimitry Ivanov
						Dimitry Ivanov