V2.0.1 fixed (#83)
* `SpannableMarkdownVisitor` Rename blockQuoteIndent to blockIndent * Fixed block new lines logic for block quote and paragraph (#82) * AsyncDrawable fix no dimensions bug (#81) * Update SpannableTheme to use Px instead of Dimension annotation * Allow TaskListSpan isDone mutation * Updated commonmark-java to 0.12.1 * Add OrderedListItemSpan measure utility method (#78) * Add SpannableBuilder#getSpans method * Fix DataUri scheme handler in image-loader (#74) * Introduced a "copy" builder for SpannableThem Thanks @c-b-h 🙌
This commit is contained in:
parent
e0563dca43
commit
26bfb7f2e2
@ -9,7 +9,7 @@ android:
|
||||
- platform-tools
|
||||
- tools
|
||||
|
||||
- build-tools-27.0.3
|
||||
- build-tools-28.0.3
|
||||
- android-27
|
||||
|
||||
branches:
|
||||
|
16
README.md
16
README.md
@ -7,6 +7,8 @@
|
||||
[](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)
|
||||
|
||||
**Markwon** is a markdown library for Android. It parses markdown
|
||||
following [commonmark-spec] with the help of amazing [commonmark-java]
|
||||
library and renders result as _Android-native_ Spannables. **No HTML**
|
||||
@ -91,6 +93,14 @@ Please visit [documentation] web-site for reference
|
||||
|
||||
[documentation]: https://noties.github.io/Markwon
|
||||
|
||||
---
|
||||
|
||||
## Applications using Markwon
|
||||
|
||||
* [Partiko](https://partiko.app)
|
||||
* [FairNote Notepad](https://play.google.com/store/apps/details?id=com.rgiskard.fairnote)
|
||||
|
||||
|
||||
---
|
||||
|
||||
# Demo
|
||||
@ -277,12 +287,6 @@ ___
|
||||
|
||||
Underscores (`_`)
|
||||
|
||||
---
|
||||
|
||||
## Applications using Markwon
|
||||
|
||||
* [FairNote Notepad](https://play.google.com/store/apps/details?id=com.rgiskard.fairnote)
|
||||
|
||||
|
||||
## License
|
||||
|
||||
|
@ -36,7 +36,6 @@ dependencies {
|
||||
implementation it['okhttp']
|
||||
implementation it['prism4j']
|
||||
implementation it['debug']
|
||||
implementation it['better-link-movement']
|
||||
implementation it['dagger']
|
||||
}
|
||||
|
||||
|
@ -11,7 +11,6 @@ import android.widget.TextView;
|
||||
|
||||
import javax.inject.Inject;
|
||||
|
||||
import me.saket.bettermovementmethod.BetterLinkMovementMethod;
|
||||
import ru.noties.debug.Debug;
|
||||
|
||||
public class MainActivity extends Activity {
|
||||
@ -71,7 +70,7 @@ public class MainActivity extends Activity {
|
||||
@Override
|
||||
public void onMarkdownReady(CharSequence markdown) {
|
||||
|
||||
Markwon.setText(textView, markdown, BetterLinkMovementMethod.getInstance());
|
||||
Markwon.setText(textView, markdown);
|
||||
|
||||
gifProcessor.process(textView);
|
||||
|
||||
|
14
build.gradle
14
build.gradle
@ -1,10 +1,10 @@
|
||||
buildscript {
|
||||
repositories {
|
||||
jcenter()
|
||||
google()
|
||||
jcenter()
|
||||
}
|
||||
dependencies {
|
||||
classpath 'com.android.tools.build:gradle:3.0.1'
|
||||
classpath 'com.android.tools.build:gradle:3.2.1'
|
||||
classpath 'com.github.ben-manes:gradle-versions-plugin:0.20.0'
|
||||
}
|
||||
}
|
||||
@ -14,8 +14,8 @@ allprojects {
|
||||
if (project.hasProperty('LOCAL_MAVEN_URL')) {
|
||||
maven { url LOCAL_MAVEN_URL }
|
||||
}
|
||||
jcenter()
|
||||
google()
|
||||
jcenter()
|
||||
}
|
||||
version = VERSION_NAME
|
||||
group = GROUP
|
||||
@ -26,7 +26,7 @@ task clean(type: Delete) {
|
||||
}
|
||||
|
||||
task wrapper(type: Wrapper) {
|
||||
gradleVersion '4.8.1'
|
||||
gradleVersion '4.10.2'
|
||||
distributionType 'all'
|
||||
}
|
||||
|
||||
@ -40,8 +40,9 @@ if (hasProperty('local')) {
|
||||
|
||||
ext {
|
||||
|
||||
// NB, updating build-tools or compile-sdk will require updating Travis config (.travis.yml)
|
||||
config = [
|
||||
'build-tools' : '27.0.3',
|
||||
'build-tools' : '28.0.3',
|
||||
'compile-sdk' : 27,
|
||||
'target-sdk' : 27,
|
||||
'min-sdk' : 16,
|
||||
@ -49,7 +50,7 @@ ext {
|
||||
]
|
||||
|
||||
final def supportVersion = '27.1.1'
|
||||
final def commonMarkVersion = '0.11.0'
|
||||
final def commonMarkVersion = '0.12.1'
|
||||
final def daggerVersion = '2.10'
|
||||
|
||||
deps = [
|
||||
@ -63,7 +64,6 @@ ext {
|
||||
'okhttp' : 'com.squareup.okhttp3:okhttp:3.9.0',
|
||||
'prism4j' : 'ru.noties:prism4j:1.1.0',
|
||||
'debug' : 'ru.noties:debug:3.0.0@jar',
|
||||
'better-link-movement' : 'me.saket:better-link-movement-method:2.2.0',
|
||||
'dagger' : "com.google.dagger:dagger:$daggerVersion"
|
||||
]
|
||||
|
||||
|
@ -82,6 +82,9 @@ textView.setMovementMethod(LinkMovementMethod.getInstance());
|
||||
Markwon.unscheduleDrawables(textView);
|
||||
Markwon.unscheduleTableRows(textView);
|
||||
|
||||
// @since 2.0.1 we must measure ordered list items _before_ they are rendered
|
||||
OrderedListItemSpan.measure(view, text);
|
||||
|
||||
textView.setText(text);
|
||||
|
||||
Markwon.scheduleDrawables(textView);
|
||||
|
@ -6,7 +6,7 @@ org.gradle.configureondemand=true
|
||||
android.enableBuildCache=true
|
||||
android.buildCacheDir=build/pre-dex-cache
|
||||
|
||||
VERSION_NAME=2.0.0
|
||||
VERSION_NAME=2.0.1-SNAPSHOT
|
||||
|
||||
GROUP=ru.noties
|
||||
POM_DESCRIPTION=Markwon
|
||||
|
BIN
gradle/wrapper/gradle-wrapper.jar
vendored
BIN
gradle/wrapper/gradle-wrapper.jar
vendored
Binary file not shown.
2
gradle/wrapper/gradle-wrapper.properties
vendored
2
gradle/wrapper/gradle-wrapper.properties
vendored
@ -1,5 +1,5 @@
|
||||
distributionBase=GRADLE_USER_HOME
|
||||
distributionPath=wrapper/dists
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-4.10.2-all.zip
|
||||
zipStoreBase=GRADLE_USER_HOME
|
||||
zipStorePath=wrapper/dists
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-4.8.1-all.zip
|
||||
|
@ -3,7 +3,6 @@ package ru.noties.markwon.il;
|
||||
import android.net.Uri;
|
||||
import android.support.annotation.NonNull;
|
||||
import android.support.annotation.Nullable;
|
||||
import android.text.TextUtils;
|
||||
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.util.Collection;
|
||||
@ -19,7 +18,7 @@ public class DataUriSchemeHandler extends SchemeHandler {
|
||||
return new DataUriSchemeHandler(DataUriParser.create(), DataUriDecoder.create());
|
||||
}
|
||||
|
||||
private static final String START = "data://";
|
||||
private static final String START = "data:";
|
||||
|
||||
private final DataUriParser uriParser;
|
||||
private final DataUriDecoder uriDecoder;
|
||||
@ -38,7 +37,12 @@ public class DataUriSchemeHandler extends SchemeHandler {
|
||||
return null;
|
||||
}
|
||||
|
||||
final String part = raw.substring(START.length());
|
||||
String part = raw.substring(START.length());
|
||||
|
||||
// this part is added to support `data://` with which this functionality was released
|
||||
if (part.startsWith("//")) {
|
||||
part = part.substring(2);
|
||||
}
|
||||
|
||||
final DataUri dataUri = uriParser.parse(part);
|
||||
if (dataUri == null) {
|
||||
|
@ -71,6 +71,33 @@ public class DataUriSchemeHandlerTest {
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void correct_real() {
|
||||
|
||||
final class Item {
|
||||
|
||||
final String contentType;
|
||||
final String data;
|
||||
|
||||
Item(String contentType, String data) {
|
||||
this.contentType = contentType;
|
||||
this.data = data;
|
||||
}
|
||||
}
|
||||
|
||||
final Map<String, Item> expected = new HashMap<String, Item>() {{
|
||||
put("data:text/plain;,123", new Item("text/plain", "123"));
|
||||
put("data:image/svg+xml;base64,MTIz", new Item("image/svg+xml", "123"));
|
||||
}};
|
||||
|
||||
for (Map.Entry<String, Item> entry : expected.entrySet()) {
|
||||
final ImageItem item = handler.handle(entry.getKey(), Uri.parse(entry.getKey()));
|
||||
assertNotNull(entry.getKey(), item);
|
||||
assertEquals(entry.getKey(), entry.getValue().contentType, item.contentType());
|
||||
assertEquals(entry.getKey(), entry.getValue().data, readStream(item.inputStream()));
|
||||
}
|
||||
}
|
||||
|
||||
@NonNull
|
||||
private static String readStream(@NonNull InputStream stream) {
|
||||
try {
|
||||
|
@ -15,6 +15,7 @@ import org.commonmark.parser.Parser;
|
||||
import java.util.Arrays;
|
||||
|
||||
import ru.noties.markwon.renderer.SpannableRenderer;
|
||||
import ru.noties.markwon.spans.OrderedListItemSpan;
|
||||
import ru.noties.markwon.tasklist.TaskListExtension;
|
||||
|
||||
@SuppressWarnings({"WeakerAccess", "unused"})
|
||||
@ -100,6 +101,12 @@ public abstract class Markwon {
|
||||
unscheduleDrawables(view);
|
||||
unscheduleTableRows(view);
|
||||
|
||||
// @since 2.0.1 we must measure ordered-list-item-spans before applying text to a TextView.
|
||||
// if markdown has a lot of ordered list items (or text size is relatively big, or block-margin
|
||||
// is relatively small) then this list won't be rendered properly: it will take correct
|
||||
// layout (width and margin) but will be clipped if margin is not _consistent_ between calls.
|
||||
OrderedListItemSpan.measure(view, text);
|
||||
|
||||
// update movement method (for links to be clickable)
|
||||
view.setMovementMethod(movementMethod);
|
||||
view.setText(text);
|
||||
|
@ -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,7 +163,93 @@ public class SpannableBuilder implements Appendable, CharSequence {
|
||||
*/
|
||||
@Override
|
||||
public CharSequence subSequence(int start, int end) {
|
||||
return builder.subSequence(start, end);
|
||||
|
||||
final CharSequence out;
|
||||
|
||||
// @since 2.0.1 we copy spans to resulting subSequence
|
||||
final List<Span> spans = getSpans(start, end);
|
||||
if (spans.isEmpty()) {
|
||||
out = builder.subSequence(start, end);
|
||||
} else {
|
||||
|
||||
// we should not be SpannableStringBuilderReversed here
|
||||
final SpannableStringBuilder builder = new SpannableStringBuilder(this.builder.subSequence(start, end));
|
||||
|
||||
final int length = builder.length();
|
||||
|
||||
int s;
|
||||
int e;
|
||||
|
||||
for (Span span : spans) {
|
||||
|
||||
// we should limit start/end to resulting subSequence length
|
||||
//
|
||||
// for example, originally it was 5-7 and range 5-7 requested
|
||||
// span should have 0-2
|
||||
//
|
||||
// if a span was fully including resulting subSequence it's start and
|
||||
// end must be within 0..length bounds
|
||||
s = Math.max(0, span.start - start);
|
||||
e = Math.max(length, s + (span.end - span.start));
|
||||
|
||||
builder.setSpan(
|
||||
span.what,
|
||||
s,
|
||||
e,
|
||||
span.flags
|
||||
);
|
||||
}
|
||||
out = builder;
|
||||
}
|
||||
|
||||
return out;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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() {
|
||||
@ -173,7 +265,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();
|
||||
|
||||
@ -206,7 +298,7 @@ public class SpannableBuilder implements Appendable, CharSequence {
|
||||
/**
|
||||
* 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
|
||||
* CharSequence and makes the thing more explicit
|
||||
*
|
||||
* @since 2.0.0
|
||||
*/
|
||||
@ -222,13 +314,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 +333,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 +341,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,12 +368,15 @@ 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;
|
||||
int end;
|
||||
final int flags;
|
||||
public final Object what;
|
||||
public int start;
|
||||
public int end;
|
||||
public final int flags;
|
||||
|
||||
Span(@NonNull Object what, int start, int end, int flags) {
|
||||
this.what = what;
|
||||
@ -288,4 +385,13 @@ 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -51,6 +51,14 @@ public class SpannableConfiguration {
|
||||
this.htmlAllowNonClosedTags = builder.htmlAllowNonClosedTags;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a new builder based on this configuration
|
||||
*/
|
||||
@NonNull
|
||||
public Builder newBuilder(@NonNull Context context) {
|
||||
return new Builder(context, this);
|
||||
}
|
||||
|
||||
@NonNull
|
||||
public SpannableTheme theme() {
|
||||
return theme;
|
||||
@ -138,6 +146,21 @@ public class SpannableConfiguration {
|
||||
this.context = context;
|
||||
}
|
||||
|
||||
Builder(@NonNull Context context, @NonNull SpannableConfiguration configuration) {
|
||||
this(context);
|
||||
this.theme = configuration.theme;
|
||||
this.asyncDrawableLoader = configuration.asyncDrawableLoader;
|
||||
this.syntaxHighlight = configuration.syntaxHighlight;
|
||||
this.linkResolver = configuration.linkResolver;
|
||||
this.urlProcessor = configuration.urlProcessor;
|
||||
this.imageSizeResolver = configuration.imageSizeResolver;
|
||||
this.factory = configuration.factory;
|
||||
this.softBreakAddsNewLine = configuration.softBreakAddsNewLine;
|
||||
this.htmlParser = configuration.htmlParser;
|
||||
this.htmlRenderer = configuration.htmlRenderer;
|
||||
this.htmlAllowNonClosedTags = configuration.htmlAllowNonClosedTags;
|
||||
}
|
||||
|
||||
@NonNull
|
||||
public Builder theme(@NonNull SpannableTheme theme) {
|
||||
this.theme = theme;
|
||||
|
@ -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 {
|
||||
}
|
@ -56,7 +56,7 @@ public class SpannableMarkdownVisitor extends AbstractVisitor {
|
||||
private final SpannableTheme theme;
|
||||
private final SpannableFactory factory;
|
||||
|
||||
private int blockQuoteIndent;
|
||||
private int blockIndent;
|
||||
private int listLevel;
|
||||
|
||||
private List<TableRowSpan.Cell> pendingTableRow;
|
||||
@ -105,25 +105,20 @@ public class SpannableMarkdownVisitor extends AbstractVisitor {
|
||||
public void visit(BlockQuote blockQuote) {
|
||||
|
||||
newLine();
|
||||
if (blockQuoteIndent != 0) {
|
||||
builder.append('\n');
|
||||
}
|
||||
|
||||
final int length = builder.length();
|
||||
|
||||
blockQuoteIndent += 1;
|
||||
blockIndent += 1;
|
||||
|
||||
visitChildren(blockQuote);
|
||||
|
||||
setSpan(length, factory.blockQuote(theme));
|
||||
|
||||
blockQuoteIndent -= 1;
|
||||
blockIndent -= 1;
|
||||
|
||||
if (hasNext(blockQuote)) {
|
||||
newLine();
|
||||
if (blockQuoteIndent == 0) {
|
||||
builder.append('\n');
|
||||
}
|
||||
forceNewLine();
|
||||
}
|
||||
}
|
||||
|
||||
@ -180,7 +175,7 @@ public class SpannableMarkdownVisitor extends AbstractVisitor {
|
||||
|
||||
if (hasNext(node)) {
|
||||
newLine();
|
||||
builder.append('\n');
|
||||
forceNewLine();
|
||||
}
|
||||
}
|
||||
|
||||
@ -202,9 +197,7 @@ public class SpannableMarkdownVisitor extends AbstractVisitor {
|
||||
|
||||
if (hasNext(node)) {
|
||||
newLine();
|
||||
if (listLevel == 0 && blockQuoteIndent == 0) {
|
||||
builder.append('\n');
|
||||
}
|
||||
forceNewLine();
|
||||
}
|
||||
}
|
||||
|
||||
@ -213,7 +206,7 @@ public class SpannableMarkdownVisitor extends AbstractVisitor {
|
||||
|
||||
final int length = builder.length();
|
||||
|
||||
blockQuoteIndent += 1;
|
||||
blockIndent += 1;
|
||||
listLevel += 1;
|
||||
|
||||
final Node parent = listItem.getParent();
|
||||
@ -236,7 +229,7 @@ public class SpannableMarkdownVisitor extends AbstractVisitor {
|
||||
setSpan(length, factory.bulletListItem(theme, listLevel - 1));
|
||||
}
|
||||
|
||||
blockQuoteIndent -= 1;
|
||||
blockIndent -= 1;
|
||||
listLevel -= 1;
|
||||
|
||||
if (hasNext(listItem)) {
|
||||
@ -256,7 +249,7 @@ public class SpannableMarkdownVisitor extends AbstractVisitor {
|
||||
|
||||
if (hasNext(thematicBreak)) {
|
||||
newLine();
|
||||
builder.append('\n');
|
||||
forceNewLine();
|
||||
}
|
||||
}
|
||||
|
||||
@ -272,7 +265,7 @@ public class SpannableMarkdownVisitor extends AbstractVisitor {
|
||||
if (hasNext(heading)) {
|
||||
newLine();
|
||||
// after heading we add another line anyway (no additional checks)
|
||||
builder.append('\n');
|
||||
forceNewLine();
|
||||
}
|
||||
}
|
||||
|
||||
@ -298,13 +291,14 @@ public class SpannableMarkdownVisitor extends AbstractVisitor {
|
||||
public void visit(CustomBlock customBlock) {
|
||||
|
||||
if (customBlock instanceof TaskListBlock) {
|
||||
blockQuoteIndent += 1;
|
||||
|
||||
blockIndent += 1;
|
||||
visitChildren(customBlock);
|
||||
blockQuoteIndent -= 1;
|
||||
blockIndent -= 1;
|
||||
|
||||
if (hasNext(customBlock)) {
|
||||
newLine();
|
||||
builder.append('\n');
|
||||
forceNewLine();
|
||||
}
|
||||
|
||||
} else {
|
||||
@ -329,17 +323,17 @@ public class SpannableMarkdownVisitor extends AbstractVisitor {
|
||||
|
||||
final int length = builder.length();
|
||||
|
||||
blockQuoteIndent += listItem.indent();
|
||||
blockIndent += listItem.indent();
|
||||
|
||||
visitChildren(customNode);
|
||||
|
||||
setSpan(length, factory.taskListItem(theme, blockQuoteIndent, listItem.done()));
|
||||
setSpan(length, factory.taskListItem(theme, blockIndent, listItem.done()));
|
||||
|
||||
if (hasNext(customNode)) {
|
||||
newLine();
|
||||
}
|
||||
|
||||
blockQuoteIndent -= listItem.indent();
|
||||
blockIndent -= listItem.indent();
|
||||
|
||||
} else if (!handleTableNodes(customNode)) {
|
||||
super.visit(customNode);
|
||||
@ -358,7 +352,7 @@ public class SpannableMarkdownVisitor extends AbstractVisitor {
|
||||
|
||||
if (hasNext(node)) {
|
||||
newLine();
|
||||
builder.append('\n');
|
||||
forceNewLine();
|
||||
}
|
||||
|
||||
} else if (node instanceof TableRow || node instanceof TableHead) {
|
||||
@ -445,9 +439,7 @@ public class SpannableMarkdownVisitor extends AbstractVisitor {
|
||||
|
||||
if (hasNext(paragraph) && !inTightList) {
|
||||
newLine();
|
||||
if (blockQuoteIndent == 0) {
|
||||
builder.append('\n');
|
||||
}
|
||||
forceNewLine();
|
||||
}
|
||||
}
|
||||
|
||||
@ -518,6 +510,10 @@ public class SpannableMarkdownVisitor extends AbstractVisitor {
|
||||
}
|
||||
}
|
||||
|
||||
private void forceNewLine() {
|
||||
builder.append('\n');
|
||||
}
|
||||
|
||||
private boolean isInTightList(Paragraph paragraph) {
|
||||
final Node parent = paragraph.getParent();
|
||||
if (parent != null) {
|
||||
|
@ -33,6 +33,9 @@ public class AsyncDrawable extends Drawable {
|
||||
private int canvasWidth;
|
||||
private float textSize;
|
||||
|
||||
// @since 2.0.1 for use-cases when image is loaded faster than span is drawn and knows canvas width
|
||||
private boolean waitingForDimensions;
|
||||
|
||||
/**
|
||||
* @since 1.0.1
|
||||
*/
|
||||
@ -98,6 +101,19 @@ public class AsyncDrawable extends Drawable {
|
||||
this.result = result;
|
||||
this.result.setCallback(callback);
|
||||
|
||||
initBounds();
|
||||
}
|
||||
|
||||
private void initBounds() {
|
||||
|
||||
if (canvasWidth == 0) {
|
||||
// we still have no bounds - wait for them
|
||||
waitingForDimensions = true;
|
||||
return;
|
||||
}
|
||||
|
||||
waitingForDimensions = false;
|
||||
|
||||
final Rect bounds = resolveBounds();
|
||||
result.setBounds(bounds);
|
||||
setBounds(bounds);
|
||||
@ -112,6 +128,10 @@ public class AsyncDrawable extends Drawable {
|
||||
public void initWithKnownDimensions(int width, float textSize) {
|
||||
this.canvasWidth = width;
|
||||
this.textSize = textSize;
|
||||
|
||||
if (waitingForDimensions) {
|
||||
initBounds();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -4,10 +4,44 @@ import android.graphics.Canvas;
|
||||
import android.graphics.Paint;
|
||||
import android.support.annotation.NonNull;
|
||||
import android.text.Layout;
|
||||
import android.text.Spanned;
|
||||
import android.text.TextPaint;
|
||||
import android.text.style.LeadingMarginSpan;
|
||||
import android.widget.TextView;
|
||||
|
||||
public class OrderedListItemSpan implements LeadingMarginSpan {
|
||||
|
||||
/**
|
||||
* Process supplied `text` argument and supply TextView paint to all OrderedListItemSpans
|
||||
* in order for them to measure number.
|
||||
* <p>
|
||||
* NB, this method must be called <em>before</em> setting text to a TextView (`TextView#setText`
|
||||
* internally can trigger new Layout creation which will ask for leading margins right away)
|
||||
*
|
||||
* @param textView to which markdown will be applied
|
||||
* @param text parsed markdown to process
|
||||
* @since 2.0.1
|
||||
*/
|
||||
public static void measure(@NonNull TextView textView, @NonNull CharSequence text) {
|
||||
|
||||
if (!(text instanceof Spanned)) {
|
||||
// nothing to do here
|
||||
return;
|
||||
}
|
||||
|
||||
final OrderedListItemSpan[] spans = ((Spanned) text).getSpans(
|
||||
0,
|
||||
text.length(),
|
||||
OrderedListItemSpan.class);
|
||||
|
||||
if (spans != null) {
|
||||
final TextPaint paint = textView.getPaint();
|
||||
for (OrderedListItemSpan span : spans) {
|
||||
span.margin = (int) (paint.measureText(span.number) + .5F);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private final SpannableTheme theme;
|
||||
private final String number;
|
||||
private final Paint paint = ObjectsPool.paint();
|
||||
@ -27,8 +61,8 @@ public class OrderedListItemSpan implements LeadingMarginSpan {
|
||||
|
||||
@Override
|
||||
public int getLeadingMargin(boolean first) {
|
||||
// @since 1.0.3
|
||||
return margin > 0 ? margin : theme.getBlockMargin();
|
||||
// @since 2.0.1 we return maximum value of both (now we should measure number before)
|
||||
return Math.max(margin, theme.getBlockMargin());
|
||||
}
|
||||
|
||||
@Override
|
||||
@ -44,11 +78,16 @@ public class OrderedListItemSpan implements LeadingMarginSpan {
|
||||
|
||||
theme.applyListItemStyle(paint);
|
||||
|
||||
final int numberWidth = (int) (p.measureText(number) + .5F);
|
||||
// if we could force usage of #measure method then we might want skip this measuring here
|
||||
// but this won't hold against new values that a TextView can receive (new text size for
|
||||
// example...)
|
||||
final int numberWidth = (int) (paint.measureText(number) + .5F);
|
||||
|
||||
// @since 1.0.3
|
||||
int width = theme.getBlockMargin();
|
||||
if (numberWidth > width) {
|
||||
// let's keep this logic here in case a user decided not to call #measure and is fine
|
||||
// with current implementation
|
||||
width = numberWidth;
|
||||
margin = numberWidth;
|
||||
} else {
|
||||
|
@ -12,6 +12,7 @@ import android.support.annotation.FloatRange;
|
||||
import android.support.annotation.IntRange;
|
||||
import android.support.annotation.NonNull;
|
||||
import android.support.annotation.Nullable;
|
||||
import android.support.annotation.Px;
|
||||
import android.support.annotation.Size;
|
||||
import android.text.TextPaint;
|
||||
import android.util.TypedValue;
|
||||
@ -600,13 +601,13 @@ public class SpannableTheme {
|
||||
}
|
||||
|
||||
@NonNull
|
||||
public Builder blockMargin(@Dimension int blockMargin) {
|
||||
public Builder blockMargin(@Px int blockMargin) {
|
||||
this.blockMargin = blockMargin;
|
||||
return this;
|
||||
}
|
||||
|
||||
@NonNull
|
||||
public Builder blockQuoteWidth(@Dimension int blockQuoteWidth) {
|
||||
public Builder blockQuoteWidth(@Px int blockQuoteWidth) {
|
||||
this.blockQuoteWidth = blockQuoteWidth;
|
||||
return this;
|
||||
}
|
||||
@ -625,13 +626,13 @@ public class SpannableTheme {
|
||||
}
|
||||
|
||||
@NonNull
|
||||
public Builder bulletListItemStrokeWidth(@Dimension int bulletListItemStrokeWidth) {
|
||||
public Builder bulletListItemStrokeWidth(@Px int bulletListItemStrokeWidth) {
|
||||
this.bulletListItemStrokeWidth = bulletListItemStrokeWidth;
|
||||
return this;
|
||||
}
|
||||
|
||||
@NonNull
|
||||
public Builder bulletWidth(@Dimension int bulletWidth) {
|
||||
public Builder bulletWidth(@Px int bulletWidth) {
|
||||
this.bulletWidth = bulletWidth;
|
||||
return this;
|
||||
}
|
||||
@ -668,7 +669,7 @@ public class SpannableTheme {
|
||||
}
|
||||
|
||||
@NonNull
|
||||
public Builder codeMultilineMargin(@Dimension int codeMultilineMargin) {
|
||||
public Builder codeMultilineMargin(@Px int codeMultilineMargin) {
|
||||
this.codeMultilineMargin = codeMultilineMargin;
|
||||
return this;
|
||||
}
|
||||
@ -680,13 +681,13 @@ public class SpannableTheme {
|
||||
}
|
||||
|
||||
@NonNull
|
||||
public Builder codeTextSize(@Dimension int codeTextSize) {
|
||||
public Builder codeTextSize(@Px int codeTextSize) {
|
||||
this.codeTextSize = codeTextSize;
|
||||
return this;
|
||||
}
|
||||
|
||||
@NonNull
|
||||
public Builder headingBreakHeight(@Dimension int headingBreakHeight) {
|
||||
public Builder headingBreakHeight(@Px int headingBreakHeight) {
|
||||
this.headingBreakHeight = headingBreakHeight;
|
||||
return this;
|
||||
}
|
||||
@ -733,13 +734,13 @@ public class SpannableTheme {
|
||||
}
|
||||
|
||||
@NonNull
|
||||
public Builder thematicBreakHeight(@Dimension int thematicBreakHeight) {
|
||||
public Builder thematicBreakHeight(@Px int thematicBreakHeight) {
|
||||
this.thematicBreakHeight = thematicBreakHeight;
|
||||
return this;
|
||||
}
|
||||
|
||||
@NonNull
|
||||
public Builder tableCellPadding(@Dimension int tableCellPadding) {
|
||||
public Builder tableCellPadding(@Px int tableCellPadding) {
|
||||
this.tableCellPadding = tableCellPadding;
|
||||
return this;
|
||||
}
|
||||
@ -751,7 +752,7 @@ public class SpannableTheme {
|
||||
}
|
||||
|
||||
@NonNull
|
||||
public Builder tableBorderWidth(@Dimension int tableBorderWidth) {
|
||||
public Builder tableBorderWidth(@Px int tableBorderWidth) {
|
||||
this.tableBorderWidth = tableBorderWidth;
|
||||
return this;
|
||||
}
|
||||
@ -775,7 +776,7 @@ public class SpannableTheme {
|
||||
* @since 1.1.1
|
||||
*/
|
||||
@NonNull
|
||||
public Builder tableHeaderRowBackgroundColor(int tableHeaderRowBackgroundColor) {
|
||||
public Builder tableHeaderRowBackgroundColor(@ColorInt int tableHeaderRowBackgroundColor) {
|
||||
this.tableHeaderRowBackgroundColor = tableHeaderRowBackgroundColor;
|
||||
return this;
|
||||
}
|
||||
|
@ -18,7 +18,9 @@ public class TaskListSpan implements LeadingMarginSpan {
|
||||
|
||||
private final SpannableTheme theme;
|
||||
private final int blockIndent;
|
||||
private final boolean isDone;
|
||||
|
||||
// @since 2.0.1 field is NOT final (to allow mutation)
|
||||
private boolean isDone;
|
||||
|
||||
public TaskListSpan(@NonNull SpannableTheme theme, int blockIndent, boolean isDone) {
|
||||
this.theme = theme;
|
||||
@ -26,6 +28,23 @@ public class TaskListSpan implements LeadingMarginSpan {
|
||||
this.isDone = isDone;
|
||||
}
|
||||
|
||||
/**
|
||||
* @since 2.0.1
|
||||
*/
|
||||
public boolean isDone() {
|
||||
return isDone;
|
||||
}
|
||||
|
||||
/**
|
||||
* Update {@link #isDone} property of this span. Please note that this is merely a visual change
|
||||
* which is not changing underlying text in any means.
|
||||
*
|
||||
* @since 2.0.1
|
||||
*/
|
||||
public void setDone(boolean isDone) {
|
||||
this.isDone = isDone;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getLeadingMargin(boolean first) {
|
||||
return theme.getBlockMargin() * blockIndent;
|
||||
|
@ -0,0 +1,359 @@
|
||||
package ru.noties.markwon;
|
||||
|
||||
import android.support.annotation.NonNull;
|
||||
import android.text.SpannableStringBuilder;
|
||||
import android.text.Spanned;
|
||||
|
||||
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;
|
||||
import static ru.noties.markwon.SpannableBuilder.setSpans;
|
||||
|
||||
@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 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();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void set_spans_position_invalid() {
|
||||
// if supplied position is invalid, no spans should be added
|
||||
|
||||
builder.append('0');
|
||||
|
||||
assertTrue(builder.getSpans(0, builder.length()).isEmpty());
|
||||
|
||||
setSpans(builder, new Object(), -1, -1);
|
||||
|
||||
assertTrue(builder.getSpans(0, builder.length()).isEmpty());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void set_spans_single() {
|
||||
// single span as `spans` argument correctly added
|
||||
|
||||
builder.append('0');
|
||||
|
||||
assertTrue(builder.getSpans(0, builder.length()).isEmpty());
|
||||
|
||||
final Object span = new Object();
|
||||
setSpans(builder, span, 0, 1);
|
||||
|
||||
final List<SpannableBuilder.Span> spans = builder.getSpans(0, builder.length());
|
||||
assertEquals(1, spans.size());
|
||||
assertEquals(span, spans.get(0).what);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void set_spans_array_detected() {
|
||||
// if supplied `spans` argument is an array -> it should be expanded
|
||||
|
||||
builder.append('0');
|
||||
|
||||
assertTrue(builder.getSpans(0, builder.length()).isEmpty());
|
||||
|
||||
final Object[] spans = {
|
||||
new Object(),
|
||||
new Object(),
|
||||
new Object()
|
||||
};
|
||||
|
||||
setSpans(builder, spans, 0, 1);
|
||||
|
||||
final List<SpannableBuilder.Span> actual = builder.getSpans(0, builder.length());
|
||||
assertEquals(spans.length, actual.size());
|
||||
|
||||
for (int i = 0, length = spans.length; i < length; i++) {
|
||||
assertEquals(spans[i], actual.get(i).what);
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void set_spans_array_of_arrays() {
|
||||
// if array of arrays is supplied -> it won't be expanded to single elements
|
||||
|
||||
builder.append('0');
|
||||
|
||||
assertTrue(builder.getSpans(0, builder.length()).isEmpty());
|
||||
|
||||
final Object[] spans = {
|
||||
new Object[]{
|
||||
new Object(), new Object()
|
||||
},
|
||||
new Object[]{
|
||||
new Object(), new Object(), new Object()
|
||||
}
|
||||
};
|
||||
|
||||
setSpans(builder, spans, 0, 1);
|
||||
|
||||
final List<SpannableBuilder.Span> actual = builder.getSpans(0, builder.length());
|
||||
assertEquals(2, actual.size());
|
||||
|
||||
for (int i = 0, length = spans.length; i < length; i++) {
|
||||
assertEquals(spans[i], actual.get(i).what);
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void set_spans_null() {
|
||||
// if `spans` argument is null, then nothing will be added
|
||||
|
||||
builder.append('0');
|
||||
|
||||
assertTrue(builder.getSpans(0, builder.length()).isEmpty());
|
||||
|
||||
setSpans(builder, null, 0, builder.length());
|
||||
|
||||
assertTrue(builder.getSpans(0, builder.length()).isEmpty());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void spans_reversed() {
|
||||
// resulting SpannableStringBuilder should have spans reversed
|
||||
|
||||
final Object[] spans = {
|
||||
0,
|
||||
1,
|
||||
2
|
||||
};
|
||||
|
||||
for (Object span : spans) {
|
||||
builder.append(span.toString(), span);
|
||||
}
|
||||
|
||||
final SpannableStringBuilder spannableStringBuilder = builder.spannableStringBuilder();
|
||||
final Object[] actual = spannableStringBuilder.getSpans(0, builder.length(), Object.class);
|
||||
|
||||
for (int start = 0, length = spans.length, end = length - 1; start < length; start++, end--) {
|
||||
assertEquals(spans[start], actual[end]);
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void append_spanned_normal() {
|
||||
// #append is called with regular Spanned content -> spans should be added in reverse
|
||||
|
||||
final SpannableStringBuilder ssb = new SpannableStringBuilder();
|
||||
for (int i = 0; i < 3; i++) {
|
||||
ssb.append(String.valueOf(i));
|
||||
ssb.setSpan(i, i, i + 1, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
|
||||
}
|
||||
|
||||
assertTrue(builder.getSpans(0, builder.length()).isEmpty());
|
||||
|
||||
builder.append(ssb);
|
||||
|
||||
assertEquals("012", builder.toString());
|
||||
|
||||
// this one would return normal order as spans are reversed here
|
||||
// final List<SpannableBuilder.Span> spans = builder.getSpans(0, builder.length());
|
||||
|
||||
final SpannableStringBuilder spannableStringBuilder = builder.spannableStringBuilder();
|
||||
final Object[] spans = spannableStringBuilder.getSpans(0, builder.length(), Object.class);
|
||||
assertEquals(3, spans.length);
|
||||
|
||||
for (int i = 0, length = spans.length; i < length; i++) {
|
||||
assertEquals(length - 1 - i, spans[i]);
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void append_spanned_reversed() {
|
||||
// #append is called with reversed spanned content -> spans should be added as-are
|
||||
|
||||
final SpannableBuilder spannableBuilder = new SpannableBuilder();
|
||||
for (int i = 0; i < 3; i++) {
|
||||
spannableBuilder.append(String.valueOf(i), i);
|
||||
}
|
||||
|
||||
assertTrue(builder.getSpans(0, builder.length()).isEmpty());
|
||||
|
||||
builder.append(spannableBuilder.spannableStringBuilder());
|
||||
|
||||
final SpannableStringBuilder spannableStringBuilder = builder.spannableStringBuilder();
|
||||
final Object[] spans = spannableStringBuilder.getSpans(0, spannableStringBuilder.length(), Object.class);
|
||||
assertEquals(3, spans.length);
|
||||
|
||||
for (int i = 0, length = spans.length; i < length; i++) {
|
||||
// in the end order should be as we expect in order to properly render it
|
||||
// (no matter if reversed is used or not)
|
||||
assertEquals(length - 1 - i, spans[i]);
|
||||
}
|
||||
}
|
||||
|
||||
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 +
|
||||
'}';
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,52 @@
|
||||
package ru.noties.markwon.renderer;
|
||||
|
||||
import org.junit.Test;
|
||||
|
||||
import ru.noties.markwon.SpannableConfiguration;
|
||||
import ru.noties.markwon.SpannableFactory;
|
||||
import ru.noties.markwon.SyntaxHighlight;
|
||||
import ru.noties.markwon.UrlProcessor;
|
||||
import ru.noties.markwon.html.api.MarkwonHtmlParser;
|
||||
import ru.noties.markwon.renderer.html2.MarkwonHtmlRenderer;
|
||||
import ru.noties.markwon.spans.AsyncDrawable;
|
||||
import ru.noties.markwon.spans.LinkSpan;
|
||||
import ru.noties.markwon.spans.SpannableTheme;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.mockito.Mockito.mock;
|
||||
public class SpannableConfigurationTest {
|
||||
|
||||
@Test
|
||||
public void testNewBuilder() {
|
||||
final SpannableConfiguration configuration = SpannableConfiguration
|
||||
.builder(null)
|
||||
.theme(mock(SpannableTheme.class))
|
||||
.asyncDrawableLoader(mock(AsyncDrawable.Loader.class))
|
||||
.syntaxHighlight(mock(SyntaxHighlight.class))
|
||||
.linkResolver(mock(LinkSpan.Resolver.class))
|
||||
.urlProcessor(mock(UrlProcessor.class))
|
||||
.imageSizeResolver(mock(ImageSizeResolver.class))
|
||||
.factory(mock(SpannableFactory.class))
|
||||
.softBreakAddsNewLine(true)
|
||||
.htmlParser(mock(MarkwonHtmlParser.class))
|
||||
.htmlRenderer(mock(MarkwonHtmlRenderer.class))
|
||||
.htmlAllowNonClosedTags(true)
|
||||
.build();
|
||||
|
||||
final SpannableConfiguration newConfiguration = configuration
|
||||
.newBuilder(null)
|
||||
.build();
|
||||
|
||||
assertEquals(configuration.theme(), newConfiguration.theme());
|
||||
assertEquals(configuration.asyncDrawableLoader(), newConfiguration.asyncDrawableLoader());
|
||||
assertEquals(configuration.syntaxHighlight(), newConfiguration.syntaxHighlight());
|
||||
assertEquals(configuration.linkResolver(), newConfiguration.linkResolver());
|
||||
assertEquals(configuration.urlProcessor(), newConfiguration.urlProcessor());
|
||||
assertEquals(configuration.imageSizeResolver(), newConfiguration.imageSizeResolver());
|
||||
assertEquals(configuration.factory(), newConfiguration.factory());
|
||||
assertEquals(configuration.softBreakAddsNewLine(), newConfiguration.softBreakAddsNewLine());
|
||||
assertEquals(configuration.htmlParser(), newConfiguration.htmlParser());
|
||||
assertEquals(configuration.htmlRenderer(), newConfiguration.htmlRenderer());
|
||||
assertEquals(configuration.htmlAllowNonClosedTags(), newConfiguration.htmlAllowNonClosedTags());
|
||||
}
|
||||
}
|
@ -0,0 +1,111 @@
|
||||
package ru.noties.markwon.renderer;
|
||||
|
||||
import android.content.Context;
|
||||
import android.os.Build;
|
||||
import android.support.annotation.NonNull;
|
||||
import android.support.annotation.Nullable;
|
||||
import android.text.SpannableStringBuilder;
|
||||
import android.text.Spanned;
|
||||
|
||||
import org.commonmark.node.FencedCodeBlock;
|
||||
import org.junit.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
import org.robolectric.RobolectricTestRunner;
|
||||
import org.robolectric.annotation.Config;
|
||||
|
||||
import ru.noties.markwon.SpannableBuilder;
|
||||
import ru.noties.markwon.SpannableConfiguration;
|
||||
import ru.noties.markwon.SpannableFactory;
|
||||
import ru.noties.markwon.SyntaxHighlight;
|
||||
import ru.noties.markwon.spans.SpannableTheme;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.ArgumentMatchers.anyBoolean;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
@RunWith(RobolectricTestRunner.class)
|
||||
@Config(manifest = Config.NONE, sdk = {
|
||||
Build.VERSION_CODES.JELLY_BEAN,
|
||||
Build.VERSION_CODES.M,
|
||||
Build.VERSION_CODES.O
|
||||
})
|
||||
public class SyntaxHighlightTest {
|
||||
|
||||
// codeSpan must be before actual highlight spans (true reverse of builder)
|
||||
|
||||
// if we go with path of reversing spans inside SpannableBuilder (which
|
||||
// might extend SpannableStringBuilder like https://github.com/noties/Markwon/pull/71)
|
||||
// then on M (23) codeSpan will always be _before_ actual highlight and thus
|
||||
// no highlight will be present
|
||||
// note that bad behaviour is present on M (emulator/device/robolectric)
|
||||
// other SDKs are added to validate that they do not fail
|
||||
@Test
|
||||
public void test() {
|
||||
|
||||
class Highlight {
|
||||
}
|
||||
|
||||
final Object codeSpan = new Object();
|
||||
|
||||
final SyntaxHighlight highlight = new SyntaxHighlight() {
|
||||
@NonNull
|
||||
@Override
|
||||
public CharSequence highlight(@Nullable String info, @NonNull String code) {
|
||||
final SpannableStringBuilder builder = new SpannableStringBuilder(code);
|
||||
for (int i = 0, length = code.length(); i < length; i++) {
|
||||
builder.setSpan(new Highlight(), i, i + 1, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
|
||||
}
|
||||
return builder;
|
||||
}
|
||||
};
|
||||
|
||||
final SpannableFactory factory = mock(SpannableFactory.class);
|
||||
when(factory.code(any(SpannableTheme.class), anyBoolean())).thenReturn(codeSpan);
|
||||
|
||||
final SpannableConfiguration configuration = SpannableConfiguration.builder(mock(Context.class))
|
||||
.syntaxHighlight(highlight)
|
||||
.factory(factory)
|
||||
.theme(mock(SpannableTheme.class))
|
||||
.build();
|
||||
|
||||
final SpannableBuilder builder = new SpannableBuilder();
|
||||
|
||||
append(builder, "# Header 1\n", new Object());
|
||||
append(builder, "## Header 2\n", new Object());
|
||||
append(builder, "### Header 3\n", new Object());
|
||||
|
||||
final int start = builder.length();
|
||||
|
||||
final SpannableMarkdownVisitor visitor = new SpannableMarkdownVisitor(configuration, builder);
|
||||
final FencedCodeBlock fencedCodeBlock = new FencedCodeBlock();
|
||||
fencedCodeBlock.setLiteral("{code}");
|
||||
|
||||
visitor.visit(fencedCodeBlock);
|
||||
|
||||
final int end = builder.length();
|
||||
|
||||
append(builder, "### Footer 3\n", new Object());
|
||||
append(builder, "## Footer 2\n", new Object());
|
||||
append(builder, "# Footer 1\n", new Object());
|
||||
|
||||
final Object[] spans = builder.spannableStringBuilder().getSpans(start, end, Object.class);
|
||||
|
||||
// each character + code span
|
||||
final int length = fencedCodeBlock.getLiteral().length() + 1;
|
||||
assertEquals(length, spans.length);
|
||||
assertEquals(codeSpan, spans[0]);
|
||||
|
||||
for (int i = 1; i < length; i++) {
|
||||
assertTrue(spans[i] instanceof Highlight);
|
||||
}
|
||||
}
|
||||
|
||||
private static void append(@NonNull SpannableBuilder builder, @NonNull String text, @NonNull Object span) {
|
||||
final int start = builder.length();
|
||||
builder.append(text);
|
||||
builder.setSpan(span, start, builder.length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
|
||||
}
|
||||
}
|
@ -0,0 +1,107 @@
|
||||
package ru.noties.markwon.spans;
|
||||
|
||||
import android.graphics.Canvas;
|
||||
import android.graphics.ColorFilter;
|
||||
import android.graphics.Rect;
|
||||
import android.graphics.drawable.Drawable;
|
||||
import android.support.annotation.NonNull;
|
||||
import android.support.annotation.Nullable;
|
||||
|
||||
import org.junit.Before;
|
||||
import org.junit.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
import org.robolectric.RobolectricTestRunner;
|
||||
import org.robolectric.annotation.Config;
|
||||
|
||||
import ru.noties.markwon.renderer.ImageSize;
|
||||
import ru.noties.markwon.renderer.ImageSizeResolver;
|
||||
import ru.noties.markwon.renderer.ImageSizeResolverDef;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertFalse;
|
||||
import static org.junit.Assert.assertNotNull;
|
||||
import static org.junit.Assert.assertNull;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
import static org.mockito.Mockito.mock;
|
||||
|
||||
@RunWith(RobolectricTestRunner.class)
|
||||
@Config(manifest = Config.NONE)
|
||||
public class AsyncDrawableTest {
|
||||
|
||||
private ImageSizeResolver imageSizeResolver;
|
||||
|
||||
@Before
|
||||
public void before() {
|
||||
imageSizeResolver = new ImageSizeResolverDef();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void no_dimensions_await() {
|
||||
// when drawable have no known dimensions yet, it will await for them
|
||||
|
||||
final AsyncDrawable drawable = new AsyncDrawable("",
|
||||
mock(AsyncDrawable.Loader.class),
|
||||
imageSizeResolver,
|
||||
new ImageSize(new ImageSize.Dimension(100.F, "%"), null));
|
||||
|
||||
final Drawable result = new AbstractDrawable();
|
||||
result.setBounds(0, 0, 0, 0);
|
||||
|
||||
assertFalse(drawable.hasResult());
|
||||
drawable.setResult(result);
|
||||
assertTrue(drawable.hasResult());
|
||||
|
||||
assertTrue(result.getBounds().isEmpty());
|
||||
|
||||
drawable.initWithKnownDimensions(100, 1);
|
||||
assertEquals(
|
||||
new Rect(0, 0, 100, 0),
|
||||
result.getBounds()
|
||||
);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void previous_result_detached() {
|
||||
// when result is present it will be detached (setCallback(null))
|
||||
|
||||
final AsyncDrawable drawable = new AsyncDrawable("",
|
||||
mock(AsyncDrawable.Loader.class),
|
||||
imageSizeResolver,
|
||||
null);
|
||||
|
||||
drawable.setCallback2(mock(Drawable.Callback.class));
|
||||
drawable.initWithKnownDimensions(100, 1);
|
||||
|
||||
final Drawable result1 = new AbstractDrawable();
|
||||
final Drawable result2 = new AbstractDrawable();
|
||||
|
||||
drawable.setResult(result1);
|
||||
assertNotNull(result1.getCallback());
|
||||
drawable.setResult(result2);
|
||||
assertNull(result1.getCallback());
|
||||
assertNotNull(result2.getCallback());
|
||||
}
|
||||
|
||||
private static class AbstractDrawable extends Drawable {
|
||||
|
||||
@Override
|
||||
public void draw(@NonNull Canvas canvas) {
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setAlpha(int alpha) {
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setColorFilter(@Nullable ColorFilter colorFilter) {
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getOpacity() {
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user