Introduce priority abstraction (order and dependency for plugins)

This commit is contained in:
Dimitry Ivanov 2018-12-26 19:07:31 +03:00
parent 2593539b65
commit 581265a22a
14 changed files with 824 additions and 11 deletions

View File

@ -14,6 +14,8 @@ import ru.noties.markwon.RenderProps;
import ru.noties.markwon.SpanFactory; import ru.noties.markwon.SpanFactory;
import ru.noties.markwon.image.AsyncDrawableSpan; import ru.noties.markwon.image.AsyncDrawableSpan;
import ru.noties.markwon.image.ImageProps; import ru.noties.markwon.image.ImageProps;
import ru.noties.markwon.image.ImagesPlugin;
import ru.noties.markwon.priority.Priority;
public class GifAwarePlugin extends AbstractMarkwonPlugin { 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 @Override
public void afterSetText(@NonNull TextView textView) { public void afterSetText(@NonNull TextView textView) {
processor.process(textView); processor.process(textView);

View File

@ -21,8 +21,10 @@ import ru.noties.markwon.image.AsyncDrawableLoader;
import ru.noties.markwon.image.ImageItem; import ru.noties.markwon.image.ImageItem;
import ru.noties.markwon.image.ImageProps; import ru.noties.markwon.image.ImageProps;
import ru.noties.markwon.image.ImageSize; import ru.noties.markwon.image.ImageSize;
import ru.noties.markwon.image.ImagesPlugin;
import ru.noties.markwon.image.MediaDecoder; import ru.noties.markwon.image.MediaDecoder;
import ru.noties.markwon.image.SchemeHandler; import ru.noties.markwon.image.SchemeHandler;
import ru.noties.markwon.priority.Priority;
public class JLatexMathPlugin extends AbstractMarkwonPlugin { public class JLatexMathPlugin extends AbstractMarkwonPlugin {
@ -136,4 +138,10 @@ public class JLatexMathPlugin extends AbstractMarkwonPlugin {
} }
}); });
} }
@NonNull
@Override
public Priority priority() {
return Priority.after(ImagesPlugin.class);
}
} }

View File

@ -4,6 +4,8 @@ import android.support.annotation.NonNull;
import ru.noties.markwon.AbstractMarkwonPlugin; import ru.noties.markwon.AbstractMarkwonPlugin;
import ru.noties.markwon.image.AsyncDrawableLoader; import ru.noties.markwon.image.AsyncDrawableLoader;
import ru.noties.markwon.image.ImagesPlugin;
import ru.noties.markwon.priority.Priority;
public class GifPlugin extends AbstractMarkwonPlugin { public class GifPlugin extends AbstractMarkwonPlugin {
@ -27,4 +29,10 @@ public class GifPlugin extends AbstractMarkwonPlugin {
public void configureImages(@NonNull AsyncDrawableLoader.Builder builder) { public void configureImages(@NonNull AsyncDrawableLoader.Builder builder) {
builder.addMediaDecoder(GifMediaDecoder.CONTENT_TYPE, GifMediaDecoder.create(autoPlay)); builder.addMediaDecoder(GifMediaDecoder.CONTENT_TYPE, GifMediaDecoder.create(autoPlay));
} }
@NonNull
@Override
public Priority priority() {
return Priority.after(ImagesPlugin.class);
}
} }

View File

@ -7,7 +7,9 @@ import java.util.Arrays;
import okhttp3.OkHttpClient; import okhttp3.OkHttpClient;
import ru.noties.markwon.AbstractMarkwonPlugin; import ru.noties.markwon.AbstractMarkwonPlugin;
import ru.noties.markwon.image.AsyncDrawableLoader; import ru.noties.markwon.image.AsyncDrawableLoader;
import ru.noties.markwon.image.ImagesPlugin;
import ru.noties.markwon.image.network.NetworkSchemeHandler; 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) * 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 * @since 3.0.0
*/ */
@SuppressWarnings("WeakerAccess") @SuppressWarnings("WeakerAccess")
public class MarkwonImageOkHttpPlugin extends AbstractMarkwonPlugin { public class OkHttpImagesPlugin extends AbstractMarkwonPlugin {
@NonNull @NonNull
public static MarkwonImageOkHttpPlugin create() { public static OkHttpImagesPlugin create() {
return new MarkwonImageOkHttpPlugin(new OkHttpClient()); return new OkHttpImagesPlugin(new OkHttpClient());
} }
@NonNull @NonNull
public static MarkwonImageOkHttpPlugin create(@NonNull OkHttpClient okHttpClient) { public static OkHttpImagesPlugin create(@NonNull OkHttpClient okHttpClient) {
return new MarkwonImageOkHttpPlugin(okHttpClient); return new OkHttpImagesPlugin(okHttpClient);
} }
private final OkHttpClient client; private final OkHttpClient client;
MarkwonImageOkHttpPlugin(@NonNull OkHttpClient client) { OkHttpImagesPlugin(@NonNull OkHttpClient client) {
this.client = client; this.client = client;
} }
@ -42,4 +44,10 @@ public class MarkwonImageOkHttpPlugin extends AbstractMarkwonPlugin {
new OkHttpSchemeHandler(client) new OkHttpSchemeHandler(client)
); );
} }
@NonNull
@Override
public Priority priority() {
return Priority.after(ImagesPlugin.class);
}
} }

View File

@ -5,6 +5,8 @@ import android.support.annotation.NonNull;
import ru.noties.markwon.AbstractMarkwonPlugin; import ru.noties.markwon.AbstractMarkwonPlugin;
import ru.noties.markwon.image.AsyncDrawableLoader; import ru.noties.markwon.image.AsyncDrawableLoader;
import ru.noties.markwon.image.ImagesPlugin;
import ru.noties.markwon.priority.Priority;
public class SvgPlugin extends AbstractMarkwonPlugin { public class SvgPlugin extends AbstractMarkwonPlugin {
@ -23,4 +25,10 @@ public class SvgPlugin extends AbstractMarkwonPlugin {
public void configureImages(@NonNull AsyncDrawableLoader.Builder builder) { public void configureImages(@NonNull AsyncDrawableLoader.Builder builder) {
builder.addMediaDecoder(SvgMediaDecoder.CONTENT_TYPE, SvgMediaDecoder.create(resources)); builder.addMediaDecoder(SvgMediaDecoder.CONTENT_TYPE, SvgMediaDecoder.create(resources));
} }
@NonNull
@Override
public Priority priority() {
return Priority.after(ImagesPlugin.class);
}
} }

View File

@ -7,8 +7,10 @@ import android.widget.TextView;
import org.commonmark.node.Node; import org.commonmark.node.Node;
import org.commonmark.parser.Parser; import org.commonmark.parser.Parser;
import ru.noties.markwon.core.CorePlugin;
import ru.noties.markwon.core.MarkwonTheme; import ru.noties.markwon.core.MarkwonTheme;
import ru.noties.markwon.image.AsyncDrawableLoader; import ru.noties.markwon.image.AsyncDrawableLoader;
import ru.noties.markwon.priority.Priority;
/** /**
* Class that extends {@link MarkwonPlugin} with all methods implemented (empty body) * 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 * @inheritDoc
*/ */

View File

@ -7,6 +7,8 @@ import android.widget.TextView;
import org.commonmark.node.Node; 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 * 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)} * 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 { public abstract class Markwon {
/** /**
* Factory method to obtain an instance of {@link Builder} * Factory method to create a <em>minimally</em> functional {@link Markwon} instance. This
* instance will have <strong>only</strong> {@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 * @see Builder
* @since 3.0.0 * @since 3.0.0

View File

@ -2,6 +2,7 @@ package ru.noties.markwon;
import android.content.Context; import android.content.Context;
import android.support.annotation.NonNull; import android.support.annotation.NonNull;
import android.util.Log;
import android.widget.TextView; import android.widget.TextView;
import org.commonmark.parser.Parser; import org.commonmark.parser.Parser;
@ -13,6 +14,7 @@ import java.util.List;
import ru.noties.markwon.core.MarkwonTheme; import ru.noties.markwon.core.MarkwonTheme;
import ru.noties.markwon.image.AsyncDrawableLoader; import ru.noties.markwon.image.AsyncDrawableLoader;
import ru.noties.markwon.priority.PriorityProcessor;
/** /**
* @since 3.0.0 * @since 3.0.0
@ -22,8 +24,11 @@ class MarkwonBuilderImpl implements Markwon.Builder {
private final Context context; private final Context context;
private final List<MarkwonPlugin> plugins = new ArrayList<>(3); private final List<MarkwonPlugin> plugins = new ArrayList<>(3);
private TextView.BufferType bufferType = TextView.BufferType.SPANNABLE; private TextView.BufferType bufferType = TextView.BufferType.SPANNABLE;
private PriorityProcessor priorityProcessor;
MarkwonBuilderImpl(@NonNull Context context) { MarkwonBuilderImpl(@NonNull Context context) {
this.context = context; this.context = context;
} }
@ -61,6 +66,12 @@ class MarkwonBuilderImpl implements Markwon.Builder {
return this; return this;
} }
@NonNull
public MarkwonBuilderImpl priorityProcessor(@NonNull PriorityProcessor priorityProcessor) {
this.priorityProcessor = priorityProcessor;
return this;
}
@NonNull @NonNull
@Override @Override
public Markwon build() { public Markwon build() {
@ -73,7 +84,18 @@ class MarkwonBuilderImpl implements Markwon.Builder {
final MarkwonSpansFactory.Builder spanFactoryBuilder = new MarkwonSpansFactoryImpl.BuilderImpl(); final MarkwonSpansFactory.Builder spanFactoryBuilder = new MarkwonSpansFactoryImpl.BuilderImpl();
final RenderProps renderProps = new RenderPropsImpl(); 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<MarkwonPlugin> plugins = priorityProcessor.process(this.plugins);
for (MarkwonPlugin plugin : plugins) { for (MarkwonPlugin plugin : plugins) {
if (true) {
Log.e("PLUGIN", plugin.getClass().getName());
}
plugin.configureParser(parserBuilder); plugin.configureParser(parserBuilder);
plugin.configureTheme(themeBuilder); plugin.configureTheme(themeBuilder);
plugin.configureImages(asyncDrawableLoaderBuilder); plugin.configureImages(asyncDrawableLoaderBuilder);

View File

@ -11,6 +11,7 @@ import ru.noties.markwon.core.MarkwonTheme;
import ru.noties.markwon.image.AsyncDrawableLoader; import ru.noties.markwon.image.AsyncDrawableLoader;
import ru.noties.markwon.image.MediaDecoder; import ru.noties.markwon.image.MediaDecoder;
import ru.noties.markwon.image.SchemeHandler; 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 * 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); void configureRenderProps(@NonNull RenderProps renderProps);
@NonNull
Priority priority();
/** /**
* Process input markdown and return new string to be used in parsing stage further. * Process input markdown and return new string to be used in parsing stage further.
* Can be described as <code>pre-processing</code> of markdown String. * Can be described as <code>pre-processing</code> of markdown String.

View File

@ -39,6 +39,7 @@ import ru.noties.markwon.core.factory.ListItemSpanFactory;
import ru.noties.markwon.core.factory.StrongEmphasisSpanFactory; import ru.noties.markwon.core.factory.StrongEmphasisSpanFactory;
import ru.noties.markwon.core.factory.ThematicBreakSpanFactory; import ru.noties.markwon.core.factory.ThematicBreakSpanFactory;
import ru.noties.markwon.core.spans.OrderedListItemSpan; import ru.noties.markwon.core.spans.OrderedListItemSpan;
import ru.noties.markwon.priority.Priority;
/** /**
* @since 3.0.0 * @since 3.0.0
@ -55,12 +56,8 @@ public class CorePlugin extends AbstractMarkwonPlugin {
return new CorePlugin(softBreakAddsNewLine); 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; private final boolean softBreakAddsNewLine;
// todo: test that visitors are registered for all expected nodes
protected CorePlugin(boolean softBreakAddsNewLine) { protected CorePlugin(boolean softBreakAddsNewLine) {
this.softBreakAddsNewLine = softBreakAddsNewLine; this.softBreakAddsNewLine = softBreakAddsNewLine;
} }
@ -104,6 +101,12 @@ public class CorePlugin extends AbstractMarkwonPlugin {
.setFactory(ThematicBreak.class, new ThematicBreakSpanFactory()); .setFactory(ThematicBreak.class, new ThematicBreakSpanFactory());
} }
@NonNull
@Override
public Priority priority() {
return Priority.none();
}
@Override @Override
public void beforeSetText(@NonNull TextView textView, @NonNull Spanned markdown) { public void beforeSetText(@NonNull TextView textView, @NonNull Spanned markdown) {
OrderedListItemSpan.measure(textView, markdown); OrderedListItemSpan.measure(textView, markdown);

View File

@ -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<Class<? extends MarkwonPlugin>> after();
static class Impl extends Priority {
private final List<Class<? extends MarkwonPlugin>> after;
Impl(@NonNull List<Class<? extends MarkwonPlugin>> after) {
this.after = after;
}
@NonNull
@Override
public List<Class<? extends MarkwonPlugin>> after() {
return after;
}
@Override
public String toString() {
return "Priority{" +
"after=" + after +
'}';
}
static class BuilderImpl implements Builder {
private final List<Class<? extends MarkwonPlugin>> 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));
}
}
}
}

View File

@ -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<MarkwonPlugin> process(@NonNull List<MarkwonPlugin> plugins);
}

View File

@ -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<MarkwonPlugin> process(@NonNull List<MarkwonPlugin> plugins) {
final int size = plugins.size();
final Map<Class<? extends MarkwonPlugin>, Set<Class<? extends MarkwonPlugin>>> 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<Plugin,Int>
final Map<MarkwonPlugin, Integer> 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<Class<? extends MarkwonPlugin>, Set<Class<? extends MarkwonPlugin>>> map) {
final Set<Class<? extends MarkwonPlugin>> 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<Class<? extends MarkwonPlugin>, Set<Class<? extends MarkwonPlugin>>> map) {
// exact match
Set<Class<? extends MarkwonPlugin>> set = map.get(plugin);
if (set == null) {
// let's try to find inexact type (overridden/subclassed)
for (Map.Entry<Class<? extends MarkwonPlugin>, Set<Class<? extends MarkwonPlugin>>> 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<MarkwonPlugin> {
private final Map<MarkwonPlugin, Integer> map;
PriorityComparator(@NonNull Map<MarkwonPlugin, Integer> 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 {
}
}

View File

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