Stabilizing editor API
This commit is contained in:
		
							parent
							
								
									a6201b1b35
								
							
						
					
					
						commit
						c6fd779f33
					
				| @ -0,0 +1,18 @@ | ||||
| package io.noties.markwon.editor; | ||||
| 
 | ||||
| import androidx.annotation.NonNull; | ||||
| 
 | ||||
| import io.noties.markwon.Markwon; | ||||
| 
 | ||||
| /** | ||||
|  * @see EditHandler | ||||
|  * @see io.noties.markwon.editor.handler.EmphasisEditHandler | ||||
|  * @see io.noties.markwon.editor.handler.StrongEmphasisEditHandler | ||||
|  * @since 4.2.0-SNAPSHOT | ||||
|  */ | ||||
| public abstract class AbstractEditHandler<T> implements EditHandler<T> { | ||||
|     @Override | ||||
|     public void init(@NonNull Markwon markwon) { | ||||
| 
 | ||||
|     } | ||||
| } | ||||
| @ -0,0 +1,47 @@ | ||||
| package io.noties.markwon.editor; | ||||
| 
 | ||||
| import android.text.Editable; | ||||
| 
 | ||||
| import androidx.annotation.NonNull; | ||||
| 
 | ||||
| import io.noties.markwon.Markwon; | ||||
| import io.noties.markwon.editor.handler.EmphasisEditHandler; | ||||
| import io.noties.markwon.editor.handler.StrongEmphasisEditHandler; | ||||
| 
 | ||||
| /** | ||||
|  * @see EmphasisEditHandler | ||||
|  * @see StrongEmphasisEditHandler | ||||
|  * @since 4.2.0-SNAPSHOT | ||||
|  */ | ||||
| public interface EditHandler<T> { | ||||
| 
 | ||||
|     void init(@NonNull Markwon markwon); | ||||
| 
 | ||||
|     void configurePersistedSpans(@NonNull PersistedSpans.Builder builder); | ||||
| 
 | ||||
|     // span is present only in off-screen rendered markdown, it must be processed and | ||||
|     //  a NEW one must be added to editable (via edit-persist-spans) | ||||
|     // | ||||
|     // NB, editable.setSpan must obtain span from `spans` and must be configured beforehand | ||||
|     // multiple spans are OK as long as they are configured | ||||
| 
 | ||||
|     /** | ||||
|      * @param persistedSpans | ||||
|      * @param editable | ||||
|      * @param input | ||||
|      * @param span | ||||
|      * @param spanStart | ||||
|      * @param spanTextLength | ||||
|      * @see MarkwonEditorUtils | ||||
|      */ | ||||
|     void handleMarkdownSpan( | ||||
|             @NonNull PersistedSpans persistedSpans, | ||||
|             @NonNull Editable editable, | ||||
|             @NonNull String input, | ||||
|             @NonNull T span, | ||||
|             int spanStart, | ||||
|             int spanTextLength); | ||||
| 
 | ||||
|     @NonNull | ||||
|     Class<T> markdownSpanType(); | ||||
| } | ||||
| @ -1,72 +0,0 @@ | ||||
| package io.noties.markwon.editor; | ||||
| 
 | ||||
| import android.text.Editable; | ||||
| 
 | ||||
| import androidx.annotation.NonNull; | ||||
| import androidx.annotation.Nullable; | ||||
| 
 | ||||
| import java.util.HashMap; | ||||
| import java.util.Map; | ||||
| 
 | ||||
| /** | ||||
|  * @since 4.2.0-SNAPSHOT | ||||
|  */ | ||||
| public class EditSpanHandlerBuilder { | ||||
| 
 | ||||
|     public interface EditSpanHandlerTyped<T> { | ||||
|         void handle( | ||||
|                 @NonNull MarkwonEditor.EditSpanStore store, | ||||
|                 @NonNull Editable editable, | ||||
|                 @NonNull String input, | ||||
|                 @NonNull T span, | ||||
|                 int spanStart, | ||||
|                 int spanTextLength); | ||||
|     } | ||||
| 
 | ||||
|     @NonNull | ||||
|     public static EditSpanHandlerBuilder create() { | ||||
|         return new EditSpanHandlerBuilder(); | ||||
|     } | ||||
| 
 | ||||
|     private final Map<Class<?>, EditSpanHandlerTyped> map = new HashMap<>(3); | ||||
| 
 | ||||
|     @NonNull | ||||
|     public <T> EditSpanHandlerBuilder handleMarkdownSpan( | ||||
|             @NonNull Class<T> type, | ||||
|             @NonNull EditSpanHandlerTyped<T> handler) { | ||||
|         map.put(type, handler); | ||||
|         return this; | ||||
|     } | ||||
| 
 | ||||
|     @Nullable | ||||
|     public MarkwonEditor.EditSpanHandler build() { | ||||
|         if (map.size() == 0) { | ||||
|             return null; | ||||
|         } | ||||
|         return new EditSpanHandlerImpl(map); | ||||
|     } | ||||
| 
 | ||||
|     private static class EditSpanHandlerImpl implements MarkwonEditor.EditSpanHandler { | ||||
| 
 | ||||
|         private final Map<Class<?>, EditSpanHandlerTyped> map; | ||||
| 
 | ||||
|         EditSpanHandlerImpl(@NonNull Map<Class<?>, EditSpanHandlerTyped> map) { | ||||
|             this.map = map; | ||||
|         } | ||||
| 
 | ||||
|         @Override | ||||
|         public void handle( | ||||
|                 @NonNull MarkwonEditor.EditSpanStore store, | ||||
|                 @NonNull Editable editable, | ||||
|                 @NonNull String input, | ||||
|                 @NonNull Object span, | ||||
|                 int spanStart, | ||||
|                 int spanTextLength) { | ||||
|             final EditSpanHandlerTyped handler = map.get(span.getClass()); | ||||
|             if (handler != null) { | ||||
|                 //noinspection unchecked | ||||
|                 handler.handle(store, editable, input, span, spanStart, spanTextLength); | ||||
|             } | ||||
|         } | ||||
|     } | ||||
| } | ||||
| @ -3,7 +3,6 @@ package io.noties.markwon.editor; | ||||
| import android.text.Editable; | ||||
| 
 | ||||
| import androidx.annotation.NonNull; | ||||
| import androidx.annotation.Nullable; | ||||
| 
 | ||||
| import java.util.HashMap; | ||||
| import java.util.Map; | ||||
| @ -20,44 +19,8 @@ import io.noties.markwon.Markwon; | ||||
| public abstract class MarkwonEditor { | ||||
| 
 | ||||
|     /** | ||||
|      * Represents cache of spans that are used during highlight | ||||
|      * @see #preRender(Editable, PreRenderResultListener) | ||||
|      */ | ||||
|     public interface EditSpanStore { | ||||
| 
 | ||||
|         /** | ||||
|          * If a span of specified type was not registered with {@link Builder#includeEditSpan(Class, EditSpanFactory)} | ||||
|          * then an exception is raised. | ||||
|          * | ||||
|          * @param type of a span to obtain | ||||
|          * @return cached or newly created span | ||||
|          */ | ||||
|         @NonNull | ||||
|         <T> T get(Class<T> type); | ||||
|     } | ||||
| 
 | ||||
|     public interface EditSpanFactory<T> { | ||||
|         @NonNull | ||||
|         T create(); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Interface to handle _original_ span that is present in rendered markdown. Can be useful | ||||
|      * to add specific spans for EditText (for example, make text bold to better indicate | ||||
|      * strong emphasis used in markdown input). | ||||
|      * | ||||
|      * @see Builder#withEditSpanHandler(EditSpanHandler) | ||||
|      * @see EditSpanHandlerBuilder | ||||
|      */ | ||||
|     public interface EditSpanHandler { | ||||
|         void handle( | ||||
|                 @NonNull EditSpanStore store, | ||||
|                 @NonNull Editable editable, | ||||
|                 @NonNull String input, | ||||
|                 @NonNull Object span, | ||||
|                 int spanStart, | ||||
|                 int spanTextLength); | ||||
|     } | ||||
| 
 | ||||
|     public interface PreRenderResult { | ||||
| 
 | ||||
|         /** | ||||
| @ -116,7 +79,7 @@ public abstract class MarkwonEditor { | ||||
|      * thread. | ||||
|      * <p> | ||||
|      * Please note that currently only `setSpan` and `removeSpan` actions will be recorded (and thus dispatched). | ||||
|      * Make sure you use only these methods in your {@link EditSpanHandler}, or implement the required | ||||
|      * Make sure you use only these methods in your {@link EditHandler}, or implement the required | ||||
|      * functionality some other way. | ||||
|      * | ||||
|      * @param editable          to process and pre-render | ||||
| @ -129,15 +92,22 @@ public abstract class MarkwonEditor { | ||||
|     public static class Builder { | ||||
| 
 | ||||
|         private final Markwon markwon; | ||||
|         private final PersistedSpans.Provider persistedSpansProvider = PersistedSpans.provider(); | ||||
|         private final Map<Class<?>, EditHandler> editHandlers = new HashMap<>(0); | ||||
| 
 | ||||
|         private Class<?> punctuationSpanType; | ||||
|         private Map<Class<?>, EditSpanFactory> spans = new HashMap<>(3); | ||||
|         private EditSpanHandler editSpanHandler; | ||||
| 
 | ||||
|         Builder(@NonNull Markwon markwon) { | ||||
|             this.markwon = markwon; | ||||
|         } | ||||
| 
 | ||||
|         @NonNull | ||||
|         public <T> Builder useEditHandler(@NonNull EditHandler<T> handler) { | ||||
|             this.editHandlers.put(handler.markdownSpanType(), handler); | ||||
|             return this; | ||||
|         } | ||||
| 
 | ||||
| 
 | ||||
|         /** | ||||
|          * Specify which punctuation span will be used. | ||||
|          * | ||||
| @ -145,43 +115,9 @@ public abstract class MarkwonEditor { | ||||
|          * @param factory to create a new instance of the span | ||||
|          */ | ||||
|         @NonNull | ||||
|         public <T> Builder withPunctuationSpan(@NonNull Class<T> type, @NonNull EditSpanFactory<T> factory) { | ||||
|         public <T> Builder punctuationSpan(@NonNull Class<T> type, @NonNull PersistedSpans.SpanFactory<T> factory) { | ||||
|             this.punctuationSpanType = type; | ||||
|             this.spans.put(type, factory); | ||||
|             return this; | ||||
|         } | ||||
| 
 | ||||
|         /** | ||||
|          * Include additional span handling that is used in highlighting. It is important to understand | ||||
|          * that it is not the span that is used by Markwon, but instead your own span that you | ||||
|          * apply in a custom {@link EditSpanHandler} specified by {@link #withEditSpanHandler(EditSpanHandler)}. | ||||
|          * You can apply a Markwon bundled span (or any other) but it must be still explicitly | ||||
|          * included by this method. | ||||
|          * <p> | ||||
|          * The span will be exposed via {@link EditSpanStore} in your custom {@link EditSpanHandler}. | ||||
|          * If you do not use a custom {@link EditSpanHandler} you do not need to specify any span here. | ||||
|          * | ||||
|          * @param type    of a span to include | ||||
|          * @param factory to create a new instance of a span if one is missing from processed Editable | ||||
|          */ | ||||
|         @NonNull | ||||
|         public <T> Builder includeEditSpan( | ||||
|                 @NonNull Class<T> type, | ||||
|                 @NonNull EditSpanFactory<T> factory) { | ||||
|             this.spans.put(type, factory); | ||||
|             return this; | ||||
|         } | ||||
| 
 | ||||
|         /** | ||||
|          * Additional handling of markdown spans. | ||||
|          * | ||||
|          * @param editSpanHandler handler for additional highlight spans | ||||
|          * @see EditSpanHandler | ||||
|          * @see EditSpanHandlerBuilder | ||||
|          */ | ||||
|         @NonNull | ||||
|         public Builder withEditSpanHandler(@Nullable EditSpanHandler editSpanHandler) { | ||||
|             this.editSpanHandler = editSpanHandler; | ||||
|             this.persistedSpansProvider.persistSpan(type, factory); | ||||
|             return this; | ||||
|         } | ||||
| 
 | ||||
| @ -190,7 +126,7 @@ public abstract class MarkwonEditor { | ||||
| 
 | ||||
|             Class<?> punctuationSpanType = this.punctuationSpanType; | ||||
|             if (punctuationSpanType == null) { | ||||
|                 withPunctuationSpan(PunctuationSpan.class, new EditSpanFactory<PunctuationSpan>() { | ||||
|                 punctuationSpan(PunctuationSpan.class, new PersistedSpans.SpanFactory<PunctuationSpan>() { | ||||
|                     @NonNull | ||||
|                     @Override | ||||
|                     public PunctuationSpan create() { | ||||
| @ -200,17 +136,54 @@ public abstract class MarkwonEditor { | ||||
|                 punctuationSpanType = this.punctuationSpanType; | ||||
|             } | ||||
| 
 | ||||
|             // if we have no editSpanHandler, but spans are registered -> throw an error | ||||
|             if (spans.size() > 1 && editSpanHandler == null) { | ||||
|                 throw new IllegalStateException("There is no need to include edit spans " + | ||||
|                         "when you do not use custom EditSpanHandler"); | ||||
|             for (EditHandler handler : editHandlers.values()) { | ||||
|                 handler.init(markwon); | ||||
|                 handler.configurePersistedSpans(persistedSpansProvider); | ||||
|             } | ||||
| 
 | ||||
|             final SpansHandler spansHandler = editHandlers.size() == 0 | ||||
|                     ? null | ||||
|                     : new SpansHandlerImpl(editHandlers); | ||||
| 
 | ||||
|             return new MarkwonEditorImpl( | ||||
|                     markwon, | ||||
|                     spans, | ||||
|                     persistedSpansProvider, | ||||
|                     punctuationSpanType, | ||||
|                     editSpanHandler); | ||||
|                     spansHandler); | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     interface SpansHandler { | ||||
|         void handle( | ||||
|                 @NonNull PersistedSpans spans, | ||||
|                 @NonNull Editable editable, | ||||
|                 @NonNull String input, | ||||
|                 @NonNull Object span, | ||||
|                 int spanStart, | ||||
|                 int spanTextLength); | ||||
|     } | ||||
| 
 | ||||
|     static class SpansHandlerImpl implements SpansHandler { | ||||
| 
 | ||||
|         private final Map<Class<?>, EditHandler> spanHandlers; | ||||
| 
 | ||||
|         SpansHandlerImpl(@NonNull Map<Class<?>, EditHandler> spanHandlers) { | ||||
|             this.spanHandlers = spanHandlers; | ||||
|         } | ||||
| 
 | ||||
|         @Override | ||||
|         public void handle( | ||||
|                 @NonNull PersistedSpans spans, | ||||
|                 @NonNull Editable editable, | ||||
|                 @NonNull String input, | ||||
|                 @NonNull Object span, | ||||
|                 int spanStart, | ||||
|                 int spanTextLength) { | ||||
|             final EditHandler handler = spanHandlers.get(span.getClass()); | ||||
|             if (handler != null) { | ||||
|                 //noinspection unchecked | ||||
|                 handler.handleMarkdownSpan(spans, editable, input, span, spanStart, spanTextLength); | ||||
|             } | ||||
|         } | ||||
|     } | ||||
| } | ||||
|  | ||||
| @ -9,10 +9,7 @@ import androidx.annotation.NonNull; | ||||
| import androidx.annotation.Nullable; | ||||
| 
 | ||||
| import java.util.ArrayList; | ||||
| import java.util.Collection; | ||||
| import java.util.HashMap; | ||||
| import java.util.List; | ||||
| import java.util.Map; | ||||
| 
 | ||||
| import io.noties.markwon.Markwon; | ||||
| import io.noties.markwon.editor.diff_match_patch.Diff; | ||||
| @ -20,21 +17,21 @@ import io.noties.markwon.editor.diff_match_patch.Diff; | ||||
| class MarkwonEditorImpl extends MarkwonEditor { | ||||
| 
 | ||||
|     private final Markwon markwon; | ||||
|     private final Map<Class<?>, EditSpanFactory> spans; | ||||
|     private final PersistedSpans.Provider persistedSpansProvider; | ||||
|     private final Class<?> punctuationSpanType; | ||||
| 
 | ||||
|     @Nullable | ||||
|     private final EditSpanHandler editSpanHandler; | ||||
|     private final SpansHandler spansHandler; | ||||
| 
 | ||||
|     MarkwonEditorImpl( | ||||
|             @NonNull Markwon markwon, | ||||
|             @NonNull Map<Class<?>, EditSpanFactory> spans, | ||||
|             @NonNull PersistedSpans.Provider persistedSpansProvider, | ||||
|             @NonNull Class<?> punctuationSpanType, | ||||
|             @Nullable EditSpanHandler editSpanHandler) { | ||||
|             @Nullable SpansHandler spansHandler) { | ||||
|         this.markwon = markwon; | ||||
|         this.spans = spans; | ||||
|         this.persistedSpansProvider = persistedSpansProvider; | ||||
|         this.punctuationSpanType = punctuationSpanType; | ||||
|         this.editSpanHandler = editSpanHandler; | ||||
|         this.spansHandler = spansHandler; | ||||
|     } | ||||
| 
 | ||||
|     @Override | ||||
| @ -49,10 +46,10 @@ class MarkwonEditorImpl extends MarkwonEditor { | ||||
| 
 | ||||
|         final String markdown = renderedMarkdown.toString(); | ||||
| 
 | ||||
|         final EditSpanHandler editSpanHandler = this.editSpanHandler; | ||||
|         final boolean hasAdditionalSpans = editSpanHandler != null; | ||||
|         final SpansHandler spansHandler = this.spansHandler; | ||||
|         final boolean hasAdditionalSpans = spansHandler != null; | ||||
| 
 | ||||
|         final EditSpanStoreImpl store = new EditSpanStoreImpl(editable, spans); | ||||
|         final PersistedSpans persistedSpans = persistedSpansProvider.provide(editable); | ||||
|         try { | ||||
| 
 | ||||
|             final List<Diff> diffs = diff_match_patch.diff_main(input, markdown); | ||||
| @ -68,8 +65,9 @@ class MarkwonEditorImpl extends MarkwonEditor { | ||||
| 
 | ||||
|                         final int start = inputLength; | ||||
|                         inputLength += diff.text.length(); | ||||
| 
 | ||||
|                         editable.setSpan( | ||||
|                                 store.get(punctuationSpanType), | ||||
|                                 persistedSpans.get(punctuationSpanType), | ||||
|                                 start, | ||||
|                                 inputLength, | ||||
|                                 Spanned.SPAN_EXCLUSIVE_EXCLUSIVE | ||||
| @ -84,8 +82,8 @@ class MarkwonEditorImpl extends MarkwonEditor { | ||||
|                             for (Object span : spans) { | ||||
|                                 if (markdownLength == renderedMarkdown.getSpanStart(span)) { | ||||
| 
 | ||||
|                                     editSpanHandler.handle( | ||||
|                                             store, | ||||
|                                     spansHandler.handle( | ||||
|                                             persistedSpans, | ||||
|                                             editable, | ||||
|                                             input, | ||||
|                                             span, | ||||
| @ -126,8 +124,8 @@ class MarkwonEditorImpl extends MarkwonEditor { | ||||
|                                     final int end = renderedMarkdown.getSpanEnd(span); | ||||
|                                     if (end <= markdownLength) { | ||||
| 
 | ||||
|                                         editSpanHandler.handle( | ||||
|                                                 store, | ||||
|                                         spansHandler.handle( | ||||
|                                                 persistedSpans, | ||||
|                                                 editable, | ||||
|                                                 input, | ||||
|                                                 span, | ||||
| @ -149,7 +147,7 @@ class MarkwonEditorImpl extends MarkwonEditor { | ||||
|             } | ||||
| 
 | ||||
|         } finally { | ||||
|             store.removeUnused(); | ||||
|             persistedSpans.removeUnused(); | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
| @ -177,75 +175,6 @@ class MarkwonEditorImpl extends MarkwonEditor { | ||||
|         }); | ||||
|     } | ||||
| 
 | ||||
|     @NonNull | ||||
|     static Map<Class<?>, List<Object>> extractSpans(@NonNull Spanned spanned, @NonNull Collection<Class<?>> types) { | ||||
| 
 | ||||
|         final Object[] spans = spanned.getSpans(0, spanned.length(), Object.class); | ||||
|         final Map<Class<?>, List<Object>> map = new HashMap<>(3); | ||||
| 
 | ||||
|         Class<?> type; | ||||
| 
 | ||||
|         for (Object span : spans) { | ||||
|             type = span.getClass(); | ||||
|             if (types.contains(type)) { | ||||
|                 List<Object> list = map.get(type); | ||||
|                 if (list == null) { | ||||
|                     list = new ArrayList<>(3); | ||||
|                     map.put(type, list); | ||||
|                 } | ||||
|                 list.add(span); | ||||
|             } | ||||
|         } | ||||
| 
 | ||||
|         return map; | ||||
|     } | ||||
| 
 | ||||
|     static class EditSpanStoreImpl implements EditSpanStore { | ||||
| 
 | ||||
|         private final Spannable spannable; | ||||
|         private final Map<Class<?>, EditSpanFactory> spans; | ||||
|         private final Map<Class<?>, List<Object>> map; | ||||
| 
 | ||||
|         EditSpanStoreImpl(@NonNull Spannable spannable, @NonNull Map<Class<?>, EditSpanFactory> spans) { | ||||
|             this.spannable = spannable; | ||||
|             this.spans = spans; | ||||
|             this.map = extractSpans(spannable, spans.keySet()); | ||||
|         } | ||||
| 
 | ||||
|         @NonNull | ||||
|         @Override | ||||
|         public <T> T get(Class<T> type) { | ||||
| 
 | ||||
|             final Object span; | ||||
| 
 | ||||
|             final List<Object> list = map.get(type); | ||||
|             if (list != null && list.size() > 0) { | ||||
|                 span = list.remove(0); | ||||
|             } else { | ||||
|                 final EditSpanFactory spanFactory = spans.get(type); | ||||
|                 if (spanFactory == null) { | ||||
|                     throw new IllegalStateException("Requested type `" + type.getName() + "` was " + | ||||
|                             "not registered, use Builder#includeEditSpan method to register"); | ||||
|                 } | ||||
|                 span = spanFactory.create(); | ||||
|             } | ||||
| 
 | ||||
|             //noinspection unchecked | ||||
|             return (T) span; | ||||
|         } | ||||
| 
 | ||||
|         void removeUnused() { | ||||
|             for (List<Object> spans : map.values()) { | ||||
|                 if (spans != null | ||||
|                         && spans.size() > 0) { | ||||
|                     for (Object span : spans) { | ||||
|                         spannable.removeSpan(span); | ||||
|                     } | ||||
|                 } | ||||
|             } | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     private static class Span { | ||||
|         final Object what; | ||||
|         final int start; | ||||
|  | ||||
| @ -129,14 +129,18 @@ public abstract class MarkwonEditorTextWatcher implements TextWatcher { | ||||
|             future = executorService.submit(new Runnable() { | ||||
|                 @Override | ||||
|                 public void run() { | ||||
|                     try { | ||||
|                         editor.preRender(s, new MarkwonEditor.PreRenderResultListener() { | ||||
|                             @Override | ||||
|                             public void onPreRenderResult(@NonNull final MarkwonEditor.PreRenderResult result) { | ||||
|                             if (editText != null) { | ||||
|                                 editText.post(new Runnable() { | ||||
|                                 final EditText et = editText; | ||||
|                                 if (et != null) { | ||||
|                                     et.post(new Runnable() { | ||||
|                                         @Override | ||||
|                                         public void run() { | ||||
|                                             if (key == generator) { | ||||
|                                                 final EditText et = editText; | ||||
|                                                 if (et != null) { | ||||
|                                                     selfChange = true; | ||||
|                                                     try { | ||||
|                                                         result.dispatchTo(editText.getText()); | ||||
| @ -145,10 +149,23 @@ public abstract class MarkwonEditorTextWatcher implements TextWatcher { | ||||
|                                                     } | ||||
|                                                 } | ||||
|                                             } | ||||
|                                         } | ||||
|                                     }); | ||||
|                                 } | ||||
|                             } | ||||
|                         }); | ||||
|                     } catch (final Throwable t) { | ||||
|                         final EditText et = editText; | ||||
|                         if (et != null) { | ||||
|                             // propagate exception to main thread | ||||
|                             et.post(new Runnable() { | ||||
|                                 @Override | ||||
|                                 public void run() { | ||||
|                                     throw new RuntimeException(t); | ||||
|                                 } | ||||
|                             }); | ||||
|                         } | ||||
|                     } | ||||
|                 } | ||||
|             }); | ||||
|         } | ||||
|  | ||||
| @ -1,13 +1,44 @@ | ||||
| package io.noties.markwon.editor; | ||||
| 
 | ||||
| import android.text.Spanned; | ||||
| 
 | ||||
| import androidx.annotation.NonNull; | ||||
| import androidx.annotation.Nullable; | ||||
| 
 | ||||
| import java.util.ArrayList; | ||||
| import java.util.Collection; | ||||
| import java.util.HashMap; | ||||
| import java.util.List; | ||||
| import java.util.Map; | ||||
| 
 | ||||
| /** | ||||
|  * @since 4.2.0-SNAPSHOT | ||||
|  */ | ||||
| public abstract class MarkwonEditorUtils { | ||||
| 
 | ||||
|     @NonNull | ||||
|     public static Map<Class<?>, List<Object>> extractSpans(@NonNull Spanned spanned, @NonNull Collection<Class<?>> types) { | ||||
| 
 | ||||
|         final Object[] spans = spanned.getSpans(0, spanned.length(), Object.class); | ||||
|         final Map<Class<?>, List<Object>> map = new HashMap<>(3); | ||||
| 
 | ||||
|         Class<?> type; | ||||
| 
 | ||||
|         for (Object span : spans) { | ||||
|             type = span.getClass(); | ||||
|             if (types.contains(type)) { | ||||
|                 List<Object> list = map.get(type); | ||||
|                 if (list == null) { | ||||
|                     list = new ArrayList<>(3); | ||||
|                     map.put(type, list); | ||||
|                 } | ||||
|                 list.add(span); | ||||
|             } | ||||
|         } | ||||
| 
 | ||||
|         return map; | ||||
|     } | ||||
| 
 | ||||
|     public interface Match { | ||||
| 
 | ||||
|         @NonNull | ||||
|  | ||||
| @ -0,0 +1,116 @@ | ||||
| package io.noties.markwon.editor; | ||||
| 
 | ||||
| import android.text.Editable; | ||||
| import android.text.Spannable; | ||||
| import android.util.Log; | ||||
| 
 | ||||
| import androidx.annotation.NonNull; | ||||
| 
 | ||||
| import java.util.HashMap; | ||||
| import java.util.List; | ||||
| import java.util.Locale; | ||||
| import java.util.Map; | ||||
| 
 | ||||
| import static io.noties.markwon.editor.MarkwonEditorUtils.extractSpans; | ||||
| 
 | ||||
| /** | ||||
|  * Cache for spans that present in user input. These spans are reused between different | ||||
|  * {@link MarkwonEditor#process(Editable)} and {@link MarkwonEditor#preRender(Editable, MarkwonEditor.PreRenderResultListener)} | ||||
|  * calls. | ||||
|  * | ||||
|  * @see EditHandler#handleMarkdownSpan(PersistedSpans, Editable, String, Object, int, int) | ||||
|  * @see EditHandler#configurePersistedSpans(Builder) | ||||
|  * @since 4.2.0-SNAPSHOT | ||||
|  */ | ||||
| public abstract class PersistedSpans { | ||||
| 
 | ||||
|     public interface SpanFactory<T> { | ||||
|         @NonNull | ||||
|         T create(); | ||||
|     } | ||||
| 
 | ||||
|     public interface Builder { | ||||
|         @SuppressWarnings("UnusedReturnValue") | ||||
|         @NonNull | ||||
|         <T> Builder persistSpan(@NonNull Class<T> type, @NonNull SpanFactory<T> spanFactory); | ||||
|     } | ||||
| 
 | ||||
|     @NonNull | ||||
|     public abstract <T> T get(@NonNull Class<T> type); | ||||
| 
 | ||||
|     abstract void removeUnused(); | ||||
| 
 | ||||
| 
 | ||||
|     @NonNull | ||||
|     static Provider provider() { | ||||
|         return new Provider(); | ||||
|     } | ||||
| 
 | ||||
|     static class Provider implements Builder { | ||||
| 
 | ||||
|         private final Map<Class<?>, SpanFactory> map = new HashMap<>(3); | ||||
| 
 | ||||
|         @NonNull | ||||
|         @Override | ||||
|         public <T> Builder persistSpan(@NonNull Class<T> type, @NonNull SpanFactory<T> spanFactory) { | ||||
|             if (map.put(type, spanFactory) != null) { | ||||
|                 Log.e("MD-EDITOR", String.format( | ||||
|                         Locale.ROOT, | ||||
|                         "Re-declaration of persisted span for '%s'", type.getName())); | ||||
|             } | ||||
|             return this; | ||||
|         } | ||||
| 
 | ||||
|         @NonNull | ||||
|         PersistedSpans provide(@NonNull Spannable spannable) { | ||||
|             return new Impl(spannable, map); | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     static class Impl extends PersistedSpans { | ||||
| 
 | ||||
|         private final Spannable spannable; | ||||
|         private final Map<Class<?>, SpanFactory> spans; | ||||
|         private final Map<Class<?>, List<Object>> map; | ||||
| 
 | ||||
|         Impl(@NonNull Spannable spannable, @NonNull Map<Class<?>, SpanFactory> spans) { | ||||
|             this.spannable = spannable; | ||||
|             this.spans = spans; | ||||
|             this.map = extractSpans(spannable, spans.keySet()); | ||||
|         } | ||||
| 
 | ||||
|         @NonNull | ||||
|         @Override | ||||
|         public <T> T get(@NonNull Class<T> type) { | ||||
| 
 | ||||
|             final Object span; | ||||
| 
 | ||||
|             final List<Object> list = map.get(type); | ||||
|             if (list != null && list.size() > 0) { | ||||
|                 span = list.remove(0); | ||||
|             } else { | ||||
|                 final SpanFactory spanFactory = spans.get(type); | ||||
|                 if (spanFactory == null) { | ||||
|                     throw new IllegalStateException("Requested type `" + type.getName() + "` was " + | ||||
|                             "not registered, use PersistedSpans.Builder#persistSpan method to register"); | ||||
|                 } | ||||
|                 span = spanFactory.create(); | ||||
|             } | ||||
| 
 | ||||
|             //noinspection unchecked | ||||
|             return (T) span; | ||||
|         } | ||||
| 
 | ||||
|         @Override | ||||
|         void removeUnused() { | ||||
|             for (List<Object> spans : map.values()) { | ||||
|                 if (spans != null | ||||
|                         && spans.size() > 0) { | ||||
|                     for (Object span : spans) { | ||||
|                         spannable.removeSpan(span); | ||||
|                     } | ||||
|                 } | ||||
|             } | ||||
|         } | ||||
|     } | ||||
| } | ||||
| @ -0,0 +1,54 @@ | ||||
| package io.noties.markwon.editor.handler; | ||||
| 
 | ||||
| import android.text.Editable; | ||||
| import android.text.Spanned; | ||||
| 
 | ||||
| import androidx.annotation.NonNull; | ||||
| 
 | ||||
| import io.noties.markwon.core.spans.EmphasisSpan; | ||||
| import io.noties.markwon.editor.AbstractEditHandler; | ||||
| import io.noties.markwon.editor.MarkwonEditorUtils; | ||||
| import io.noties.markwon.editor.PersistedSpans; | ||||
| 
 | ||||
| /** | ||||
|  * @since 4.2.0-SNAPSHOT | ||||
|  */ | ||||
| public class EmphasisEditHandler extends AbstractEditHandler<EmphasisSpan> { | ||||
| 
 | ||||
|     @Override | ||||
|     public void configurePersistedSpans(@NonNull PersistedSpans.Builder builder) { | ||||
|         builder.persistSpan(EmphasisSpan.class, new PersistedSpans.SpanFactory<EmphasisSpan>() { | ||||
|             @NonNull | ||||
|             @Override | ||||
|             public EmphasisSpan create() { | ||||
|                 return new EmphasisSpan(); | ||||
|             } | ||||
|         }); | ||||
|     } | ||||
| 
 | ||||
|     @Override | ||||
|     public void handleMarkdownSpan( | ||||
|             @NonNull PersistedSpans persistedSpans, | ||||
|             @NonNull Editable editable, | ||||
|             @NonNull String input, | ||||
|             @NonNull EmphasisSpan span, | ||||
|             int spanStart, | ||||
|             int spanTextLength) { | ||||
|         final MarkwonEditorUtils.Match match = | ||||
|                 MarkwonEditorUtils.findDelimited(input, spanStart, "*", "_"); | ||||
|         if (match != null) { | ||||
|             editable.setSpan( | ||||
|                     persistedSpans.get(EmphasisSpan.class), | ||||
|                     match.start(), | ||||
|                     match.end(), | ||||
|                     Spanned.SPAN_EXCLUSIVE_EXCLUSIVE | ||||
|             ); | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     @NonNull | ||||
|     @Override | ||||
|     public Class<EmphasisSpan> markdownSpanType() { | ||||
|         return EmphasisSpan.class; | ||||
|     } | ||||
| } | ||||
| @ -0,0 +1,62 @@ | ||||
| package io.noties.markwon.editor.handler; | ||||
| 
 | ||||
| import android.text.Editable; | ||||
| import android.text.Spanned; | ||||
| 
 | ||||
| import androidx.annotation.NonNull; | ||||
| 
 | ||||
| import io.noties.markwon.core.spans.StrongEmphasisSpan; | ||||
| import io.noties.markwon.editor.AbstractEditHandler; | ||||
| import io.noties.markwon.editor.MarkwonEditorUtils; | ||||
| import io.noties.markwon.editor.PersistedSpans; | ||||
| 
 | ||||
| /** | ||||
|  * @since 4.2.0-SNAPSHOT | ||||
|  */ | ||||
| public class StrongEmphasisEditHandler extends AbstractEditHandler<StrongEmphasisSpan> { | ||||
| 
 | ||||
|     @NonNull | ||||
|     public static StrongEmphasisEditHandler create() { | ||||
|         return new StrongEmphasisEditHandler(); | ||||
|     } | ||||
| 
 | ||||
|     @Override | ||||
|     public void configurePersistedSpans(@NonNull PersistedSpans.Builder builder) { | ||||
|         builder.persistSpan(StrongEmphasisSpan.class, new PersistedSpans.SpanFactory<StrongEmphasisSpan>() { | ||||
|             @NonNull | ||||
|             @Override | ||||
|             public StrongEmphasisSpan create() { | ||||
|                 return new StrongEmphasisSpan(); | ||||
|             } | ||||
|         }); | ||||
|     } | ||||
| 
 | ||||
|     @Override | ||||
|     public void handleMarkdownSpan( | ||||
|             @NonNull PersistedSpans persistedSpans, | ||||
|             @NonNull Editable editable, | ||||
|             @NonNull String input, | ||||
|             @NonNull StrongEmphasisSpan span, | ||||
|             int spanStart, | ||||
|             int spanTextLength) { | ||||
|         // inline spans can delimit other inline spans, | ||||
|         //  for example: `**_~~hey~~_**`, this is why we must additionally find delimiter used | ||||
|         //  and its actual start/end positions | ||||
|         final MarkwonEditorUtils.Match match = | ||||
|                 MarkwonEditorUtils.findDelimited(input, spanStart, "**", "__"); | ||||
|         if (match != null) { | ||||
|             editable.setSpan( | ||||
|                     persistedSpans.get(StrongEmphasisSpan.class), | ||||
|                     match.start(), | ||||
|                     match.end(), | ||||
|                     Spanned.SPAN_EXCLUSIVE_EXCLUSIVE | ||||
|             ); | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     @NonNull | ||||
|     @Override | ||||
|     public Class<StrongEmphasisSpan> markdownSpanType() { | ||||
|         return StrongEmphasisSpan.class; | ||||
|     } | ||||
| } | ||||
| @ -1,9 +1,6 @@ | ||||
| package io.noties.markwon.editor; | ||||
| 
 | ||||
| import android.text.SpannableStringBuilder; | ||||
| import android.text.Spanned; | ||||
| 
 | ||||
| import androidx.annotation.NonNull; | ||||
| 
 | ||||
| import org.junit.Test; | ||||
| import org.junit.runner.RunWith; | ||||
| @ -11,135 +8,14 @@ import org.robolectric.RobolectricTestRunner; | ||||
| import org.robolectric.RuntimeEnvironment; | ||||
| import org.robolectric.annotation.Config; | ||||
| 
 | ||||
| import java.util.Arrays; | ||||
| import java.util.Collections; | ||||
| import java.util.HashMap; | ||||
| import java.util.List; | ||||
| import java.util.Map; | ||||
| 
 | ||||
| import io.noties.markwon.Markwon; | ||||
| import io.noties.markwon.editor.MarkwonEditorImpl.EditSpanStoreImpl; | ||||
| 
 | ||||
| import static org.junit.Assert.assertEquals; | ||||
| import static org.junit.Assert.assertNotNull; | ||||
| import static org.junit.Assert.assertNull; | ||||
| import static org.junit.Assert.assertTrue; | ||||
| import static org.junit.Assert.fail; | ||||
| import static org.mockito.Mockito.mock; | ||||
| import static org.mockito.Mockito.never; | ||||
| import static org.mockito.Mockito.times; | ||||
| import static org.mockito.Mockito.verify; | ||||
| 
 | ||||
| @RunWith(RobolectricTestRunner.class) | ||||
| @Config(manifest = Config.NONE) | ||||
| public class MarkwonEditorImplTest { | ||||
| 
 | ||||
|     @Test | ||||
|     public void extract_spans() { | ||||
| 
 | ||||
|         final class One { | ||||
|         } | ||||
|         final class Two { | ||||
|         } | ||||
|         final class Three { | ||||
|         } | ||||
| 
 | ||||
|         final SpannableStringBuilder builder = new SpannableStringBuilder(); | ||||
|         append(builder, "one", new One()); | ||||
|         append(builder, "two", new Two(), new Two()); | ||||
|         append(builder, "three", new Three(), new Three(), new Three()); | ||||
| 
 | ||||
|         final Map<Class<?>, List<Object>> map = MarkwonEditorImpl.extractSpans( | ||||
|                 builder, | ||||
|                 Arrays.asList(One.class, Three.class)); | ||||
| 
 | ||||
|         assertEquals(2, map.size()); | ||||
| 
 | ||||
|         assertNotNull(map.get(One.class)); | ||||
|         assertNull(map.get(Two.class)); | ||||
|         assertNotNull(map.get(Three.class)); | ||||
| 
 | ||||
|         //noinspection ConstantConditions | ||||
|         assertEquals(1, map.get(One.class).size()); | ||||
|         //noinspection ConstantConditions | ||||
|         assertEquals(3, map.get(Three.class).size()); | ||||
|     } | ||||
| 
 | ||||
|     private static void append(@NonNull SpannableStringBuilder builder, @NonNull String text, Object... spans) { | ||||
|         final int start = builder.length(); | ||||
|         builder.append(text); | ||||
|         final int end = builder.length(); | ||||
|         for (Object span : spans) { | ||||
|             builder.setSpan(span, start, end, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     @Test | ||||
|     public void edit_span_store_span_not_included() { | ||||
|         // When store is requesting a span that is not included -> exception is raised | ||||
| 
 | ||||
|         final Map<Class<?>, MarkwonEditor.EditSpanFactory> map = Collections.emptyMap(); | ||||
| 
 | ||||
|         final EditSpanStoreImpl impl = new EditSpanStoreImpl(new SpannableStringBuilder(), map); | ||||
| 
 | ||||
|         try { | ||||
|             impl.get(Object.class); | ||||
|             fail(); | ||||
|         } catch (IllegalStateException e) { | ||||
|             assertTrue(e.getMessage(), e.getMessage().contains("not registered, use Builder#includeEditSpan method to register")); | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     @Test | ||||
|     public void edit_span_store_reuse() { | ||||
|         // when a span is present in supplied spannable -> it will be used | ||||
| 
 | ||||
|         final class One { | ||||
|         } | ||||
|         final SpannableStringBuilder builder = new SpannableStringBuilder(); | ||||
|         final One one = new One(); | ||||
|         append(builder, "One", one); | ||||
| 
 | ||||
|         final Map<Class<?>, MarkwonEditor.EditSpanFactory> map = new HashMap<Class<?>, MarkwonEditor.EditSpanFactory>() {{ | ||||
|             // null in case it _will_ be used -> thus NPE | ||||
|             put(One.class, null); | ||||
|         }}; | ||||
| 
 | ||||
|         final EditSpanStoreImpl impl = new EditSpanStoreImpl(builder, map); | ||||
| 
 | ||||
|         assertEquals(one, impl.get(One.class)); | ||||
|     } | ||||
| 
 | ||||
|     @Test | ||||
|     public void edit_span_store_factory_create() { | ||||
|         // when span is not present in spannable -> new one will be created via factory | ||||
| 
 | ||||
|         final class Two { | ||||
|         } | ||||
| 
 | ||||
|         final SpannableStringBuilder builder = new SpannableStringBuilder(); | ||||
|         final Two two = new Two(); | ||||
|         append(builder, "two", two); | ||||
| 
 | ||||
|         final MarkwonEditor.EditSpanFactory factory = mock(MarkwonEditor.EditSpanFactory.class); | ||||
| 
 | ||||
|         final Map<Class<?>, MarkwonEditor.EditSpanFactory> map = new HashMap<Class<?>, MarkwonEditor.EditSpanFactory>() {{ | ||||
|             put(Two.class, factory); | ||||
|         }}; | ||||
| 
 | ||||
|         final EditSpanStoreImpl impl = new EditSpanStoreImpl(builder, map); | ||||
| 
 | ||||
|         // first one will be the same as we had created before, | ||||
|         // second one will be created via factory | ||||
| 
 | ||||
|         assertEquals(two, impl.get(Two.class)); | ||||
| 
 | ||||
|         verify(factory, never()).create(); | ||||
| 
 | ||||
|         impl.get(Two.class); | ||||
|         verify(factory, times(1)).create(); | ||||
|     } | ||||
| 
 | ||||
|     @Test | ||||
|     public void process() { | ||||
|         // create markwon | ||||
|  | ||||
| @ -27,19 +27,4 @@ public class MarkwonEditorTest { | ||||
|             fail(t.getMessage()); | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     @Test | ||||
|     public void builder_with_edit_spans_but_no_handler() { | ||||
|         // if edit spans are specified, but no edit span handler is present -> exception is thrown | ||||
| 
 | ||||
|         try { | ||||
|             //noinspection unchecked | ||||
|             new Builder(mock(Markwon.class)) | ||||
|                     .includeEditSpan(Object.class, mock(MarkwonEditor.EditSpanFactory.class)) | ||||
|                     .build(); | ||||
|             fail(); | ||||
|         } catch (IllegalStateException e) { | ||||
|             assertTrue(e.getMessage(), e.getMessage().contains("There is no need to include edit spans ")); | ||||
|         } | ||||
|     } | ||||
| } | ||||
| @ -16,8 +16,11 @@ import java.util.concurrent.ExecutorService; | ||||
| import io.noties.markwon.editor.MarkwonEditor.PreRenderResult; | ||||
| import io.noties.markwon.editor.MarkwonEditor.PreRenderResultListener; | ||||
| 
 | ||||
| import static org.junit.Assert.assertEquals; | ||||
| import static org.junit.Assert.fail; | ||||
| import static org.mockito.ArgumentMatchers.any; | ||||
| import static org.mockito.ArgumentMatchers.eq; | ||||
| import static org.mockito.Mockito.RETURNS_MOCKS; | ||||
| import static org.mockito.Mockito.doAnswer; | ||||
| import static org.mockito.Mockito.mock; | ||||
| import static org.mockito.Mockito.times; | ||||
| @ -88,7 +91,49 @@ public class MarkwonEditorTextWatcherTest { | ||||
| 
 | ||||
|         listener.onPreRenderResult(result); | ||||
| 
 | ||||
|         verify(result, times(1)).resultEditable(); | ||||
|         // if we would check for hashCode then this method would've been invoked | ||||
| //        verify(result, times(1)).resultEditable(); | ||||
|         verify(result, times(1)).dispatchTo(eq(editable)); | ||||
|     } | ||||
| 
 | ||||
|     @Test | ||||
|     public void pre_render_posts_exception_to_main_thread() { | ||||
| 
 | ||||
|         final RuntimeException e = new RuntimeException(); | ||||
| 
 | ||||
|         final MarkwonEditor editor = mock(MarkwonEditor.class); | ||||
|         final ExecutorService service = mock(ExecutorService.class); | ||||
|         final EditText editText = mock(EditText.class, RETURNS_MOCKS); | ||||
| 
 | ||||
|         doAnswer(new Answer() { | ||||
|             @Override | ||||
|             public Object answer(InvocationOnMock invocation) { | ||||
|                 throw e; | ||||
|             } | ||||
|         }).when(editor).preRender(any(Editable.class), any(PreRenderResultListener.class)); | ||||
| 
 | ||||
|         when(service.submit(any(Runnable.class))).thenAnswer(new Answer<Object>() { | ||||
|             @Override | ||||
|             public Object answer(InvocationOnMock invocation) { | ||||
|                 ((Runnable) invocation.getArgument(0)).run(); | ||||
|                 return null; | ||||
|             } | ||||
|         }); | ||||
| 
 | ||||
|         final ArgumentCaptor<Runnable> captor = ArgumentCaptor.forClass(Runnable.class); | ||||
| 
 | ||||
|         final MarkwonEditorTextWatcher textWatcher = | ||||
|                 MarkwonEditorTextWatcher.withPreRender(editor, service, editText); | ||||
| 
 | ||||
|         textWatcher.afterTextChanged(mock(Editable.class)); | ||||
| 
 | ||||
|         verify(editText, times(1)).post(captor.capture()); | ||||
| 
 | ||||
|         try { | ||||
|             captor.getValue().run(); | ||||
|             fail(); | ||||
|         } catch (Throwable t) { | ||||
|             assertEquals(e, t.getCause()); | ||||
|         } | ||||
|     } | ||||
| } | ||||
|  | ||||
| @ -1,5 +1,7 @@ | ||||
| package io.noties.markwon.editor; | ||||
| 
 | ||||
| import android.text.SpannableStringBuilder; | ||||
| 
 | ||||
| import androidx.annotation.NonNull; | ||||
| import androidx.annotation.Nullable; | ||||
| 
 | ||||
| @ -8,19 +10,55 @@ import org.junit.runner.RunWith; | ||||
| import org.robolectric.RobolectricTestRunner; | ||||
| import org.robolectric.annotation.Config; | ||||
| 
 | ||||
| import java.util.Arrays; | ||||
| import java.util.List; | ||||
| import java.util.Locale; | ||||
| import java.util.Map; | ||||
| 
 | ||||
| import io.noties.markwon.editor.MarkwonEditorUtils.Match; | ||||
| 
 | ||||
| import static io.noties.markwon.editor.MarkwonEditorUtils.findDelimited; | ||||
| import static io.noties.markwon.editor.SpannableUtils.append; | ||||
| import static java.lang.String.format; | ||||
| import static org.junit.Assert.assertEquals; | ||||
| import static org.junit.Assert.assertNotNull; | ||||
| import static org.junit.Assert.assertNull; | ||||
| 
 | ||||
| @RunWith(RobolectricTestRunner.class) | ||||
| @Config(manifest = Config.NONE) | ||||
| public class MarkwonEditorUtilsTest { | ||||
| 
 | ||||
|     @Test | ||||
|     public void extract_spans() { | ||||
| 
 | ||||
|         final class One { | ||||
|         } | ||||
|         final class Two { | ||||
|         } | ||||
|         final class Three { | ||||
|         } | ||||
| 
 | ||||
|         final SpannableStringBuilder builder = new SpannableStringBuilder(); | ||||
|         append(builder, "one", new One()); | ||||
|         append(builder, "two", new Two(), new Two()); | ||||
|         append(builder, "three", new Three(), new Three(), new Three()); | ||||
| 
 | ||||
|         final Map<Class<?>, List<Object>> map = MarkwonEditorUtils.extractSpans( | ||||
|                 builder, | ||||
|                 Arrays.asList(One.class, Three.class)); | ||||
| 
 | ||||
|         assertEquals(2, map.size()); | ||||
| 
 | ||||
|         assertNotNull(map.get(One.class)); | ||||
|         assertNull(map.get(Two.class)); | ||||
|         assertNotNull(map.get(Three.class)); | ||||
| 
 | ||||
|         //noinspection ConstantConditions | ||||
|         assertEquals(1, map.get(One.class).size()); | ||||
|         //noinspection ConstantConditions | ||||
|         assertEquals(3, map.get(Three.class).size()); | ||||
|     } | ||||
| 
 | ||||
|     @Test | ||||
|     public void delimited_single() { | ||||
|         final String input = "**bold**"; | ||||
|  | ||||
| @ -0,0 +1,96 @@ | ||||
| package io.noties.markwon.editor; | ||||
| 
 | ||||
| import android.text.SpannableStringBuilder; | ||||
| 
 | ||||
| import org.junit.Test; | ||||
| import org.junit.runner.RunWith; | ||||
| import org.robolectric.RobolectricTestRunner; | ||||
| import org.robolectric.annotation.Config; | ||||
| 
 | ||||
| import java.util.Collections; | ||||
| import java.util.HashMap; | ||||
| import java.util.Map; | ||||
| 
 | ||||
| import io.noties.markwon.editor.PersistedSpans.Impl; | ||||
| import io.noties.markwon.editor.PersistedSpans.SpanFactory; | ||||
| 
 | ||||
| import static io.noties.markwon.editor.SpannableUtils.append; | ||||
| import static org.junit.Assert.assertEquals; | ||||
| import static org.junit.Assert.assertTrue; | ||||
| import static org.junit.Assert.fail; | ||||
| import static org.mockito.Mockito.mock; | ||||
| import static org.mockito.Mockito.never; | ||||
| import static org.mockito.Mockito.times; | ||||
| import static org.mockito.Mockito.verify; | ||||
| 
 | ||||
| @RunWith(RobolectricTestRunner.class) | ||||
| @Config(manifest = Config.NONE) | ||||
| public class PersistedSpansTest { | ||||
| 
 | ||||
|     @Test | ||||
|     public void not_included() { | ||||
|         // When a span that is not included is requested -> exception is raised | ||||
| 
 | ||||
|         final Map<Class<?>, SpanFactory> map = Collections.emptyMap(); | ||||
| 
 | ||||
|         final Impl impl = new Impl(new SpannableStringBuilder(), map); | ||||
| 
 | ||||
|         try { | ||||
|             impl.get(Object.class); | ||||
|             fail(); | ||||
|         } catch (IllegalStateException e) { | ||||
|             assertTrue(e.getMessage(), e.getMessage().contains("not registered, use PersistedSpans.Builder#persistSpan method to register")); | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     @Test | ||||
|     public void re_use() { | ||||
|         // when a span is present in supplied spannable -> it will be used | ||||
| 
 | ||||
|         final class One { | ||||
|         } | ||||
| 
 | ||||
|         final SpannableStringBuilder builder = new SpannableStringBuilder(); | ||||
|         final One one = new One(); | ||||
|         append(builder, "One", one); | ||||
| 
 | ||||
|         final Map<Class<?>, SpanFactory> map = new HashMap<Class<?>, SpanFactory>() {{ | ||||
|             // null in case it _will_ be used -> thus NPE | ||||
|             put(One.class, null); | ||||
|         }}; | ||||
| 
 | ||||
|         final Impl impl = new Impl(builder, map); | ||||
| 
 | ||||
|         assertEquals(one, impl.get(One.class)); | ||||
|     } | ||||
| 
 | ||||
|     @Test | ||||
|     public void factory_create() { | ||||
|         // when span is not present in spannable -> new one will be created via factory | ||||
| 
 | ||||
|         final class Two { | ||||
|         } | ||||
| 
 | ||||
|         final SpannableStringBuilder builder = new SpannableStringBuilder(); | ||||
|         final Two two = new Two(); | ||||
|         append(builder, "two", two); | ||||
| 
 | ||||
|         final SpanFactory factory = mock(SpanFactory.class); | ||||
| 
 | ||||
|         final Map<Class<?>, SpanFactory> map = new HashMap<Class<?>, SpanFactory>() {{ | ||||
|             put(Two.class, factory); | ||||
|         }}; | ||||
| 
 | ||||
|         final Impl impl = new Impl(builder, map); | ||||
| 
 | ||||
|         // first one will be the same as we had created before, | ||||
|         // second one will be created via factory | ||||
| 
 | ||||
|         assertEquals(two, impl.get(Two.class)); | ||||
| 
 | ||||
|         verify(factory, never()).create(); | ||||
| 
 | ||||
|         impl.get(Two.class); | ||||
|         verify(factory, times(1)).create(); | ||||
|     } | ||||
| } | ||||
| @ -0,0 +1,21 @@ | ||||
| package io.noties.markwon.editor; | ||||
| 
 | ||||
| import android.text.SpannableStringBuilder; | ||||
| import android.text.Spanned; | ||||
| 
 | ||||
| import androidx.annotation.NonNull; | ||||
| 
 | ||||
| abstract class SpannableUtils { | ||||
| 
 | ||||
|     static void append(@NonNull SpannableStringBuilder builder, @NonNull String text, Object... spans) { | ||||
|         final int start = builder.length(); | ||||
|         builder.append(text); | ||||
|         final int end = builder.length(); | ||||
|         for (Object span : spans) { | ||||
|             builder.setSpan(span, start, end, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     private SpannableUtils() { | ||||
|     } | ||||
| } | ||||
| @ -0,0 +1,50 @@ | ||||
| package io.noties.markwon.sample.editor; | ||||
| 
 | ||||
| import android.text.Editable; | ||||
| import android.text.Spanned; | ||||
| 
 | ||||
| import androidx.annotation.NonNull; | ||||
| 
 | ||||
| import io.noties.markwon.Markwon; | ||||
| import io.noties.markwon.core.MarkwonTheme; | ||||
| import io.noties.markwon.core.spans.BlockQuoteSpan; | ||||
| import io.noties.markwon.editor.EditHandler; | ||||
| import io.noties.markwon.editor.PersistedSpans; | ||||
| 
 | ||||
| class BlockQuoteEditHandler implements EditHandler<BlockQuoteSpan> { | ||||
| 
 | ||||
|     private MarkwonTheme theme; | ||||
| 
 | ||||
|     @Override | ||||
|     public void init(@NonNull Markwon markwon) { | ||||
|         this.theme = markwon.configuration().theme(); | ||||
|     } | ||||
| 
 | ||||
|     @Override | ||||
|     public void configurePersistedSpans(@NonNull PersistedSpans.Builder builder) { | ||||
|         builder.persistSpan(BlockQuoteSpan.class, () -> new BlockQuoteSpan(theme)); | ||||
|     } | ||||
| 
 | ||||
|     @Override | ||||
|     public void handleMarkdownSpan( | ||||
|             @NonNull PersistedSpans persistedSpans, | ||||
|             @NonNull Editable editable, | ||||
|             @NonNull String input, | ||||
|             @NonNull BlockQuoteSpan span, | ||||
|             int spanStart, | ||||
|             int spanTextLength) { | ||||
|         // todo: here we should actually find a proper ending of a block quote... | ||||
|         editable.setSpan( | ||||
|                 persistedSpans.get(BlockQuoteSpan.class), | ||||
|                 spanStart, | ||||
|                 spanStart + spanTextLength, | ||||
|                 Spanned.SPAN_EXCLUSIVE_EXCLUSIVE | ||||
|         ); | ||||
|     } | ||||
| 
 | ||||
|     @NonNull | ||||
|     @Override | ||||
|     public Class<BlockQuoteSpan> markdownSpanType() { | ||||
|         return BlockQuoteSpan.class; | ||||
|     } | ||||
| } | ||||
| @ -0,0 +1,54 @@ | ||||
| package io.noties.markwon.sample.editor; | ||||
| 
 | ||||
| import android.text.Editable; | ||||
| import android.text.Spanned; | ||||
| 
 | ||||
| import androidx.annotation.NonNull; | ||||
| 
 | ||||
| import io.noties.markwon.Markwon; | ||||
| import io.noties.markwon.core.MarkwonTheme; | ||||
| import io.noties.markwon.core.spans.CodeSpan; | ||||
| import io.noties.markwon.editor.EditHandler; | ||||
| import io.noties.markwon.editor.MarkwonEditorUtils; | ||||
| import io.noties.markwon.editor.PersistedSpans; | ||||
| 
 | ||||
| class CodeEditHandler implements EditHandler<CodeSpan> { | ||||
| 
 | ||||
|     private MarkwonTheme theme; | ||||
| 
 | ||||
|     @Override | ||||
|     public void init(@NonNull Markwon markwon) { | ||||
|         this.theme = markwon.configuration().theme(); | ||||
|     } | ||||
| 
 | ||||
|     @Override | ||||
|     public void configurePersistedSpans(@NonNull PersistedSpans.Builder builder) { | ||||
|         builder.persistSpan(CodeSpan.class, () -> new CodeSpan(theme)); | ||||
|     } | ||||
| 
 | ||||
|     @Override | ||||
|     public void handleMarkdownSpan( | ||||
|             @NonNull PersistedSpans persistedSpans, | ||||
|             @NonNull Editable editable, | ||||
|             @NonNull String input, | ||||
|             @NonNull CodeSpan span, | ||||
|             int spanStart, | ||||
|             int spanTextLength) { | ||||
|         final MarkwonEditorUtils.Match match = | ||||
|                 MarkwonEditorUtils.findDelimited(input, spanStart, "`"); | ||||
|         if (match != null) { | ||||
|             editable.setSpan( | ||||
|                     persistedSpans.get(CodeSpan.class), | ||||
|                     match.start(), | ||||
|                     match.end(), | ||||
|                     Spanned.SPAN_EXCLUSIVE_EXCLUSIVE | ||||
|             ); | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     @NonNull | ||||
|     @Override | ||||
|     public Class<CodeSpan> markdownSpanType() { | ||||
|         return CodeSpan.class; | ||||
|     } | ||||
| } | ||||
| @ -7,7 +7,6 @@ import android.text.SpannableStringBuilder; | ||||
| import android.text.Spanned; | ||||
| import android.text.TextPaint; | ||||
| import android.text.method.LinkMovementMethod; | ||||
| import android.text.style.ClickableSpan; | ||||
| import android.text.style.ForegroundColorSpan; | ||||
| import android.text.style.MetricAffectingSpan; | ||||
| import android.text.style.StrikethroughSpan; | ||||
| @ -19,21 +18,23 @@ import android.widget.TextView; | ||||
| import androidx.annotation.NonNull; | ||||
| import androidx.annotation.Nullable; | ||||
| 
 | ||||
| import org.commonmark.parser.Parser; | ||||
| 
 | ||||
| import java.util.ArrayList; | ||||
| import java.util.List; | ||||
| import java.util.concurrent.Executors; | ||||
| 
 | ||||
| import io.noties.markwon.AbstractMarkwonPlugin; | ||||
| import io.noties.markwon.Markwon; | ||||
| import io.noties.markwon.core.MarkwonTheme; | ||||
| import io.noties.markwon.core.spans.BlockQuoteSpan; | ||||
| import io.noties.markwon.core.spans.CodeSpan; | ||||
| import io.noties.markwon.core.spans.EmphasisSpan; | ||||
| import io.noties.markwon.core.spans.LinkSpan; | ||||
| import io.noties.markwon.core.spans.StrongEmphasisSpan; | ||||
| import io.noties.markwon.editor.EditSpanHandlerBuilder; | ||||
| import io.noties.markwon.editor.AbstractEditHandler; | ||||
| import io.noties.markwon.editor.MarkwonEditor; | ||||
| import io.noties.markwon.editor.MarkwonEditorTextWatcher; | ||||
| import io.noties.markwon.editor.MarkwonEditorUtils; | ||||
| import io.noties.markwon.editor.PersistedSpans; | ||||
| import io.noties.markwon.editor.handler.EmphasisEditHandler; | ||||
| import io.noties.markwon.editor.handler.StrongEmphasisEditHandler; | ||||
| import io.noties.markwon.ext.strikethrough.StrikethroughPlugin; | ||||
| import io.noties.markwon.linkify.LinkifyPlugin; | ||||
| import io.noties.markwon.sample.R; | ||||
| @ -92,7 +93,7 @@ public class EditorActivity extends Activity { | ||||
|         // Use own punctuation span | ||||
| 
 | ||||
|         final MarkwonEditor editor = MarkwonEditor.builder(Markwon.create(this)) | ||||
|                 .withPunctuationSpan(CustomPunctuationSpan.class, CustomPunctuationSpan::new) | ||||
|                 .punctuationSpan(CustomPunctuationSpan.class, CustomPunctuationSpan::new) | ||||
|                 .build(); | ||||
| 
 | ||||
|         editText.addTextChangedListener(MarkwonEditorTextWatcher.withProcess(editor)); | ||||
| @ -102,32 +103,46 @@ public class EditorActivity extends Activity { | ||||
|         // An additional span is used to highlight strong-emphasis | ||||
| 
 | ||||
|         final MarkwonEditor editor = MarkwonEditor.builder(Markwon.create(this)) | ||||
|                 // This is required for edit-span cache | ||||
|                 // We could use Markwon `StrongEmphasisSpan` here, but I use a different | ||||
|                 //  one to indicate that those are completely unrelated spans and must be | ||||
|                 //  treated differently. | ||||
|                 .includeEditSpan(Bold.class, Bold::new) | ||||
|                 .withEditSpanHandler(new MarkwonEditor.EditSpanHandler() { | ||||
|                 .useEditHandler(new AbstractEditHandler<StrongEmphasisSpan>() { | ||||
|                     @Override | ||||
|                     public void handle( | ||||
|                             @NonNull MarkwonEditor.EditSpanStore store, | ||||
|                     public void configurePersistedSpans(@NonNull PersistedSpans.Builder builder) { | ||||
|                         // Here we define which span is _persisted_ in EditText, it is not removed | ||||
|                         //  from EditText between text changes, but instead - reused (by changing | ||||
|                         //  position). Consider it as a cache for spans. We could use `StrongEmphasisSpan` | ||||
|                         //  here also, but I chose Bold to indicate that this span is not the same | ||||
|                         //  as in off-screen rendered markdown | ||||
|                         builder.persistSpan(Bold.class, Bold::new); | ||||
|                     } | ||||
| 
 | ||||
|                     @Override | ||||
|                     public void handleMarkdownSpan( | ||||
|                             @NonNull PersistedSpans persistedSpans, | ||||
|                             @NonNull Editable editable, | ||||
|                             @NonNull String input, | ||||
|                             @NonNull Object span, | ||||
|                             @NonNull StrongEmphasisSpan span, | ||||
|                             int spanStart, | ||||
|                             int spanTextLength) { | ||||
|                         if (span instanceof StrongEmphasisSpan) { | ||||
|                         // Unfortunately we cannot hardcode delimiters length here (aka spanTextLength + 4) | ||||
|                         //  because multiple inline markdown nodes can refer to the same text. | ||||
|                         //  For example, `**_~~hey~~_**` - we will receive `**_~~` in this method, | ||||
|                         //  and thus will have to manually find actual position in raw user input | ||||
|                         final MarkwonEditorUtils.Match match = | ||||
|                                 MarkwonEditorUtils.findDelimited(input, spanStart, "**", "__"); | ||||
|                         if (match != null) { | ||||
|                             editable.setSpan( | ||||
|                                     // `includeEditSpan(Bold.class, Bold::new)` ensured that we have | ||||
|                                     //      a span here to use (either reuse existing or create a new one) | ||||
|                                     store.get(Bold.class), | ||||
|                                     spanStart, | ||||
|                                     // we know that strong emphasis is delimited with 2 characters on both sides | ||||
|                                     spanStart + spanTextLength + 4, | ||||
|                                     persistedSpans.get(Bold.class), | ||||
|                                     match.start(), | ||||
|                                     match.end(), | ||||
|                                     Spanned.SPAN_EXCLUSIVE_EXCLUSIVE | ||||
|                             ); | ||||
|                         } | ||||
|                     } | ||||
| 
 | ||||
|                     @NonNull | ||||
|                     @Override | ||||
|                     public Class<StrongEmphasisSpan> markdownSpanType() { | ||||
|                         return StrongEmphasisSpan.class; | ||||
|                     } | ||||
|                 }) | ||||
|                 .build(); | ||||
| 
 | ||||
| @ -156,20 +171,24 @@ public class EditorActivity extends Activity { | ||||
|         final Markwon markwon = Markwon.builder(this) | ||||
|                 .usePlugin(StrikethroughPlugin.create()) | ||||
|                 .usePlugin(LinkifyPlugin.create()) | ||||
|                 .usePlugin(new AbstractMarkwonPlugin() { | ||||
|                     @Override | ||||
|                     public void configureParser(@NonNull Parser.Builder builder) { | ||||
|                         // disable all commonmark-java blocks, only inlines will be parsed | ||||
| //                        builder.enabledBlockTypes(Collections.emptySet()); | ||||
|                     } | ||||
|                 }) | ||||
|                 .build(); | ||||
| 
 | ||||
|         final MarkwonTheme theme = markwon.configuration().theme(); | ||||
| 
 | ||||
|         final EditLinkSpan.OnClick onClick = (widget, link) -> markwon.configuration().linkResolver().resolve(widget, link); | ||||
|         final LinkEditHandler.OnClick onClick = (widget, link) -> markwon.configuration().linkResolver().resolve(widget, link); | ||||
| 
 | ||||
|         final MarkwonEditor editor = MarkwonEditor.builder(markwon) | ||||
|                 .includeEditSpan(StrongEmphasisSpan.class, StrongEmphasisSpan::new) | ||||
|                 .includeEditSpan(EmphasisSpan.class, EmphasisSpan::new) | ||||
|                 .includeEditSpan(StrikethroughSpan.class, StrikethroughSpan::new) | ||||
|                 .includeEditSpan(CodeSpan.class, () -> new CodeSpan(theme)) | ||||
|                 .includeEditSpan(BlockQuoteSpan.class, () -> new BlockQuoteSpan(theme)) | ||||
|                 .includeEditSpan(EditLinkSpan.class, () -> new EditLinkSpan(onClick)) | ||||
|                 .withEditSpanHandler(createEditSpanHandler()) | ||||
|                 .useEditHandler(new EmphasisEditHandler()) | ||||
|                 .useEditHandler(new StrongEmphasisEditHandler()) | ||||
|                 .useEditHandler(new StrikethroughEditHandler()) | ||||
|                 .useEditHandler(new CodeEditHandler()) | ||||
|                 .useEditHandler(new BlockQuoteEditHandler()) | ||||
|                 .useEditHandler(new LinkEditHandler(onClick)) | ||||
|                 .build(); | ||||
| 
 | ||||
| //        editText.addTextChangedListener(MarkwonEditorTextWatcher.withProcess(editor)); | ||||
| @ -177,100 +196,6 @@ public class EditorActivity extends Activity { | ||||
|                 editor, Executors.newSingleThreadExecutor(), editText)); | ||||
|     } | ||||
| 
 | ||||
|     private static MarkwonEditor.EditSpanHandler createEditSpanHandler() { | ||||
|         // Please note that here we specify spans THAT ARE USED IN MARKDOWN | ||||
|         return EditSpanHandlerBuilder.create() | ||||
|                 .handleMarkdownSpan(StrongEmphasisSpan.class, (store, editable, input, span, spanStart, spanTextLength) -> { | ||||
|                     // inline spans can delimit other inline spans, | ||||
|                     //  for example: `**_~~hey~~_**`, this is why we must additionally find delimiter used | ||||
|                     //  and its actual start/end positions | ||||
|                     final MarkwonEditorUtils.Match match = | ||||
|                             MarkwonEditorUtils.findDelimited(input, spanStart, "**", "__"); | ||||
|                     if (match != null) { | ||||
|                         editable.setSpan( | ||||
|                                 store.get(StrongEmphasisSpan.class), | ||||
|                                 match.start(), | ||||
|                                 match.end(), | ||||
|                                 Spanned.SPAN_EXCLUSIVE_EXCLUSIVE | ||||
|                         ); | ||||
|                     } | ||||
|                 }) | ||||
|                 .handleMarkdownSpan(EmphasisSpan.class, (store, editable, input, span, spanStart, spanTextLength) -> { | ||||
|                     final MarkwonEditorUtils.Match match = | ||||
|                             MarkwonEditorUtils.findDelimited(input, spanStart, "*", "_"); | ||||
|                     if (match != null) { | ||||
|                         editable.setSpan( | ||||
|                                 store.get(EmphasisSpan.class), | ||||
|                                 match.start(), | ||||
|                                 match.end(), | ||||
|                                 Spanned.SPAN_EXCLUSIVE_EXCLUSIVE | ||||
|                         ); | ||||
|                     } | ||||
|                 }) | ||||
|                 .handleMarkdownSpan(StrikethroughSpan.class, (store, editable, input, span, spanStart, spanTextLength) -> { | ||||
|                     final MarkwonEditorUtils.Match match = | ||||
|                             MarkwonEditorUtils.findDelimited(input, spanStart, "~~"); | ||||
|                     if (match != null) { | ||||
|                         editable.setSpan( | ||||
|                                 store.get(StrikethroughSpan.class), | ||||
|                                 match.start(), | ||||
|                                 match.end(), | ||||
|                                 Spanned.SPAN_EXCLUSIVE_EXCLUSIVE | ||||
|                         ); | ||||
|                     } | ||||
|                 }) | ||||
|                 .handleMarkdownSpan(CodeSpan.class, (store, editable, input, span, spanStart, spanTextLength) -> { | ||||
|                     // we do not add offset here because markwon (by default) adds spaces | ||||
|                     // around inline code | ||||
|                     final MarkwonEditorUtils.Match match = | ||||
|                             MarkwonEditorUtils.findDelimited(input, spanStart, "`"); | ||||
|                     if (match != null) { | ||||
|                         editable.setSpan( | ||||
|                                 store.get(CodeSpan.class), | ||||
|                                 match.start(), | ||||
|                                 match.end(), | ||||
|                                 Spanned.SPAN_EXCLUSIVE_EXCLUSIVE | ||||
|                         ); | ||||
|                     } | ||||
|                 }) | ||||
|                 .handleMarkdownSpan(BlockQuoteSpan.class, (store, editable, input, span, spanStart, spanTextLength) -> { | ||||
|                     // todo: here we should actually find a proper ending of a block quote... | ||||
|                     editable.setSpan( | ||||
|                             store.get(BlockQuoteSpan.class), | ||||
|                             spanStart, | ||||
|                             spanStart + spanTextLength, | ||||
|                             Spanned.SPAN_EXCLUSIVE_EXCLUSIVE | ||||
|                     ); | ||||
|                 }) | ||||
|                 .handleMarkdownSpan(LinkSpan.class, (store, editable, input, span, spanStart, spanTextLength) -> { | ||||
| 
 | ||||
|                     final EditLinkSpan editLinkSpan = store.get(EditLinkSpan.class); | ||||
|                     editLinkSpan.link = span.getLink(); | ||||
| 
 | ||||
|                     final int s; | ||||
|                     final int e; | ||||
| 
 | ||||
|                     // markdown link vs. autolink | ||||
|                     if ('[' == input.charAt(spanStart)) { | ||||
|                         s = spanStart + 1; | ||||
|                         e = spanStart + 1 + spanTextLength; | ||||
|                     } else { | ||||
|                         s = spanStart; | ||||
|                         e = spanStart + spanTextLength; | ||||
|                     } | ||||
| 
 | ||||
|                     editable.setSpan( | ||||
|                             editLinkSpan, | ||||
|                             // add underline only for link text | ||||
|                             s, | ||||
|                             e, | ||||
|                             Spanned.SPAN_EXCLUSIVE_EXCLUSIVE | ||||
|                     ); | ||||
|                 }) | ||||
|                 // returns nullable type | ||||
|                 .build(); | ||||
|     } | ||||
| 
 | ||||
|     private void initBottomBar() { | ||||
|         // all except block-quote wraps if have selection, or inserts at current cursor position | ||||
| 
 | ||||
| @ -382,26 +307,4 @@ public class EditorActivity extends Activity { | ||||
|             paint.setFakeBoldText(true); | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     private static class EditLinkSpan extends ClickableSpan { | ||||
| 
 | ||||
|         interface OnClick { | ||||
|             void onClick(@NonNull View widget, @NonNull String link); | ||||
|         } | ||||
| 
 | ||||
|         private final OnClick onClick; | ||||
| 
 | ||||
|         String link; | ||||
| 
 | ||||
|         EditLinkSpan(@NonNull OnClick onClick) { | ||||
|             this.onClick = onClick; | ||||
|         } | ||||
| 
 | ||||
|         @Override | ||||
|         public void onClick(@NonNull View widget) { | ||||
|             if (link != null) { | ||||
|                 onClick.onClick(widget, link); | ||||
|             } | ||||
|         } | ||||
|     } | ||||
| } | ||||
|  | ||||
| @ -0,0 +1,86 @@ | ||||
| package io.noties.markwon.sample.editor; | ||||
| 
 | ||||
| import android.text.Editable; | ||||
| import android.text.Spanned; | ||||
| import android.text.style.ClickableSpan; | ||||
| import android.view.View; | ||||
| 
 | ||||
| import androidx.annotation.NonNull; | ||||
| 
 | ||||
| import io.noties.markwon.core.spans.LinkSpan; | ||||
| import io.noties.markwon.editor.AbstractEditHandler; | ||||
| import io.noties.markwon.editor.PersistedSpans; | ||||
| 
 | ||||
| class LinkEditHandler extends AbstractEditHandler<LinkSpan> { | ||||
| 
 | ||||
|     interface OnClick { | ||||
|         void onClick(@NonNull View widget, @NonNull String link); | ||||
|     } | ||||
| 
 | ||||
|     private final OnClick onClick; | ||||
| 
 | ||||
|     LinkEditHandler(@NonNull OnClick onClick) { | ||||
|         this.onClick = onClick; | ||||
|     } | ||||
| 
 | ||||
|     @Override | ||||
|     public void configurePersistedSpans(@NonNull PersistedSpans.Builder builder) { | ||||
|         builder.persistSpan(EditLinkSpan.class, () -> new EditLinkSpan(onClick)); | ||||
|     } | ||||
| 
 | ||||
|     @Override | ||||
|     public void handleMarkdownSpan( | ||||
|             @NonNull PersistedSpans persistedSpans, | ||||
|             @NonNull Editable editable, | ||||
|             @NonNull String input, | ||||
|             @NonNull LinkSpan span, | ||||
|             int spanStart, | ||||
|             int spanTextLength) { | ||||
| 
 | ||||
|         final EditLinkSpan editLinkSpan = persistedSpans.get(EditLinkSpan.class); | ||||
|         editLinkSpan.link = span.getLink(); | ||||
| 
 | ||||
|         final int s; | ||||
|         final int e; | ||||
| 
 | ||||
|         // markdown link vs. autolink | ||||
|         if ('[' == input.charAt(spanStart)) { | ||||
|             s = spanStart + 1; | ||||
|             e = spanStart + 1 + spanTextLength; | ||||
|         } else { | ||||
|             s = spanStart; | ||||
|             e = spanStart + spanTextLength; | ||||
|         } | ||||
| 
 | ||||
|         editable.setSpan( | ||||
|                 editLinkSpan, | ||||
|                 s, | ||||
|                 e, | ||||
|                 Spanned.SPAN_EXCLUSIVE_EXCLUSIVE | ||||
|         ); | ||||
|     } | ||||
| 
 | ||||
|     @NonNull | ||||
|     @Override | ||||
|     public Class<LinkSpan> markdownSpanType() { | ||||
|         return LinkSpan.class; | ||||
|     } | ||||
| 
 | ||||
|     static class EditLinkSpan extends ClickableSpan { | ||||
| 
 | ||||
|         private final OnClick onClick; | ||||
| 
 | ||||
|         String link; | ||||
| 
 | ||||
|         EditLinkSpan(@NonNull OnClick onClick) { | ||||
|             this.onClick = onClick; | ||||
|         } | ||||
| 
 | ||||
|         @Override | ||||
|         public void onClick(@NonNull View widget) { | ||||
|             if (link != null) { | ||||
|                 onClick.onClick(widget, link); | ||||
|             } | ||||
|         } | ||||
|     } | ||||
| } | ||||
| @ -0,0 +1,45 @@ | ||||
| package io.noties.markwon.sample.editor; | ||||
| 
 | ||||
| import android.text.Editable; | ||||
| import android.text.Spanned; | ||||
| import android.text.style.StrikethroughSpan; | ||||
| 
 | ||||
| import androidx.annotation.NonNull; | ||||
| 
 | ||||
| import io.noties.markwon.editor.AbstractEditHandler; | ||||
| import io.noties.markwon.editor.MarkwonEditorUtils; | ||||
| import io.noties.markwon.editor.PersistedSpans; | ||||
| 
 | ||||
| class StrikethroughEditHandler extends AbstractEditHandler<StrikethroughSpan> { | ||||
| 
 | ||||
|     @Override | ||||
|     public void configurePersistedSpans(@NonNull PersistedSpans.Builder builder) { | ||||
|         builder.persistSpan(StrikethroughSpan.class, StrikethroughSpan::new); | ||||
|     } | ||||
| 
 | ||||
|     @Override | ||||
|     public void handleMarkdownSpan( | ||||
|             @NonNull PersistedSpans persistedSpans, | ||||
|             @NonNull Editable editable, | ||||
|             @NonNull String input, | ||||
|             @NonNull StrikethroughSpan span, | ||||
|             int spanStart, | ||||
|             int spanTextLength) { | ||||
|         final MarkwonEditorUtils.Match match = | ||||
|                 MarkwonEditorUtils.findDelimited(input, spanStart, "~~"); | ||||
|         if (match != null) { | ||||
|             editable.setSpan( | ||||
|                     persistedSpans.get(StrikethroughSpan.class), | ||||
|                     match.start(), | ||||
|                     match.end(), | ||||
|                     Spanned.SPAN_EXCLUSIVE_EXCLUSIVE | ||||
|             ); | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     @NonNull | ||||
|     @Override | ||||
|     public Class<StrikethroughSpan> markdownSpanType() { | ||||
|         return StrikethroughSpan.class; | ||||
|     } | ||||
| } | ||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user
	 Dimitry Ivanov
						Dimitry Ivanov