From a2a5857f06c041dab5513fff6c465dc6f9d50bb3 Mon Sep 17 00:00:00 2001 From: Dimitry Ivanov Date: Sat, 8 Jun 2019 15:50:07 +0300 Subject: [PATCH] registryImpl test --- _CHANGES.md | 3 +- .../noties/markwon/app/MarkdownRenderer.java | 15 +- build.gradle | 2 +- .../io/noties/markwon/MarkwonBuilderImpl.java | 107 +--------- .../noties/markwon/MarkwonConfiguration.java | 42 ---- .../java/io/noties/markwon/RegistryImpl.java | 118 +++++++++++ .../markwon/html/MarkwonHtmlParserNoOp.java | 27 --- .../java/io/noties/markwon/MarkwonAssert.java | 15 ++ .../markwon/MarkwonBuilderImplTest.java | 34 --- .../io/noties/markwon/RegistryImplTest.java | 194 ++++++++++++++++++ .../io/noties/markwon/html/HtmlPlugin.java | 21 +- .../java/io/noties/markwon/html/HtmlTag.java | 0 .../markwon/html/MarkwonHtmlParser.java | 8 - .../markwon/html/MarkwonHtmlRenderer.java | 8 - .../markwon/html/MarkwonHtmlRendererNoOp.java | 0 .../io/noties/markwon/html/TagHandler.java | 0 .../io/noties/markwon/image/ImagesPlugin.java | 20 +- .../image/AsyncDrawableLoaderBuilderTest.java | 13 ++ .../markwon/image/ImagesPluginTest.java | 67 ++++++ 19 files changed, 447 insertions(+), 247 deletions(-) create mode 100644 markwon-core/src/main/java/io/noties/markwon/RegistryImpl.java delete mode 100644 markwon-core/src/main/java/io/noties/markwon/html/MarkwonHtmlParserNoOp.java create mode 100644 markwon-core/src/test/java/io/noties/markwon/MarkwonAssert.java create mode 100644 markwon-core/src/test/java/io/noties/markwon/RegistryImplTest.java rename {markwon-core => markwon-html}/src/main/java/io/noties/markwon/html/HtmlTag.java (100%) rename {markwon-core => markwon-html}/src/main/java/io/noties/markwon/html/MarkwonHtmlParser.java (91%) rename {markwon-core => markwon-html}/src/main/java/io/noties/markwon/html/MarkwonHtmlRenderer.java (73%) rename {markwon-core => markwon-html}/src/main/java/io/noties/markwon/html/MarkwonHtmlRendererNoOp.java (100%) rename {markwon-core => markwon-html}/src/main/java/io/noties/markwon/html/TagHandler.java (100%) diff --git a/_CHANGES.md b/_CHANGES.md index 28856328..0c8ea7de 100644 --- a/_CHANGES.md +++ b/_CHANGES.md @@ -8,4 +8,5 @@ * removed priority * images-plugin moved to standalone again * removed MarkwonPlugin#configureHtmlRenderer -> now part of HtmlPlugin -* TagHandler now has `supportedTags()` method \ No newline at end of file +* TagHandler now has `supportedTags()` method +* html is moved completely to html-plugin \ No newline at end of file diff --git a/app/src/main/java/io/noties/markwon/app/MarkdownRenderer.java b/app/src/main/java/io/noties/markwon/app/MarkdownRenderer.java index 667ef6a4..8d23ca68 100644 --- a/app/src/main/java/io/noties/markwon/app/MarkdownRenderer.java +++ b/app/src/main/java/io/noties/markwon/app/MarkdownRenderer.java @@ -13,27 +13,24 @@ import java.util.concurrent.Future; import javax.inject.Inject; +import io.noties.debug.Debug; import io.noties.markwon.AbstractMarkwonPlugin; import io.noties.markwon.Markwon; import io.noties.markwon.MarkwonConfiguration; +import io.noties.markwon.app.gif.GifAwarePlugin; import io.noties.markwon.ext.strikethrough.StrikethroughPlugin; import io.noties.markwon.ext.tables.TablePlugin; import io.noties.markwon.ext.tasklist.TaskListPlugin; -import io.noties.markwon.app.gif.GifAwarePlugin; import io.noties.markwon.html.HtmlPlugin; import io.noties.markwon.image.ImagesPlugin; -import io.noties.markwon.image.data.DataUriSchemeHandler; import io.noties.markwon.image.file.FileSchemeHandler; -import io.noties.markwon.image.gif.GifMediaDecoder; import io.noties.markwon.image.network.OkHttpNetworkSchemeHandler; -import io.noties.markwon.image.svg.SvgMediaDecoder; import io.noties.markwon.syntax.Prism4jTheme; import io.noties.markwon.syntax.Prism4jThemeDarkula; import io.noties.markwon.syntax.Prism4jThemeDefault; import io.noties.markwon.syntax.SyntaxHighlightPlugin; import io.noties.markwon.urlprocessor.UrlProcessor; import io.noties.markwon.urlprocessor.UrlProcessorRelativeToAbsolute; -import io.noties.debug.Debug; import ru.noties.prism4j.Prism4j; @ActivityScope @@ -102,12 +99,12 @@ public class MarkdownRenderer { .usePlugin(ImagesPlugin.create(new ImagesPlugin.ImagesConfigure() { @Override public void configureImages(@NonNull ImagesPlugin plugin) { + // data uri scheme handler is added automatically + // SVG & GIF will be added if required dependencies are present in the classpath + // default-media-decoder is also added automatically plugin - .addSchemeHandler(DataUriSchemeHandler.create()) .addSchemeHandler(OkHttpNetworkSchemeHandler.create()) - .addSchemeHandler(FileSchemeHandler.createWithAssets(context.getAssets())) - .addMediaDecoder(GifMediaDecoder.create(false)) - .addMediaDecoder(SvgMediaDecoder.create()); + .addSchemeHandler(FileSchemeHandler.createWithAssets(context.getAssets())); } })) .usePlugin(SyntaxHighlightPlugin.create(prism4j, prism4jTheme)) diff --git a/build.gradle b/build.gradle index 17105439..daec9a93 100644 --- a/build.gradle +++ b/build.gradle @@ -5,7 +5,7 @@ buildscript { } dependencies { classpath 'com.android.tools.build:gradle:3.4.1' - classpath 'com.github.ben-manes:gradle-versions-plugin:0.20.0' + classpath 'com.github.ben-manes:gradle-versions-plugin:0.21.0' } } diff --git a/markwon-core/src/main/java/io/noties/markwon/MarkwonBuilderImpl.java b/markwon-core/src/main/java/io/noties/markwon/MarkwonBuilderImpl.java index 7166aedc..4362fbfc 100644 --- a/markwon-core/src/main/java/io/noties/markwon/MarkwonBuilderImpl.java +++ b/markwon-core/src/main/java/io/noties/markwon/MarkwonBuilderImpl.java @@ -2,18 +2,14 @@ package io.noties.markwon; import android.content.Context; import android.support.annotation.NonNull; -import android.support.annotation.Nullable; -import android.support.annotation.VisibleForTesting; import android.widget.TextView; import org.commonmark.parser.Parser; import java.util.ArrayList; import java.util.Collections; -import java.util.HashSet; import java.util.Iterator; import java.util.List; -import java.util.Set; import io.noties.markwon.core.MarkwonTheme; @@ -106,109 +102,8 @@ class MarkwonBuilderImpl implements Markwon.Builder { ); } - @VisibleForTesting @NonNull - static List preparePlugins(@NonNull List plugins) { + private static List preparePlugins(@NonNull List plugins) { return new RegistryImpl(plugins).process(); } - - // @since 4.0.0-SNAPSHOT - private static class RegistryImpl implements MarkwonPlugin.Registry { - - private final List origin; - private final List plugins; - private final Set pending; - - RegistryImpl(@NonNull List origin) { - this.origin = origin; - this.plugins = new ArrayList<>(origin.size()); - this.pending = new HashSet<>(3); - } - - @NonNull - @Override - public

P require(@NonNull Class

plugin) { - return get(plugin); - } - - @Override - public

void require( - @NonNull Class

plugin, - @NonNull MarkwonPlugin.Action action) { - action.apply(get(plugin)); - } - - @NonNull - List process() { - for (MarkwonPlugin plugin : origin) { - configure(plugin); - } - return plugins; - } - - private void configure(@NonNull MarkwonPlugin plugin) { - - // important -> check if it's in plugins - // if it is -> no need to configure (already configured) - - if (!plugins.contains(plugin)) { - - if (pending.contains(plugin)) { - throw new IllegalStateException("Cyclic dependency chain found: " + pending); - } - - // start tracking plugins that are pending for configuration - pending.add(plugin); - - plugin.configure(this); - - // stop pending tracking - pending.remove(plugin); - - // check again if it's included (a child might've configured it already) - // add to out-collection if not already present - // this is a bit different from `find` method as it does check for exact instance - // and not a sub-type - if (!plugins.contains(plugin)) { - plugins.add(plugin); - } - } - } - - @NonNull - private

P get(@NonNull Class

type) { - - // check if present already in plugins - // find in origin, if not found -> throw, else add to out-plugins - - P plugin = find(plugins, type); - - if (plugin == null) { - - plugin = find(origin, type); - - if (plugin == null) { - throw new IllegalStateException("Requested plugin is not added: " + - "" + type.getName() + ", plugins: " + origin); - } - - configure(plugin); - } - - return plugin; - } - - @Nullable - private static

P find( - @NonNull List plugins, - @NonNull Class

type) { - for (MarkwonPlugin plugin : plugins) { - if (type.isAssignableFrom(plugin.getClass())) { - //noinspection unchecked - return (P) plugin; - } - } - return null; - } - } } diff --git a/markwon-core/src/main/java/io/noties/markwon/MarkwonConfiguration.java b/markwon-core/src/main/java/io/noties/markwon/MarkwonConfiguration.java index 9b4ffb95..a544ef6e 100644 --- a/markwon-core/src/main/java/io/noties/markwon/MarkwonConfiguration.java +++ b/markwon-core/src/main/java/io/noties/markwon/MarkwonConfiguration.java @@ -4,8 +4,6 @@ import android.support.annotation.NonNull; import io.noties.markwon.core.MarkwonTheme; import io.noties.markwon.core.spans.LinkSpan; -import io.noties.markwon.html.MarkwonHtmlParser; -import io.noties.markwon.html.MarkwonHtmlRenderer; import io.noties.markwon.image.AsyncDrawableLoader; import io.noties.markwon.image.ImageSizeResolver; import io.noties.markwon.image.ImageSizeResolverDef; @@ -31,8 +29,6 @@ public class MarkwonConfiguration { private final LinkSpan.Resolver linkResolver; private final UrlProcessor urlProcessor; private final ImageSizeResolver imageSizeResolver; - private final MarkwonHtmlParser htmlParser; - private final MarkwonHtmlRenderer htmlRenderer; // @since 3.0.0 private final MarkwonSpansFactory spansFactory; @@ -45,8 +41,6 @@ public class MarkwonConfiguration { this.urlProcessor = builder.urlProcessor; this.imageSizeResolver = builder.imageSizeResolver; this.spansFactory = builder.spansFactory; - this.htmlParser = builder.htmlParser; - this.htmlRenderer = builder.htmlRenderer; } @NonNull @@ -79,16 +73,6 @@ public class MarkwonConfiguration { return imageSizeResolver; } - @NonNull - public MarkwonHtmlParser htmlParser() { - return htmlParser; - } - - @NonNull - public MarkwonHtmlRenderer htmlRenderer() { - return htmlRenderer; - } - /** * @since 3.0.0 */ @@ -106,8 +90,6 @@ public class MarkwonConfiguration { private LinkSpan.Resolver linkResolver; private UrlProcessor urlProcessor; private ImageSizeResolver imageSizeResolver; - private MarkwonHtmlParser htmlParser; - private MarkwonHtmlRenderer htmlRenderer; private MarkwonSpansFactory spansFactory; Builder() { @@ -122,15 +104,6 @@ public class MarkwonConfiguration { return this; } - /** - * @since 4.0.0-SNAPSHOT - */ - @NonNull - public Builder htmlRenderer(@NonNull MarkwonHtmlRenderer htmlRenderer) { - this.htmlRenderer = htmlRenderer; - return this; - } - @NonNull public Builder syntaxHighlight(@NonNull SyntaxHighlight syntaxHighlight) { this.syntaxHighlight = syntaxHighlight; @@ -149,12 +122,6 @@ public class MarkwonConfiguration { return this; } - @NonNull - public Builder htmlParser(@NonNull MarkwonHtmlParser htmlParser) { - this.htmlParser = htmlParser; - return this; - } - /** * @since 1.0.1 */ @@ -177,11 +144,6 @@ public class MarkwonConfiguration { asyncDrawableLoader = AsyncDrawableLoader.noOp(); } - // @since 4.0.0-SNAPSHOT - if (htmlRenderer == null) { - htmlRenderer = MarkwonHtmlRenderer.noOp(); - } - if (syntaxHighlight == null) { syntaxHighlight = new SyntaxHighlightNoOp(); } @@ -198,10 +160,6 @@ public class MarkwonConfiguration { imageSizeResolver = new ImageSizeResolverDef(); } - if (htmlParser == null) { - htmlParser = MarkwonHtmlParser.noOp(); - } - return new MarkwonConfiguration(this); } } diff --git a/markwon-core/src/main/java/io/noties/markwon/RegistryImpl.java b/markwon-core/src/main/java/io/noties/markwon/RegistryImpl.java new file mode 100644 index 00000000..17a85dc8 --- /dev/null +++ b/markwon-core/src/main/java/io/noties/markwon/RegistryImpl.java @@ -0,0 +1,118 @@ +package io.noties.markwon; + +import android.support.annotation.NonNull; +import android.support.annotation.Nullable; + +import java.util.ArrayList; +import java.util.HashSet; +import java.util.List; +import java.util.Set; + +import io.noties.markwon.core.CorePlugin; + +// @since 4.0.0-SNAPSHOT +class RegistryImpl implements MarkwonPlugin.Registry { + + // todo: core-plugin must be (better be) first any way + + private final List origin; + private final List plugins; + private final Set pending; + + RegistryImpl(@NonNull List origin) { + this.origin = origin; + this.plugins = new ArrayList<>(origin.size()); + this.pending = new HashSet<>(3); + } + + @NonNull + @Override + public

P require(@NonNull Class

plugin) { + return get(plugin); + } + + @Override + public

void require( + @NonNull Class

plugin, + @NonNull MarkwonPlugin.Action action) { + action.apply(get(plugin)); + } + + @NonNull + List process() { + for (MarkwonPlugin plugin : origin) { + configure(plugin); + } + return plugins; + } + + private void configure(@NonNull MarkwonPlugin plugin) { + + // important -> check if it's in plugins + // if it is -> no need to configure (already configured) + + if (!plugins.contains(plugin)) { + + if (pending.contains(plugin)) { + throw new IllegalStateException("Cyclic dependency chain found: " + pending); + } + + // start tracking plugins that are pending for configuration + pending.add(plugin); + + plugin.configure(this); + + // stop pending tracking + pending.remove(plugin); + + // check again if it's included (a child might've configured it already) + // add to out-collection if not already present + // this is a bit different from `find` method as it does check for exact instance + // and not a sub-type + if (!plugins.contains(plugin)) { + // core-plugin must always be the first one (if it's present) + if (CorePlugin.class.isAssignableFrom(plugin.getClass())) { + plugins.add(0, plugin); + } else { + plugins.add(plugin); + } + } + } + } + + @NonNull + private

P get(@NonNull Class

type) { + + // check if present already in plugins + // find in origin, if not found -> throw, else add to out-plugins + + P plugin = find(plugins, type); + + if (plugin == null) { + + plugin = find(origin, type); + + if (plugin == null) { + throw new IllegalStateException("Requested plugin is not added: " + + "" + type.getName() + ", plugins: " + origin); + } + + configure(plugin); + } + + return plugin; + } + + @Nullable + private static

P find( + @NonNull List plugins, + @NonNull Class

type) { + for (MarkwonPlugin plugin : plugins) { + if (type.isAssignableFrom(plugin.getClass())) { + //noinspection unchecked + return (P) plugin; + } + } + return null; + } +} diff --git a/markwon-core/src/main/java/io/noties/markwon/html/MarkwonHtmlParserNoOp.java b/markwon-core/src/main/java/io/noties/markwon/html/MarkwonHtmlParserNoOp.java deleted file mode 100644 index 9057630b..00000000 --- a/markwon-core/src/main/java/io/noties/markwon/html/MarkwonHtmlParserNoOp.java +++ /dev/null @@ -1,27 +0,0 @@ -package io.noties.markwon.html; - -import android.support.annotation.NonNull; - -import java.util.Collections; - -class MarkwonHtmlParserNoOp extends MarkwonHtmlParser { - @Override - public void processFragment(@NonNull T output, @NonNull String htmlFragment) { - // no op - } - - @Override - public void flushInlineTags(int documentLength, @NonNull FlushAction action) { - action.apply(Collections.emptyList()); - } - - @Override - public void flushBlockTags(int documentLength, @NonNull FlushAction action) { - action.apply(Collections.emptyList()); - } - - @Override - public void reset() { - // no op - } -} diff --git a/markwon-core/src/test/java/io/noties/markwon/MarkwonAssert.java b/markwon-core/src/test/java/io/noties/markwon/MarkwonAssert.java new file mode 100644 index 00000000..b220c7f8 --- /dev/null +++ b/markwon-core/src/test/java/io/noties/markwon/MarkwonAssert.java @@ -0,0 +1,15 @@ +package io.noties.markwon; + +import android.support.annotation.NonNull; + +import org.junit.Assert; + +public abstract class MarkwonAssert { + + public static void assertMessageContains(@NonNull Throwable t, @NonNull String contains) { + Assert.assertTrue(t.getMessage(), t.getMessage().contains(contains)); + } + + private MarkwonAssert() { + } +} diff --git a/markwon-core/src/test/java/io/noties/markwon/MarkwonBuilderImplTest.java b/markwon-core/src/test/java/io/noties/markwon/MarkwonBuilderImplTest.java index 84a3a38e..bc0fd409 100644 --- a/markwon-core/src/test/java/io/noties/markwon/MarkwonBuilderImplTest.java +++ b/markwon-core/src/test/java/io/noties/markwon/MarkwonBuilderImplTest.java @@ -12,7 +12,6 @@ import org.robolectric.RuntimeEnvironment; import org.robolectric.annotation.Config; import io.noties.markwon.core.MarkwonTheme; -import io.noties.markwon.html.MarkwonHtmlRenderer; import static org.hamcrest.Matchers.containsString; import static org.junit.Assert.assertThat; @@ -27,39 +26,6 @@ import static org.mockito.Mockito.verify; @Config(manifest = Config.NONE) public class MarkwonBuilderImplTest { - @Test - public void prepare_plugins() { - // validate that prepare plugins is calling `ensureImplicitCoreIfHasDependents` and - // priority processor - -// final PriorityProcessor priorityProcessor = mock(PriorityProcessor.class); -// when(priorityProcessor.process(ArgumentMatchers.anyList())) -// .thenAnswer(new Answer() { -// @Override -// public Object answer(InvocationOnMock invocation) { -// return invocation.getArgument(0); -// } -// }); -// -// final MarkwonPlugin plugin = new AbstractMarkwonPlugin() { -// @NonNull -// @Override -// public Priority priority() { -// return Priority.after(CorePlugin.class); -// } -// }; -// -// final List plugins = preparePlugins(priorityProcessor, Collections.singletonList(plugin)); -// assertThat(plugins, hasSize(2)); -// assertThat(plugins, hasItem(plugin)); -// assertThat(plugins, hasItem(isA(CorePlugin.class))); -// -// verify(priorityProcessor, times(1)) -// .process(ArgumentMatchers.anyList()); - - fail(); - } - @Test public void no_plugins_added_throws() { // there is no sense in having an instance with no plugins registered diff --git a/markwon-core/src/test/java/io/noties/markwon/RegistryImplTest.java b/markwon-core/src/test/java/io/noties/markwon/RegistryImplTest.java new file mode 100644 index 00000000..283a799e --- /dev/null +++ b/markwon-core/src/test/java/io/noties/markwon/RegistryImplTest.java @@ -0,0 +1,194 @@ +package io.noties.markwon; + +import android.support.annotation.NonNull; + +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.HashMap; +import java.util.List; +import java.util.Map; + +import io.noties.markwon.core.CorePlugin; + +import static io.noties.markwon.MarkwonAssert.assertMessageContains; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.fail; +import static org.mockito.Mockito.mock; + +@RunWith(RobolectricTestRunner.class) +@Config(manifest = Config.NONE) +public class RegistryImplTest { + + @Test + public void single_plugin_requires_self() { + // detect recursive require + + final class Plugin extends AbstractMarkwonPlugin { + @Override + public void configure(@NonNull Registry registry) { + registry.require(Plugin.class); + } + } + final MarkwonPlugin plugin = new Plugin(); + + final RegistryImpl impl = new RegistryImpl(Collections.singletonList(plugin)); + + try { + impl.process(); + } catch (Throwable t) { + assertMessageContains(t, "Cyclic dependency chain found"); + } + } + + @Test + public void plugins_dependency_cycle() { + + final Map> map = new HashMap<>(); + + final class A extends AbstractMarkwonPlugin { + @Override + public void configure(@NonNull Registry registry) { + //noinspection ConstantConditions + registry.require(map.get("A")); + } + } + + final class B extends AbstractMarkwonPlugin { + @Override + public void configure(@NonNull Registry registry) { + //noinspection ConstantConditions + registry.require(map.get("B")); + } + } + + final class C extends AbstractMarkwonPlugin { + @Override + public void configure(@NonNull Registry registry) { + //noinspection ConstantConditions + registry.require(map.get("C")); + } + } + + map.put("A", B.class); + map.put("B", C.class); + map.put("C", A.class); + + final RegistryImpl impl = + new RegistryImpl(Arrays.asList((MarkwonPlugin) new A(), new B(), new C())); + + try { + impl.process(); + fail(); + } catch (Throwable t) { + assertMessageContains(t, "Cyclic dependency chain found"); + } + } + + @Test + public void plugins_no_dependency_cycle() { + + final class C extends AbstractMarkwonPlugin { + } + + final class B extends AbstractMarkwonPlugin { + @Override + public void configure(@NonNull Registry registry) { + registry.require(C.class); + } + } + + final class A extends AbstractMarkwonPlugin { + @Override + public void configure(@NonNull Registry registry) { + registry.require(B.class); + } + } + + final RegistryImpl impl = + new RegistryImpl(Arrays.asList((MarkwonPlugin) new A(), new B(), new C())); + + impl.process(); + } + + @Test + public void dependency_not_satisfied() { + // when require is called for plugin not added + + final class A extends AbstractMarkwonPlugin { + } + + final class B extends AbstractMarkwonPlugin { + @Override + public void configure(@NonNull Registry registry) { + registry.require(A.class); + } + } + + final RegistryImpl impl = + new RegistryImpl(Collections.singletonList((MarkwonPlugin) new B())); + + try { + impl.process(); + fail(); + } catch (Throwable t) { + assertMessageContains(t, "Requested plugin is not added"); + assertMessageContains(t, A.class.getName()); // ? if it's null for local class? + } + } + + @Test + public void core_plugin_first() { + // if core-plugin is present, hen it should be the first one + + final CorePlugin plugin = CorePlugin.create(); + + final RegistryImpl impl = new RegistryImpl(Arrays.asList( + mock(MarkwonPlugin.class), + mock(MarkwonPlugin.class), + plugin + )); + + final List plugins = impl.process(); + assertEquals(3, plugins.size()); + assertEquals(plugin, plugins.get(0)); + } + + @Test + public void correct_order() { + + final class A extends AbstractMarkwonPlugin { + } + + final class B extends AbstractMarkwonPlugin { + @Override + public void configure(@NonNull Registry registry) { + registry.require(A.class); + } + } + + final class C extends AbstractMarkwonPlugin { + @Override + public void configure(@NonNull Registry registry) { + registry.require(B.class); + } + } + + final A a = new A(); + final B b = new B(); + final C c = new C(); + + final RegistryImpl impl = new RegistryImpl(Arrays.asList( + (MarkwonPlugin) c, b, a)); + + final List plugins = impl.process(); + assertEquals(3, plugins.size()); + assertEquals(a, plugins.get(0)); + assertEquals(b, plugins.get(1)); + assertEquals(c, plugins.get(2)); + } +} \ No newline at end of file diff --git a/markwon-html/src/main/java/io/noties/markwon/html/HtmlPlugin.java b/markwon-html/src/main/java/io/noties/markwon/html/HtmlPlugin.java index 526fb323..6a20ada6 100644 --- a/markwon-html/src/main/java/io/noties/markwon/html/HtmlPlugin.java +++ b/markwon-html/src/main/java/io/noties/markwon/html/HtmlPlugin.java @@ -7,13 +7,13 @@ import org.commonmark.node.HtmlBlock; import org.commonmark.node.HtmlInline; import org.commonmark.node.Node; -import io.noties.markwon.html.tag.ImageHandler; import io.noties.markwon.AbstractMarkwonPlugin; import io.noties.markwon.MarkwonConfiguration; import io.noties.markwon.MarkwonVisitor; import io.noties.markwon.html.tag.BlockquoteHandler; import io.noties.markwon.html.tag.EmphasisHandler; import io.noties.markwon.html.tag.HeadingHandler; +import io.noties.markwon.html.tag.ImageHandler; import io.noties.markwon.html.tag.LinkHandler; import io.noties.markwon.html.tag.ListHandler; import io.noties.markwon.html.tag.StrikeHandler; @@ -53,10 +53,13 @@ public class HtmlPlugin extends AbstractMarkwonPlugin { public static final float SCRIPT_DEF_TEXT_SIZE_RATIO = .75F; private final MarkwonHtmlRendererImpl.Builder builder; + private final MarkwonHtmlParser htmlParser; + private MarkwonHtmlRenderer htmlRenderer; @SuppressWarnings("WeakerAccess") HtmlPlugin() { this.builder = new MarkwonHtmlRendererImpl.Builder(); + this.htmlParser = MarkwonHtmlParserImpl.create(); } /** @@ -104,6 +107,8 @@ public class HtmlPlugin extends AbstractMarkwonPlugin { @Override public void configureConfiguration(@NonNull MarkwonConfiguration.Builder configurationBuilder) { + // @since 4.0.0-SNAPSHOT we init internal html-renderer here (marks the end of configuration) + final MarkwonHtmlRendererImpl.Builder builder = this.builder; if (!builder.excludeDefaults()) { @@ -123,15 +128,17 @@ public class HtmlPlugin extends AbstractMarkwonPlugin { builder.addDefaultTagHandler(new HeadingHandler()); } - configurationBuilder - .htmlRenderer(builder.build()) - .htmlParser(MarkwonHtmlParserImpl.create()); + htmlRenderer = builder.build(); } @Override public void afterRender(@NonNull Node node, @NonNull MarkwonVisitor visitor) { - final MarkwonConfiguration configuration = visitor.configuration(); - configuration.htmlRenderer().render(visitor, configuration.htmlParser()); + final MarkwonHtmlRenderer htmlRenderer = this.htmlRenderer; + if (htmlRenderer != null) { + htmlRenderer.render(visitor, htmlParser); + } else { + throw new IllegalStateException("Unexpected state, html-renderer is not defined"); + } } @Override @@ -153,7 +160,7 @@ public class HtmlPlugin extends AbstractMarkwonPlugin { private void visitHtml(@NonNull MarkwonVisitor visitor, @Nullable String html) { if (html != null) { - visitor.configuration().htmlParser().processFragment(visitor.builder(), html); + htmlParser.processFragment(visitor.builder(), html); } } } diff --git a/markwon-core/src/main/java/io/noties/markwon/html/HtmlTag.java b/markwon-html/src/main/java/io/noties/markwon/html/HtmlTag.java similarity index 100% rename from markwon-core/src/main/java/io/noties/markwon/html/HtmlTag.java rename to markwon-html/src/main/java/io/noties/markwon/html/HtmlTag.java diff --git a/markwon-core/src/main/java/io/noties/markwon/html/MarkwonHtmlParser.java b/markwon-html/src/main/java/io/noties/markwon/html/MarkwonHtmlParser.java similarity index 91% rename from markwon-core/src/main/java/io/noties/markwon/html/MarkwonHtmlParser.java rename to markwon-html/src/main/java/io/noties/markwon/html/MarkwonHtmlParser.java index ec3b32c8..368306a7 100644 --- a/markwon-core/src/main/java/io/noties/markwon/html/MarkwonHtmlParser.java +++ b/markwon-html/src/main/java/io/noties/markwon/html/MarkwonHtmlParser.java @@ -9,14 +9,6 @@ import java.util.List; */ public abstract class MarkwonHtmlParser { - /** - * Factory method to create a `no-op` implementation (no parsing) - */ - @NonNull - public static MarkwonHtmlParser noOp() { - return new MarkwonHtmlParserNoOp(); - } - public interface FlushAction { void apply(@NonNull List tags); } diff --git a/markwon-core/src/main/java/io/noties/markwon/html/MarkwonHtmlRenderer.java b/markwon-html/src/main/java/io/noties/markwon/html/MarkwonHtmlRenderer.java similarity index 73% rename from markwon-core/src/main/java/io/noties/markwon/html/MarkwonHtmlRenderer.java rename to markwon-html/src/main/java/io/noties/markwon/html/MarkwonHtmlRenderer.java index 26970f2a..f0e87f37 100644 --- a/markwon-core/src/main/java/io/noties/markwon/html/MarkwonHtmlRenderer.java +++ b/markwon-html/src/main/java/io/noties/markwon/html/MarkwonHtmlRenderer.java @@ -10,14 +10,6 @@ import io.noties.markwon.MarkwonVisitor; */ public abstract class MarkwonHtmlRenderer { - /** - * @since 4.0.0-SNAPSHOT - */ - @NonNull - public static MarkwonHtmlRenderer noOp() { - return new MarkwonHtmlRendererNoOp(); - } - public abstract void render( @NonNull MarkwonVisitor visitor, @NonNull MarkwonHtmlParser parser diff --git a/markwon-core/src/main/java/io/noties/markwon/html/MarkwonHtmlRendererNoOp.java b/markwon-html/src/main/java/io/noties/markwon/html/MarkwonHtmlRendererNoOp.java similarity index 100% rename from markwon-core/src/main/java/io/noties/markwon/html/MarkwonHtmlRendererNoOp.java rename to markwon-html/src/main/java/io/noties/markwon/html/MarkwonHtmlRendererNoOp.java diff --git a/markwon-core/src/main/java/io/noties/markwon/html/TagHandler.java b/markwon-html/src/main/java/io/noties/markwon/html/TagHandler.java similarity index 100% rename from markwon-core/src/main/java/io/noties/markwon/html/TagHandler.java rename to markwon-html/src/main/java/io/noties/markwon/html/TagHandler.java diff --git a/markwon-image/src/main/java/io/noties/markwon/image/ImagesPlugin.java b/markwon-image/src/main/java/io/noties/markwon/image/ImagesPlugin.java index 4d7ad275..7a9a8df8 100644 --- a/markwon-image/src/main/java/io/noties/markwon/image/ImagesPlugin.java +++ b/markwon-image/src/main/java/io/noties/markwon/image/ImagesPlugin.java @@ -3,6 +3,7 @@ package io.noties.markwon.image; import android.graphics.drawable.Drawable; import android.support.annotation.NonNull; import android.support.annotation.Nullable; +import android.support.annotation.VisibleForTesting; import android.text.Spanned; import android.widget.TextView; @@ -10,16 +11,16 @@ import org.commonmark.node.Image; import java.util.concurrent.ExecutorService; +import io.noties.markwon.AbstractMarkwonPlugin; +import io.noties.markwon.MarkwonConfiguration; import io.noties.markwon.MarkwonPlugin; +import io.noties.markwon.MarkwonSpansFactory; import io.noties.markwon.image.data.DataUriSchemeHandler; import io.noties.markwon.image.file.FileSchemeHandler; import io.noties.markwon.image.gif.GifMediaDecoder; import io.noties.markwon.image.network.NetworkSchemeHandler; import io.noties.markwon.image.network.OkHttpNetworkSchemeHandler; import io.noties.markwon.image.svg.SvgMediaDecoder; -import io.noties.markwon.AbstractMarkwonPlugin; -import io.noties.markwon.MarkwonConfiguration; -import io.noties.markwon.MarkwonSpansFactory; @SuppressWarnings({"UnusedReturnValue", "WeakerAccess"}) public class ImagesPlugin extends AbstractMarkwonPlugin { @@ -70,7 +71,18 @@ public class ImagesPlugin extends AbstractMarkwonPlugin { return plugin; } - private final AsyncDrawableLoaderBuilder builder = new AsyncDrawableLoaderBuilder(); + private final AsyncDrawableLoaderBuilder builder; + + // @since 4.0.0-SNAPSHOT + ImagesPlugin() { + this(new AsyncDrawableLoaderBuilder()); + } + + // @since 4.0.0-SNAPSHOT + @VisibleForTesting + ImagesPlugin(@NonNull AsyncDrawableLoaderBuilder builder) { + this.builder = builder; + } /** * Optional (by default new cached thread executor will be used) diff --git a/markwon-image/src/test/java/io/noties/markwon/image/AsyncDrawableLoaderBuilderTest.java b/markwon-image/src/test/java/io/noties/markwon/image/AsyncDrawableLoaderBuilderTest.java index 51006e38..a5861f6b 100644 --- a/markwon-image/src/test/java/io/noties/markwon/image/AsyncDrawableLoaderBuilderTest.java +++ b/markwon-image/src/test/java/io/noties/markwon/image/AsyncDrawableLoaderBuilderTest.java @@ -18,6 +18,7 @@ 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.junit.Assert.fail; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; @@ -203,4 +204,16 @@ public class AsyncDrawableLoaderBuilderTest { builder.removeMediaDecoder(media); assertNull(builder.mediaDecoders.get(media)); } + + @Test + public void cannot_build_twice() { + + builder.build(); + try { + builder.build(); + fail(); + } catch (Throwable t) { + assertTrue(t.getMessage(), t.getMessage().contains("has already been configured")); + } + } } \ No newline at end of file diff --git a/markwon-image/src/test/java/io/noties/markwon/image/ImagesPluginTest.java b/markwon-image/src/test/java/io/noties/markwon/image/ImagesPluginTest.java index 27f2fbb9..f19b1563 100644 --- a/markwon-image/src/test/java/io/noties/markwon/image/ImagesPluginTest.java +++ b/markwon-image/src/test/java/io/noties/markwon/image/ImagesPluginTest.java @@ -27,6 +27,7 @@ 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.verifyNoMoreInteractions; import static org.mockito.Mockito.when; @RunWith(RobolectricTestRunner.class) @@ -175,4 +176,70 @@ public class ImagesPluginTest { verify(textView, times(1)) .getTag(eq(R.id.markwon_drawables_scheduler_last_text_hashcode)); } + + @Test + public void methods_redirected_to_builder() { + + final AsyncDrawableLoaderBuilder builder = mock(AsyncDrawableLoaderBuilder.class); + final ImagesPlugin plugin = new ImagesPlugin(builder); + + // executor service + { + final ExecutorService executorService = mock(ExecutorService.class); + plugin.executorService(executorService); + verify(builder, times(1)).executorService(eq(executorService)); + } + + // add scheme-handler + { + final SchemeHandler schemeHandler = mock(SchemeHandler.class); + plugin.addSchemeHandler(schemeHandler); + verify(builder, times(1)).addSchemeHandler(eq(schemeHandler)); + } + + // add media-decoder + { + final MediaDecoder mediaDecoder = mock(MediaDecoder.class); + plugin.addMediaDecoder(mediaDecoder); + verify(builder, times(1)).addMediaDecoder(eq(mediaDecoder)); + } + + // default-media-decoder + { + final MediaDecoder mediaDecoder = mock(MediaDecoder.class); + plugin.defaultMediaDecoder(mediaDecoder); + verify(builder, times(1)).defaultMediaDecoder(eq(mediaDecoder)); + } + + // remove scheme-handler + { + final String scheme = "yo"; + plugin.removeSchemeHandler(scheme); + verify(builder, times(1)).removeSchemeHandler(eq(scheme)); + } + + // remove media-decoder + { + final String contentType = "fa/ke"; + plugin.removeMediaDecoder(contentType); + verify(builder, times(1)).removeMediaDecoder(eq(contentType)); + } + + // placeholder provider + { + final ImagesPlugin.PlaceholderProvider placeholderProvider = + mock(ImagesPlugin.PlaceholderProvider.class); + plugin.placeholderProvider(placeholderProvider); + verify(builder, times(1)).placeholderProvider(eq(placeholderProvider)); + } + + // error-handler + { + final ImagesPlugin.ErrorHandler errorHandler = mock(ImagesPlugin.ErrorHandler.class); + plugin.errorHandler(errorHandler); + verify(builder, times(1)).errorHandler(eq(errorHandler)); + } + + verifyNoMoreInteractions(builder); + } } \ No newline at end of file