diff --git a/markwon/src/main/java/ru/noties/markwon/MarkwonBuilderImpl.java b/markwon/src/main/java/ru/noties/markwon/MarkwonBuilderImpl.java index a9dac49a..e9d6aa2f 100644 --- a/markwon/src/main/java/ru/noties/markwon/MarkwonBuilderImpl.java +++ b/markwon/src/main/java/ru/noties/markwon/MarkwonBuilderImpl.java @@ -155,6 +155,10 @@ class MarkwonBuilderImpl implements Markwon.Builder { // here we do not check for exact match (a user could've subclasses CorePlugin // and supplied it. In this case we DO NOT implicitly add CorePlugin + // + // if core is present already we do not need to iterate anymore -> as nothing + // will be changed (and we actually do not care if there are any dependents of Core + // as it's present anyway) if (CorePlugin.class.isAssignableFrom(plugin.getClass())) { hasCore = true; break; diff --git a/markwon/src/main/java/ru/noties/markwon/Prop.java b/markwon/src/main/java/ru/noties/markwon/Prop.java index caacbfab..c02c88a7 100644 --- a/markwon/src/main/java/ru/noties/markwon/Prop.java +++ b/markwon/src/main/java/ru/noties/markwon/Prop.java @@ -11,7 +11,7 @@ import android.support.annotation.Nullable; * @see #of(Class, String) * @since 3.0.0 */ -public final class Prop { +public class Prop { @SuppressWarnings("unused") @NonNull @@ -26,7 +26,7 @@ public final class Prop { private final String name; - private Prop(@NonNull String name) { + Prop(@NonNull String name) { this.name = name; } diff --git a/markwon/src/main/java/ru/noties/markwon/RenderPropsImpl.java b/markwon/src/main/java/ru/noties/markwon/RenderPropsImpl.java index 75b6ac4b..24557975 100644 --- a/markwon/src/main/java/ru/noties/markwon/RenderPropsImpl.java +++ b/markwon/src/main/java/ru/noties/markwon/RenderPropsImpl.java @@ -9,7 +9,7 @@ import java.util.Map; /** * @since 3.0.0 */ -public class RenderPropsImpl implements RenderProps { +class RenderPropsImpl implements RenderProps { private final Map values = new HashMap<>(3); diff --git a/markwon/src/test/java/ru/noties/markwon/core/CorePluginTest.java b/markwon/src/test/java/ru/noties/markwon/core/CorePluginTest.java new file mode 100644 index 00000000..c0481ff2 --- /dev/null +++ b/markwon/src/test/java/ru/noties/markwon/core/CorePluginTest.java @@ -0,0 +1,278 @@ +package ru.noties.markwon.core; + +import android.support.annotation.NonNull; +import android.support.annotation.Nullable; + +import org.commonmark.node.BlockQuote; +import org.commonmark.node.BulletList; +import org.commonmark.node.Code; +import org.commonmark.node.Emphasis; +import org.commonmark.node.FencedCodeBlock; +import org.commonmark.node.HardLineBreak; +import org.commonmark.node.Heading; +import org.commonmark.node.IndentedCodeBlock; +import org.commonmark.node.Link; +import org.commonmark.node.ListItem; +import org.commonmark.node.Node; +import org.commonmark.node.OrderedList; +import org.commonmark.node.Paragraph; +import org.commonmark.node.SoftLineBreak; +import org.commonmark.node.StrongEmphasis; +import org.commonmark.node.Text; +import org.commonmark.node.ThematicBreak; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.ArgumentCaptor; +import org.robolectric.RobolectricTestRunner; +import org.robolectric.annotation.Config; + +import java.lang.reflect.Method; +import java.lang.reflect.Modifier; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import ix.Ix; +import ix.IxFunction; +import ix.IxPredicate; +import ru.noties.markwon.MarkwonConfiguration; +import ru.noties.markwon.MarkwonSpansFactory; +import ru.noties.markwon.MarkwonVisitor; +import ru.noties.markwon.RenderProps; +import ru.noties.markwon.SpanFactory; +import ru.noties.markwon.SpannableBuilder; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertTrue; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +@RunWith(RobolectricTestRunner.class) +@Config(manifest = Config.NONE) +public class CorePluginTest { + + @Test + public void visitors_registered() { + + // only these must be registered (everything else is an error) + // Paragraph has registered visitor but no Span by default + + //noinspection unchecked + final Class[] expected = new Class[]{ + BlockQuote.class, + BulletList.class, + Code.class, + Emphasis.class, + FencedCodeBlock.class, + HardLineBreak.class, + Heading.class, + IndentedCodeBlock.class, + Link.class, + ListItem.class, + OrderedList.class, + Paragraph.class, + SoftLineBreak.class, + StrongEmphasis.class, + Text.class, + ThematicBreak.class + }; + + final CorePlugin plugin = CorePlugin.create(); + + final class BuilderImpl implements MarkwonVisitor.Builder { + + private final Map, MarkwonVisitor.NodeVisitor> map = + new HashMap<>(); + + @NonNull + @Override + public MarkwonVisitor.Builder on(@NonNull Class node, @Nullable MarkwonVisitor.NodeVisitor nodeVisitor) { + if (map.put(node, nodeVisitor) != null) { + throw new RuntimeException("Multiple visitors registered for the node: " + node.getClass().getName()); + } + return this; + } + + @NonNull + @Override + public MarkwonVisitor build(@NonNull MarkwonConfiguration configuration, @NonNull RenderProps renderProps) { + throw new RuntimeException(); + } + } + final BuilderImpl impl = new BuilderImpl(); + + plugin.configureVisitor(impl); + + for (Class node : expected) { + assertNotNull("Node visitor registered: " + node.getName(), impl.map.remove(node)); + } + + // all other nodes (that could've been registered is an error) + assertEquals(impl.map.toString(), 0, impl.map.size()); + } + + @Test + public void spans_registered() { + + // paragraph has visitor registered, but no span associated by default + + //noinspection unchecked + final Class[] expected = new Class[]{ + BlockQuote.class, + Code.class, + Emphasis.class, + FencedCodeBlock.class, + Heading.class, + IndentedCodeBlock.class, + Link.class, + ListItem.class, + StrongEmphasis.class, + ThematicBreak.class + }; + + final CorePlugin plugin = CorePlugin.create(); + + final class BuilderImpl implements MarkwonSpansFactory.Builder { + + private final Map, SpanFactory> map = + new HashMap<>(); + + @NonNull + @Override + public MarkwonSpansFactory.Builder setFactory(@NonNull Class node, @NonNull SpanFactory factory) { + if (map.put(node, factory) != null) { + throw new RuntimeException("Multiple SpanFactories registered for the node: " + node.getName()); + } + return this; + } + + @NonNull + @Override + public MarkwonSpansFactory build() { + throw new RuntimeException(); + } + } + final BuilderImpl impl = new BuilderImpl(); + + plugin.configureSpansFactory(impl); + + for (Class node : expected) { + assertNotNull("SpanFactory registered: " + node.getName(), impl.map.remove(node)); + } + + assertEquals(impl.map.toString(), 0, impl.map.size()); + } + + @Test + public void priority_none() { + // CorePlugin returns none as priority (thus 0) + + assertEquals(0, CorePlugin.create().priority().after().size()); + } + + @Test + public void plugin_methods() { + // checks that only expected plugin methods are overridden + + // these represent actual methods that are present (we expect them to be present) + final Set usedMethods = new HashSet() {{ + add("configureVisitor"); + add("configureSpansFactory"); + add("beforeSetText"); + add("priority"); + }}; + + // we will use declaredMethods because it won't return inherited ones + final Method[] declaredMethods = CorePlugin.class.getDeclaredMethods(); + assertNotNull(declaredMethods); + assertTrue(declaredMethods.length > 0); + + final List methods = Ix.fromArray(declaredMethods) + .filter(new IxPredicate() { + @Override + public boolean test(Method method) { + // ignore private, static + final int modifiers = method.getModifiers(); + return !Modifier.isStatic(modifiers) + && !Modifier.isPrivate(modifiers); + } + }) + .map(new IxFunction() { + @Override + public String apply(Method method) { + return method.getName(); + } + }) + .filter(new IxPredicate() { + @Override + public boolean test(String s) { + return !usedMethods.contains(s); + } + }) + .toList(); + + assertEquals(methods.toString(), 0, methods.size()); + } + + @Test + public void softbreak_adds_new_line_default() { + // default is false + softbreak_adds_new_line(CorePlugin.create(), false); + } + + @Test + public void softbreak_adds_new_line_false() { + // a space character will be added + softbreak_adds_new_line(CorePlugin.create(false), false); + } + + @Test + public void softbreak_adds_new_line_true() { + // a new line will be added + softbreak_adds_new_line(CorePlugin.create(true), true); + } + + private static void softbreak_adds_new_line( + @NonNull CorePlugin plugin, + boolean softBreakAddsNewLine) { + + final MarkwonVisitor.Builder builder = mock(MarkwonVisitor.Builder.class); + when(builder.on(any(Class.class), any(MarkwonVisitor.NodeVisitor.class))).thenReturn(builder); + + final ArgumentCaptor captor = + ArgumentCaptor.forClass(MarkwonVisitor.NodeVisitor.class); + + plugin.configureVisitor(builder); + + //noinspection unchecked + verify(builder).on(eq(SoftLineBreak.class), captor.capture()); + + //noinspection unchecked + final MarkwonVisitor.NodeVisitor nodeVisitor = captor.getValue(); + final MarkwonVisitor visitor = mock(MarkwonVisitor.class); + + if (!softBreakAddsNewLine) { + + // we must mock SpannableBuilder and verify that it has a space character appended + final SpannableBuilder spannableBuilder = mock(SpannableBuilder.class); + when(visitor.builder()).thenReturn(spannableBuilder); + nodeVisitor.visit(visitor, mock(SoftLineBreak.class)); + + verify(visitor, times(1)).builder(); + verify(spannableBuilder, times(1)).append(eq(' ')); + + } else { + + nodeVisitor.visit(visitor, mock(SoftLineBreak.class)); + + verify(visitor, times(1)).ensureNewLine(); + } + } +} \ No newline at end of file