commit
						3fe514aeea
					
				| @ -1,5 +1,14 @@ | ||||
| # Changelog | ||||
| 
 | ||||
| # 4.1.0 | ||||
| * Add `Markwon.TextSetter` interface to be able to use PrecomputedText/PrecomputedTextCompat | ||||
| * Add `PrecomputedTextSetterCompat` and `compileOnly` dependency on `androidx.core:core`  | ||||
| (clients must have this dependency in the classpath) | ||||
| * Add `requirePlugin(Class)` and `getPlugins` for `Markwon` instance | ||||
| * TablePlugin -> defer table invalidation (via `View.post`), so only one invalidation  | ||||
| happens with each draw-call | ||||
| * AsyncDrawableSpan -> defer invalidation | ||||
| 
 | ||||
| # 4.0.2 | ||||
| * Fix `JLatexMathPlugin` formula placeholder (cannot have line breaks) ([#149]) | ||||
| * Fix `JLatexMathPlugin` to update resulting formula bounds when `fitCanvas=true` and  | ||||
|  | ||||
| @ -5,6 +5,7 @@ import android.content.Intent; | ||||
| import android.net.Uri; | ||||
| import android.os.Bundle; | ||||
| import android.text.Spanned; | ||||
| import android.text.method.LinkMovementMethod; | ||||
| import android.view.View; | ||||
| import android.widget.TextView; | ||||
| 
 | ||||
| @ -15,6 +16,7 @@ import javax.inject.Inject; | ||||
| 
 | ||||
| import io.noties.debug.Debug; | ||||
| import io.noties.markwon.Markwon; | ||||
| import io.noties.markwon.utils.NoCopySpannableFactory; | ||||
| 
 | ||||
| public class MainActivity extends Activity { | ||||
| 
 | ||||
| @ -60,6 +62,9 @@ public class MainActivity extends Activity { | ||||
| 
 | ||||
|         appBarRenderer.render(appBarState()); | ||||
| 
 | ||||
|         textView.setMovementMethod(LinkMovementMethod.getInstance()); | ||||
|         textView.setSpannableFactory(NoCopySpannableFactory.getInstance()); | ||||
| 
 | ||||
|         markdownLoader.load(uri(), new MarkdownLoader.OnMarkdownTextLoaded() { | ||||
|             @Override | ||||
|             public void apply(final String text) { | ||||
|  | ||||
| @ -18,9 +18,12 @@ | ||||
|             android:id="@+id/text" | ||||
|             android:layout_width="match_parent" | ||||
|             android:layout_height="wrap_content" | ||||
|             android:breakStrategy="simple" | ||||
|             android:hyphenationFrequency="none" | ||||
|             android:lineSpacingExtra="2dip" | ||||
|             android:textSize="16sp" | ||||
|             tools:text="yo\nman" /> | ||||
|             tools:text="yo\nman" | ||||
|             tools:ignore="UnusedAttribute" /> | ||||
| 
 | ||||
|     </ScrollView> | ||||
| 
 | ||||
|  | ||||
| @ -59,6 +59,7 @@ ext { | ||||
|     deps = [ | ||||
|             'x-annotations'           : 'androidx.annotation:annotation:1.1.0', | ||||
|             'x-recycler-view'         : 'androidx.recyclerview:recyclerview:1.0.0', | ||||
|             'x-core'                  : 'androidx.core:core:1.0.2', | ||||
|             'commonmark'              : "com.atlassian.commonmark:commonmark:$commonMarkVersion", | ||||
|             'commonmark-strikethrough': "com.atlassian.commonmark:commonmark-ext-gfm-strikethrough:$commonMarkVersion", | ||||
|             'commonmark-table'        : "com.atlassian.commonmark:commonmark-ext-gfm-tables:$commonMarkVersion", | ||||
|  | ||||
| @ -8,7 +8,7 @@ android.enableJetifier=true | ||||
| android.enableBuildCache=true | ||||
| android.buildCacheDir=build/pre-dex-cache | ||||
| 
 | ||||
| VERSION_NAME=4.0.2 | ||||
| VERSION_NAME=4.1.0 | ||||
| 
 | ||||
| GROUP=io.noties.markwon | ||||
| POM_DESCRIPTION=Markwon markdown for Android | ||||
|  | ||||
| @ -18,6 +18,10 @@ dependencies { | ||||
|     deps.with { | ||||
|         api it['x-annotations'] | ||||
|         api it['commonmark'] | ||||
| 
 | ||||
|         // @since 4.1.0 to allow PrecomputedTextSetterCompat | ||||
|         // note that this dependency must be added on a client side explicitly | ||||
|         compileOnly it['x-core'] | ||||
|     } | ||||
| 
 | ||||
|     deps['test'].with { | ||||
|  | ||||
| @ -9,6 +9,8 @@ import androidx.annotation.Nullable; | ||||
| 
 | ||||
| import org.commonmark.node.Node; | ||||
| 
 | ||||
| import java.util.List; | ||||
| 
 | ||||
| import io.noties.markwon.core.CorePlugin; | ||||
| 
 | ||||
| /** | ||||
| @ -119,6 +121,42 @@ public abstract class Markwon { | ||||
|     @Nullable | ||||
|     public abstract <P extends MarkwonPlugin> P getPlugin(@NonNull Class<P> type); | ||||
| 
 | ||||
|     /** | ||||
|      * @since 4.1.0 | ||||
|      */ | ||||
|     @NonNull | ||||
|     public abstract <P extends MarkwonPlugin> P requirePlugin(@NonNull Class<P> type); | ||||
| 
 | ||||
|     /** | ||||
|      * @return a list of registered {@link MarkwonPlugin} | ||||
|      * @since 4.1.0 | ||||
|      */ | ||||
|     @NonNull | ||||
|     public abstract List<? extends MarkwonPlugin> getPlugins(); | ||||
| 
 | ||||
|     /** | ||||
|      * Interface to set text on a TextView. Primary goal is to give a way to use PrecomputedText | ||||
|      * functionality | ||||
|      * | ||||
|      * @see PrecomputedTextSetterCompat | ||||
|      * @since 4.1.0 | ||||
|      */ | ||||
|     public interface TextSetter { | ||||
|         /** | ||||
|          * @param textView   TextView | ||||
|          * @param markdown   prepared markdown | ||||
|          * @param bufferType BufferType specified when building {@link Markwon} instance | ||||
|          *                   via {@link Builder#bufferType(TextView.BufferType)} | ||||
|          * @param onComplete action to run when set-text is finished (required to call in order | ||||
|          *                   to execute {@link MarkwonPlugin#afterSetText(TextView)}) | ||||
|          */ | ||||
|         void setText( | ||||
|                 @NonNull TextView textView, | ||||
|                 @NonNull Spanned markdown, | ||||
|                 @NonNull TextView.BufferType bufferType, | ||||
|                 @NonNull Runnable onComplete); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Builder for {@link Markwon}. | ||||
|      * <p> | ||||
| @ -138,6 +176,13 @@ public abstract class Markwon { | ||||
|         @NonNull | ||||
|         Builder bufferType(@NonNull TextView.BufferType bufferType); | ||||
| 
 | ||||
|         /** | ||||
|          * @param textSetter {@link TextSetter} to apply text to a TextView | ||||
|          * @since 4.1.0 | ||||
|          */ | ||||
|         @NonNull | ||||
|         Builder textSetter(@NonNull TextSetter textSetter); | ||||
| 
 | ||||
|         @NonNull | ||||
|         Builder usePlugin(@NonNull MarkwonPlugin plugin); | ||||
| 
 | ||||
|  | ||||
| @ -25,6 +25,8 @@ class MarkwonBuilderImpl implements Markwon.Builder { | ||||
| 
 | ||||
|     private TextView.BufferType bufferType = TextView.BufferType.SPANNABLE; | ||||
| 
 | ||||
|     private Markwon.TextSetter textSetter; | ||||
| 
 | ||||
|     MarkwonBuilderImpl(@NonNull Context context) { | ||||
|         this.context = context; | ||||
|     } | ||||
| @ -36,6 +38,13 @@ class MarkwonBuilderImpl implements Markwon.Builder { | ||||
|         return this; | ||||
|     } | ||||
| 
 | ||||
|     @NonNull | ||||
|     @Override | ||||
|     public Markwon.Builder textSetter(@NonNull Markwon.TextSetter textSetter) { | ||||
|         this.textSetter = textSetter; | ||||
|         return this; | ||||
|     } | ||||
| 
 | ||||
|     @NonNull | ||||
|     @Override | ||||
|     public Markwon.Builder usePlugin(@NonNull MarkwonPlugin plugin) { | ||||
| @ -97,6 +106,7 @@ class MarkwonBuilderImpl implements Markwon.Builder { | ||||
| 
 | ||||
|         return new MarkwonImpl( | ||||
|                 bufferType, | ||||
|                 textSetter, | ||||
|                 parserBuilder.build(), | ||||
|                 visitorBuilder.build(configuration, renderProps), | ||||
|                 Collections.unmodifiableList(plugins) | ||||
|  | ||||
| @ -9,7 +9,9 @@ import androidx.annotation.Nullable; | ||||
| import org.commonmark.node.Node; | ||||
| import org.commonmark.parser.Parser; | ||||
| 
 | ||||
| import java.util.Collections; | ||||
| import java.util.List; | ||||
| import java.util.Locale; | ||||
| 
 | ||||
| /** | ||||
|  * @since 3.0.0 | ||||
| @ -21,12 +23,18 @@ class MarkwonImpl extends Markwon { | ||||
|     private final MarkwonVisitor visitor; | ||||
|     private final List<MarkwonPlugin> plugins; | ||||
| 
 | ||||
|     // @since 4.1.0 | ||||
|     @Nullable | ||||
|     private final TextSetter textSetter; | ||||
| 
 | ||||
|     MarkwonImpl( | ||||
|             @NonNull TextView.BufferType bufferType, | ||||
|             @Nullable TextSetter textSetter, | ||||
|             @NonNull Parser parser, | ||||
|             @NonNull MarkwonVisitor visitor, | ||||
|             @NonNull List<MarkwonPlugin> plugins) { | ||||
|         this.bufferType = bufferType; | ||||
|         this.textSetter = textSetter; | ||||
|         this.parser = parser; | ||||
|         this.visitor = visitor; | ||||
|         this.plugins = plugins; | ||||
| @ -78,16 +86,31 @@ class MarkwonImpl extends Markwon { | ||||
|     } | ||||
| 
 | ||||
|     @Override | ||||
|     public void setParsedMarkdown(@NonNull TextView textView, @NonNull Spanned markdown) { | ||||
|     public void setParsedMarkdown(@NonNull final TextView textView, @NonNull Spanned markdown) { | ||||
| 
 | ||||
|         for (MarkwonPlugin plugin : plugins) { | ||||
|             plugin.beforeSetText(textView, markdown); | ||||
|         } | ||||
| 
 | ||||
|         textView.setText(markdown, bufferType); | ||||
|         // @since 4.1.0 | ||||
|         if (textSetter != null) { | ||||
|             textSetter.setText(textView, markdown, bufferType, new Runnable() { | ||||
|                 @Override | ||||
|                 public void run() { | ||||
|                     // on-complete we just must call `afterSetText` on all plugins | ||||
|                     for (MarkwonPlugin plugin : plugins) { | ||||
|                         plugin.afterSetText(textView); | ||||
|                     } | ||||
|                 } | ||||
|             }); | ||||
|         } else { | ||||
| 
 | ||||
|         for (MarkwonPlugin plugin : plugins) { | ||||
|             plugin.afterSetText(textView); | ||||
|             // if no text-setter is specified -> just a regular sync operation | ||||
|             textView.setText(markdown, bufferType); | ||||
| 
 | ||||
|             for (MarkwonPlugin plugin : plugins) { | ||||
|                 plugin.afterSetText(textView); | ||||
|             } | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
| @ -108,4 +131,21 @@ class MarkwonImpl extends Markwon { | ||||
|         //noinspection unchecked | ||||
|         return (P) out; | ||||
|     } | ||||
| 
 | ||||
|     @NonNull | ||||
|     @Override | ||||
|     public <P extends MarkwonPlugin> P requirePlugin(@NonNull Class<P> type) { | ||||
|         final P plugin = getPlugin(type); | ||||
|         if (plugin == null) { | ||||
|             throw new IllegalStateException(String.format(Locale.US, "Requested plugin `%s` is not " + | ||||
|                     "registered with this Markwon instance", type.getName())); | ||||
|         } | ||||
|         return plugin; | ||||
|     } | ||||
| 
 | ||||
|     @NonNull | ||||
|     @Override | ||||
|     public List<? extends MarkwonPlugin> getPlugins() { | ||||
|         return Collections.unmodifiableList(plugins); | ||||
|     } | ||||
| } | ||||
|  | ||||
| @ -0,0 +1,119 @@ | ||||
| package io.noties.markwon; | ||||
| 
 | ||||
| import android.os.Build; | ||||
| import android.text.Spanned; | ||||
| import android.util.Log; | ||||
| import android.widget.TextView; | ||||
| 
 | ||||
| import androidx.annotation.NonNull; | ||||
| import androidx.annotation.Nullable; | ||||
| import androidx.core.text.PrecomputedTextCompat; | ||||
| 
 | ||||
| import java.lang.ref.WeakReference; | ||||
| import java.util.concurrent.Executor; | ||||
| 
 | ||||
| /** | ||||
|  * Please note this class requires `androidx.core:core` artifact being explicitly added to your dependencies. | ||||
|  * Please do not use with `markwon-recycler` as it will lead to bad item rendering (due to async nature) | ||||
|  * | ||||
|  * @see io.noties.markwon.Markwon.TextSetter | ||||
|  * @since 4.1.0 | ||||
|  */ | ||||
| public class PrecomputedTextSetterCompat implements Markwon.TextSetter { | ||||
| 
 | ||||
|     /** | ||||
|      * @param executor for background execution of text pre-computation | ||||
|      */ | ||||
|     @NonNull | ||||
|     public static PrecomputedTextSetterCompat create(@NonNull Executor executor) { | ||||
|         return new PrecomputedTextSetterCompat(executor); | ||||
|     } | ||||
| 
 | ||||
|     private final Executor executor; | ||||
| 
 | ||||
|     @SuppressWarnings("WeakerAccess") | ||||
|     PrecomputedTextSetterCompat(@NonNull Executor executor) { | ||||
|         this.executor = executor; | ||||
|     } | ||||
| 
 | ||||
|     @Override | ||||
|     public void setText( | ||||
|             @NonNull TextView textView, | ||||
|             @NonNull final Spanned markdown, | ||||
|             @NonNull final TextView.BufferType bufferType, | ||||
|             @NonNull final Runnable onComplete) { | ||||
| 
 | ||||
|         // insert version check and do not execute on a device < 21 | ||||
|         if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) { | ||||
|             // it's still no-op, so there is no need to start background execution | ||||
|             applyText(textView, markdown, bufferType, onComplete); | ||||
|             return; | ||||
|         } | ||||
| 
 | ||||
|         final WeakReference<TextView> reference = new WeakReference<>(textView); | ||||
|         executor.execute(new Runnable() { | ||||
|             @Override | ||||
|             public void run() { | ||||
|                 try { | ||||
|                     final PrecomputedTextCompat precomputedTextCompat = precomputedText(reference.get(), markdown); | ||||
|                     if (precomputedTextCompat != null) { | ||||
|                         applyText(reference.get(), precomputedTextCompat, bufferType, onComplete); | ||||
|                     } | ||||
|                 } catch (Throwable t) { | ||||
|                     Log.e("PrecomputdTxtSetterCmpt", "Exception during pre-computing text", t); | ||||
|                     // apply initial markdown | ||||
|                     applyText(reference.get(), markdown, bufferType, onComplete); | ||||
|                 } | ||||
|             } | ||||
|         }); | ||||
|     } | ||||
| 
 | ||||
|     @Nullable | ||||
|     private static PrecomputedTextCompat precomputedText(@Nullable TextView textView, @NonNull Spanned spanned) { | ||||
| 
 | ||||
|         if (textView == null) { | ||||
|             return null; | ||||
|         } | ||||
| 
 | ||||
|         final PrecomputedTextCompat.Params params; | ||||
| 
 | ||||
|         if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) { | ||||
|             // use native parameters on P | ||||
|             params = new PrecomputedTextCompat.Params(textView.getTextMetricsParams()); | ||||
|         } else { | ||||
| 
 | ||||
|             final PrecomputedTextCompat.Params.Builder builder = | ||||
|                     new PrecomputedTextCompat.Params.Builder(textView.getPaint()); | ||||
| 
 | ||||
|             // please note that text-direction initialization is omitted | ||||
|             // by default it will be determined by the first locale-specific character | ||||
| 
 | ||||
|             if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { | ||||
|                 // another miss on API surface, this can easily be done by the compat class itself | ||||
|                 builder | ||||
|                         .setBreakStrategy(textView.getBreakStrategy()) | ||||
|                         .setHyphenationFrequency(textView.getHyphenationFrequency()); | ||||
|             } | ||||
| 
 | ||||
|             params = builder.build(); | ||||
|         } | ||||
| 
 | ||||
|         return PrecomputedTextCompat.create(spanned, params); | ||||
|     } | ||||
| 
 | ||||
|     private static void applyText( | ||||
|             @Nullable final TextView textView, | ||||
|             @NonNull final Spanned text, | ||||
|             @NonNull final TextView.BufferType bufferType, | ||||
|             @NonNull final Runnable onComplete) { | ||||
|         if (textView != null) { | ||||
|             textView.post(new Runnable() { | ||||
|                 @Override | ||||
|                 public void run() { | ||||
|                     textView.setText(text, bufferType); | ||||
|                     onComplete.run(); | ||||
|                 } | ||||
|             }); | ||||
|         } | ||||
|     } | ||||
| } | ||||
| @ -57,11 +57,14 @@ public abstract class AsyncDrawableScheduler { | ||||
|                 textView.setTag(R.id.markwon_drawables_scheduler, listener); | ||||
|             } | ||||
| 
 | ||||
|             // @since 4.1.0 | ||||
|             final DrawableCallbackImpl.Invalidator invalidator = new TextViewInvalidator(textView); | ||||
| 
 | ||||
|             AsyncDrawable drawable; | ||||
| 
 | ||||
|             for (AsyncDrawableSpan span : spans) { | ||||
|                 drawable = span.getDrawable(); | ||||
|                 drawable.setCallback2(new DrawableCallbackImpl(textView, drawable.getBounds())); | ||||
|                 drawable.setCallback2(new DrawableCallbackImpl(textView, invalidator, drawable.getBounds())); | ||||
|             } | ||||
|         } | ||||
|     } | ||||
| @ -109,11 +112,23 @@ public abstract class AsyncDrawableScheduler { | ||||
| 
 | ||||
|     private static class DrawableCallbackImpl implements Drawable.Callback { | ||||
| 
 | ||||
|         // @since 4.1.0 | ||||
|         // interface to be used when bounds change and view must be invalidated | ||||
|         interface Invalidator { | ||||
|             void invalidate(); | ||||
|         } | ||||
| 
 | ||||
|         private final TextView view; | ||||
|         private final Invalidator invalidator; // @since 4.1.0 | ||||
| 
 | ||||
|         private Rect previousBounds; | ||||
| 
 | ||||
|         DrawableCallbackImpl(TextView view, Rect initialBounds) { | ||||
|         DrawableCallbackImpl( | ||||
|                 @NonNull TextView view, | ||||
|                 @NonNull Invalidator invalidator, | ||||
|                 Rect initialBounds) { | ||||
|             this.view = view; | ||||
|             this.invalidator = invalidator; | ||||
|             this.previousBounds = new Rect(initialBounds); | ||||
|         } | ||||
| 
 | ||||
| @ -136,8 +151,10 @@ public abstract class AsyncDrawableScheduler { | ||||
|             // but if the size has changed, then we need to update the whole layout... | ||||
| 
 | ||||
|             if (!previousBounds.equals(rect)) { | ||||
|                 // the only method that seems to work when bounds have changed | ||||
|                 view.setText(view.getText()); | ||||
|                 // @since 4.1.0 | ||||
|                 // invalidation moved to upper level (so invalidation can be deferred, | ||||
|                 // and multiple calls combined) | ||||
|                 invalidator.invalidate(); | ||||
|                 previousBounds = new Rect(rect); | ||||
|             } else { | ||||
| 
 | ||||
| @ -156,4 +173,24 @@ public abstract class AsyncDrawableScheduler { | ||||
|             view.removeCallbacks(what); | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     private static class TextViewInvalidator implements DrawableCallbackImpl.Invalidator, Runnable { | ||||
| 
 | ||||
|         private final TextView textView; | ||||
| 
 | ||||
|         TextViewInvalidator(@NonNull TextView textView) { | ||||
|             this.textView = textView; | ||||
|         } | ||||
| 
 | ||||
|         @Override | ||||
|         public void invalidate() { | ||||
|             textView.removeCallbacks(this); | ||||
|             textView.post(this); | ||||
|         } | ||||
| 
 | ||||
|         @Override | ||||
|         public void run() { | ||||
|             textView.setText(textView.getText()); | ||||
|         } | ||||
|     } | ||||
| } | ||||
|  | ||||
| @ -14,6 +14,7 @@ import org.mockito.stubbing.Answer; | ||||
| import org.robolectric.RobolectricTestRunner; | ||||
| import org.robolectric.annotation.Config; | ||||
| 
 | ||||
| import java.util.ArrayList; | ||||
| import java.util.Arrays; | ||||
| import java.util.Collections; | ||||
| import java.util.List; | ||||
| @ -21,7 +22,9 @@ import java.util.concurrent.atomic.AtomicBoolean; | ||||
| 
 | ||||
| import static org.junit.Assert.assertEquals; | ||||
| import static org.junit.Assert.assertFalse; | ||||
| import static org.junit.Assert.assertNotNull; | ||||
| import static org.junit.Assert.assertTrue; | ||||
| import static org.junit.Assert.fail; | ||||
| import static org.mockito.ArgumentMatchers.any; | ||||
| import static org.mockito.ArgumentMatchers.anyString; | ||||
| import static org.mockito.ArgumentMatchers.eq; | ||||
| @ -42,6 +45,7 @@ public class MarkwonImplTest { | ||||
|         final MarkwonPlugin plugin = mock(MarkwonPlugin.class); | ||||
|         final MarkwonImpl impl = new MarkwonImpl( | ||||
|                 TextView.BufferType.SPANNABLE, | ||||
|                 null, | ||||
|                 mock(Parser.class), | ||||
|                 mock(MarkwonVisitor.class), | ||||
|                 Collections.singletonList(plugin)); | ||||
| @ -64,6 +68,7 @@ public class MarkwonImplTest { | ||||
| 
 | ||||
|         final MarkwonImpl impl = new MarkwonImpl( | ||||
|                 TextView.BufferType.SPANNABLE, | ||||
|                 null, | ||||
|                 parser, | ||||
|                 mock(MarkwonVisitor.class), | ||||
|                 Arrays.asList(first, second)); | ||||
| @ -89,6 +94,7 @@ public class MarkwonImplTest { | ||||
| 
 | ||||
|         final MarkwonImpl impl = new MarkwonImpl( | ||||
|                 TextView.BufferType.SPANNABLE, | ||||
|                 null, | ||||
|                 mock(Parser.class), | ||||
|                 visitor, | ||||
|                 Collections.singletonList(plugin)); | ||||
| @ -130,6 +136,7 @@ public class MarkwonImplTest { | ||||
| 
 | ||||
|         final MarkwonImpl impl = new MarkwonImpl( | ||||
|                 TextView.BufferType.SPANNABLE, | ||||
|                 null, | ||||
|                 mock(Parser.class), | ||||
|                 visitor, | ||||
|                 Collections.<MarkwonPlugin>emptyList()); | ||||
| @ -160,6 +167,7 @@ public class MarkwonImplTest { | ||||
| 
 | ||||
|         final MarkwonImpl impl = new MarkwonImpl( | ||||
|                 TextView.BufferType.SPANNABLE, | ||||
|                 null, | ||||
|                 mock(Parser.class), | ||||
|                 visitor, | ||||
|                 Collections.singletonList(plugin)); | ||||
| @ -195,6 +203,7 @@ public class MarkwonImplTest { | ||||
|         final MarkwonPlugin plugin = mock(MarkwonPlugin.class); | ||||
|         final MarkwonImpl impl = new MarkwonImpl( | ||||
|                 TextView.BufferType.EDITABLE, | ||||
|                 null, | ||||
|                 mock(Parser.class), | ||||
|                 mock(MarkwonVisitor.class, RETURNS_MOCKS), | ||||
|                 Collections.singletonList(plugin)); | ||||
| @ -241,6 +250,7 @@ public class MarkwonImplTest { | ||||
| 
 | ||||
|         final MarkwonImpl impl = new MarkwonImpl( | ||||
|                 TextView.BufferType.SPANNABLE, | ||||
|                 null, | ||||
|                 mock(Parser.class), | ||||
|                 mock(MarkwonVisitor.class), | ||||
|                 plugins); | ||||
| @ -253,4 +263,103 @@ public class MarkwonImplTest { | ||||
|         assertTrue("AbstractMarkwonPlugin", impl.hasPlugin(AbstractMarkwonPlugin.class)); | ||||
|         assertTrue("MarkwonPlugin", impl.hasPlugin(MarkwonPlugin.class)); | ||||
|     } | ||||
| 
 | ||||
|     @Test | ||||
|     public void text_setter() { | ||||
| 
 | ||||
|         final Markwon.TextSetter textSetter = mock(Markwon.TextSetter.class); | ||||
|         final MarkwonPlugin plugin = mock(MarkwonPlugin.class); | ||||
| 
 | ||||
|         final MarkwonImpl impl = new MarkwonImpl( | ||||
|                 TextView.BufferType.EDITABLE, | ||||
|                 textSetter, | ||||
|                 mock(Parser.class), | ||||
|                 mock(MarkwonVisitor.class), | ||||
|                 Collections.singletonList(plugin)); | ||||
| 
 | ||||
|         final TextView textView = mock(TextView.class); | ||||
|         final Spanned spanned = mock(Spanned.class); | ||||
| 
 | ||||
|         impl.setParsedMarkdown(textView, spanned); | ||||
| 
 | ||||
|         final ArgumentCaptor<TextView> textViewArgumentCaptor = | ||||
|                 ArgumentCaptor.forClass(TextView.class); | ||||
|         final ArgumentCaptor<Spanned> spannedArgumentCaptor = | ||||
|                 ArgumentCaptor.forClass(Spanned.class); | ||||
|         final ArgumentCaptor<TextView.BufferType> bufferTypeArgumentCaptor = | ||||
|                 ArgumentCaptor.forClass(TextView.BufferType.class); | ||||
|         final ArgumentCaptor<Runnable> runnableArgumentCaptor = | ||||
|                 ArgumentCaptor.forClass(Runnable.class); | ||||
| 
 | ||||
|         verify(textSetter, times(1)).setText( | ||||
|                 textViewArgumentCaptor.capture(), | ||||
|                 spannedArgumentCaptor.capture(), | ||||
|                 bufferTypeArgumentCaptor.capture(), | ||||
|                 runnableArgumentCaptor.capture()); | ||||
| 
 | ||||
|         assertEquals(textView, textViewArgumentCaptor.getValue()); | ||||
|         assertEquals(spanned, spannedArgumentCaptor.getValue()); | ||||
|         assertEquals(TextView.BufferType.EDITABLE, bufferTypeArgumentCaptor.getValue()); | ||||
|         assertNotNull(runnableArgumentCaptor.getValue()); | ||||
|     } | ||||
| 
 | ||||
|     @Test | ||||
|     public void require_plugin_throws() { | ||||
|         // if plugin is `required`, but it's not added -> an exception is thrown | ||||
| 
 | ||||
|         final class NotPresent extends AbstractMarkwonPlugin { | ||||
|         } | ||||
| 
 | ||||
|         final List<MarkwonPlugin> plugins = | ||||
|                 Arrays.asList(mock(MarkwonPlugin.class), mock(MarkwonPlugin.class)); | ||||
| 
 | ||||
|         final MarkwonImpl impl = new MarkwonImpl( | ||||
|                 TextView.BufferType.SPANNABLE, | ||||
|                 null, | ||||
|                 mock(Parser.class), | ||||
|                 mock(MarkwonVisitor.class), plugins); | ||||
| 
 | ||||
|         // should be returned | ||||
|         assertNotNull(impl.requirePlugin(MarkwonPlugin.class)); | ||||
| 
 | ||||
|         try { | ||||
|             impl.requirePlugin(NotPresent.class); | ||||
|             fail(); | ||||
|         } catch (Throwable t) { | ||||
|             assertTrue(t.getMessage(), t.getMessage().contains(NotPresent.class.getName())); | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     @Test | ||||
|     public void plugins_unmodifiable() { | ||||
|         // returned plugins list must not be modifiable | ||||
| 
 | ||||
|         // modifiable list (created from Arrays.asList -> which returns non) | ||||
|         final List<MarkwonPlugin> plugins = new ArrayList<>( | ||||
|                 Arrays.asList(mock(MarkwonPlugin.class), mock(MarkwonPlugin.class))); | ||||
| 
 | ||||
|         // validate that list is modifiable | ||||
|         plugins.add(mock(MarkwonPlugin.class)); | ||||
|         assertEquals(3, plugins.size()); | ||||
| 
 | ||||
|         final MarkwonImpl impl = new MarkwonImpl( | ||||
|                 TextView.BufferType.SPANNABLE, | ||||
|                 null, | ||||
|                 mock(Parser.class), | ||||
|                 mock(MarkwonVisitor.class), | ||||
|                 plugins); | ||||
| 
 | ||||
|         final List<? extends MarkwonPlugin> list = impl.getPlugins(); | ||||
| 
 | ||||
|         // instance check (different list) | ||||
|         //noinspection SimplifiableJUnitAssertion | ||||
|         assertTrue(plugins != list); | ||||
| 
 | ||||
|         try { | ||||
|             list.add(null); | ||||
|             fail(); | ||||
|         } catch (UnsupportedOperationException e) { | ||||
|             assertTrue(e.getMessage(), true); | ||||
|         } | ||||
|     } | ||||
| } | ||||
| @ -34,9 +34,22 @@ abstract class TableRowsScheduler { | ||||
|             } | ||||
| 
 | ||||
|             final TableRowSpan.Invalidator invalidator = new TableRowSpan.Invalidator() { | ||||
| 
 | ||||
|                 // @since 4.1.0 | ||||
|                 // let's stack-up invalidation calls (so invalidation happens, | ||||
|                 // but not with each table-row-span draw call) | ||||
|                 final Runnable runnable = new Runnable() { | ||||
|                     @Override | ||||
|                     public void run() { | ||||
|                         view.setText(view.getText()); | ||||
|                     } | ||||
|                 }; | ||||
| 
 | ||||
|                 @Override | ||||
|                 public void invalidate() { | ||||
|                     view.setText(view.getText()); | ||||
|                     // @since 4.1.0 post invalidation (combine multiple calls) | ||||
|                     view.removeCallbacks(runnable); | ||||
|                     view.post(runnable); | ||||
|                 } | ||||
|             }; | ||||
| 
 | ||||
|  | ||||
| @ -49,6 +49,7 @@ dependencies { | ||||
| 
 | ||||
|     deps.with { | ||||
|         implementation it['x-recycler-view'] | ||||
|         implementation it['x-core'] // for precomputedTextCompat | ||||
|         implementation it['okhttp'] | ||||
|         implementation it['prism4j'] | ||||
|         implementation it['debug'] | ||||
|  | ||||
| @ -27,6 +27,7 @@ | ||||
|         <activity android:name=".html.HtmlActivity" /> | ||||
|         <activity android:name=".simpleext.SimpleExtActivity" /> | ||||
|         <activity android:name=".customextension2.CustomExtensionActivity2" /> | ||||
|         <activity android:name=".precomputed.PrecomputedActivity" /> | ||||
| 
 | ||||
|     </application> | ||||
| 
 | ||||
|  | ||||
| @ -24,6 +24,7 @@ import io.noties.markwon.sample.customextension.CustomExtensionActivity; | ||||
| import io.noties.markwon.sample.customextension2.CustomExtensionActivity2; | ||||
| import io.noties.markwon.sample.html.HtmlActivity; | ||||
| import io.noties.markwon.sample.latex.LatexActivity; | ||||
| import io.noties.markwon.sample.precomputed.PrecomputedActivity; | ||||
| import io.noties.markwon.sample.recycler.RecyclerActivity; | ||||
| import io.noties.markwon.sample.simpleext.SimpleExtActivity; | ||||
| 
 | ||||
| @ -112,6 +113,10 @@ public class MainActivity extends Activity { | ||||
|                 activity = CustomExtensionActivity2.class; | ||||
|                 break; | ||||
| 
 | ||||
|             case PRECOMPUTED_TEXT: | ||||
|                 activity = PrecomputedActivity.class; | ||||
|                 break; | ||||
| 
 | ||||
|             default: | ||||
|                 throw new IllegalStateException("No Activity is associated with sample-item: " + item); | ||||
|         } | ||||
|  | ||||
| @ -19,7 +19,9 @@ public enum Sample { | ||||
| 
 | ||||
|     SIMPLE_EXT(R.string.sample_simple_ext), | ||||
| 
 | ||||
|     CUSTOM_EXTENSION_2(R.string.sample_custom_extension_2); | ||||
|     CUSTOM_EXTENSION_2(R.string.sample_custom_extension_2), | ||||
| 
 | ||||
|     PRECOMPUTED_TEXT(R.string.sample_precomputed_text); | ||||
| 
 | ||||
|     private final int textResId; | ||||
| 
 | ||||
|  | ||||
| @ -0,0 +1,38 @@ | ||||
| package io.noties.markwon.sample.precomputed; | ||||
| 
 | ||||
| import android.app.Activity; | ||||
| import android.os.Bundle; | ||||
| import android.widget.TextView; | ||||
| 
 | ||||
| import androidx.annotation.Nullable; | ||||
| 
 | ||||
| import java.util.concurrent.Executors; | ||||
| 
 | ||||
| import io.noties.markwon.Markwon; | ||||
| import io.noties.markwon.PrecomputedTextSetterCompat; | ||||
| import io.noties.markwon.sample.R; | ||||
| 
 | ||||
| public class PrecomputedActivity extends Activity { | ||||
| 
 | ||||
|     @Override | ||||
|     public void onCreate(@Nullable Bundle savedInstanceState) { | ||||
|         super.onCreate(savedInstanceState); | ||||
|         setContentView(R.layout.activity_text_view); | ||||
| 
 | ||||
|         final Markwon markwon = Markwon.builder(this) | ||||
|                 // please note that precomputedTextCompat is no-op on devices lower than L (21) | ||||
|                 .textSetter(PrecomputedTextSetterCompat.create(Executors.newCachedThreadPool())) | ||||
|                 .build(); | ||||
| 
 | ||||
|         final TextView textView = findViewById(R.id.text_view); | ||||
|         final String markdown = "# Hello!\n\n" + | ||||
|                 "This _displays_ how to implement and use `PrecomputedTextCompat` with the **Markwon**\n\n" + | ||||
|                 "> consider using PrecomputedText only if your markdown content is large enough\n> \n" + | ||||
|                 "> **please note** that it works badly with `markwon-recycler` due to asynchronous nature"; | ||||
| 
 | ||||
|         // please note that _sometimes_ (if done without `post` here) further `textView.post` | ||||
|         // (that is used in PrecomputedTextSetterCompat to deliver result to main-thread) won't be called | ||||
|         // making the result of pre-computation absent and text-view clear (no text) | ||||
|         textView.post(() -> markwon.setMarkdown(textView, markdown)); | ||||
|     } | ||||
| } | ||||
| @ -23,4 +23,6 @@ | ||||
|     <string name="sample_custom_extension_2"># \# Custom extension 2\n\nAutomatically | ||||
|         convert `#1` and `@user` to Github links</string> | ||||
| 
 | ||||
|     <string name="sample_precomputed_text"># \# PrecomputedText\n\nUsage of TextSetter and PrecomputedTextCompat</string> | ||||
| 
 | ||||
| </resources> | ||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user
	 Dimitry
						Dimitry