Add TestSpan module
This commit is contained in:
parent
5a18aa3a01
commit
448a620399
24
markwon-test-span/build.gradle
Normal file
24
markwon-test-span/build.gradle
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
apply plugin: 'com.android.library'
|
||||||
|
|
||||||
|
android {
|
||||||
|
|
||||||
|
compileSdkVersion config['compile-sdk']
|
||||||
|
buildToolsVersion config['build-tools']
|
||||||
|
|
||||||
|
defaultConfig {
|
||||||
|
minSdkVersion config['min-sdk']
|
||||||
|
targetSdkVersion config['target-sdk']
|
||||||
|
versionCode 1
|
||||||
|
versionName version
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
dependencies {
|
||||||
|
|
||||||
|
api deps['support-annotations']
|
||||||
|
|
||||||
|
deps['test'].with {
|
||||||
|
api it['junit']
|
||||||
|
api it['ix-java']
|
||||||
|
}
|
||||||
|
}
|
1
markwon-test-span/src/main/AndroidManifest.xml
Normal file
1
markwon-test-span/src/main/AndroidManifest.xml
Normal file
@ -0,0 +1 @@
|
|||||||
|
<manifest package="ru.noties.markwon.test" />
|
@ -0,0 +1,124 @@
|
|||||||
|
package ru.noties.markwon.test;
|
||||||
|
|
||||||
|
import android.support.annotation.NonNull;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Utility class to validate spannable content
|
||||||
|
*
|
||||||
|
* @since 3.0.0
|
||||||
|
*/
|
||||||
|
public abstract class TestSpan {
|
||||||
|
|
||||||
|
@NonNull
|
||||||
|
public static TestSpan.Document document(TestSpan... children) {
|
||||||
|
return new TestSpanDocument(children(children));
|
||||||
|
}
|
||||||
|
|
||||||
|
@NonNull
|
||||||
|
public static TestSpan.Span span(@NonNull String name, TestSpan... children) {
|
||||||
|
return span(name, Collections.<String, Object>emptyMap(), children);
|
||||||
|
}
|
||||||
|
|
||||||
|
@NonNull
|
||||||
|
public static TestSpan.Span span(@NonNull String name, @NonNull Map<String, Object> arguments, TestSpan... children) {
|
||||||
|
return new TestSpanSpan(name, children(children), arguments);
|
||||||
|
}
|
||||||
|
|
||||||
|
@NonNull
|
||||||
|
public static TestSpan.Text text(@NonNull String literal) {
|
||||||
|
return new TestSpanText(literal);
|
||||||
|
}
|
||||||
|
|
||||||
|
@NonNull
|
||||||
|
public static List<TestSpan> children(TestSpan... children) {
|
||||||
|
final int length = children.length;
|
||||||
|
final List<TestSpan> list;
|
||||||
|
if (length == 0) {
|
||||||
|
list = Collections.emptyList();
|
||||||
|
} else if (length == 1) {
|
||||||
|
list = Collections.singletonList(children[0]);
|
||||||
|
} else {
|
||||||
|
final List<TestSpan> spans = new ArrayList<>(length);
|
||||||
|
Collections.addAll(spans, children);
|
||||||
|
list = Collections.unmodifiableList(spans);
|
||||||
|
}
|
||||||
|
return list;
|
||||||
|
}
|
||||||
|
|
||||||
|
@NonNull
|
||||||
|
public static Map<String, Object> args(Object... args) {
|
||||||
|
|
||||||
|
final int length = args.length;
|
||||||
|
if (length == 0) {
|
||||||
|
return Collections.emptyMap();
|
||||||
|
}
|
||||||
|
|
||||||
|
// validate that length is even (k=v)
|
||||||
|
if ((length % 2) != 0) {
|
||||||
|
throw new IllegalStateException("Supplied key-values array must contain " +
|
||||||
|
"even number of arguments");
|
||||||
|
}
|
||||||
|
|
||||||
|
final Map<String, Object> map = new HashMap<>(length / 2 + 1);
|
||||||
|
|
||||||
|
String key;
|
||||||
|
Object value;
|
||||||
|
|
||||||
|
for (int i = 0; i < length; i += 2) {
|
||||||
|
// possible class-cast exception
|
||||||
|
key = (String) args[i];
|
||||||
|
value = args[i + 1];
|
||||||
|
map.put(key, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
return Collections.unmodifiableMap(map);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@NonNull
|
||||||
|
public abstract List<TestSpan> children();
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public abstract int hashCode();
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public abstract boolean equals(Object o);
|
||||||
|
|
||||||
|
|
||||||
|
public static abstract class Document extends TestSpan {
|
||||||
|
|
||||||
|
@NonNull
|
||||||
|
public abstract String wholeText();
|
||||||
|
}
|
||||||
|
|
||||||
|
public static abstract class Text extends TestSpan {
|
||||||
|
|
||||||
|
@NonNull
|
||||||
|
public abstract String literal();
|
||||||
|
|
||||||
|
public abstract int length();
|
||||||
|
}
|
||||||
|
|
||||||
|
public static abstract class Span extends TestSpan {
|
||||||
|
|
||||||
|
@NonNull
|
||||||
|
public abstract String name();
|
||||||
|
|
||||||
|
@NonNull
|
||||||
|
public abstract Map<String, Object> arguments();
|
||||||
|
|
||||||
|
@NonNull
|
||||||
|
@Override
|
||||||
|
public abstract List<TestSpan> children();
|
||||||
|
}
|
||||||
|
|
||||||
|
// package-private constructor
|
||||||
|
TestSpan() {
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,60 @@
|
|||||||
|
package ru.noties.markwon.test;
|
||||||
|
|
||||||
|
import android.support.annotation.NonNull;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
class TestSpanDocument extends TestSpan.Document {
|
||||||
|
|
||||||
|
private static void fillWholeText(@NonNull StringBuilder builder, @NonNull TestSpan span) {
|
||||||
|
if (span instanceof Text) {
|
||||||
|
builder.append(((Text) span).literal());
|
||||||
|
} else if (span instanceof Span) {
|
||||||
|
for (TestSpan child : span.children()) {
|
||||||
|
fillWholeText(builder, child);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
throw new IllegalStateException("Unexpected state. Found unexpected TestSpan " +
|
||||||
|
"object of type `" + span.getClass().getName() + "`");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private final List<TestSpan> children;
|
||||||
|
|
||||||
|
TestSpanDocument(@NonNull List<TestSpan> children) {
|
||||||
|
this.children = children;
|
||||||
|
}
|
||||||
|
|
||||||
|
@NonNull
|
||||||
|
@Override
|
||||||
|
public List<TestSpan> children() {
|
||||||
|
return children;
|
||||||
|
}
|
||||||
|
|
||||||
|
@NonNull
|
||||||
|
@Override
|
||||||
|
public String wholeText() {
|
||||||
|
final StringBuilder builder = new StringBuilder();
|
||||||
|
|
||||||
|
for (TestSpan child : children) {
|
||||||
|
fillWholeText(builder, child);
|
||||||
|
}
|
||||||
|
|
||||||
|
return builder.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean equals(Object o) {
|
||||||
|
if (this == o) return true;
|
||||||
|
if (o == null || getClass() != o.getClass()) return false;
|
||||||
|
|
||||||
|
TestSpanDocument that = (TestSpanDocument) o;
|
||||||
|
|
||||||
|
return children.equals(that.children);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int hashCode() {
|
||||||
|
return children.hashCode();
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,34 @@
|
|||||||
|
package ru.noties.markwon.test;
|
||||||
|
|
||||||
|
import android.support.annotation.NonNull;
|
||||||
|
|
||||||
|
public class TestSpanEnumerator {
|
||||||
|
|
||||||
|
public interface Listener {
|
||||||
|
void onNext(int start, int end, @NonNull TestSpan span);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void enumerate(@NonNull TestSpan.Document document, @NonNull Listener listener) {
|
||||||
|
visit(0, document, listener);
|
||||||
|
}
|
||||||
|
|
||||||
|
private int visit(int start, @NonNull TestSpan span, @NonNull Listener listener) {
|
||||||
|
|
||||||
|
if (span instanceof TestSpan.Text) {
|
||||||
|
final int end = start + ((TestSpan.Text) span).length();
|
||||||
|
listener.onNext(start, end, span);
|
||||||
|
return end;
|
||||||
|
}
|
||||||
|
|
||||||
|
// yeah, we will need end... and from recursive call also -> children can have text inside
|
||||||
|
int s = start;
|
||||||
|
|
||||||
|
for (TestSpan child : span.children()) {
|
||||||
|
s = visit(s, child, listener);
|
||||||
|
}
|
||||||
|
|
||||||
|
listener.onNext(start, s, span);
|
||||||
|
|
||||||
|
return s;
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,131 @@
|
|||||||
|
package ru.noties.markwon.test;
|
||||||
|
|
||||||
|
import android.support.annotation.NonNull;
|
||||||
|
import android.text.Spanned;
|
||||||
|
|
||||||
|
import junit.framework.Assert;
|
||||||
|
import junit.framework.ComparisonFailure;
|
||||||
|
|
||||||
|
import java.util.Locale;
|
||||||
|
import java.util.concurrent.atomic.AtomicInteger;
|
||||||
|
|
||||||
|
import ix.Ix;
|
||||||
|
import ix.IxPredicate;
|
||||||
|
|
||||||
|
public abstract class TestSpanMatcher {
|
||||||
|
|
||||||
|
public static void matches(@NonNull final Spanned spanned, @NonNull TestSpan.Document document) {
|
||||||
|
|
||||||
|
// assert number for spans
|
||||||
|
// assert raw text
|
||||||
|
|
||||||
|
final TestSpanEnumerator enumerator = new TestSpanEnumerator();
|
||||||
|
|
||||||
|
// keep track of total spans encountered
|
||||||
|
final AtomicInteger counter = new AtomicInteger();
|
||||||
|
|
||||||
|
enumerator.enumerate(document, new TestSpanEnumerator.Listener() {
|
||||||
|
@Override
|
||||||
|
public void onNext(final int start, final int end, @NonNull TestSpan span) {
|
||||||
|
if (span instanceof TestSpan.Document) {
|
||||||
|
|
||||||
|
TestSpanMatcher.documentMatches(spanned, (TestSpan.Document) span);
|
||||||
|
} else if (span instanceof TestSpan.Span) {
|
||||||
|
|
||||||
|
// increment span count so after enumeration we match total number of spans
|
||||||
|
counter.incrementAndGet();
|
||||||
|
|
||||||
|
TestSpanMatcher.spanMatches(spanned, start, end, (TestSpan.Span) span);
|
||||||
|
|
||||||
|
} else if (span instanceof TestSpan.Text) {
|
||||||
|
TestSpanMatcher.textMatches(spanned, start, end, (TestSpan.Text) span);
|
||||||
|
} else {
|
||||||
|
// in case we add a new type
|
||||||
|
throw new IllegalStateException("Unexpected type of a TestSpan: `"
|
||||||
|
+ span.getClass().getName() + "`, " + span);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
final Object[] spans = spanned.getSpans(0, spanned.length(), Object.class);
|
||||||
|
Assert.assertEquals("Total spans count", counter.get(), spans.length);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void documentMatches(
|
||||||
|
@NonNull Spanned spanned,
|
||||||
|
@NonNull TestSpan.Document document) {
|
||||||
|
|
||||||
|
// match full text
|
||||||
|
|
||||||
|
final String expected = document.wholeText();
|
||||||
|
final String actual = spanned.toString();
|
||||||
|
|
||||||
|
if (!expected.equals(actual)) {
|
||||||
|
throw new ComparisonFailure(
|
||||||
|
"Document text mismatch",
|
||||||
|
expected,
|
||||||
|
actual);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void spanMatches(
|
||||||
|
@NonNull final Spanned spanned,
|
||||||
|
final int start,
|
||||||
|
final int end,
|
||||||
|
@NonNull TestSpan.Span expected) {
|
||||||
|
|
||||||
|
// when queried multiple spans can be returned (for example if one span
|
||||||
|
// wraps another one. so [0 1 [2 3] 4 5] where [] represents start/end of
|
||||||
|
// a span of same type, when queried for spans at 2-3 position, both will be returned
|
||||||
|
final TestSpan.Span actual = Ix.fromArray(spanned.getSpans(start, end, expected.getClass()))
|
||||||
|
.filter(new IxPredicate<TestSpan.Span>() {
|
||||||
|
@Override
|
||||||
|
public boolean test(TestSpan.Span span) {
|
||||||
|
return start == spanned.getSpanStart(span)
|
||||||
|
&& end == spanned.getSpanEnd(span);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.first(null);
|
||||||
|
|
||||||
|
if (!expected.equals(actual)) {
|
||||||
|
|
||||||
|
final String expectedSpan = expected.arguments().isEmpty()
|
||||||
|
? expected.name()
|
||||||
|
: expected.name() + ": " + expected.arguments();
|
||||||
|
|
||||||
|
final String actualSpan;
|
||||||
|
if (actual == null) {
|
||||||
|
actualSpan = "null";
|
||||||
|
} else {
|
||||||
|
actualSpan = actual.arguments().isEmpty()
|
||||||
|
? actual.name()
|
||||||
|
: actual.name() + ": " + actual.arguments();
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new AssertionError(
|
||||||
|
String.format(Locale.US, "Expected span{%s} at {start: %d, end: %d}, found: %s",
|
||||||
|
expectedSpan, start, end, actualSpan));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void textMatches(
|
||||||
|
@NonNull Spanned spanned,
|
||||||
|
int start,
|
||||||
|
int end,
|
||||||
|
@NonNull TestSpan.Text text) {
|
||||||
|
|
||||||
|
final String expected = text.literal();
|
||||||
|
final String actual = spanned.subSequence(start, end).toString();
|
||||||
|
|
||||||
|
if (!expected.equals(actual)) {
|
||||||
|
throw new ComparisonFailure(
|
||||||
|
String.format(Locale.US, "Text mismatch at {start: %d, end: %d}", start, end),
|
||||||
|
expected,
|
||||||
|
actual
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private TestSpanMatcher() {
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,60 @@
|
|||||||
|
package ru.noties.markwon.test;
|
||||||
|
|
||||||
|
import android.support.annotation.NonNull;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
class TestSpanSpan extends TestSpan.Span {
|
||||||
|
|
||||||
|
private final String name;
|
||||||
|
private final List<TestSpan> children;
|
||||||
|
private final Map<String, Object> arguments;
|
||||||
|
|
||||||
|
public TestSpanSpan(
|
||||||
|
@NonNull String name,
|
||||||
|
@NonNull List<TestSpan> children,
|
||||||
|
@NonNull Map<String, Object> arguments) {
|
||||||
|
this.name = name;
|
||||||
|
this.children = children;
|
||||||
|
this.arguments = arguments;
|
||||||
|
}
|
||||||
|
|
||||||
|
@NonNull
|
||||||
|
@Override
|
||||||
|
public String name() {
|
||||||
|
return name;
|
||||||
|
}
|
||||||
|
|
||||||
|
@NonNull
|
||||||
|
@Override
|
||||||
|
public Map<String, Object> arguments() {
|
||||||
|
return arguments;
|
||||||
|
}
|
||||||
|
|
||||||
|
@NonNull
|
||||||
|
@Override
|
||||||
|
public List<TestSpan> children() {
|
||||||
|
return children;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean equals(Object o) {
|
||||||
|
if (this == o) return true;
|
||||||
|
if (o == null || getClass() != o.getClass()) return false;
|
||||||
|
|
||||||
|
TestSpanSpan that = (TestSpanSpan) o;
|
||||||
|
|
||||||
|
if (!name.equals(that.name)) return false;
|
||||||
|
if (!children.equals(that.children)) return false;
|
||||||
|
return arguments.equals(that.arguments);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int hashCode() {
|
||||||
|
int result = name.hashCode();
|
||||||
|
result = 31 * result + children.hashCode();
|
||||||
|
result = 31 * result + arguments.hashCode();
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,47 @@
|
|||||||
|
package ru.noties.markwon.test;
|
||||||
|
|
||||||
|
import android.support.annotation.NonNull;
|
||||||
|
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
class TestSpanText extends TestSpan.Text {
|
||||||
|
|
||||||
|
private final String literal;
|
||||||
|
|
||||||
|
TestSpanText(@NonNull String literal) {
|
||||||
|
this.literal = literal;
|
||||||
|
}
|
||||||
|
|
||||||
|
@NonNull
|
||||||
|
@Override
|
||||||
|
public String literal() {
|
||||||
|
return literal;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int length() {
|
||||||
|
return literal.length();
|
||||||
|
}
|
||||||
|
|
||||||
|
@NonNull
|
||||||
|
@Override
|
||||||
|
public List<TestSpan> children() {
|
||||||
|
return Collections.emptyList();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean equals(Object o) {
|
||||||
|
if (this == o) return true;
|
||||||
|
if (o == null || getClass() != o.getClass()) return false;
|
||||||
|
|
||||||
|
TestSpanText that = (TestSpanText) o;
|
||||||
|
|
||||||
|
return literal.equals(that.literal);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int hashCode() {
|
||||||
|
return literal.hashCode();
|
||||||
|
}
|
||||||
|
}
|
@ -10,5 +10,6 @@ include ':app',
|
|||||||
':markwon-syntax-highlight',
|
':markwon-syntax-highlight',
|
||||||
':markwon-html',
|
':markwon-html',
|
||||||
':markwon-view',
|
':markwon-view',
|
||||||
|
':markwon-test-span',
|
||||||
':sample-custom-extension',
|
':sample-custom-extension',
|
||||||
':sample-latex-math'
|
':sample-latex-math'
|
||||||
|
Loading…
x
Reference in New Issue
Block a user