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-syntax-highlight%22) | ||||||
| [](http://search.maven.org/#search|ga|1|g%3A%22ru.noties%22%20AND%20a%3A%22markwon-view%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) | [](https://travis-ci.org/noties/Markwon) | ||||||
| 
 | 
 | ||||||
| **Markwon** is a markdown library for Android. It parses markdown | **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.NonNull; | ||||||
| import android.support.annotation.Nullable; | import android.support.annotation.Nullable; | ||||||
|  | import android.support.annotation.VisibleForTesting; | ||||||
| import android.text.SpannableStringBuilder; | import android.text.SpannableStringBuilder; | ||||||
| import android.text.Spanned; | import android.text.Spanned; | ||||||
| 
 | 
 | ||||||
| import java.util.ArrayDeque; | import java.util.ArrayDeque; | ||||||
|  | import java.util.ArrayList; | ||||||
|  | import java.util.Collections; | ||||||
| import java.util.Deque; | import java.util.Deque; | ||||||
| import java.util.Iterator; | import java.util.Iterator; | ||||||
|  | import java.util.List; | ||||||
| 
 | 
 | ||||||
| /** | /** | ||||||
|  * This class is used to _revert_ order of applied spans. Original SpannableStringBuilder |  * 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 |         return end > start | ||||||
|                 && start >= 0 |                 && start >= 0 | ||||||
|                 && end <= length; |                 && end <= length; | ||||||
| @ -157,9 +163,60 @@ public class SpannableBuilder implements Appendable, CharSequence { | |||||||
|      */ |      */ | ||||||
|     @Override |     @Override | ||||||
|     public CharSequence subSequence(int start, int end) { |     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); |         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() { |     public char lastChar() { | ||||||
|         return builder.charAt(length() - 1); |         return builder.charAt(length() - 1); | ||||||
|     } |     } | ||||||
| @ -173,7 +230,7 @@ public class SpannableBuilder implements Appendable, CharSequence { | |||||||
|         final int end = length(); |         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 |         // 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(); |         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 |         // 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) { |         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) { |     private void copySpans(final int index, @Nullable CharSequence cs) { | ||||||
| @ -239,7 +298,7 @@ public class SpannableBuilder implements Appendable, CharSequence { | |||||||
|         if (cs instanceof Spanned) { |         if (cs instanceof Spanned) { | ||||||
| 
 | 
 | ||||||
|             final Spanned spanned = (Spanned) cs; |             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 Object[] spans = spanned.getSpans(0, spanned.length(), Object.class); | ||||||
|             final int length = spans != null |             final int length = spans != null | ||||||
| @ -247,7 +306,7 @@ public class SpannableBuilder implements Appendable, CharSequence { | |||||||
|                     : 0; |                     : 0; | ||||||
| 
 | 
 | ||||||
|             if (length > 0) { |             if (length > 0) { | ||||||
|                 if (reverse) { |                 if (reversed) { | ||||||
|                     Object o; |                     Object o; | ||||||
|                     for (int i = length - 1; i >= 0; i--) { |                     for (int i = length - 1; i >= 0; i--) { | ||||||
|                         o = spans[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; |         final Object what; | ||||||
|         int start; |         int start; | ||||||
| @ -288,4 +350,14 @@ public class SpannableBuilder implements Appendable, CharSequence { | |||||||
|             this.flags = flags; |             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