diff --git a/README.md b/README.md
index cc471fdd..dee13c57 100644
--- a/README.md
+++ b/README.md
@@ -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
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