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 extends MarkwonPlugin> plugin) {
+ return builder().after(plugin).build();
+ }
+
+ @NonNull
+ public static Priority after(
+ @NonNull Class extends MarkwonPlugin> plugin1,
+ @NonNull Class extends MarkwonPlugin> 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 extends MarkwonPlugin> 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 extends MarkwonPlugin> 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 extends MarkwonPlugin> who = plugin.getClass();
+
+ int max = 0;
+
+ for (Class extends MarkwonPlugin> 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 extends MarkwonPlugin> who,
+ @NonNull Class extends MarkwonPlugin> 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 extends MarkwonPlugin> 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 extends MarkwonPlugin> 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 extends MarkwonPlugin> 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