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