registryImpl test

This commit is contained in:
Dimitry Ivanov 2019-06-08 15:50:07 +03:00
parent 173425ed53
commit a2a5857f06
19 changed files with 447 additions and 247 deletions

View File

@ -9,3 +9,4 @@
* images-plugin moved to standalone again * images-plugin moved to standalone again
* removed MarkwonPlugin#configureHtmlRenderer -> now part of HtmlPlugin * removed MarkwonPlugin#configureHtmlRenderer -> now part of HtmlPlugin
* TagHandler now has `supportedTags()` method * TagHandler now has `supportedTags()` method
* html is moved completely to html-plugin

View File

@ -13,27 +13,24 @@ import java.util.concurrent.Future;
import javax.inject.Inject; import javax.inject.Inject;
import io.noties.debug.Debug;
import io.noties.markwon.AbstractMarkwonPlugin; import io.noties.markwon.AbstractMarkwonPlugin;
import io.noties.markwon.Markwon; import io.noties.markwon.Markwon;
import io.noties.markwon.MarkwonConfiguration; import io.noties.markwon.MarkwonConfiguration;
import io.noties.markwon.app.gif.GifAwarePlugin;
import io.noties.markwon.ext.strikethrough.StrikethroughPlugin; import io.noties.markwon.ext.strikethrough.StrikethroughPlugin;
import io.noties.markwon.ext.tables.TablePlugin; import io.noties.markwon.ext.tables.TablePlugin;
import io.noties.markwon.ext.tasklist.TaskListPlugin; import io.noties.markwon.ext.tasklist.TaskListPlugin;
import io.noties.markwon.app.gif.GifAwarePlugin;
import io.noties.markwon.html.HtmlPlugin; import io.noties.markwon.html.HtmlPlugin;
import io.noties.markwon.image.ImagesPlugin; 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.file.FileSchemeHandler;
import io.noties.markwon.image.gif.GifMediaDecoder;
import io.noties.markwon.image.network.OkHttpNetworkSchemeHandler; 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.Prism4jTheme;
import io.noties.markwon.syntax.Prism4jThemeDarkula; import io.noties.markwon.syntax.Prism4jThemeDarkula;
import io.noties.markwon.syntax.Prism4jThemeDefault; import io.noties.markwon.syntax.Prism4jThemeDefault;
import io.noties.markwon.syntax.SyntaxHighlightPlugin; import io.noties.markwon.syntax.SyntaxHighlightPlugin;
import io.noties.markwon.urlprocessor.UrlProcessor; import io.noties.markwon.urlprocessor.UrlProcessor;
import io.noties.markwon.urlprocessor.UrlProcessorRelativeToAbsolute; import io.noties.markwon.urlprocessor.UrlProcessorRelativeToAbsolute;
import io.noties.debug.Debug;
import ru.noties.prism4j.Prism4j; import ru.noties.prism4j.Prism4j;
@ActivityScope @ActivityScope
@ -102,12 +99,12 @@ public class MarkdownRenderer {
.usePlugin(ImagesPlugin.create(new ImagesPlugin.ImagesConfigure() { .usePlugin(ImagesPlugin.create(new ImagesPlugin.ImagesConfigure() {
@Override @Override
public void configureImages(@NonNull ImagesPlugin plugin) { 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 plugin
.addSchemeHandler(DataUriSchemeHandler.create())
.addSchemeHandler(OkHttpNetworkSchemeHandler.create()) .addSchemeHandler(OkHttpNetworkSchemeHandler.create())
.addSchemeHandler(FileSchemeHandler.createWithAssets(context.getAssets())) .addSchemeHandler(FileSchemeHandler.createWithAssets(context.getAssets()));
.addMediaDecoder(GifMediaDecoder.create(false))
.addMediaDecoder(SvgMediaDecoder.create());
} }
})) }))
.usePlugin(SyntaxHighlightPlugin.create(prism4j, prism4jTheme)) .usePlugin(SyntaxHighlightPlugin.create(prism4j, prism4jTheme))

View File

@ -5,7 +5,7 @@ buildscript {
} }
dependencies { dependencies {
classpath 'com.android.tools.build:gradle:3.4.1' 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'
} }
} }

View File

@ -2,18 +2,14 @@ package io.noties.markwon;
import android.content.Context; import android.content.Context;
import android.support.annotation.NonNull; import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.support.annotation.VisibleForTesting;
import android.widget.TextView; import android.widget.TextView;
import org.commonmark.parser.Parser; import org.commonmark.parser.Parser;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Collections; import java.util.Collections;
import java.util.HashSet;
import java.util.Iterator; import java.util.Iterator;
import java.util.List; import java.util.List;
import java.util.Set;
import io.noties.markwon.core.MarkwonTheme; import io.noties.markwon.core.MarkwonTheme;
@ -106,109 +102,8 @@ class MarkwonBuilderImpl implements Markwon.Builder {
); );
} }
@VisibleForTesting
@NonNull @NonNull
static List<MarkwonPlugin> preparePlugins(@NonNull List<MarkwonPlugin> plugins) { private static List<MarkwonPlugin> preparePlugins(@NonNull List<MarkwonPlugin> plugins) {
return new RegistryImpl(plugins).process(); return new RegistryImpl(plugins).process();
} }
// @since 4.0.0-SNAPSHOT
private static class RegistryImpl implements MarkwonPlugin.Registry {
private final List<MarkwonPlugin> origin;
private final List<MarkwonPlugin> plugins;
private final Set<MarkwonPlugin> pending;
RegistryImpl(@NonNull List<MarkwonPlugin> origin) {
this.origin = origin;
this.plugins = new ArrayList<>(origin.size());
this.pending = new HashSet<>(3);
}
@NonNull
@Override
public <P extends MarkwonPlugin> P require(@NonNull Class<P> plugin) {
return get(plugin);
}
@Override
public <P extends MarkwonPlugin> void require(
@NonNull Class<P> plugin,
@NonNull MarkwonPlugin.Action<? super P> action) {
action.apply(get(plugin));
}
@NonNull
List<MarkwonPlugin> 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 extends MarkwonPlugin> P get(@NonNull Class<P> 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 extends MarkwonPlugin> P find(
@NonNull List<MarkwonPlugin> plugins,
@NonNull Class<P> type) {
for (MarkwonPlugin plugin : plugins) {
if (type.isAssignableFrom(plugin.getClass())) {
//noinspection unchecked
return (P) plugin;
}
}
return null;
}
}
} }

View File

@ -4,8 +4,6 @@ import android.support.annotation.NonNull;
import io.noties.markwon.core.MarkwonTheme; import io.noties.markwon.core.MarkwonTheme;
import io.noties.markwon.core.spans.LinkSpan; 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.AsyncDrawableLoader;
import io.noties.markwon.image.ImageSizeResolver; import io.noties.markwon.image.ImageSizeResolver;
import io.noties.markwon.image.ImageSizeResolverDef; import io.noties.markwon.image.ImageSizeResolverDef;
@ -31,8 +29,6 @@ public class MarkwonConfiguration {
private final LinkSpan.Resolver linkResolver; private final LinkSpan.Resolver linkResolver;
private final UrlProcessor urlProcessor; private final UrlProcessor urlProcessor;
private final ImageSizeResolver imageSizeResolver; private final ImageSizeResolver imageSizeResolver;
private final MarkwonHtmlParser htmlParser;
private final MarkwonHtmlRenderer htmlRenderer;
// @since 3.0.0 // @since 3.0.0
private final MarkwonSpansFactory spansFactory; private final MarkwonSpansFactory spansFactory;
@ -45,8 +41,6 @@ public class MarkwonConfiguration {
this.urlProcessor = builder.urlProcessor; this.urlProcessor = builder.urlProcessor;
this.imageSizeResolver = builder.imageSizeResolver; this.imageSizeResolver = builder.imageSizeResolver;
this.spansFactory = builder.spansFactory; this.spansFactory = builder.spansFactory;
this.htmlParser = builder.htmlParser;
this.htmlRenderer = builder.htmlRenderer;
} }
@NonNull @NonNull
@ -79,16 +73,6 @@ public class MarkwonConfiguration {
return imageSizeResolver; return imageSizeResolver;
} }
@NonNull
public MarkwonHtmlParser htmlParser() {
return htmlParser;
}
@NonNull
public MarkwonHtmlRenderer htmlRenderer() {
return htmlRenderer;
}
/** /**
* @since 3.0.0 * @since 3.0.0
*/ */
@ -106,8 +90,6 @@ public class MarkwonConfiguration {
private LinkSpan.Resolver linkResolver; private LinkSpan.Resolver linkResolver;
private UrlProcessor urlProcessor; private UrlProcessor urlProcessor;
private ImageSizeResolver imageSizeResolver; private ImageSizeResolver imageSizeResolver;
private MarkwonHtmlParser htmlParser;
private MarkwonHtmlRenderer htmlRenderer;
private MarkwonSpansFactory spansFactory; private MarkwonSpansFactory spansFactory;
Builder() { Builder() {
@ -122,15 +104,6 @@ public class MarkwonConfiguration {
return this; return this;
} }
/**
* @since 4.0.0-SNAPSHOT
*/
@NonNull
public Builder htmlRenderer(@NonNull MarkwonHtmlRenderer htmlRenderer) {
this.htmlRenderer = htmlRenderer;
return this;
}
@NonNull @NonNull
public Builder syntaxHighlight(@NonNull SyntaxHighlight syntaxHighlight) { public Builder syntaxHighlight(@NonNull SyntaxHighlight syntaxHighlight) {
this.syntaxHighlight = syntaxHighlight; this.syntaxHighlight = syntaxHighlight;
@ -149,12 +122,6 @@ public class MarkwonConfiguration {
return this; return this;
} }
@NonNull
public Builder htmlParser(@NonNull MarkwonHtmlParser htmlParser) {
this.htmlParser = htmlParser;
return this;
}
/** /**
* @since 1.0.1 * @since 1.0.1
*/ */
@ -177,11 +144,6 @@ public class MarkwonConfiguration {
asyncDrawableLoader = AsyncDrawableLoader.noOp(); asyncDrawableLoader = AsyncDrawableLoader.noOp();
} }
// @since 4.0.0-SNAPSHOT
if (htmlRenderer == null) {
htmlRenderer = MarkwonHtmlRenderer.noOp();
}
if (syntaxHighlight == null) { if (syntaxHighlight == null) {
syntaxHighlight = new SyntaxHighlightNoOp(); syntaxHighlight = new SyntaxHighlightNoOp();
} }
@ -198,10 +160,6 @@ public class MarkwonConfiguration {
imageSizeResolver = new ImageSizeResolverDef(); imageSizeResolver = new ImageSizeResolverDef();
} }
if (htmlParser == null) {
htmlParser = MarkwonHtmlParser.noOp();
}
return new MarkwonConfiguration(this); return new MarkwonConfiguration(this);
} }
} }

View File

@ -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<MarkwonPlugin> origin;
private final List<MarkwonPlugin> plugins;
private final Set<MarkwonPlugin> pending;
RegistryImpl(@NonNull List<MarkwonPlugin> origin) {
this.origin = origin;
this.plugins = new ArrayList<>(origin.size());
this.pending = new HashSet<>(3);
}
@NonNull
@Override
public <P extends MarkwonPlugin> P require(@NonNull Class<P> plugin) {
return get(plugin);
}
@Override
public <P extends MarkwonPlugin> void require(
@NonNull Class<P> plugin,
@NonNull MarkwonPlugin.Action<? super P> action) {
action.apply(get(plugin));
}
@NonNull
List<MarkwonPlugin> 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 extends MarkwonPlugin> P get(@NonNull Class<P> 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 extends MarkwonPlugin> P find(
@NonNull List<MarkwonPlugin> plugins,
@NonNull Class<P> type) {
for (MarkwonPlugin plugin : plugins) {
if (type.isAssignableFrom(plugin.getClass())) {
//noinspection unchecked
return (P) plugin;
}
}
return null;
}
}

View File

@ -1,27 +0,0 @@
package io.noties.markwon.html;
import android.support.annotation.NonNull;
import java.util.Collections;
class MarkwonHtmlParserNoOp extends MarkwonHtmlParser {
@Override
public <T extends Appendable & CharSequence> void processFragment(@NonNull T output, @NonNull String htmlFragment) {
// no op
}
@Override
public void flushInlineTags(int documentLength, @NonNull FlushAction<HtmlTag.Inline> action) {
action.apply(Collections.<HtmlTag.Inline>emptyList());
}
@Override
public void flushBlockTags(int documentLength, @NonNull FlushAction<HtmlTag.Block> action) {
action.apply(Collections.<HtmlTag.Block>emptyList());
}
@Override
public void reset() {
// no op
}
}

View File

@ -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() {
}
}

View File

@ -12,7 +12,6 @@ import org.robolectric.RuntimeEnvironment;
import org.robolectric.annotation.Config; import org.robolectric.annotation.Config;
import io.noties.markwon.core.MarkwonTheme; import io.noties.markwon.core.MarkwonTheme;
import io.noties.markwon.html.MarkwonHtmlRenderer;
import static org.hamcrest.Matchers.containsString; import static org.hamcrest.Matchers.containsString;
import static org.junit.Assert.assertThat; import static org.junit.Assert.assertThat;
@ -27,39 +26,6 @@ import static org.mockito.Mockito.verify;
@Config(manifest = Config.NONE) @Config(manifest = Config.NONE)
public class MarkwonBuilderImplTest { 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.<MarkwonPlugin>anyList()))
// .thenAnswer(new Answer<Object>() {
// @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<MarkwonPlugin> 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.<MarkwonPlugin>anyList());
fail();
}
@Test @Test
public void no_plugins_added_throws() { public void no_plugins_added_throws() {
// there is no sense in having an instance with no plugins registered // there is no sense in having an instance with no plugins registered

View File

@ -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<String, Class<? extends MarkwonPlugin>> 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<MarkwonPlugin> 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<MarkwonPlugin> plugins = impl.process();
assertEquals(3, plugins.size());
assertEquals(a, plugins.get(0));
assertEquals(b, plugins.get(1));
assertEquals(c, plugins.get(2));
}
}

View File

@ -7,13 +7,13 @@ import org.commonmark.node.HtmlBlock;
import org.commonmark.node.HtmlInline; import org.commonmark.node.HtmlInline;
import org.commonmark.node.Node; import org.commonmark.node.Node;
import io.noties.markwon.html.tag.ImageHandler;
import io.noties.markwon.AbstractMarkwonPlugin; import io.noties.markwon.AbstractMarkwonPlugin;
import io.noties.markwon.MarkwonConfiguration; import io.noties.markwon.MarkwonConfiguration;
import io.noties.markwon.MarkwonVisitor; import io.noties.markwon.MarkwonVisitor;
import io.noties.markwon.html.tag.BlockquoteHandler; import io.noties.markwon.html.tag.BlockquoteHandler;
import io.noties.markwon.html.tag.EmphasisHandler; import io.noties.markwon.html.tag.EmphasisHandler;
import io.noties.markwon.html.tag.HeadingHandler; 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.LinkHandler;
import io.noties.markwon.html.tag.ListHandler; import io.noties.markwon.html.tag.ListHandler;
import io.noties.markwon.html.tag.StrikeHandler; 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; public static final float SCRIPT_DEF_TEXT_SIZE_RATIO = .75F;
private final MarkwonHtmlRendererImpl.Builder builder; private final MarkwonHtmlRendererImpl.Builder builder;
private final MarkwonHtmlParser htmlParser;
private MarkwonHtmlRenderer htmlRenderer;
@SuppressWarnings("WeakerAccess") @SuppressWarnings("WeakerAccess")
HtmlPlugin() { HtmlPlugin() {
this.builder = new MarkwonHtmlRendererImpl.Builder(); this.builder = new MarkwonHtmlRendererImpl.Builder();
this.htmlParser = MarkwonHtmlParserImpl.create();
} }
/** /**
@ -104,6 +107,8 @@ public class HtmlPlugin extends AbstractMarkwonPlugin {
@Override @Override
public void configureConfiguration(@NonNull MarkwonConfiguration.Builder configurationBuilder) { 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; final MarkwonHtmlRendererImpl.Builder builder = this.builder;
if (!builder.excludeDefaults()) { if (!builder.excludeDefaults()) {
@ -123,15 +128,17 @@ public class HtmlPlugin extends AbstractMarkwonPlugin {
builder.addDefaultTagHandler(new HeadingHandler()); builder.addDefaultTagHandler(new HeadingHandler());
} }
configurationBuilder htmlRenderer = builder.build();
.htmlRenderer(builder.build())
.htmlParser(MarkwonHtmlParserImpl.create());
} }
@Override @Override
public void afterRender(@NonNull Node node, @NonNull MarkwonVisitor visitor) { public void afterRender(@NonNull Node node, @NonNull MarkwonVisitor visitor) {
final MarkwonConfiguration configuration = visitor.configuration(); final MarkwonHtmlRenderer htmlRenderer = this.htmlRenderer;
configuration.htmlRenderer().render(visitor, configuration.htmlParser()); if (htmlRenderer != null) {
htmlRenderer.render(visitor, htmlParser);
} else {
throw new IllegalStateException("Unexpected state, html-renderer is not defined");
}
} }
@Override @Override
@ -153,7 +160,7 @@ public class HtmlPlugin extends AbstractMarkwonPlugin {
private void visitHtml(@NonNull MarkwonVisitor visitor, @Nullable String html) { private void visitHtml(@NonNull MarkwonVisitor visitor, @Nullable String html) {
if (html != null) { if (html != null) {
visitor.configuration().htmlParser().processFragment(visitor.builder(), html); htmlParser.processFragment(visitor.builder(), html);
} }
} }
} }

View File

@ -9,14 +9,6 @@ import java.util.List;
*/ */
public abstract class MarkwonHtmlParser { 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<T> { public interface FlushAction<T> {
void apply(@NonNull List<T> tags); void apply(@NonNull List<T> tags);
} }

View File

@ -10,14 +10,6 @@ import io.noties.markwon.MarkwonVisitor;
*/ */
public abstract class MarkwonHtmlRenderer { public abstract class MarkwonHtmlRenderer {
/**
* @since 4.0.0-SNAPSHOT
*/
@NonNull
public static MarkwonHtmlRenderer noOp() {
return new MarkwonHtmlRendererNoOp();
}
public abstract void render( public abstract void render(
@NonNull MarkwonVisitor visitor, @NonNull MarkwonVisitor visitor,
@NonNull MarkwonHtmlParser parser @NonNull MarkwonHtmlParser parser

View File

@ -3,6 +3,7 @@ package io.noties.markwon.image;
import android.graphics.drawable.Drawable; import android.graphics.drawable.Drawable;
import android.support.annotation.NonNull; import android.support.annotation.NonNull;
import android.support.annotation.Nullable; import android.support.annotation.Nullable;
import android.support.annotation.VisibleForTesting;
import android.text.Spanned; import android.text.Spanned;
import android.widget.TextView; import android.widget.TextView;
@ -10,16 +11,16 @@ import org.commonmark.node.Image;
import java.util.concurrent.ExecutorService; import java.util.concurrent.ExecutorService;
import io.noties.markwon.AbstractMarkwonPlugin;
import io.noties.markwon.MarkwonConfiguration;
import io.noties.markwon.MarkwonPlugin; import io.noties.markwon.MarkwonPlugin;
import io.noties.markwon.MarkwonSpansFactory;
import io.noties.markwon.image.data.DataUriSchemeHandler; import io.noties.markwon.image.data.DataUriSchemeHandler;
import io.noties.markwon.image.file.FileSchemeHandler; import io.noties.markwon.image.file.FileSchemeHandler;
import io.noties.markwon.image.gif.GifMediaDecoder; import io.noties.markwon.image.gif.GifMediaDecoder;
import io.noties.markwon.image.network.NetworkSchemeHandler; import io.noties.markwon.image.network.NetworkSchemeHandler;
import io.noties.markwon.image.network.OkHttpNetworkSchemeHandler; import io.noties.markwon.image.network.OkHttpNetworkSchemeHandler;
import io.noties.markwon.image.svg.SvgMediaDecoder; 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"}) @SuppressWarnings({"UnusedReturnValue", "WeakerAccess"})
public class ImagesPlugin extends AbstractMarkwonPlugin { public class ImagesPlugin extends AbstractMarkwonPlugin {
@ -70,7 +71,18 @@ public class ImagesPlugin extends AbstractMarkwonPlugin {
return plugin; 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) * Optional (by default new cached thread executor will be used)

View File

@ -18,6 +18,7 @@ import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertNull; import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertTrue; import static org.junit.Assert.assertTrue;
import static org.junit.Assert.fail;
import static org.mockito.Mockito.mock; import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when; import static org.mockito.Mockito.when;
@ -203,4 +204,16 @@ public class AsyncDrawableLoaderBuilderTest {
builder.removeMediaDecoder(media); builder.removeMediaDecoder(media);
assertNull(builder.mediaDecoders.get(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"));
}
}
} }

View File

@ -27,6 +27,7 @@ import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.mock; import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.times; import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify; import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.verifyNoMoreInteractions;
import static org.mockito.Mockito.when; import static org.mockito.Mockito.when;
@RunWith(RobolectricTestRunner.class) @RunWith(RobolectricTestRunner.class)
@ -175,4 +176,70 @@ public class ImagesPluginTest {
verify(textView, times(1)) verify(textView, times(1))
.getTag(eq(R.id.markwon_drawables_scheduler_last_text_hashcode)); .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);
}
} }