Add SpannableBuilder#getSpans method

This commit is contained in:
Dimitry Ivanov 2018-11-04 15:37:17 +03:00
parent cbf9a7b4a6
commit 7a1b76af66
5 changed files with 274 additions and 31 deletions

@ -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

@ -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 +
'}';
}
}
}