diff --git a/markwon-core/src/main/java/ru/noties/markwon/core/CorePlugin.java b/markwon-core/src/main/java/ru/noties/markwon/core/CorePlugin.java index 96d8bb11..506da2b9 100644 --- a/markwon-core/src/main/java/ru/noties/markwon/core/CorePlugin.java +++ b/markwon-core/src/main/java/ru/noties/markwon/core/CorePlugin.java @@ -165,14 +165,17 @@ public class CorePlugin extends AbstractMarkwonPlugin { @Override public void visit(@NonNull MarkwonVisitor visitor, @NonNull Text text) { - final int length = visitor.length(); final String literal = text.getLiteral(); visitor.builder().append(literal); // @since 4.0.0-SNAPSHOT - for (OnTextAddedListener onTextAddedListener : onTextAddedListeners) { - onTextAddedListener.onTextAdded(visitor, literal, length); + if (!onTextAddedListeners.isEmpty()) { + // calculate the start position + final int length = visitor.length() - literal.length(); + for (OnTextAddedListener onTextAddedListener : onTextAddedListeners) { + onTextAddedListener.onTextAdded(visitor, literal, length); + } } } }); diff --git a/markwon-core/src/main/java/ru/noties/markwon/image/AsyncDrawableLoader.java b/markwon-core/src/main/java/ru/noties/markwon/image/AsyncDrawableLoader.java index f9bbb4b7..314e537f 100644 --- a/markwon-core/src/main/java/ru/noties/markwon/image/AsyncDrawableLoader.java +++ b/markwon-core/src/main/java/ru/noties/markwon/image/AsyncDrawableLoader.java @@ -27,6 +27,4 @@ public abstract class AsyncDrawableLoader { @Nullable public abstract Drawable placeholder(@NonNull AsyncDrawable drawable); - - } diff --git a/markwon-core/src/main/java/ru/noties/markwon/image/AsyncDrawableLoaderNoOp.java b/markwon-core/src/main/java/ru/noties/markwon/image/AsyncDrawableLoaderNoOp.java index 7c3475b7..e3a6331a 100644 --- a/markwon-core/src/main/java/ru/noties/markwon/image/AsyncDrawableLoaderNoOp.java +++ b/markwon-core/src/main/java/ru/noties/markwon/image/AsyncDrawableLoaderNoOp.java @@ -4,7 +4,7 @@ import android.graphics.drawable.Drawable; import android.support.annotation.NonNull; import android.support.annotation.Nullable; -public class AsyncDrawableLoaderNoOp extends AsyncDrawableLoader { +class AsyncDrawableLoaderNoOp extends AsyncDrawableLoader { @Override public void load(@NonNull AsyncDrawable drawable) { diff --git a/markwon-ext-latex/src/main/java/ru/noties/markwon/ext/latex/JLatexMathPlugin.java b/markwon-ext-latex/src/main/java/ru/noties/markwon/ext/latex/JLatexMathPlugin.java index ee28e04e..decdb6d1 100644 --- a/markwon-ext-latex/src/main/java/ru/noties/markwon/ext/latex/JLatexMathPlugin.java +++ b/markwon-ext-latex/src/main/java/ru/noties/markwon/ext/latex/JLatexMathPlugin.java @@ -93,6 +93,7 @@ public class JLatexMathPlugin extends AbstractMarkwonPlugin { private final JLatextAsyncDrawableLoader jLatextAsyncDrawableLoader; + @SuppressWarnings("WeakerAccess") JLatexMathPlugin(@NonNull Config config) { this.jLatextAsyncDrawableLoader = new JLatextAsyncDrawableLoader(config); } diff --git a/markwon-image/src/main/java/ru/noties/markwon/image/AsyncDrawableLoaderBuilder.java b/markwon-image/src/main/java/ru/noties/markwon/image/AsyncDrawableLoaderBuilder.java index e82730f4..57347b70 100644 --- a/markwon-image/src/main/java/ru/noties/markwon/image/AsyncDrawableLoaderBuilder.java +++ b/markwon-image/src/main/java/ru/noties/markwon/image/AsyncDrawableLoaderBuilder.java @@ -8,6 +8,9 @@ import java.util.Map; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; +import ru.noties.markwon.image.data.DataUriSchemeHandler; +import ru.noties.markwon.image.network.NetworkSchemeHandler; + class AsyncDrawableLoaderBuilder { ExecutorService executorService; @@ -19,6 +22,15 @@ class AsyncDrawableLoaderBuilder { boolean isBuilt; + AsyncDrawableLoaderBuilder() { + + // @since 4.0.0-SNAPSHOT + // okay, let's add supported schemes at the start, this would be : data-uri and default network + // we should not use file-scheme as it's a bit complicated to assume file usage (lack of permissions) + addSchemeHandler(DataUriSchemeHandler.create()); + addSchemeHandler(NetworkSchemeHandler.create()); + } + void executorService(@NonNull ExecutorService executorService) { this.executorService = executorService; } @@ -66,12 +78,6 @@ class AsyncDrawableLoaderBuilder { isBuilt = true; - // we must have schemeHandlers registered (we will provide - // default media decoder if it's absent) - if (schemeHandlers.size() == 0) { - return new AsyncDrawableLoaderNoOp(); - } - // @since 4.0.0-SNAPSHOT if (defaultMediaDecoder == null) { defaultMediaDecoder = DefaultImageMediaDecoder.create(); @@ -83,5 +89,4 @@ class AsyncDrawableLoaderBuilder { return new AsyncDrawableLoaderImpl(this); } - } diff --git a/markwon-image/src/main/java/ru/noties/markwon/image/ImagesPlugin.java b/markwon-image/src/main/java/ru/noties/markwon/image/ImagesPlugin.java index 0a7d9446..e5cac9b3 100644 --- a/markwon-image/src/main/java/ru/noties/markwon/image/ImagesPlugin.java +++ b/markwon-image/src/main/java/ru/noties/markwon/image/ImagesPlugin.java @@ -14,6 +14,7 @@ import ru.noties.markwon.AbstractMarkwonPlugin; import ru.noties.markwon.MarkwonConfiguration; import ru.noties.markwon.MarkwonSpansFactory; +@SuppressWarnings({"UnusedReturnValue", "WeakerAccess"}) public class ImagesPlugin extends AbstractMarkwonPlugin { /** @@ -91,6 +92,12 @@ public class ImagesPlugin extends AbstractMarkwonPlugin { return this; } + /** + * @see DefaultImageMediaDecoder + * @see ru.noties.markwon.image.svg.SvgMediaDecoder + * @see ru.noties.markwon.image.gif.GifMediaDecoder + * @since 4.0.0-SNAPSHOT + */ @NonNull public ImagesPlugin addMediaDecoder(@NonNull MediaDecoder mediaDecoder) { checkBuilderState(); @@ -98,13 +105,23 @@ public class ImagesPlugin extends AbstractMarkwonPlugin { return this; } + /** + * Please note that if not specified a {@link DefaultImageMediaDecoder} will be used. So + * if you need to disable default-image-media-decoder specify here own no-op implementation. + * + * @see DefaultImageMediaDecoder + * @since 4.0.0-SNAPSHOT + */ @NonNull - public ImagesPlugin defaultMediaDecoder(@Nullable MediaDecoder mediaDecoder) { + public ImagesPlugin defaultMediaDecoder(@NonNull MediaDecoder mediaDecoder) { checkBuilderState(); builder.defaultMediaDecoder(mediaDecoder); return this; } + /** + * @since 4.0.0-SNAPSHOT + */ @NonNull public ImagesPlugin removeSchemeHandler(@NonNull String scheme) { checkBuilderState(); @@ -112,6 +129,9 @@ public class ImagesPlugin extends AbstractMarkwonPlugin { return this; } + /** + * @since 4.0.0-SNAPSHOT + */ @NonNull public ImagesPlugin removeMediaDecoder(@NonNull String contentType) { checkBuilderState(); @@ -119,6 +139,9 @@ public class ImagesPlugin extends AbstractMarkwonPlugin { return this; } + /** + * @since 4.0.0-SNAPSHOT + */ @NonNull public ImagesPlugin placeholderProvider(@NonNull PlaceholderProvider placeholderProvider) { checkBuilderState(); @@ -126,6 +149,10 @@ public class ImagesPlugin extends AbstractMarkwonPlugin { return this; } + /** + * @see ErrorHandler + * @since 4.0.0-SNAPSHOT + */ @NonNull public ImagesPlugin errorHandler(@NonNull ErrorHandler errorHandler) { checkBuilderState(); diff --git a/markwon-image/src/test/java/ru/noties/markwon/image/AsyncDrawableLoaderBuilderTest.java b/markwon-image/src/test/java/ru/noties/markwon/image/AsyncDrawableLoaderBuilderTest.java new file mode 100644 index 00000000..b2400a37 --- /dev/null +++ b/markwon-image/src/test/java/ru/noties/markwon/image/AsyncDrawableLoaderBuilderTest.java @@ -0,0 +1,194 @@ +package ru.noties.markwon.image; + +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.Collections; +import java.util.concurrent.ExecutorService; + +import ru.noties.markwon.image.data.DataUriSchemeHandler; +import ru.noties.markwon.image.network.NetworkSchemeHandler; + +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; +import static org.mockito.Mockito.when; + +@RunWith(RobolectricTestRunner.class) +@Config(manifest = Config.NONE) +public class AsyncDrawableLoaderBuilderTest { + + private AsyncDrawableLoaderBuilder builder; + + @Before + public void before() { + builder = new AsyncDrawableLoaderBuilder(); + } + + @Test + public void default_scheme_handlers() { + // builder adds default data-uri and network scheme-handlers + + final String[] registered = { + DataUriSchemeHandler.SCHEME, + NetworkSchemeHandler.SCHEME_HTTP, + NetworkSchemeHandler.SCHEME_HTTPS + }; + + for (String scheme : registered) { + assertNotNull(scheme, builder.schemeHandlers.get(scheme)); + } + } + + @Test + public void built_flag() { + // isBuilt flag must be set after `build` method call + + assertFalse(builder.isBuilt); + + builder.build(); + + assertTrue(builder.isBuilt); + } + + @Test + public void defaults_initialized() { + // default-media-decoder and executor-service must be initialized + + assertNull(builder.defaultMediaDecoder); + assertNull(builder.executorService); + + builder.build(); + + assertNotNull(builder.defaultMediaDecoder); + assertNotNull(builder.executorService); + } + + @Test + public void executor() { + // supplied executor-service must be used + + assertNull(builder.executorService); + + final ExecutorService service = mock(ExecutorService.class); + builder.executorService(service); + + builder.build(); + + assertEquals(service, builder.executorService); + } + + @Test + public void add_scheme_handler() { + + final String scheme = "mock"; + assertNull(builder.schemeHandlers.get(scheme)); + + final SchemeHandler schemeHandler = mock(SchemeHandler.class); + when(schemeHandler.supportedSchemes()).thenReturn(Collections.singleton(scheme)); + + builder.addSchemeHandler(schemeHandler); + builder.build(); + + assertEquals(schemeHandler, builder.schemeHandlers.get(scheme)); + } + + @Test + public void add_scheme_handler_multiple_types() { + // all supported types are registered + + final String[] schemes = { + "mock-1", + "mock-2" + }; + + final SchemeHandler schemeHandler = mock(SchemeHandler.class); + when(schemeHandler.supportedSchemes()).thenReturn(Arrays.asList(schemes)); + + builder.addSchemeHandler(schemeHandler); + + for (String scheme : schemes) { + assertEquals(scheme, schemeHandler, builder.schemeHandlers.get(scheme)); + } + } + + @Test + public void add_media_decoder() { + + final String media = "mocked/type"; + assertNull(builder.mediaDecoders.get(media)); + + final MediaDecoder mediaDecoder = mock(MediaDecoder.class); + when(mediaDecoder.supportedTypes()).thenReturn(Collections.singleton(media)); + + builder.addMediaDecoder(mediaDecoder); + builder.build(); + + assertEquals(mediaDecoder, builder.mediaDecoders.get(media)); + } + + @Test + public void add_media_decoder_multiple_types() { + + final String[] types = { + "mock/type1", + "mock/type2" + }; + + final MediaDecoder mediaDecoder = mock(MediaDecoder.class); + when(mediaDecoder.supportedTypes()).thenReturn(Arrays.asList(types)); + + builder.addMediaDecoder(mediaDecoder); + + for (String type : types) { + assertEquals(type, mediaDecoder, builder.mediaDecoders.get(type)); + } + } + + @Test + public void default_media_decoder() { + + assertNull(builder.defaultMediaDecoder); + + final MediaDecoder mediaDecoder = mock(MediaDecoder.class); + builder.defaultMediaDecoder(mediaDecoder); + builder.build(); + + assertEquals(mediaDecoder, builder.defaultMediaDecoder); + } + + @Test + public void remove_scheme_handler() { + + final String scheme = "mock"; + final SchemeHandler schemeHandler = mock(SchemeHandler.class); + when(schemeHandler.supportedSchemes()).thenReturn(Collections.singleton(scheme)); + + assertNull(builder.schemeHandlers.get(scheme)); + builder.addSchemeHandler(schemeHandler); + assertNotNull(builder.schemeHandlers.get(scheme)); + builder.removeSchemeHandler(scheme); + assertNull(builder.schemeHandlers.get(scheme)); + } + + @Test + public void remove_media_decoder() { + + final String media = "mock/type"; + final MediaDecoder mediaDecoder = mock(MediaDecoder.class); + when(mediaDecoder.supportedTypes()).thenReturn(Collections.singleton(media)); + + assertNull(builder.mediaDecoders.get(media)); + builder.addMediaDecoder(mediaDecoder); + assertNotNull(builder.mediaDecoders.get(media)); + builder.removeMediaDecoder(media); + assertNull(builder.mediaDecoders.get(media)); + } +} \ No newline at end of file diff --git a/markwon-image/src/test/java/ru/noties/markwon/image/ImagesPluginTest.java b/markwon-image/src/test/java/ru/noties/markwon/image/ImagesPluginTest.java new file mode 100644 index 00000000..e141d456 --- /dev/null +++ b/markwon-image/src/test/java/ru/noties/markwon/image/ImagesPluginTest.java @@ -0,0 +1,177 @@ +package ru.noties.markwon.image; + +import android.support.annotation.NonNull; +import android.text.Spanned; +import android.widget.TextView; + +import org.commonmark.node.Image; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.ArgumentCaptor; +import org.robolectric.RobolectricTestRunner; +import org.robolectric.annotation.Config; + +import java.util.concurrent.ExecutorService; + +import ru.noties.markwon.MarkwonConfiguration; +import ru.noties.markwon.MarkwonSpansFactory; +import ru.noties.markwon.SpanFactory; +import ru.noties.markwon.image.data.DataUriSchemeHandler; + +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +@RunWith(RobolectricTestRunner.class) +@Config(manifest = Config.NONE) +public class ImagesPluginTest { + + private ImagesPlugin plugin; + + @Before + public void before() { + plugin = ImagesPlugin.create(); + } + + @Test + public void build_state() { + // it's not possible to mutate images-plugin after `configureConfiguration` call + + // validate that it doesn't throw here + plugin.addSchemeHandler(DataUriSchemeHandler.create()); + + // mark the state + plugin.configureConfiguration(mock(MarkwonConfiguration.Builder.class)); + + final class Throws { + private void assertThrows(@NonNull Runnable action) { + //noinspection CatchMayIgnoreException + try { + action.run(); + fail(); + } catch (Throwable t) { + assertTrue(t.getMessage(), t.getMessage().contains("ImagesPlugin has already been configured")); + } + } + } + final Throws check = new Throws(); + + // executor-service + check.assertThrows(new Runnable() { + @Override + public void run() { + plugin.executorService(mock(ExecutorService.class)); + } + }); + + // add-scheme-handler + check.assertThrows(new Runnable() { + @Override + public void run() { + plugin.addSchemeHandler(mock(SchemeHandler.class)); + } + }); + + // add-media-decoder + check.assertThrows(new Runnable() { + @Override + public void run() { + plugin.addMediaDecoder(mock(MediaDecoder.class)); + } + }); + + // default-media-decoder + check.assertThrows(new Runnable() { + @Override + public void run() { + plugin.defaultMediaDecoder(mock(MediaDecoder.class)); + } + }); + + // remove-scheme-handler + check.assertThrows(new Runnable() { + @Override + public void run() { + plugin.removeSchemeHandler("mock"); + } + }); + + // remove-media-decoder + check.assertThrows(new Runnable() { + @Override + public void run() { + plugin.removeMediaDecoder("mock/type"); + } + }); + + // placeholder-provider + check.assertThrows(new Runnable() { + @Override + public void run() { + plugin.placeholderProvider(mock(ImagesPlugin.PlaceholderProvider.class)); + } + }); + + // error-handler + check.assertThrows(new Runnable() { + @Override + public void run() { + plugin.errorHandler(mock(ImagesPlugin.ErrorHandler.class)); + } + }); + + // final check if for actual `configureConfiguration` call (must be called only once) + check.assertThrows(new Runnable() { + @Override + public void run() { + plugin.configureConfiguration(mock(MarkwonConfiguration.Builder.class)); + } + }); + } + + @Test + public void image_span_factory_registered() { + + final MarkwonSpansFactory.Builder builder = mock(MarkwonSpansFactory.Builder.class); + + plugin.configureSpansFactory(builder); + + final ArgumentCaptor captor = ArgumentCaptor.forClass(SpanFactory.class); + + verify(builder, times(1)) + .setFactory(eq(Image.class), captor.capture()); + + assertNotNull(captor.getValue()); + } + + @Test + public void before_set_text() { + // verify that AsyncDrawableScheduler is called + + final TextView textView = mock(TextView.class); + + plugin.beforeSetText(textView, mock(Spanned.class)); + + verify(textView, times(1)) + .getTag(eq(R.id.markwon_drawables_scheduler_last_text_hashcode)); + } + + @Test + public void after_set_text() { + // verify that AsyncDrawableScheduler is called + + final TextView textView = mock(TextView.class); + when(textView.getText()).thenReturn("some text"); + + plugin.afterSetText(textView); + + verify(textView, times(1)) + .getTag(eq(R.id.markwon_drawables_scheduler_last_text_hashcode)); + } +} \ No newline at end of file diff --git a/markwon-linkify/README.md b/markwon-linkify/README.md new file mode 100644 index 00000000..62d2cc75 --- /dev/null +++ b/markwon-linkify/README.md @@ -0,0 +1,5 @@ +# Linkify + +Use this module (or take a hint from it) if you would need _linkify_ capabilities. Do not +use `TextView.setAutolinkMask` (or specify `autolink` in XML) because it will remove all +existing links and keep only the ones it creates. \ No newline at end of file diff --git a/markwon-linkify/src/main/java/ru/noties/markwon/linkify/LinkifyPlugin.java b/markwon-linkify/src/main/java/ru/noties/markwon/linkify/LinkifyPlugin.java index ff29c1a1..b24f3b49 100644 --- a/markwon-linkify/src/main/java/ru/noties/markwon/linkify/LinkifyPlugin.java +++ b/markwon-linkify/src/main/java/ru/noties/markwon/linkify/LinkifyPlugin.java @@ -18,8 +18,7 @@ public class LinkifyPlugin extends AbstractMarkwonPlugin { @IntDef(flag = true, value = { Linkify.EMAIL_ADDRESSES, Linkify.PHONE_NUMBERS, - Linkify.WEB_URLS, - Linkify.ALL + Linkify.WEB_URLS }) @Retention(RetentionPolicy.SOURCE) @interface LinkifyMask {