Replaced the implementation of SpannableBuilder

SpannableBuilder will now extend SpannableStringBuilder. The major diff-
erence will be that the span order will be reverted upon a call of
`getSpans()`. Another addition will be support for setting multiple
multiple spans by array.

The reverse order implementation is credited Uncodin/bypass
This commit is contained in:
Cyrus Bakhtiari-Haftlang 2018-10-01 11:04:57 +02:00
parent e0563dca43
commit 02fc054ad9
14 changed files with 81 additions and 302 deletions

View File

@ -5,41 +5,52 @@ import android.support.annotation.Nullable;
import android.text.SpannableStringBuilder; import android.text.SpannableStringBuilder;
import android.text.Spanned; import android.text.Spanned;
import java.util.ArrayDeque;
import java.util.Deque;
import java.util.Iterator;
/** /**
* This class is used to _revert_ order of applied spans. Original SpannableStringBuilder * This class is used to _revert_ order of applied spans. Original SpannableStringBuilder
* is using an array to store all the information about spans. So, a span that is added first * is using an array to store all the information about spans. So, a span that is added first
* will be drawn first, which leads to subtle bugs (spans receive wrong `x` values when * will be drawn first, which leads to subtle bugs (spans receive wrong `x` values when
* requested to draw itself) * requested to draw itself)
* <p>
* since 2.0.0 implements Appendable and CharSequence
*
* @since 1.0.1
*/ */
@SuppressWarnings({"WeakerAccess", "unused"}) public class SpannableBuilder extends SpannableStringBuilder {
public class SpannableBuilder implements Appendable, CharSequence {
public SpannableBuilder() {
super();
}
public SpannableBuilder(CharSequence text, int start, int end) {
super(text, start, end);
}
@Override
public <T> T[] getSpans(int queryStart, int queryEnd, Class<T> kind) {
T[] ret = super.getSpans(queryStart, queryEnd, kind);
reverse(ret);
return ret;
}
/** /**
* @since 2.0.0 * Convenience for allowing {@link Nullable} and {@link NonNull} spans, as well as
* {@link NonNull} array of spans. {@link Spanned#SPAN_EXCLUSIVE_EXCLUSIVE} will be applied as
* flag.
*
* @param spans A span object, an array of span objects or null
* @param start Start index (inclusive)
* @param end End index (exclusive)
*/ */
public static void setSpans(@NonNull SpannableBuilder builder, @Nullable Object spans, int start, int end) { public void setSpans(@Nullable Object spans, int start, int end) {
if (spans != null) { if (spans != null) {
// setting a span for an invalid position can lead to silent fail (no exception, // setting a span for an invalid position can lead to silent fail (no exception,
// but execution is stopped) // but execution is stopped)
if (!isPositionValid(builder.length(), start, end)) { if (!isPositionValid(length(), start, end)) {
return; return;
} }
if (spans.getClass().isArray()) { if (spans.getClass().isArray()) {
for (Object o : ((Object[]) spans)) { for (Object o : ((Object[]) spans)) {
builder.setSpan(o, start, end, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); setSpan(o, start, end, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
} }
} else { } else {
builder.setSpan(spans, start, end, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); setSpan(spans, start, end, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
} }
} }
} }
@ -50,242 +61,20 @@ public class SpannableBuilder implements Appendable, CharSequence {
&& end <= length; && end <= length;
} }
private static void reverse(Object[] arr) {
private final StringBuilder builder; if (arr == null) {
return;
// actually we might be just using ArrayList
private final Deque<Span> spans = new ArrayDeque<>(8);
public SpannableBuilder() {
this("");
}
public SpannableBuilder(@NonNull CharSequence cs) {
this.builder = new StringBuilder(cs);
copySpans(0, cs);
}
/**
* Additional method that takes a String, which is proven to NOT contain any spans
*
* @param text String to append
* @return this instance
*/
@NonNull
public SpannableBuilder append(@NonNull String text) {
builder.append(text);
return this;
}
@NonNull
@Override
public SpannableBuilder append(char c) {
builder.append(c);
return this;
}
@NonNull
@Override
public SpannableBuilder append(@NonNull CharSequence cs) {
copySpans(length(), cs);
builder.append(cs);
return this;
}
/**
* @since 2.0.0 to follow Appendable interface
*/
@NonNull
@Override
public SpannableBuilder append(CharSequence csq, int start, int end) {
final CharSequence cs = csq.subSequence(start, end);
copySpans(length(), cs);
builder.append(cs);
return this;
}
@NonNull
public SpannableBuilder append(@NonNull CharSequence cs, @NonNull Object span) {
final int length = length();
append(cs);
setSpan(span, length);
return this;
}
@NonNull
public SpannableBuilder append(@NonNull CharSequence cs, @NonNull Object span, int flags) {
final int length = length();
append(cs);
setSpan(span, length, length(), flags);
return this;
}
@NonNull
public SpannableBuilder setSpan(@NonNull Object span, int start) {
return setSpan(span, start, length());
}
@NonNull
public SpannableBuilder setSpan(@NonNull Object span, int start, int end) {
return setSpan(span, start, end, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
}
@NonNull
public SpannableBuilder setSpan(@NonNull Object span, int start, int end, int flags) {
spans.push(new Span(span, start, end, flags));
return this;
}
@Override
public int length() {
return builder.length();
}
@Override
public char charAt(int index) {
return builder.charAt(index);
}
/**
* @since 2.0.0 to follow CharSequence interface
*/
@Override
public CharSequence subSequence(int start, int end) {
return builder.subSequence(start, end);
}
public char lastChar() {
return builder.charAt(length() - 1);
}
@NonNull
public CharSequence removeFromEnd(int start) {
// this method is not intended to be used by clients
// it's a workaround to support tables
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 Iterator<Span> iterator = spans.iterator();
Span span;
while (iterator.hasNext() && ((span = iterator.next())) != null) {
if (span.start >= start && span.end <= end) {
impl.setSpan(span.what, span.start - start, span.end - start, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
iterator.remove();
}
} }
builder.replace(start, end, ""); int i = 0;
int j = arr.length - 1;
return impl; Object tmp;
} while (j > i) {
tmp = arr[j];
@Override arr[j] = arr[i];
@NonNull arr[i] = tmp;
public String toString() { j--;
return builder.toString(); i++;
}
@NonNull
public CharSequence text() {
// @since 2.0.0 redirects this call to `#spannableStringBuilder()`
return spannableStringBuilder();
}
/**
* Simple method to create a SpannableStringBuilder, which is created anyway. Unlike {@link #text()}
* method which returns the same SpannableStringBuilder there is no need to cast the resulting
* CharSequence
*
* @since 2.0.0
*/
@NonNull
public SpannableStringBuilder spannableStringBuilder() {
// okay, in order to not allow external modification and keep our spans order
// we should not return our builder
//
// plus, if this method was called -> all spans would be applied, which potentially
// breaks the order that we intend to use
// so, we will defensively copy builder
// 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);
for (Span span : spans) {
impl.setSpan(span.what, span.start, span.end, span.flags);
}
return impl;
}
private void copySpans(final int index, @Nullable CharSequence cs) {
// we must identify already reversed Spanned...
// and (!) iterate backwards when adding (to preserve order)
if (cs instanceof Spanned) {
final Spanned spanned = (Spanned) cs;
final boolean reverse = spanned instanceof SpannedReversed;
final Object[] spans = spanned.getSpans(0, spanned.length(), Object.class);
final int length = spans != null
? spans.length
: 0;
if (length > 0) {
if (reverse) {
Object o;
for (int i = length - 1; i >= 0; i--) {
o = spans[i];
setSpan(
o,
index + spanned.getSpanStart(o),
index + spanned.getSpanEnd(o),
spanned.getSpanFlags(o)
);
}
} else {
Object o;
for (int i = 0; i < length; i++) {
o = spans[i];
setSpan(
o,
index + spanned.getSpanStart(o),
index + spanned.getSpanEnd(o),
spanned.getSpanFlags(o)
);
}
}
}
} }
} }
}
static class Span {
final Object what;
int start;
int end;
final int flags;
Span(@NonNull Object what, int start, int end, int flags) {
this.what = what;
this.start = start;
this.end = end;
this.flags = flags;
}
}
}

View File

@ -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);
}
}

View File

@ -1,9 +0,0 @@
package ru.noties.markwon;
import android.text.Spanned;
/**
* @since 1.0.1
*/
interface SpannedReversed extends Spanned {
}

View File

@ -415,7 +415,7 @@ public class SpannableMarkdownVisitor extends AbstractVisitor {
pendingTableRow.add(new TableRowSpan.Cell( pendingTableRow.add(new TableRowSpan.Cell(
tableCellAlignment(cell.getAlignment()), tableCellAlignment(cell.getAlignment()),
builder.removeFromEnd(length) removeFromEnd(length)
)); ));
tableRowIsHeader = cell.isHeader(); tableRowIsHeader = cell.isHeader();
@ -508,12 +508,13 @@ public class SpannableMarkdownVisitor extends AbstractVisitor {
} }
private void setSpan(int start, @Nullable Object span) { private void setSpan(int start, @Nullable Object span) {
SpannableBuilder.setSpans(builder, span, start, builder.length()); builder.setSpans(span, start, builder.length());
} }
private void newLine() { private void newLine() {
if (builder.length() > 0 final int length = builder.length();
&& '\n' != builder.lastChar()) {
if (length > 0 && '\n' != builder.charAt(length - 1)) {
builder.append('\n'); builder.append('\n');
} }
} }
@ -530,6 +531,21 @@ public class SpannableMarkdownVisitor extends AbstractVisitor {
return false; return false;
} }
@NonNull
public CharSequence removeFromEnd(int start) {
// this method is not intended to be used by clients
// it's a workaround to support tables
final int end = builder.length();
// as we do not expose builder and do no apply spans to it, we are safe to NOT to convert to String
final SpannableBuilder impl = new SpannableBuilder(builder, start, end);
builder.delete(start, end);
return impl;
}
@TableRowSpan.Alignment @TableRowSpan.Alignment
private static int tableCellAlignment(TableCell.Alignment alignment) { private static int tableCellAlignment(TableCell.Alignment alignment) {
final int out; final int out;

View File

@ -10,9 +10,9 @@ import ru.noties.markwon.SpannableConfiguration;
public class SpannableRenderer { public class SpannableRenderer {
@NonNull @NonNull
public CharSequence render(@NonNull SpannableConfiguration configuration, @NonNull Node node) { public SpannableBuilder render(@NonNull SpannableConfiguration configuration, @NonNull Node node) {
final SpannableBuilder builder = new SpannableBuilder(); final SpannableBuilder builder = new SpannableBuilder();
node.accept(new SpannableMarkdownVisitor(configuration, builder)); node.accept(new SpannableMarkdownVisitor(configuration, builder));
return builder.text(); return builder;
} }
} }

View File

@ -18,8 +18,7 @@ public class BlockquoteHandler extends TagHandler {
visitChildren(configuration, builder, tag.getAsBlock()); visitChildren(configuration, builder, tag.getAsBlock());
} }
SpannableBuilder.setSpans( builder.setSpans(
builder,
configuration.factory().blockQuote(configuration.theme()), configuration.factory().blockQuote(configuration.theme()),
tag.start(), tag.start(),
tag.end() tag.end()

View File

@ -48,7 +48,7 @@ public class ListHandler extends TagHandler {
bulletLevel bulletLevel
); );
} }
SpannableBuilder.setSpans(builder, spans, child.start(), child.end()); builder.setSpans(spans, child.start(), child.end());
} }
} }
} }

View File

@ -14,9 +14,6 @@ public abstract class SimpleTagHandler extends TagHandler {
@Override @Override
public void handle(@NonNull SpannableConfiguration configuration, @NonNull SpannableBuilder builder, @NonNull HtmlTag tag) { public void handle(@NonNull SpannableConfiguration configuration, @NonNull SpannableBuilder builder, @NonNull HtmlTag tag) {
final Object spans = getSpans(configuration, tag); builder.setSpans(getSpans(configuration, tag), tag.start(), tag.end());
if (spans != null) {
SpannableBuilder.setSpans(builder, spans, tag.start(), tag.end());
}
} }
} }

View File

@ -18,8 +18,7 @@ public class StrikeHandler extends TagHandler {
visitChildren(configuration, builder, tag.getAsBlock()); visitChildren(configuration, builder, tag.getAsBlock());
} }
SpannableBuilder.setSpans( builder.setSpans(
builder,
configuration.factory().strikethrough(), configuration.factory().strikethrough(),
tag.start(), tag.start(),
tag.end() tag.end()

View File

@ -21,8 +21,7 @@ public class UnderlineHandler extends TagHandler {
visitChildren(configuration, builder, tag.getAsBlock()); visitChildren(configuration, builder, tag.getAsBlock());
} }
SpannableBuilder.setSpans( builder.setSpans(
builder,
configuration.factory().underline(), configuration.factory().underline(),
tag.start(), tag.start(),
tag.end() tag.end()

View File

@ -1,7 +1,6 @@
package ru.noties.markwon.renderer.visitor; package ru.noties.markwon.renderer.visitor;
import android.support.annotation.NonNull; import android.support.annotation.NonNull;
import android.text.SpannableStringBuilder;
import org.commonmark.node.Node; import org.commonmark.node.Node;
import org.junit.Test; import org.junit.Test;
@ -50,20 +49,19 @@ public class SpannableMarkdownVisitorTest {
final Node node = Markwon.createParser().parse(data.input()); final Node node = Markwon.createParser().parse(data.input());
node.accept(visitor); node.accept(visitor);
final SpannableStringBuilder stringBuilder = builder.spannableStringBuilder();
final TestValidator validator = TestValidator.create(file); final TestValidator validator = TestValidator.create(file);
int index = 0; int index = 0;
for (TestNode testNode : data.output()) { for (TestNode testNode : data.output()) {
index = validator.validate(stringBuilder, index, testNode); index = validator.validate(builder, index, testNode);
} }
// assert that the whole thing is processed // assert that the whole thing is processed
assertEquals("`" + stringBuilder + "`", stringBuilder.length(), index); assertEquals("`" + builder + "`", builder.length(), index);
final Object[] spans = stringBuilder.getSpans(0, stringBuilder.length(), Object.class); final Object[] spans = builder.getSpans(0, builder.length(), Object.class);
final int length = spans != null final int length = spans != null
? spans.length ? spans.length
: 0; : 0;

View File

@ -1,6 +1,7 @@
package ru.noties.markwon.sample.extension; package ru.noties.markwon.sample.extension;
import android.support.annotation.NonNull; import android.support.annotation.NonNull;
import android.text.Spanned;
import android.text.TextUtils; import android.text.TextUtils;
import android.widget.TextView; import android.widget.TextView;
@ -51,9 +52,14 @@ public class IconVisitor extends SpannableMarkdownVisitor {
final int length = builder.length(); final int length = builder.length();
builder.append(name); builder.append(name);
builder.setSpan(iconSpanProvider.provide(name, color, size), length); builder.setSpan(
iconSpanProvider.provide(name, color, size),
length,
length,
Spanned.SPAN_EXCLUSIVE_EXCLUSIVE
);
builder.append(' '); builder.append(' ');
return true; return true;
} }
} }

View File

@ -70,6 +70,6 @@ public class MainActivity extends Activity {
node.accept(visitor); node.accept(visitor);
// apply // apply
textView.setText(builder.text()); textView.setText(builder);
} }
} }

View File

@ -8,7 +8,6 @@ import org.commonmark.node.CustomBlock;
import org.commonmark.node.Node; import org.commonmark.node.Node;
import org.commonmark.parser.Parser; import org.commonmark.parser.Parser;
import ru.noties.jlatexmath.JLatexMathAndroid;
import ru.noties.markwon.Markwon; import ru.noties.markwon.Markwon;
import ru.noties.markwon.SpannableBuilder; import ru.noties.markwon.SpannableBuilder;
import ru.noties.markwon.SpannableConfiguration; import ru.noties.markwon.SpannableConfiguration;
@ -83,8 +82,7 @@ public class MainActivity extends Activity {
final int length = builder.length(); final int length = builder.length();
builder.append(latex); builder.append(latex);
SpannableBuilder.setSpans( builder.setSpans(
builder,
configuration.factory().image( configuration.factory().image(
configuration.theme(), configuration.theme(),
JLatexMathMedia.makeDestination(latex), JLatexMathMedia.makeDestination(latex),
@ -100,6 +98,6 @@ public class MainActivity extends Activity {
}; };
node.accept(visitor); node.accept(visitor);
Markwon.setText(textView, builder.text()); Markwon.setText(textView, builder);
} }
} }