From 26bfb7f2e275b93f3ac5b51ac52181fbd702befc Mon Sep 17 00:00:00 2001 From: Dimitry Date: Mon, 10 Dec 2018 14:58:40 +0300 Subject: [PATCH 1/2] V2.0.1 fixed (#83) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * `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 🙌 --- .travis.yml | 2 +- README.md | 16 +- app/build.gradle | 1 - .../java/ru/noties/markwon/MainActivity.java | 3 +- build.gradle | 14 +- docs/docs/getting-started.md | 3 + gradle.properties | 2 +- gradle/wrapper/gradle-wrapper.jar | Bin 54329 -> 54417 bytes gradle/wrapper/gradle-wrapper.properties | 2 +- .../markwon/il/DataUriSchemeHandler.java | 10 +- .../markwon/il/DataUriSchemeHandlerTest.java | 27 ++ .../main/java/ru/noties/markwon/Markwon.java | 7 + .../ru/noties/markwon/SpannableBuilder.java | 134 ++++++- .../markwon/SpannableConfiguration.java | 23 ++ .../markwon/SpannableStringBuilderImpl.java | 13 - .../ru/noties/markwon/SpannedReversed.java | 9 - .../renderer/SpannableMarkdownVisitor.java | 50 ++- .../noties/markwon/spans/AsyncDrawable.java | 20 + .../markwon/spans/OrderedListItemSpan.java | 45 ++- .../noties/markwon/spans/SpannableTheme.java | 23 +- .../ru/noties/markwon/spans/TaskListSpan.java | 21 +- .../noties/markwon/SpannableBuilderTest.java | 359 ++++++++++++++++++ .../renderer/SpannableConfigurationTest.java | 52 +++ .../markwon/renderer/SyntaxHighlightTest.java | 111 ++++++ .../markwon/spans/AsyncDrawableTest.java | 107 ++++++ 25 files changed, 954 insertions(+), 100 deletions(-) delete mode 100644 markwon/src/main/java/ru/noties/markwon/SpannableStringBuilderImpl.java delete mode 100644 markwon/src/main/java/ru/noties/markwon/SpannedReversed.java create mode 100644 markwon/src/test/java/ru/noties/markwon/SpannableBuilderTest.java create mode 100644 markwon/src/test/java/ru/noties/markwon/renderer/SpannableConfigurationTest.java create mode 100644 markwon/src/test/java/ru/noties/markwon/renderer/SyntaxHighlightTest.java create mode 100644 markwon/src/test/java/ru/noties/markwon/spans/AsyncDrawableTest.java diff --git a/.travis.yml b/.travis.yml index 93df6e80..bd4791e7 100644 --- a/.travis.yml +++ b/.travis.yml @@ -9,7 +9,7 @@ android: - platform-tools - tools - - build-tools-27.0.3 + - build-tools-28.0.3 - android-27 branches: diff --git a/README.md b/README.md index 7545d2b4..be6976a9 100644 --- a/README.md +++ b/README.md @@ -7,6 +7,8 @@ [![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) + **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 diff --git a/app/build.gradle b/app/build.gradle index a8154b4a..0869e5e5 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -36,7 +36,6 @@ dependencies { implementation it['okhttp'] implementation it['prism4j'] implementation it['debug'] - implementation it['better-link-movement'] implementation it['dagger'] } diff --git a/app/src/main/java/ru/noties/markwon/MainActivity.java b/app/src/main/java/ru/noties/markwon/MainActivity.java index 19882a74..3bf49109 100644 --- a/app/src/main/java/ru/noties/markwon/MainActivity.java +++ b/app/src/main/java/ru/noties/markwon/MainActivity.java @@ -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); diff --git a/build.gradle b/build.gradle index a99710f4..6ba06545 100644 --- a/build.gradle +++ b/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" ] diff --git a/docs/docs/getting-started.md b/docs/docs/getting-started.md index 1e42c485..60524932 100644 --- a/docs/docs/getting-started.md +++ b/docs/docs/getting-started.md @@ -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); diff --git a/gradle.properties b/gradle.properties index 7f10785b..7ef25526 100644 --- a/gradle.properties +++ b/gradle.properties @@ -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 diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar index a5fe1cb94b9ee5ce57e6113458225bcba12d83e3..758de960ec7947253b058ff79c88ce51f3abe08a 100644 GIT binary patch delta 7592 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 delta 7527 zcmZ9R1x#F9_qSnicXulc?yf}(6dk0vyZc~+d$GaY-3n72TD-Wng<{36#a;jXnwxvy zd?z`{^Q_;>$vQhHImzBB)d(F`2+2Zdw?a0bq~PG-Kq=DNDB0&8XCCly$>bke%{kEW zYe{bxkm2A2p|qUrkN`qGvS9UV-f^1{Tmv^lyIF-rb}Woy4YW{nG-ugNX^Pi~mfp=` zPROtLj()Lc)?7ukwK~-5mOJ!-;(e=AnFyVa>VMqFzl40c*SoDc5o*a@b;>~91z+ch ztOsV^1g?v%i+~^28+(z>D4ts}4Nu!KY0@@ic}aOyN0Zg*A@O0ze6fgX4lJ)y8u7V^f&}%mO4uz`^vA6Lw-Qo5uGH1}vU$a+^|3w-WK=eF_MX>O$GLV)wcAcp zh*T6(L7XBKYUMaiM49U?hWO;vi-q?5hn!~l&|9-5j>yVW53HH{!By|ludBatTh1%I zB<5SDlSLkLXE_TG{URmGqsQ8OhUpwH)igC2r{Pupf>5+__=jg$-USeGmSt)gWkvYZ z-kZ4kYsYKeP;19vcOcmWSx| z+-U`!P#CBplekzlnu)n_x@Iuiyc^K#h`e_RucIjtwccf#Yv!rlCk-Ad{T>ugl(U)K zcNe#pomfa^@rF;Nj;3tB|G|i){vJ0%uGh6cD z2FMo4ZF!>UMst*&*CY9-S$Cus(VF%ebF~qH?wXCx#Pzts_0RRM+ zYv5Sj6g+ByZ#7_5Adc?4B8=U)D=~kH5C>uV<*=;qLoh_%)P#|P$_zYvnM4X;V@w{t zs^YP1ookyt9BdnCtmj&-sAR|gdMh&!aS}xz88G+T-@wuH5H>b&HHYMO2Og>Si1B1{ zKh9cyP>h=J)WnrLcY9Tl!|2?1o*i|FZvjWf|hn zJA~1%+!E@OTjXpyEc&CBS#OGiI2^{wB3mELt`oI@mCrNMI)1pa*3dLUp8SAx{&tqRVFF1ZJ{z$z>2gi} zTXE63p5N1T$3t2kHa}RBa`yJ=4-p~SRJ%cdmJ?kwkEC3eW{imSw-;xNUMxcTHA0OP zGd`#=MOT0UpQp>NN+Zf55Z>oyU9Z$e?`GYDyqqkm$9qAXjo#^Jkihuzlu08}ES|vm^XWsf4)4S3EMyPmjw~ano-X zU9?8uhiY_7Z9;jciUT!{C0Ku zM)M%VYP7tV-{aX%1+kEQPPE;irmt&z0_?tCgmy-iiVx!3o&u(fL2|eB zT%0gk7fXpV#$KfR8bs+Ra?S8o?Kgd)ht(V-7(^RgF

>O|5`!Y@WVeHnjeM=H)Z) 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 expected = new HashMap() {{ + 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 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 { diff --git a/markwon/src/main/java/ru/noties/markwon/Markwon.java b/markwon/src/main/java/ru/noties/markwon/Markwon.java index 01273b78..6ecb9ca8 100644 --- a/markwon/src/main/java/ru/noties/markwon/Markwon.java +++ b/markwon/src/main/java/ru/noties/markwon/Markwon.java @@ -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); diff --git a/markwon/src/main/java/ru/noties/markwon/SpannableBuilder.java b/markwon/src/main/java/ru/noties/markwon/SpannableBuilder.java index 9e3ec713..07e2bb85 100644 --- a/markwon/src/main/java/ru/noties/markwon/SpannableBuilder.java +++ b/markwon/src/main/java/ru/noties/markwon/SpannableBuilder.java @@ -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 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 overlap 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 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 list = new ArrayList<>(spans); + Collections.reverse(list); + return Collections.unmodifiableList(list); + } + + final List list = new ArrayList<>(0); + + final Iterator 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 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); + } + } } diff --git a/markwon/src/main/java/ru/noties/markwon/SpannableConfiguration.java b/markwon/src/main/java/ru/noties/markwon/SpannableConfiguration.java index cb2a34bc..5df9d316 100644 --- a/markwon/src/main/java/ru/noties/markwon/SpannableConfiguration.java +++ b/markwon/src/main/java/ru/noties/markwon/SpannableConfiguration.java @@ -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; diff --git a/markwon/src/main/java/ru/noties/markwon/SpannableStringBuilderImpl.java b/markwon/src/main/java/ru/noties/markwon/SpannableStringBuilderImpl.java deleted file mode 100644 index 7b29440b..00000000 --- a/markwon/src/main/java/ru/noties/markwon/SpannableStringBuilderImpl.java +++ /dev/null @@ -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); - } -} diff --git a/markwon/src/main/java/ru/noties/markwon/SpannedReversed.java b/markwon/src/main/java/ru/noties/markwon/SpannedReversed.java deleted file mode 100644 index 3fd7f566..00000000 --- a/markwon/src/main/java/ru/noties/markwon/SpannedReversed.java +++ /dev/null @@ -1,9 +0,0 @@ -package ru.noties.markwon; - -import android.text.Spanned; - -/** - * @since 1.0.1 - */ -interface SpannedReversed extends Spanned { -} diff --git a/markwon/src/main/java/ru/noties/markwon/renderer/SpannableMarkdownVisitor.java b/markwon/src/main/java/ru/noties/markwon/renderer/SpannableMarkdownVisitor.java index 967784e5..81e2d36e 100644 --- a/markwon/src/main/java/ru/noties/markwon/renderer/SpannableMarkdownVisitor.java +++ b/markwon/src/main/java/ru/noties/markwon/renderer/SpannableMarkdownVisitor.java @@ -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 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) { diff --git a/markwon/src/main/java/ru/noties/markwon/spans/AsyncDrawable.java b/markwon/src/main/java/ru/noties/markwon/spans/AsyncDrawable.java index 588b8a04..dbd0fe0b 100644 --- a/markwon/src/main/java/ru/noties/markwon/spans/AsyncDrawable.java +++ b/markwon/src/main/java/ru/noties/markwon/spans/AsyncDrawable.java @@ -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 diff --git a/markwon/src/main/java/ru/noties/markwon/spans/OrderedListItemSpan.java b/markwon/src/main/java/ru/noties/markwon/spans/OrderedListItemSpan.java index 29f68e9e..1db29e1a 100644 --- a/markwon/src/main/java/ru/noties/markwon/spans/OrderedListItemSpan.java +++ b/markwon/src/main/java/ru/noties/markwon/spans/OrderedListItemSpan.java @@ -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. + *

+ * NB, this method must be called before 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 { diff --git a/markwon/src/main/java/ru/noties/markwon/spans/SpannableTheme.java b/markwon/src/main/java/ru/noties/markwon/spans/SpannableTheme.java index e82d0d8b..a3ba8c55 100644 --- a/markwon/src/main/java/ru/noties/markwon/spans/SpannableTheme.java +++ b/markwon/src/main/java/ru/noties/markwon/spans/SpannableTheme.java @@ -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; } diff --git a/markwon/src/main/java/ru/noties/markwon/spans/TaskListSpan.java b/markwon/src/main/java/ru/noties/markwon/spans/TaskListSpan.java index 172b952c..25bc6a41 100644 --- a/markwon/src/main/java/ru/noties/markwon/spans/TaskListSpan.java +++ b/markwon/src/main/java/ru/noties/markwon/spans/TaskListSpan.java @@ -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; diff --git a/markwon/src/test/java/ru/noties/markwon/SpannableBuilderTest.java b/markwon/src/test/java/ru/noties/markwon/SpannableBuilderTest.java new file mode 100644 index 00000000..a1daf218 --- /dev/null +++ b/markwon/src/test/java/ru/noties/markwon/SpannableBuilderTest.java @@ -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 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 getSpans(int start, int end) { + return Ix.from(builder.getSpans(start, end)) + .map(new IxFunction() { + @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 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 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 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 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 + + '}'; + } + } +} \ No newline at end of file diff --git a/markwon/src/test/java/ru/noties/markwon/renderer/SpannableConfigurationTest.java b/markwon/src/test/java/ru/noties/markwon/renderer/SpannableConfigurationTest.java new file mode 100644 index 00000000..daa70332 --- /dev/null +++ b/markwon/src/test/java/ru/noties/markwon/renderer/SpannableConfigurationTest.java @@ -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()); + } +} diff --git a/markwon/src/test/java/ru/noties/markwon/renderer/SyntaxHighlightTest.java b/markwon/src/test/java/ru/noties/markwon/renderer/SyntaxHighlightTest.java new file mode 100644 index 00000000..1f5d88d4 --- /dev/null +++ b/markwon/src/test/java/ru/noties/markwon/renderer/SyntaxHighlightTest.java @@ -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); + } +} \ No newline at end of file diff --git a/markwon/src/test/java/ru/noties/markwon/spans/AsyncDrawableTest.java b/markwon/src/test/java/ru/noties/markwon/spans/AsyncDrawableTest.java new file mode 100644 index 00000000..873cc404 --- /dev/null +++ b/markwon/src/test/java/ru/noties/markwon/spans/AsyncDrawableTest.java @@ -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; + } + } +} \ No newline at end of file From c6349738ad37d943dc7a148679a59a0f590528f0 Mon Sep 17 00:00:00 2001 From: Dimitry Ivanov Date: Mon, 10 Dec 2018 15:15:42 +0300 Subject: [PATCH 2/2] Prepare for 2.0.1 release --- docs/CHANGELOG.md | 15 ++++++++++++++- gradle.properties | 2 +- 2 files changed, 15 insertions(+), 2 deletions(-) diff --git a/docs/CHANGELOG.md b/docs/CHANGELOG.md index 09a2c676..05ff885e 100644 --- a/docs/CHANGELOG.md +++ b/docs/CHANGELOG.md @@ -1,6 +1,19 @@ # Changelog -# 2.0.0 +# 2.0.1 +* `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 🙌 + +## 2.0.0 * Add `html-parser-api` and `html-parser-impl` modules * Add `HtmlEmptyTagReplacement` * Implement Appendable and CharSequence in SpannableBuilder diff --git a/gradle.properties b/gradle.properties index 7ef25526..7865eb58 100644 --- a/gradle.properties +++ b/gradle.properties @@ -6,7 +6,7 @@ org.gradle.configureondemand=true android.enableBuildCache=true android.buildCacheDir=build/pre-dex-cache -VERSION_NAME=2.0.1-SNAPSHOT +VERSION_NAME=2.0.1 GROUP=ru.noties POM_DESCRIPTION=Markwon