From 7a1b76af6679bcad87c91ff725e0048c66c46d72 Mon Sep 17 00:00:00 2001 From: Dimitry Ivanov Date: Sun, 4 Nov 2018 15:37:17 +0300 Subject: [PATCH] Add SpannableBuilder#getSpans method --- README.md | 1 - .../ru/noties/markwon/SpannableBuilder.java | 88 +++++++- .../markwon/SpannableStringBuilderImpl.java | 13 -- .../ru/noties/markwon/SpannedReversed.java | 9 - .../noties/markwon/SpannableBuilderTest.java | 194 ++++++++++++++++++ 5 files changed, 274 insertions(+), 31 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 create mode 100644 markwon/src/test/java/ru/noties/markwon/SpannableBuilderTest.java diff --git a/README.md b/README.md index cc471fdd..dee13c57 100644 --- a/README.md +++ b/README.md @@ -7,7 +7,6 @@ [![markwon-syntax-highlight](https://img.shields.io/maven-central/v/ru.noties/markwon-syntax-highlight.svg?label=markwon-syntax-highlight)](http://search.maven.org/#search|ga|1|g%3A%22ru.noties%22%20AND%20a%3A%22markwon-syntax-highlight%22) [![markwon-view](https://img.shields.io/maven-central/v/ru.noties/markwon-view.svg?label=markwon-view)](http://search.maven.org/#search|ga|1|g%3A%22ru.noties%22%20AND%20a%3A%22markwon-view%22) -[![Build Status](https://travis-ci.org/noties/Markwon.svg?branch=master)](https://travis-ci.org/noties/Markwon) [![Build Status](https://travis-ci.org/noties/Markwon.svg?branch=master)](https://travis-ci.org/noties/Markwon) **Markwon** is a markdown library for Android. It parses markdown diff --git a/markwon/src/main/java/ru/noties/markwon/SpannableBuilder.java b/markwon/src/main/java/ru/noties/markwon/SpannableBuilder.java index 9e3ec713..715c3c92 100644 --- a/markwon/src/main/java/ru/noties/markwon/SpannableBuilder.java +++ b/markwon/src/main/java/ru/noties/markwon/SpannableBuilder.java @@ -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 overlap 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 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 list = new ArrayList<>(spans); + Collections.reverse(list); + return Collections.unmodifiableList(list); + } + + final List list = new ArrayList<>(0); + + final Iterator 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 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); + } + } } 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/test/java/ru/noties/markwon/SpannableBuilderTest.java b/markwon/src/test/java/ru/noties/markwon/SpannableBuilderTest.java new file mode 100644 index 00000000..d94e17dc --- /dev/null +++ b/markwon/src/test/java/ru/noties/markwon/SpannableBuilderTest.java @@ -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 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 getSpans(int start, int end) { + return Ix.from(builder.getSpans(start, end)) + .map(new IxFunction() { + @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 + + '}'; + } + } +} \ No newline at end of file