diff --git a/CHANGELOG.md b/CHANGELOG.md
index d8cc8ed5..6c719a9a 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -3,9 +3,11 @@
# 4.2.2-SNAPSHOT
* Fixed `AsyncDrawable` display when it has placeholder with empty bounds ([#189])
* Fixed `syntax-highlight` where code input is empty string ([#192])
+* Add `appendFactory`/`prependFactory` in `MarkwonSpansFactory.Builder` for more explicit `SpanFactory` ordering ([#193])
[#189]: https://github.com/noties/Markwon/issues/189
[#192]: https://github.com/noties/Markwon/issues/192
+[#193]: https://github.com/noties/Markwon/issues/193
# 4.2.1
* Fix SpannableBuilder `subSequence` method
diff --git a/docs/docs/v4/core/spans-factory.md b/docs/docs/v4/core/spans-factory.md
index 9567a788..e0b11aa1 100644
--- a/docs/docs/v4/core/spans-factory.md
+++ b/docs/docs/v4/core/spans-factory.md
@@ -62,7 +62,12 @@ builder.setFactory(Link.class, new SpanFactory() {
---
-Since you can _add_ multiple `SpanFactory` for a single node:
+:::warning
+Deprecated in . Use `appendFactory` or `prependFactory` for
+more explicit factories ordering. `addFactories` behaves like new `prependFactory` method.
+:::
+
+Since you can _add_ multiple `SpanFactory` for a single node:
```java
final Markwon markwon = Markwon.builder(context)
@@ -81,6 +86,22 @@ final Markwon markwon = Markwon.builder(context)
.build();
```
+## appendFactory/prependFactory
+
+* use `appendFactory` if you wish to add a factory **after** original (can be used to post-process original factory)
+* use `prependFactory` if you wish to add a factory **before** original (original factory will be applied after this one)
+
+```java
+@Override
+public void configureSpansFactory(@NonNull MarkwonSpansFactory.Builder builder) {
+ // `RemoveUnderlineSpan` will be added AFTER original, thus it will remove underline applied by original
+ builder.appendFactory(Link.class, (configuration, props) -> new RemoveUnderlineSpan());
+
+ // will be added BEFORE origin (origin can override this)
+ builder.prependFactory(Link.class, (configuration, props) -> new AbsoluteSizeSpan(48, true));
+}
+```
+
---
If you wish to inspect existing factory you can use:
diff --git a/markwon-core/src/main/java/io/noties/markwon/MarkwonSpansFactory.java b/markwon-core/src/main/java/io/noties/markwon/MarkwonSpansFactory.java
index c4153075..1afb7212 100644
--- a/markwon-core/src/main/java/io/noties/markwon/MarkwonSpansFactory.java
+++ b/markwon-core/src/main/java/io/noties/markwon/MarkwonSpansFactory.java
@@ -39,10 +39,34 @@ public interface MarkwonSpansFactory {
* {@link SpanFactory} with the specified one.
*
* @since 3.0.1
+ * @deprecated 4.2.2-SNAPSHOT consider using {@link #appendFactory(Class, SpanFactory)} or
+ * {@link #prependFactory(Class, SpanFactory)} methods for more explicit factory ordering.
+ * `addFactory` behaved like {@link #prependFactory(Class, SpanFactory)}, so
+ * this method call can be replaced with it
*/
@NonNull
+ @Deprecated
Builder addFactory(@NonNull Class node, @NonNull SpanFactory factory);
+ /**
+ * Append a factory to existing one (or make the first one for specified node). Specified factory
+ * will be called after original (if present) factory. Can be used to
+ * change behavior or original span factory.
+ *
+ * @since 4.2.2-SNAPSHOT
+ */
+ @NonNull
+ Builder appendFactory(@NonNull Class node, @NonNull SpanFactory factory);
+
+ /**
+ * Prepend a factory to existing one (or make the first one for specified node). Specified factory
+ * will be called before original (if present) factory.
+ *
+ * @since 4.2.2-SNAPSHOT
+ */
+ @NonNull
+ Builder prependFactory(@NonNull Class node, @NonNull SpanFactory factory);
+
/**
* Can be useful when enhancing an already defined SpanFactory with another one.
*/
diff --git a/markwon-core/src/main/java/io/noties/markwon/MarkwonSpansFactoryImpl.java b/markwon-core/src/main/java/io/noties/markwon/MarkwonSpansFactoryImpl.java
index ab2c1f84..fd7a99d5 100644
--- a/markwon-core/src/main/java/io/noties/markwon/MarkwonSpansFactoryImpl.java
+++ b/markwon-core/src/main/java/io/noties/markwon/MarkwonSpansFactoryImpl.java
@@ -56,7 +56,32 @@ class MarkwonSpansFactoryImpl implements MarkwonSpansFactory {
@NonNull
@Override
+ @Deprecated
public Builder addFactory(@NonNull Class node, @NonNull SpanFactory factory) {
+ return prependFactory(node, factory);
+ }
+
+ @NonNull
+ @Override
+ public Builder appendFactory(@NonNull Class node, @NonNull SpanFactory factory) {
+ final SpanFactory existing = factories.get(node);
+ if (existing == null) {
+ factories.put(node, factory);
+ } else {
+ if (existing instanceof CompositeSpanFactory) {
+ ((CompositeSpanFactory) existing).factories.add(0, factory);
+ } else {
+ final CompositeSpanFactory compositeSpanFactory =
+ new CompositeSpanFactory(factory, existing);
+ factories.put(node, compositeSpanFactory);
+ }
+ }
+ return this;
+ }
+
+ @NonNull
+ @Override
+ public Builder prependFactory(@NonNull Class node, @NonNull SpanFactory factory) {
// if there is no factory registered for this node -> just add it
final SpanFactory existing = factories.get(node);
if (existing == null) {
diff --git a/markwon-core/src/test/java/io/noties/markwon/MarkwonSpansFactoryImplTest.java b/markwon-core/src/test/java/io/noties/markwon/MarkwonSpansFactoryImplTest.java
index ad43e6e6..998ec152 100644
--- a/markwon-core/src/test/java/io/noties/markwon/MarkwonSpansFactoryImplTest.java
+++ b/markwon-core/src/test/java/io/noties/markwon/MarkwonSpansFactoryImplTest.java
@@ -113,6 +113,7 @@ public class MarkwonSpansFactoryImplTest {
}
@Test
+ @Deprecated
public void builder_add_factory() {
// here is what we should validate:
// * if we call addFactory and there is none already -> supplied factory
@@ -146,4 +147,74 @@ public class MarkwonSpansFactoryImplTest {
assertEquals(compositeSpanFactory, builder.getFactory(node));
assertEquals(Arrays.asList(first, second, third), compositeSpanFactory.factories);
}
+
+ @Test
+ public void builder_prepend_factory() {
+ // here is what we should validate:
+ // * if we call prependFactory and there is none already -> supplied factory
+ // * if there is
+ // * * if not composite -> make composite
+ // * * if composite -> add to it
+
+ final MarkwonSpansFactoryImpl.BuilderImpl builder = new MarkwonSpansFactoryImpl.BuilderImpl();
+
+ final SpanFactory first = mock(SpanFactory.class);
+ final SpanFactory second = mock(SpanFactory.class);
+ final SpanFactory third = mock(SpanFactory.class);
+
+ final Class node = Node.class;
+
+ // assert none yet
+ assertNull(builder.getFactory(node));
+
+ // add first, none yet -> it should be added without modifications
+ builder.prependFactory(node, first);
+ assertEquals(first, builder.getFactory(node));
+
+ // add second -> composite factory will be created
+ builder.prependFactory(node, second);
+ final MarkwonSpansFactoryImpl.CompositeSpanFactory compositeSpanFactory =
+ (MarkwonSpansFactoryImpl.CompositeSpanFactory) builder.getFactory(node);
+ assertNotNull(compositeSpanFactory);
+ assertEquals(Arrays.asList(first, second), compositeSpanFactory.factories);
+
+ builder.prependFactory(node, third);
+ assertEquals(compositeSpanFactory, builder.getFactory(node));
+ assertEquals(Arrays.asList(first, second, third), compositeSpanFactory.factories);
+ }
+
+ @Test
+ public void builder_append_factory() {
+ // here is what we should validate:
+ // * if we call appendFactory and there is none already -> supplied factory
+ // * if there is
+ // * * if not composite -> make composite
+ // * * if composite -> add to it
+
+ final MarkwonSpansFactoryImpl.BuilderImpl builder = new MarkwonSpansFactoryImpl.BuilderImpl();
+
+ final SpanFactory first = mock(SpanFactory.class);
+ final SpanFactory second = mock(SpanFactory.class);
+ final SpanFactory third = mock(SpanFactory.class);
+
+ final Class node = Node.class;
+
+ // assert none yet
+ assertNull(builder.getFactory(node));
+
+ // add first, none yet -> it should be added without modifications
+ builder.appendFactory(node, first);
+ assertEquals(first, builder.getFactory(node));
+
+ // add second -> composite factory will be created
+ builder.appendFactory(node, second);
+ final MarkwonSpansFactoryImpl.CompositeSpanFactory compositeSpanFactory =
+ (MarkwonSpansFactoryImpl.CompositeSpanFactory) builder.getFactory(node);
+ assertNotNull(compositeSpanFactory);
+ assertEquals(Arrays.asList(second, first), compositeSpanFactory.factories);
+
+ builder.appendFactory(node, third);
+ assertEquals(compositeSpanFactory, builder.getFactory(node));
+ assertEquals(Arrays.asList(third, second, first), compositeSpanFactory.factories);
+ }
}
\ No newline at end of file
diff --git a/markwon-core/src/test/java/io/noties/markwon/core/CorePluginTest.java b/markwon-core/src/test/java/io/noties/markwon/core/CorePluginTest.java
index 4137b83e..9cb95028 100644
--- a/markwon-core/src/test/java/io/noties/markwon/core/CorePluginTest.java
+++ b/markwon-core/src/test/java/io/noties/markwon/core/CorePluginTest.java
@@ -166,6 +166,18 @@ public class CorePluginTest {
throw new RuntimeException();
}
+ @NonNull
+ @Override
+ public MarkwonSpansFactory.Builder appendFactory(@NonNull Class node, @NonNull SpanFactory factory) {
+ throw new RuntimeException();
+ }
+
+ @NonNull
+ @Override
+ public MarkwonSpansFactory.Builder prependFactory(@NonNull Class node, @NonNull SpanFactory factory) {
+ throw new RuntimeException();
+ }
+
@Nullable
@Override
public SpanFactory getFactory(@NonNull Class node) {
diff --git a/sample/src/main/java/io/noties/markwon/sample/recycler/RecyclerActivity.java b/sample/src/main/java/io/noties/markwon/sample/recycler/RecyclerActivity.java
index 3d86c683..84034ce9 100644
--- a/sample/src/main/java/io/noties/markwon/sample/recycler/RecyclerActivity.java
+++ b/sample/src/main/java/io/noties/markwon/sample/recycler/RecyclerActivity.java
@@ -3,10 +3,12 @@ package io.noties.markwon.sample.recycler;
import android.app.Activity;
import android.content.Context;
import android.graphics.drawable.ColorDrawable;
-import android.graphics.drawable.Drawable;
import android.net.Uri;
import android.os.Bundle;
+import android.text.TextPaint;
import android.text.TextUtils;
+import android.text.style.CharacterStyle;
+import android.text.style.UpdateAppearance;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
@@ -15,6 +17,7 @@ import androidx.recyclerview.widget.RecyclerView;
import org.commonmark.ext.gfm.tables.TableBlock;
import org.commonmark.node.FencedCodeBlock;
+import org.commonmark.node.Link;
import java.io.BufferedReader;
import java.io.IOException;
@@ -26,14 +29,13 @@ import io.noties.debug.Debug;
import io.noties.markwon.AbstractMarkwonPlugin;
import io.noties.markwon.Markwon;
import io.noties.markwon.MarkwonConfiguration;
+import io.noties.markwon.MarkwonSpansFactory;
import io.noties.markwon.MarkwonVisitor;
import io.noties.markwon.core.CorePlugin;
import io.noties.markwon.html.HtmlPlugin;
-import io.noties.markwon.image.AsyncDrawable;
import io.noties.markwon.image.ImagesPlugin;
import io.noties.markwon.image.file.FileSchemeHandler;
import io.noties.markwon.image.network.OkHttpNetworkSchemeHandler;
-import io.noties.markwon.image.picasso.PicassoImagesPlugin;
import io.noties.markwon.image.svg.SvgMediaDecoder;
import io.noties.markwon.recycler.MarkwonAdapter;
import io.noties.markwon.recycler.SimpleEntry;
@@ -114,10 +116,24 @@ public class RecyclerActivity extends Activity {
visitor.builder().append(code);
});
}
+
+ @Override
+ public void configureSpansFactory(@NonNull MarkwonSpansFactory.Builder builder) {
+ // `RemoveUnderlineSpan` will be added AFTER original, thus it will remove underline applied by original
+ builder.appendFactory(Link.class, (configuration, props) -> new RemoveUnderlineSpan());
+ }
})
.build();
}
+ private static class RemoveUnderlineSpan extends CharacterStyle implements UpdateAppearance {
+
+ @Override
+ public void updateDrawState(TextPaint tp) {
+ tp.setUnderlineText(false);
+ }
+ }
+
@NonNull
private static String loadReadMe(@NonNull Context context) {
InputStream stream = null;