From 581265a22a65bcd5ce831b85d3d15587e681487b Mon Sep 17 00:00:00 2001 From: Dimitry Ivanov Date: Wed, 26 Dec 2018 19:07:31 +0300 Subject: [PATCH] Introduce priority abstraction (order and dependency for plugins) --- .../ru/noties/markwon/gif/GifAwarePlugin.java | 8 + .../markwon/ext/latex/JLatexMathPlugin.java | 8 + .../noties/markwon/image/gif/GifPlugin.java | 8 + ...ttpPlugin.java => OkHttpImagesPlugin.java} | 20 +- .../noties/markwon/image/svg/SvgPlugin.java | 8 + .../noties/markwon/AbstractMarkwonPlugin.java | 12 + .../main/java/ru/noties/markwon/Markwon.java | 19 +- .../ru/noties/markwon/MarkwonBuilderImpl.java | 22 + .../java/ru/noties/markwon/MarkwonPlugin.java | 4 + .../ru/noties/markwon/core/CorePlugin.java | 11 +- .../ru/noties/markwon/priority/Priority.java | 96 ++++ .../markwon/priority/PriorityProcessor.java | 18 + .../priority/PriorityProcessorImpl.java | 133 +++++ .../priority/PriorityProcessorTest.java | 468 ++++++++++++++++++ 14 files changed, 824 insertions(+), 11 deletions(-) rename markwon-image-okhttp/src/main/java/ru/noties/markwon/image/okhttp/{MarkwonImageOkHttpPlugin.java => OkHttpImagesPlugin.java} (61%) create mode 100644 markwon/src/main/java/ru/noties/markwon/priority/Priority.java create mode 100644 markwon/src/main/java/ru/noties/markwon/priority/PriorityProcessor.java create mode 100644 markwon/src/main/java/ru/noties/markwon/priority/PriorityProcessorImpl.java create mode 100644 markwon/src/test/java/ru/noties/markwon/priority/PriorityProcessorTest.java diff --git a/app/src/main/java/ru/noties/markwon/gif/GifAwarePlugin.java b/app/src/main/java/ru/noties/markwon/gif/GifAwarePlugin.java index 95509525..89e49384 100644 --- a/app/src/main/java/ru/noties/markwon/gif/GifAwarePlugin.java +++ b/app/src/main/java/ru/noties/markwon/gif/GifAwarePlugin.java @@ -14,6 +14,8 @@ import ru.noties.markwon.RenderProps; import ru.noties.markwon.SpanFactory; import ru.noties.markwon.image.AsyncDrawableSpan; import ru.noties.markwon.image.ImageProps; +import ru.noties.markwon.image.ImagesPlugin; +import ru.noties.markwon.priority.Priority; public class GifAwarePlugin extends AbstractMarkwonPlugin { @@ -57,6 +59,12 @@ public class GifAwarePlugin extends AbstractMarkwonPlugin { }); } + @NonNull + @Override + public Priority priority() { + return Priority.after(ImagesPlugin.class); + } + @Override public void afterSetText(@NonNull TextView textView) { processor.process(textView); 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 ee653b2f..9bd80400 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 @@ -21,8 +21,10 @@ import ru.noties.markwon.image.AsyncDrawableLoader; import ru.noties.markwon.image.ImageItem; import ru.noties.markwon.image.ImageProps; import ru.noties.markwon.image.ImageSize; +import ru.noties.markwon.image.ImagesPlugin; import ru.noties.markwon.image.MediaDecoder; import ru.noties.markwon.image.SchemeHandler; +import ru.noties.markwon.priority.Priority; public class JLatexMathPlugin extends AbstractMarkwonPlugin { @@ -136,4 +138,10 @@ public class JLatexMathPlugin extends AbstractMarkwonPlugin { } }); } + + @NonNull + @Override + public Priority priority() { + return Priority.after(ImagesPlugin.class); + } } diff --git a/markwon-image-gif/src/main/java/ru/noties/markwon/image/gif/GifPlugin.java b/markwon-image-gif/src/main/java/ru/noties/markwon/image/gif/GifPlugin.java index 3ee0c7aa..d0db857e 100644 --- a/markwon-image-gif/src/main/java/ru/noties/markwon/image/gif/GifPlugin.java +++ b/markwon-image-gif/src/main/java/ru/noties/markwon/image/gif/GifPlugin.java @@ -4,6 +4,8 @@ import android.support.annotation.NonNull; import ru.noties.markwon.AbstractMarkwonPlugin; import ru.noties.markwon.image.AsyncDrawableLoader; +import ru.noties.markwon.image.ImagesPlugin; +import ru.noties.markwon.priority.Priority; public class GifPlugin extends AbstractMarkwonPlugin { @@ -27,4 +29,10 @@ public class GifPlugin extends AbstractMarkwonPlugin { public void configureImages(@NonNull AsyncDrawableLoader.Builder builder) { builder.addMediaDecoder(GifMediaDecoder.CONTENT_TYPE, GifMediaDecoder.create(autoPlay)); } + + @NonNull + @Override + public Priority priority() { + return Priority.after(ImagesPlugin.class); + } } diff --git a/markwon-image-okhttp/src/main/java/ru/noties/markwon/image/okhttp/MarkwonImageOkHttpPlugin.java b/markwon-image-okhttp/src/main/java/ru/noties/markwon/image/okhttp/OkHttpImagesPlugin.java similarity index 61% rename from markwon-image-okhttp/src/main/java/ru/noties/markwon/image/okhttp/MarkwonImageOkHttpPlugin.java rename to markwon-image-okhttp/src/main/java/ru/noties/markwon/image/okhttp/OkHttpImagesPlugin.java index bc197c16..fe43c289 100644 --- a/markwon-image-okhttp/src/main/java/ru/noties/markwon/image/okhttp/MarkwonImageOkHttpPlugin.java +++ b/markwon-image-okhttp/src/main/java/ru/noties/markwon/image/okhttp/OkHttpImagesPlugin.java @@ -7,7 +7,9 @@ import java.util.Arrays; import okhttp3.OkHttpClient; import ru.noties.markwon.AbstractMarkwonPlugin; import ru.noties.markwon.image.AsyncDrawableLoader; +import ru.noties.markwon.image.ImagesPlugin; import ru.noties.markwon.image.network.NetworkSchemeHandler; +import ru.noties.markwon.priority.Priority; /** * Plugin to use OkHttpClient to obtain images from network (http and https schemes) @@ -17,21 +19,21 @@ import ru.noties.markwon.image.network.NetworkSchemeHandler; * @since 3.0.0 */ @SuppressWarnings("WeakerAccess") -public class MarkwonImageOkHttpPlugin extends AbstractMarkwonPlugin { +public class OkHttpImagesPlugin extends AbstractMarkwonPlugin { @NonNull - public static MarkwonImageOkHttpPlugin create() { - return new MarkwonImageOkHttpPlugin(new OkHttpClient()); + public static OkHttpImagesPlugin create() { + return new OkHttpImagesPlugin(new OkHttpClient()); } @NonNull - public static MarkwonImageOkHttpPlugin create(@NonNull OkHttpClient okHttpClient) { - return new MarkwonImageOkHttpPlugin(okHttpClient); + public static OkHttpImagesPlugin create(@NonNull OkHttpClient okHttpClient) { + return new OkHttpImagesPlugin(okHttpClient); } private final OkHttpClient client; - MarkwonImageOkHttpPlugin(@NonNull OkHttpClient client) { + OkHttpImagesPlugin(@NonNull OkHttpClient client) { this.client = client; } @@ -42,4 +44,10 @@ public class MarkwonImageOkHttpPlugin extends AbstractMarkwonPlugin { new OkHttpSchemeHandler(client) ); } + + @NonNull + @Override + public Priority priority() { + return Priority.after(ImagesPlugin.class); + } } diff --git a/markwon-image-svg/src/main/java/ru/noties/markwon/image/svg/SvgPlugin.java b/markwon-image-svg/src/main/java/ru/noties/markwon/image/svg/SvgPlugin.java index d2396741..be357480 100644 --- a/markwon-image-svg/src/main/java/ru/noties/markwon/image/svg/SvgPlugin.java +++ b/markwon-image-svg/src/main/java/ru/noties/markwon/image/svg/SvgPlugin.java @@ -5,6 +5,8 @@ import android.support.annotation.NonNull; import ru.noties.markwon.AbstractMarkwonPlugin; import ru.noties.markwon.image.AsyncDrawableLoader; +import ru.noties.markwon.image.ImagesPlugin; +import ru.noties.markwon.priority.Priority; public class SvgPlugin extends AbstractMarkwonPlugin { @@ -23,4 +25,10 @@ public class SvgPlugin extends AbstractMarkwonPlugin { public void configureImages(@NonNull AsyncDrawableLoader.Builder builder) { builder.addMediaDecoder(SvgMediaDecoder.CONTENT_TYPE, SvgMediaDecoder.create(resources)); } + + @NonNull + @Override + public Priority priority() { + return Priority.after(ImagesPlugin.class); + } } diff --git a/markwon/src/main/java/ru/noties/markwon/AbstractMarkwonPlugin.java b/markwon/src/main/java/ru/noties/markwon/AbstractMarkwonPlugin.java index d9849e5c..c5f68fdc 100644 --- a/markwon/src/main/java/ru/noties/markwon/AbstractMarkwonPlugin.java +++ b/markwon/src/main/java/ru/noties/markwon/AbstractMarkwonPlugin.java @@ -7,8 +7,10 @@ import android.widget.TextView; import org.commonmark.node.Node; import org.commonmark.parser.Parser; +import ru.noties.markwon.core.CorePlugin; import ru.noties.markwon.core.MarkwonTheme; import ru.noties.markwon.image.AsyncDrawableLoader; +import ru.noties.markwon.priority.Priority; /** * Class that extends {@link MarkwonPlugin} with all methods implemented (empty body) @@ -75,6 +77,16 @@ public abstract class AbstractMarkwonPlugin implements MarkwonPlugin { } + /** + * @inheritDoc + */ + @NonNull + @Override + public Priority priority() { + // by default all come after CorePlugin + return Priority.after(CorePlugin.class); + } + /** * @inheritDoc */ diff --git a/markwon/src/main/java/ru/noties/markwon/Markwon.java b/markwon/src/main/java/ru/noties/markwon/Markwon.java index c5a9ea8c..ee6e14cd 100644 --- a/markwon/src/main/java/ru/noties/markwon/Markwon.java +++ b/markwon/src/main/java/ru/noties/markwon/Markwon.java @@ -7,6 +7,8 @@ import android.widget.TextView; import org.commonmark.node.Node; +import ru.noties.markwon.core.CorePlugin; + /** * Class to parse and render markdown. Since version 3.0.0 instance specific (previously consisted * of static stateless methods). An instance of builder can be obtained via {@link #builder(Context)} @@ -18,7 +20,22 @@ import org.commonmark.node.Node; public abstract class Markwon { /** - * Factory method to obtain an instance of {@link Builder} + * Factory method to create a minimally functional {@link Markwon} instance. This + * instance will have only {@link CorePlugin} registered. If you wish + * to configure this instance more consider using {@link #builder(Context)} method. + * + * @return {@link Markwon} instance with only CorePlugin registered + * @since 3.0.0 + */ + @NonNull + public static Markwon create(@NonNull Context context) { + return builder(context) + .usePlugin(CorePlugin.create()) + .build(); + } + + /** + * Factory method to obtain an instance of {@link Builder}. * * @see Builder * @since 3.0.0 diff --git a/markwon/src/main/java/ru/noties/markwon/MarkwonBuilderImpl.java b/markwon/src/main/java/ru/noties/markwon/MarkwonBuilderImpl.java index 1cf15c2c..e0eabdf6 100644 --- a/markwon/src/main/java/ru/noties/markwon/MarkwonBuilderImpl.java +++ b/markwon/src/main/java/ru/noties/markwon/MarkwonBuilderImpl.java @@ -2,6 +2,7 @@ package ru.noties.markwon; import android.content.Context; import android.support.annotation.NonNull; +import android.util.Log; import android.widget.TextView; import org.commonmark.parser.Parser; @@ -13,6 +14,7 @@ import java.util.List; import ru.noties.markwon.core.MarkwonTheme; import ru.noties.markwon.image.AsyncDrawableLoader; +import ru.noties.markwon.priority.PriorityProcessor; /** * @since 3.0.0 @@ -22,8 +24,11 @@ class MarkwonBuilderImpl implements Markwon.Builder { private final Context context; private final List plugins = new ArrayList<>(3); + private TextView.BufferType bufferType = TextView.BufferType.SPANNABLE; + private PriorityProcessor priorityProcessor; + MarkwonBuilderImpl(@NonNull Context context) { this.context = context; } @@ -61,6 +66,12 @@ class MarkwonBuilderImpl implements Markwon.Builder { return this; } + @NonNull + public MarkwonBuilderImpl priorityProcessor(@NonNull PriorityProcessor priorityProcessor) { + this.priorityProcessor = priorityProcessor; + return this; + } + @NonNull @Override public Markwon build() { @@ -73,7 +84,18 @@ class MarkwonBuilderImpl implements Markwon.Builder { final MarkwonSpansFactory.Builder spanFactoryBuilder = new MarkwonSpansFactoryImpl.BuilderImpl(); final RenderProps renderProps = new RenderPropsImpl(); + PriorityProcessor priorityProcessor = this.priorityProcessor; + if (priorityProcessor == null) { + // strictly speaking we do not need updating this field + // as we are not building this class to be reused between multiple `build` calls + priorityProcessor = this.priorityProcessor = PriorityProcessor.create(); + } + final List plugins = priorityProcessor.process(this.plugins); + for (MarkwonPlugin plugin : plugins) { + if (true) { + Log.e("PLUGIN", plugin.getClass().getName()); + } plugin.configureParser(parserBuilder); plugin.configureTheme(themeBuilder); plugin.configureImages(asyncDrawableLoaderBuilder); diff --git a/markwon/src/main/java/ru/noties/markwon/MarkwonPlugin.java b/markwon/src/main/java/ru/noties/markwon/MarkwonPlugin.java index fd99b4dd..2f9a6cb1 100644 --- a/markwon/src/main/java/ru/noties/markwon/MarkwonPlugin.java +++ b/markwon/src/main/java/ru/noties/markwon/MarkwonPlugin.java @@ -11,6 +11,7 @@ import ru.noties.markwon.core.MarkwonTheme; import ru.noties.markwon.image.AsyncDrawableLoader; import ru.noties.markwon.image.MediaDecoder; import ru.noties.markwon.image.SchemeHandler; +import ru.noties.markwon.priority.Priority; /** * Class represents a plugin (extension) to Markwon to configure how parsing and rendering @@ -87,6 +88,9 @@ public interface MarkwonPlugin { */ void configureRenderProps(@NonNull RenderProps renderProps); + @NonNull + Priority priority(); + /** * Process input markdown and return new string to be used in parsing stage further. * Can be described as pre-processing of markdown String. diff --git a/markwon/src/main/java/ru/noties/markwon/core/CorePlugin.java b/markwon/src/main/java/ru/noties/markwon/core/CorePlugin.java index 187f0173..2c315486 100644 --- a/markwon/src/main/java/ru/noties/markwon/core/CorePlugin.java +++ b/markwon/src/main/java/ru/noties/markwon/core/CorePlugin.java @@ -39,6 +39,7 @@ import ru.noties.markwon.core.factory.ListItemSpanFactory; import ru.noties.markwon.core.factory.StrongEmphasisSpanFactory; import ru.noties.markwon.core.factory.ThematicBreakSpanFactory; import ru.noties.markwon.core.spans.OrderedListItemSpan; +import ru.noties.markwon.priority.Priority; /** * @since 3.0.0 @@ -55,12 +56,8 @@ public class CorePlugin extends AbstractMarkwonPlugin { return new CorePlugin(softBreakAddsNewLine); } - // todo: can we make it configurable somewhere else? - // even possibility of options that require creating factory method for each configuration... meh private final boolean softBreakAddsNewLine; - // todo: test that visitors are registered for all expected nodes - protected CorePlugin(boolean softBreakAddsNewLine) { this.softBreakAddsNewLine = softBreakAddsNewLine; } @@ -104,6 +101,12 @@ public class CorePlugin extends AbstractMarkwonPlugin { .setFactory(ThematicBreak.class, new ThematicBreakSpanFactory()); } + @NonNull + @Override + public Priority priority() { + return Priority.none(); + } + @Override public void beforeSetText(@NonNull TextView textView, @NonNull Spanned markdown) { OrderedListItemSpan.measure(textView, markdown); diff --git a/markwon/src/main/java/ru/noties/markwon/priority/Priority.java b/markwon/src/main/java/ru/noties/markwon/priority/Priority.java new file mode 100644 index 00000000..5582ff72 --- /dev/null +++ b/markwon/src/main/java/ru/noties/markwon/priority/Priority.java @@ -0,0 +1,96 @@ +package ru.noties.markwon.priority; + +import android.support.annotation.NonNull; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +import ru.noties.markwon.MarkwonPlugin; + +// a small dependency graph also +// what if plugins cannot be constructed into a graph? for example they depend on something +// but not overlap? then it would be hard to sort them (but this doesn't make sense, if +// they do not care about other components, just put them in whatever order, no?) + +/** + * @see MarkwonPlugin#priority() + * @since 3.0.0 + */ +public abstract class Priority { + + @NonNull + public static Priority none() { + return builder().build(); + } + + @NonNull + public static Priority after(@NonNull Class plugin) { + return builder().after(plugin).build(); + } + + @NonNull + public static Priority after( + @NonNull Class plugin1, + @NonNull Class plugin2) { + return builder().after(plugin1).after(plugin2).build(); + } + + @NonNull + public static Builder builder() { + return new Impl.BuilderImpl(); + } + + public interface Builder { + + @NonNull + Builder after(@NonNull Class plugin); + + @NonNull + Priority build(); + } + + @NonNull + public abstract List> after(); + + + static class Impl extends Priority { + + private final List> after; + + Impl(@NonNull List> after) { + this.after = after; + } + + @NonNull + @Override + public List> after() { + return after; + } + + @Override + public String toString() { + return "Priority{" + + "after=" + after + + '}'; + } + + static class BuilderImpl implements Builder { + + private final List> after = new ArrayList<>(0); + + @NonNull + @Override + public Builder after(@NonNull Class plugin) { + after.add(plugin); + return this; + } + + @NonNull + @Override + public Priority build() { + return new Impl(Collections.unmodifiableList(after)); + } + } + } +} diff --git a/markwon/src/main/java/ru/noties/markwon/priority/PriorityProcessor.java b/markwon/src/main/java/ru/noties/markwon/priority/PriorityProcessor.java new file mode 100644 index 00000000..1ba1353d --- /dev/null +++ b/markwon/src/main/java/ru/noties/markwon/priority/PriorityProcessor.java @@ -0,0 +1,18 @@ +package ru.noties.markwon.priority; + +import android.support.annotation.NonNull; + +import java.util.List; + +import ru.noties.markwon.MarkwonPlugin; + +public abstract class PriorityProcessor { + + @NonNull + public static PriorityProcessor create() { + return new PriorityProcessorImpl(); + } + + @NonNull + public abstract List process(@NonNull List plugins); +} diff --git a/markwon/src/main/java/ru/noties/markwon/priority/PriorityProcessorImpl.java b/markwon/src/main/java/ru/noties/markwon/priority/PriorityProcessorImpl.java new file mode 100644 index 00000000..08e820b4 --- /dev/null +++ b/markwon/src/main/java/ru/noties/markwon/priority/PriorityProcessorImpl.java @@ -0,0 +1,133 @@ +package ru.noties.markwon.priority; + +import android.support.annotation.NonNull; + +import java.util.Collections; +import java.util.Comparator; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import ru.noties.markwon.MarkwonPlugin; + +import static java.lang.Math.max; + +class PriorityProcessorImpl extends PriorityProcessor { + + @NonNull + @Override + public List process(@NonNull List plugins) { + + final int size = plugins.size(); + + final Map, Set>> map = + new HashMap<>(size); + + for (MarkwonPlugin plugin : plugins) { + if (map.put(plugin.getClass(), new HashSet<>(plugin.priority().after())) != null) { + throw new IllegalStateException(String.format("Markwon duplicate plugin " + + "found `%s`: %s", plugin.getClass().getName(), plugin)); + } + } + + // change to Map + final Map cache = new HashMap<>(size); + for (MarkwonPlugin plugin : plugins) { + cache.put(plugin, eval(plugin, map)); + } + + Collections.sort(plugins, new PriorityComparator(cache)); + + return plugins; + } + + private static int eval( + @NonNull MarkwonPlugin plugin, + @NonNull Map, Set>> map) { + + final Set> set = map.get(plugin.getClass()); + + // no dependencies + if (set.isEmpty()) { + return 0; + } + + final Class who = plugin.getClass(); + + int max = 0; + + for (Class dependency : set) { + max = max(max, eval(who, dependency, map)); + } + + return 1 + max; + } + + // we need to count the number of steps to a root node (which has no parents) + private static int eval( + @NonNull Class who, + @NonNull Class plugin, + @NonNull Map, Set>> map) { + + // exact match + Set> set = map.get(plugin); + + if (set == null) { + + // let's try to find inexact type (overridden/subclassed) + for (Map.Entry, Set>> entry : map.entrySet()) { + if (plugin.isAssignableFrom(entry.getKey())) { + set = entry.getValue(); + break; + } + } + + if (set == null) { + // unsatisfied dependency + throw new IllegalStateException(String.format("Markwon unsatisfied dependency found. " + + "Plugin `%s` comes after `%s` but it is not added.", + who.getName(), plugin.getName())); + } + } + + if (set.isEmpty()) { + return 0; + } + + int value = 1; + + for (Class dependency : set) { + + // a case when a plugin defines `Priority.after(getClass)` or being + // referenced by own dependency (even indirect) + if (who.equals(dependency)) { + throw new IllegalStateException(String.format("Markwon plugin `%s` defined self " + + "as a dependency or being referenced by own dependency (cycle)", who.getName())); + } + + value += eval(who, dependency, map); + } + + return value; + } + + private static class PriorityComparator implements Comparator { + + private final Map map; + + PriorityComparator(@NonNull Map map) { + this.map = map; + } + + @Override + public int compare(MarkwonPlugin o1, MarkwonPlugin o2) { + return map.get(o1).compareTo(map.get(o2)); + } + } + + private static class NoCorePluginAddedException extends Exception { + + } +} diff --git a/markwon/src/test/java/ru/noties/markwon/priority/PriorityProcessorTest.java b/markwon/src/test/java/ru/noties/markwon/priority/PriorityProcessorTest.java new file mode 100644 index 00000000..28612ca4 --- /dev/null +++ b/markwon/src/test/java/ru/noties/markwon/priority/PriorityProcessorTest.java @@ -0,0 +1,468 @@ +package ru.noties.markwon.priority; + +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 java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.HashSet; +import java.util.List; + +import ru.noties.markwon.AbstractMarkwonPlugin; +import ru.noties.markwon.MarkwonPlugin; +import ru.noties.markwon.core.CorePlugin; +import ru.noties.markwon.image.ImagesPlugin; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; + +@RunWith(RobolectricTestRunner.class) +@Config(manifest = Config.NONE) +public class PriorityProcessorTest { + + private PriorityProcessor processor; + + @Before + public void before() { + processor = PriorityProcessor.create(); + } + + @Test + public void empty_list() { + final List plugins = Collections.emptyList(); + assertEquals(0, processor.process(plugins).size()); + } + + @Test + public void simple_two_plugins() { + + final MarkwonPlugin first = new AbstractMarkwonPlugin() { + @NonNull + @Override + public Priority priority() { + return Priority.none(); + } + }; + + final MarkwonPlugin second = new AbstractMarkwonPlugin() { + @NonNull + @Override + public Priority priority() { + return Priority.after(first.getClass()); + } + }; + + final List plugins = processor.process(Arrays.asList(second, first)); + + assertEquals(2, plugins.size()); + assertEquals(first, plugins.get(0)); + assertEquals(second, plugins.get(1)); + } + + @Test + public void plugin_after_self() { + + final MarkwonPlugin plugin = new AbstractMarkwonPlugin() { + @NonNull + @Override + public Priority priority() { + return Priority.after(getClass()); + } + }; + + try { + processor.process(Collections.singletonList(plugin)); + fail(); + } catch (IllegalStateException e) { + assertTrue(e.getMessage(), e.getMessage().contains("defined self as a dependency")); + } + } + + @Test + public void unsatisfied_dependency() { + + final MarkwonPlugin plugin = new AbstractMarkwonPlugin() { + @NonNull + @Override + public Priority priority() { + return Priority.after(ImagesPlugin.class); + } + }; + + try { + processor.process(Collections.singletonList(plugin)); + fail(); + } catch (IllegalStateException e) { + assertTrue(e.getMessage(), e.getMessage().contains("Markwon unsatisfied dependency found")); + } + } + + @Test + public void subclass_found() { + // when a plugin comes after another, but _another_ was subclassed and placed in the list + + final MarkwonPlugin core = new CorePlugin(false) { + }; + final MarkwonPlugin plugin = new AbstractMarkwonPlugin() { + @NonNull + @Override + public Priority priority() { + return Priority.after(CorePlugin.class); + } + }; + + final List plugins = processor.process(Arrays.asList(plugin, core)); + assertEquals(2, plugins.size()); + assertEquals(core, plugins.get(0)); + assertEquals(plugin, plugins.get(1)); + } + + @Test + public void three_plugins_sequential() { + + final MarkwonPlugin first = new AbstractMarkwonPlugin() { + @NonNull + @Override + public Priority priority() { + return Priority.none(); + } + }; + + final MarkwonPlugin second = new AbstractMarkwonPlugin() { + @NonNull + @Override + public Priority priority() { + return Priority.after(first.getClass()); + } + }; + + final MarkwonPlugin third = new AbstractMarkwonPlugin() { + @NonNull + @Override + public Priority priority() { + return Priority.after(second.getClass()); + } + }; + + final List plugins = processor.process(Arrays.asList(third, second, first)); + assertEquals(3, plugins.size()); + assertEquals(first, plugins.get(0)); + assertEquals(second, plugins.get(1)); + assertEquals(third, plugins.get(2)); + } + + @Test + public void plugin_duplicate() { + + final MarkwonPlugin plugin = new AbstractMarkwonPlugin() { + @NonNull + @Override + public Priority priority() { + return Priority.none(); + } + }; + + try { + processor.process(Arrays.asList(plugin, plugin)); + fail(); + } catch (IllegalStateException e) { + assertTrue(e.getMessage(), e.getMessage().contains("Markwon duplicate plugin found")); + } + } + + @Test + public void multiple_after_3() { + + final MarkwonPlugin a1 = new AbstractMarkwonPlugin() { + @NonNull + @Override + public Priority priority() { + return Priority.none(); + } + }; + + final MarkwonPlugin b1 = new AbstractMarkwonPlugin() { + @NonNull + @Override + public Priority priority() { + return Priority.after(a1.getClass()); + } + }; + + final MarkwonPlugin c1 = new AbstractMarkwonPlugin() { + @NonNull + @Override + public Priority priority() { + return Priority.after(a1.getClass(), b1.getClass()); + } + }; + + final List plugins = processor.process(Arrays.asList(c1, a1, b1)); + assertEquals(3, plugins.size()); + assertEquals(a1, plugins.get(0)); + assertEquals(b1, plugins.get(1)); + assertEquals(c1, plugins.get(2)); + } + + @Test + public void multiple_after_4() { + + final MarkwonPlugin a1 = new AbstractMarkwonPlugin() { + @NonNull + @Override + public Priority priority() { + return Priority.none(); + } + }; + + final MarkwonPlugin b1 = new AbstractMarkwonPlugin() { + @NonNull + @Override + public Priority priority() { + return Priority.after(a1.getClass()); + } + }; + + final MarkwonPlugin c1 = new AbstractMarkwonPlugin() { + @NonNull + @Override + public Priority priority() { + return Priority.after(a1.getClass(), b1.getClass()); + } + }; + + final MarkwonPlugin d1 = new AbstractMarkwonPlugin() { + @NonNull + @Override + public Priority priority() { + return Priority.builder() + .after(a1.getClass()) + .after(b1.getClass()) + .after(c1.getClass()) + .build(); + } + }; + + final List plugins = processor.process(Arrays.asList(c1, d1, a1, b1)); + assertEquals(4, plugins.size()); + assertEquals(a1, plugins.get(0)); + assertEquals(b1, plugins.get(1)); + assertEquals(c1, plugins.get(2)); + assertEquals(d1, plugins.get(3)); + } + + @Test + public void cycle() { + + final class Holder { + Class type; + } + final Holder holder = new Holder(); + + final MarkwonPlugin first = new AbstractMarkwonPlugin() { + @NonNull + @Override + public Priority priority() { + return Priority.after(holder.type); + } + }; + + final MarkwonPlugin second = new AbstractMarkwonPlugin() { + + { + holder.type = getClass(); + } + + @NonNull + @Override + public Priority priority() { + return Priority.after(first.getClass()); + } + }; + + try { + processor.process(Arrays.asList(second, first)); + fail(); + } catch (IllegalStateException e) { + assertTrue(e.getMessage(), e.getMessage().contains("being referenced by own dependency (cycle)")); + } + } + + @Test + public void bigger_cycle() { + + final class Plugin extends NamedPlugin { + + private Priority priority; + + private Plugin(@NonNull String name) { + super(name); + } + + private void set(@NonNull MarkwonPlugin plugin) { + priority = Priority.after(plugin.getClass()); + } + + @NonNull + @Override + public Priority priority() { + return priority; + } + } + + final Plugin a = new Plugin("a"); + + final List plugins = new ArrayList<>(); + plugins.add(a); + plugins.add(new NamedPlugin("b", plugins.get(plugins.size() - 1)) { + }); + plugins.add(new NamedPlugin("c", plugins.get(plugins.size() - 1)) { + }); + plugins.add(new NamedPlugin("d", plugins.get(plugins.size() - 1)) { + }); + plugins.add(new NamedPlugin("e", plugins.get(plugins.size() - 1)) { + }); + plugins.add(new NamedPlugin("f", plugins.get(plugins.size() - 1)) { + }); + plugins.add(new NamedPlugin("g", plugins.get(plugins.size() - 1)) { + }); + + // link with the last one + a.set(plugins.get(plugins.size() - 1)); + + try { + processor.process(plugins); + fail(); + } catch (IllegalStateException e) { + assertTrue(e.getMessage(), e.getMessage().contains("being referenced by own dependency (cycle)")); + } + } + + @Test + public void deep_tree() { + + // we must create subclasses in order to register them like this (otherwise -> duplicates) + final MarkwonPlugin a = new NamedPlugin("a") { + }; + final MarkwonPlugin b1 = new NamedPlugin("b1", a) { + }; + final MarkwonPlugin b2 = new NamedPlugin("b2", a) { + }; + final MarkwonPlugin c1 = new NamedPlugin("c1", b1) { + }; + final MarkwonPlugin c2 = new NamedPlugin("c2", b1) { + }; + final MarkwonPlugin c3 = new NamedPlugin("c3", b2) { + }; + final MarkwonPlugin c4 = new NamedPlugin("c4", b2) { + }; + final MarkwonPlugin d1 = new NamedPlugin("d1", c1) { + }; + final MarkwonPlugin e1 = new NamedPlugin("e1", d1, c2, c3, c4) { + }; + + final List plugins = processor.process(Arrays.asList(b2, b1, + a, e1, c4, c3, c2, c1, d1)); + + // a is first + // b1 + b2 -> second+third + // c1 + c2 + c3 + c4 -> forth, fifth, sixth, seventh + // d1 -> 8th + // e1 -> 9th + + assertEquals(9, plugins.size()); + assertEquals(a, plugins.get(0)); + assertEquals(new HashSet<>(Arrays.asList(b1, b2)), new HashSet<>(plugins.subList(1, 3))); + assertEquals(new HashSet<>(Arrays.asList(c1, c2, c3, c4)), new HashSet<>(plugins.subList(3, 7))); + assertEquals(d1, plugins.get(7)); + assertEquals(e1, plugins.get(8)); + } + + @Test + public void multiple_detached() { + + // when graph has independent elements that are not connected with each other + final MarkwonPlugin a0 = new NamedPlugin("a0") { + }; + final MarkwonPlugin a1 = new NamedPlugin("a1", a0) { + }; + final MarkwonPlugin a2 = new NamedPlugin("a2", a1) { + }; + + final MarkwonPlugin b0 = new NamedPlugin("b0") { + }; + final MarkwonPlugin b1 = new NamedPlugin("b1", b0) { + }; + final MarkwonPlugin b2 = new NamedPlugin("b2", b1) { + }; + + final List plugins = processor.process(Arrays.asList( + b2, a2, a0, b0, b1, a1)); + + assertEquals(6, plugins.size()); + + assertEquals(new HashSet<>(Arrays.asList(a0, b0)), new HashSet<>(plugins.subList(0, 2))); + assertEquals(new HashSet<>(Arrays.asList(a1, b1)), new HashSet<>(plugins.subList(2, 4))); + assertEquals(new HashSet<>(Arrays.asList(a2, b2)), new HashSet<>(plugins.subList(4, 6))); + } + + private static abstract class NamedPlugin extends AbstractMarkwonPlugin { + + private final String name; + private final Priority priority; + + NamedPlugin(@NonNull String name) { + this(name, (Priority) null); + } + + NamedPlugin(@NonNull String name, @Nullable MarkwonPlugin plugin) { + this(name, plugin != null ? Priority.after(plugin.getClass()) : null); + } + + NamedPlugin(@NonNull String name, MarkwonPlugin... plugins) { + this(name, of(plugins)); + } + + NamedPlugin(@NonNull String name, @Nullable Class plugin) { + this(name, plugin != null ? Priority.after(plugin) : null); + } + + NamedPlugin(@NonNull String name, @Nullable Priority priority) { + this.name = name; + this.priority = priority; + } + + @NonNull + @Override + public Priority priority() { + return priority != null + ? priority + : Priority.none(); + } + + @Override + public String toString() { + return "NamedPlugin{" + + "name='" + name + '\'' + + '}'; + } + + @NonNull + private static Priority of(@NonNull MarkwonPlugin... plugins) { + if (plugins.length == 0) return Priority.none(); + final Priority.Builder builder = Priority.builder(); + for (MarkwonPlugin plugin : plugins) { + builder.after(plugin.getClass()); + } + return builder.build(); + } + } +} \ No newline at end of file