Added editor tests
This commit is contained in:
		
							parent
							
								
									f1e750b305
								
							
						
					
					
						commit
						bd53c014a1
					
				
							
								
								
									
										2
									
								
								.github/workflows/develop.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										2
									
								
								.github/workflows/develop.yml
									
									
									
									
										vendored
									
									
								
							| @ -15,7 +15,7 @@ jobs: | ||||
|         with: | ||||
|           java-version: 1.8 | ||||
|       - name: Build with Gradle | ||||
|         run: ./gradlew build | ||||
|         run: ./gradlew build -Prelease | ||||
| 
 | ||||
|   deploy: | ||||
|     needs: build | ||||
|  | ||||
| @ -1,4 +1,4 @@ | ||||
| 
 | ||||
| // this is a generated file, do not modify. To update it run 'collectArtifacts.js' script
 | ||||
| const artifacts = [{"id":"core","name":"Core","group":"io.noties.markwon","description":"Core Markwon artifact that includes basic markdown parsing and rendering"},{"id":"ext-latex","name":"LaTeX","group":"io.noties.markwon","description":"Extension to add LaTeX formulas to Markwon markdown"},{"id":"ext-strikethrough","name":"Strikethrough","group":"io.noties.markwon","description":"Extension to add strikethrough markup to Markwon markdown"},{"id":"ext-tables","name":"Tables","group":"io.noties.markwon","description":"Extension to add tables markup (GFM) to Markwon markdown"},{"id":"ext-tasklist","name":"Task List","group":"io.noties.markwon","description":"Extension to add task lists (GFM) to Markwon markdown"},{"id":"html","name":"HTML","group":"io.noties.markwon","description":"Provides HTML parsing functionality"},{"id":"image","name":"Image","group":"io.noties.markwon","description":"Markwon image loading module (with optional GIF and SVG support)"},{"id":"image-glide","name":"Image Glide","group":"io.noties.markwon","description":"Markwon image loading module (based on Glide library)"},{"id":"image-picasso","name":"Image Picasso","group":"io.noties.markwon","description":"Markwon image loading module (based on Picasso library)"},{"id":"linkify","name":"Linkify","group":"io.noties.markwon","description":"Markwon plugin to linkify text (based on Android Linkify)"},{"id":"recycler","name":"Recycler","group":"io.noties.markwon","description":"Provides RecyclerView.Adapter to display Markwon markdown"},{"id":"recycler-table","name":"Recycler Table","group":"io.noties.markwon","description":"Provides MarkwonAdapter.Entry to render TableBlocks inside Android-native TableLayout widget"},{"id":"simple-ext","name":"Simple Extension","group":"io.noties.markwon","description":"Custom extension based on simple delimiter usage"},{"id":"syntax-highlight","name":"Syntax Highlight","group":"io.noties.markwon","description":"Add syntax highlight to Markwon markdown via Prism4j library"}]; | ||||
| const artifacts = [{"id":"core","name":"Core","group":"io.noties.markwon","description":"Core Markwon artifact that includes basic markdown parsing and rendering"},{"id":"editor","name":"Editor","group":"io.noties.markwon","description":"Markdown editor based on Markwon"},{"id":"ext-latex","name":"LaTeX","group":"io.noties.markwon","description":"Extension to add LaTeX formulas to Markwon markdown"},{"id":"ext-strikethrough","name":"Strikethrough","group":"io.noties.markwon","description":"Extension to add strikethrough markup to Markwon markdown"},{"id":"ext-tables","name":"Tables","group":"io.noties.markwon","description":"Extension to add tables markup (GFM) to Markwon markdown"},{"id":"ext-tasklist","name":"Task List","group":"io.noties.markwon","description":"Extension to add task lists (GFM) to Markwon markdown"},{"id":"html","name":"HTML","group":"io.noties.markwon","description":"Provides HTML parsing functionality"},{"id":"image","name":"Image","group":"io.noties.markwon","description":"Markwon image loading module (with optional GIF and SVG support)"},{"id":"image-glide","name":"Image Glide","group":"io.noties.markwon","description":"Markwon image loading module (based on Glide library)"},{"id":"image-picasso","name":"Image Picasso","group":"io.noties.markwon","description":"Markwon image loading module (based on Picasso library)"},{"id":"linkify","name":"Linkify","group":"io.noties.markwon","description":"Markwon plugin to linkify text (based on Android Linkify)"},{"id":"recycler","name":"Recycler","group":"io.noties.markwon","description":"Provides RecyclerView.Adapter to display Markwon markdown"},{"id":"recycler-table","name":"Recycler Table","group":"io.noties.markwon","description":"Provides MarkwonAdapter.Entry to render TableBlocks inside Android-native TableLayout widget"},{"id":"simple-ext","name":"Simple Extension","group":"io.noties.markwon","description":"Custom extension based on simple delimiter usage"},{"id":"syntax-highlight","name":"Syntax Highlight","group":"io.noties.markwon","description":"Add syntax highlight to Markwon markdown via Prism4j library"}]; | ||||
| export { artifacts }; | ||||
|  | ||||
| @ -95,6 +95,7 @@ module.exports = { | ||||
|                         '/docs/v4/core/text-setter.md' | ||||
|                     ] | ||||
|                 }, | ||||
|                 '/docs/v4/editor/', | ||||
|                 '/docs/v4/ext-latex/', | ||||
|                 '/docs/v4/ext-strikethrough/', | ||||
|                 '/docs/v4/ext-tables/', | ||||
|  | ||||
							
								
								
									
										
											BIN
										
									
								
								docs/.vuepress/public/assets/markwon-editor-preview.jpg
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								docs/.vuepress/public/assets/markwon-editor-preview.jpg
									
									
									
									
									
										Normal file
									
								
							
										
											Binary file not shown.
										
									
								
							| After Width: | Height: | Size: 21 KiB | 
							
								
								
									
										
											BIN
										
									
								
								docs/.vuepress/public/assets/markwon-editor.mp4
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								docs/.vuepress/public/assets/markwon-editor.mp4
									
									
									
									
									
										Normal file
									
								
							
										
											Binary file not shown.
										
									
								
							| @ -21,6 +21,11 @@ but also gives all the means to tweak the appearance if desired. All markdown fe | ||||
| listed in <Link name="commonmark-spec" /> are supported (including support for **inlined/block HTML code**,  | ||||
| **markdown tables**, **images** and **syntax highlight**). | ||||
| 
 | ||||
| Since version <Badge text="4.2.0" /> **Markwon** comes with an [editor] to _highlight_ markdown input | ||||
| as user types for example in **EditText**. | ||||
| 
 | ||||
| [editor]: /docs/v4/editor/ | ||||
| 
 | ||||
| ## Supported markdown features | ||||
| 
 | ||||
| * Emphasis (`*`, `_`) | ||||
|  | ||||
							
								
								
									
										25
									
								
								docs/docs/v4/editor/README.md
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										25
									
								
								docs/docs/v4/editor/README.md
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,25 @@ | ||||
| # Editor <Badge text="4.2.0" /> | ||||
| 
 | ||||
| <MavenBadge4 :artifact="'editor'" /> | ||||
| 
 | ||||
| Markdown editing highlight for Android based on **Markwon**. | ||||
| 
 | ||||
| <style> | ||||
| video { | ||||
|     max-height: 82vh; | ||||
| } | ||||
| </style> | ||||
| 
 | ||||
| <video controls="true" loop="" :poster="$withBase('/assets/markwon-editor-preview.jpg')"> | ||||
|     <source :src="$withBase('/assets/markwon-editor.mp4')" type="video/mp4"> | ||||
|     You browser does not support mp4 playback, try downloading video file  | ||||
|     <a :href="$withBase('/assets/markwon-editor.mp4')">directly</a> | ||||
| </video> | ||||
| 
 | ||||
| ## Getting started with editor | ||||
| 
 | ||||
| :::warning Implementation Detail | ||||
| It must be mentioned that highlight is implemented via text diff. Everything | ||||
| that is present in raw markdown input and missing from rendered result is considered | ||||
| to be _punctuation_. | ||||
| ::: | ||||
| @ -3,4 +3,11 @@ | ||||
| Markdown editor for Android based on `Markwon`. | ||||
| 
 | ||||
| Main principle: _difference_ between input text and rendered markdown is considered to be | ||||
| _punctuation_.  | ||||
| _punctuation_. | ||||
| 
 | ||||
| 
 | ||||
| ## Limitations | ||||
| 
 | ||||
| Tables and LaTeX nodes won't be rendered correctly. They will be treated as _punctuation_ | ||||
| as whole. This comes from their implementation - they are _mocked_ and do not present | ||||
| in final result as text and thus cannot be _diffed_.  | ||||
| @ -31,7 +31,7 @@ public class EditSpanHandlerBuilder { | ||||
|     private final Map<Class<?>, EditSpanHandlerTyped> map = new HashMap<>(3); | ||||
| 
 | ||||
|     @NonNull | ||||
|     public <T> EditSpanHandlerBuilder include( | ||||
|     public <T> EditSpanHandlerBuilder handleMarkdownSpan( | ||||
|             @NonNull Class<T> type, | ||||
|             @NonNull EditSpanHandlerTyped<T> handler) { | ||||
|         map.put(type, handler); | ||||
|  | ||||
| @ -10,11 +10,6 @@ import java.util.Map; | ||||
| 
 | ||||
| import io.noties.markwon.Markwon; | ||||
| 
 | ||||
| // todo: how to reuse existing spanFactories? to obtain a value they require render-props.... | ||||
| //  maybe.. mock them? plus, spanFactory can return multiple spans | ||||
| 
 | ||||
| // todo: as we do text-diff, then images, latex and tables won't be handled... they will be treated as punctuation as-whole.. | ||||
| 
 | ||||
| /** | ||||
|  * @see #builder(Markwon) | ||||
|  * @see #create(Markwon) | ||||
| @ -24,6 +19,9 @@ import io.noties.markwon.Markwon; | ||||
|  */ | ||||
| public abstract class MarkwonEditor { | ||||
| 
 | ||||
|     /** | ||||
|      * Represents cache of spans that are used during highlight | ||||
|      */ | ||||
|     public interface EditSpanStore { | ||||
| 
 | ||||
|         /** | ||||
|  | ||||
| @ -0,0 +1,166 @@ | ||||
| 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; | ||||
| 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 | ||||
|         final Markwon markwon = Markwon.create(RuntimeEnvironment.application); | ||||
| 
 | ||||
|         // default punctuation | ||||
|         final MarkwonEditor editor = MarkwonEditor.create(markwon); | ||||
| 
 | ||||
|         final SpannableStringBuilder builder = new SpannableStringBuilder("**bold**"); | ||||
| 
 | ||||
|         editor.process(builder); | ||||
| 
 | ||||
|         final PunctuationSpan[] spans = builder.getSpans(0, builder.length(), PunctuationSpan.class); | ||||
|         assertEquals(2, spans.length); | ||||
| 
 | ||||
|         final PunctuationSpan first = spans[0]; | ||||
|         assertEquals(0, builder.getSpanStart(first)); | ||||
|         assertEquals(2, builder.getSpanEnd(first)); | ||||
| 
 | ||||
|         final PunctuationSpan second = spans[1]; | ||||
|         assertEquals(6, builder.getSpanStart(second)); | ||||
|         assertEquals(8, builder.getSpanEnd(second)); | ||||
|     } | ||||
| } | ||||
| @ -0,0 +1,45 @@ | ||||
| package io.noties.markwon.editor; | ||||
| 
 | ||||
| import org.junit.Test; | ||||
| import org.junit.runner.RunWith; | ||||
| import org.robolectric.RobolectricTestRunner; | ||||
| import org.robolectric.annotation.Config; | ||||
| 
 | ||||
| import io.noties.markwon.Markwon; | ||||
| import io.noties.markwon.editor.MarkwonEditor.Builder; | ||||
| 
 | ||||
| import static org.junit.Assert.assertTrue; | ||||
| import static org.junit.Assert.fail; | ||||
| import static org.mockito.Mockito.mock; | ||||
| 
 | ||||
| @RunWith(RobolectricTestRunner.class) | ||||
| @Config(manifest = Config.NONE) | ||||
| public class MarkwonEditorTest { | ||||
| 
 | ||||
|     @Test | ||||
|     public void builder_no_config() { | ||||
|         // must create a default instance without exceptions | ||||
| 
 | ||||
|         try { | ||||
|             new Builder(mock(Markwon.class)).build(); | ||||
|             assertTrue(true); | ||||
|         } catch (Throwable t) { | ||||
|             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 ")); | ||||
|         } | ||||
|     } | ||||
| } | ||||
| @ -0,0 +1,94 @@ | ||||
| package io.noties.markwon.editor; | ||||
| 
 | ||||
| import android.text.Editable; | ||||
| import android.widget.EditText; | ||||
| 
 | ||||
| import org.junit.Test; | ||||
| import org.junit.runner.RunWith; | ||||
| import org.mockito.ArgumentCaptor; | ||||
| import org.mockito.invocation.InvocationOnMock; | ||||
| import org.mockito.stubbing.Answer; | ||||
| import org.robolectric.RobolectricTestRunner; | ||||
| import org.robolectric.annotation.Config; | ||||
| 
 | ||||
| import java.util.concurrent.ExecutorService; | ||||
| 
 | ||||
| import io.noties.markwon.editor.MarkwonEditor.PreRenderResult; | ||||
| import io.noties.markwon.editor.MarkwonEditor.PreRenderResultListener; | ||||
| 
 | ||||
| import static org.mockito.ArgumentMatchers.any; | ||||
| import static org.mockito.ArgumentMatchers.eq; | ||||
| import static org.mockito.Mockito.doAnswer; | ||||
| import static org.mockito.Mockito.mock; | ||||
| import static org.mockito.Mockito.times; | ||||
| import static org.mockito.Mockito.verify; | ||||
| import static org.mockito.Mockito.when; | ||||
| 
 | ||||
| @RunWith(RobolectricTestRunner.class) | ||||
| @Config(manifest = Config.NONE) | ||||
| public class MarkwonEditorTextWatcherTest { | ||||
| 
 | ||||
|     @Test | ||||
|     public void w_process() { | ||||
| 
 | ||||
|         final MarkwonEditor editor = mock(MarkwonEditor.class); | ||||
|         final Editable editable = mock(Editable.class); | ||||
| 
 | ||||
|         final MarkwonEditorTextWatcher watcher = MarkwonEditorTextWatcher.withProcess(editor); | ||||
| 
 | ||||
|         watcher.afterTextChanged(editable); | ||||
| 
 | ||||
|         verify(editor, times(1)).process(eq(editable)); | ||||
|     } | ||||
| 
 | ||||
|     @Test | ||||
|     public void w_pre_render() { | ||||
| 
 | ||||
|         final MarkwonEditor editor = mock(MarkwonEditor.class); | ||||
|         final Editable editable = mock(Editable.class); | ||||
|         final ExecutorService service = mock(ExecutorService.class); | ||||
|         final EditText editText = mock(EditText.class); | ||||
| 
 | ||||
|         when(editText.getText()).thenReturn(editable); | ||||
| 
 | ||||
|         when(service.submit(any(Runnable.class))).thenAnswer(new Answer<Object>() { | ||||
|             @Override | ||||
|             public Object answer(InvocationOnMock invocation) { | ||||
|                 ((Runnable) invocation.getArgument(0)).run(); | ||||
|                 return null; | ||||
|             } | ||||
|         }); | ||||
| 
 | ||||
|         doAnswer(new Answer() { | ||||
|             @Override | ||||
|             public Object answer(InvocationOnMock invocation) { | ||||
|                 ((Runnable) invocation.getArgument(0)).run(); | ||||
|                 return null; | ||||
|             } | ||||
|         }).when(editText).post(any(Runnable.class)); | ||||
| 
 | ||||
|         final MarkwonEditorTextWatcher watcher = MarkwonEditorTextWatcher.withPreRender( | ||||
|                 editor, | ||||
|                 service, | ||||
|                 editText); | ||||
| 
 | ||||
|         watcher.afterTextChanged(editable); | ||||
| 
 | ||||
|         final ArgumentCaptor<PreRenderResultListener> captor = | ||||
|                 ArgumentCaptor.forClass(PreRenderResultListener.class); | ||||
| 
 | ||||
|         verify(service, times(1)).submit(any(Runnable.class)); | ||||
|         verify(editor, times(1)).preRender(eq(editable), captor.capture()); | ||||
| 
 | ||||
|         final PreRenderResultListener listener = captor.getValue(); | ||||
|         final PreRenderResult result = mock(PreRenderResult.class); | ||||
| 
 | ||||
|         // for simplicity return the same editable instance (same hashCode) | ||||
|         when(result.resultEditable()).thenReturn(editable); | ||||
| 
 | ||||
|         listener.onPreRenderResult(result); | ||||
| 
 | ||||
|         verify(result, times(1)).resultEditable(); | ||||
|         verify(result, times(1)).dispatchTo(eq(editable)); | ||||
|     } | ||||
| } | ||||
| @ -28,7 +28,10 @@ | ||||
|         <activity android:name=".simpleext.SimpleExtActivity" /> | ||||
|         <activity android:name=".customextension2.CustomExtensionActivity2" /> | ||||
|         <activity android:name=".precomputed.PrecomputedActivity" /> | ||||
|         <activity android:name=".editor.EditorActivity" /> | ||||
| 
 | ||||
|         <activity | ||||
|             android:name=".editor.EditorActivity" | ||||
|             android:windowSoftInputMode="adjustResize" /> | ||||
| 
 | ||||
|     </application> | ||||
| 
 | ||||
|  | ||||
| @ -3,17 +3,23 @@ package io.noties.markwon.sample.editor; | ||||
| import android.app.Activity; | ||||
| import android.os.Bundle; | ||||
| import android.text.Editable; | ||||
| import android.text.SpannableStringBuilder; | ||||
| import android.text.Spanned; | ||||
| import android.text.TextPaint; | ||||
| import android.text.style.CharacterStyle; | ||||
| import android.text.style.ForegroundColorSpan; | ||||
| import android.text.style.MetricAffectingSpan; | ||||
| import android.text.style.StrikethroughSpan; | ||||
| import android.view.View; | ||||
| import android.widget.Button; | ||||
| import android.widget.EditText; | ||||
| import android.widget.TextView; | ||||
| 
 | ||||
| import androidx.annotation.NonNull; | ||||
| import androidx.annotation.Nullable; | ||||
| 
 | ||||
| import java.util.ArrayList; | ||||
| import java.util.List; | ||||
| import java.util.concurrent.Executors; | ||||
| 
 | ||||
| import io.noties.markwon.Markwon; | ||||
| @ -40,7 +46,7 @@ public class EditorActivity extends Activity { | ||||
|         setContentView(R.layout.activity_editor); | ||||
| 
 | ||||
|         this.editText = findViewById(R.id.edit_text); | ||||
| 
 | ||||
|         initBottomBar(); | ||||
| 
 | ||||
| //        simple_process(); | ||||
| 
 | ||||
| @ -164,7 +170,7 @@ public class EditorActivity extends Activity { | ||||
|     private static MarkwonEditor.EditSpanHandler createEditSpanHandler() { | ||||
|         // Please note that here we specify spans THAT ARE USED IN MARKDOWN | ||||
|         return EditSpanHandlerBuilder.create() | ||||
|                 .include(StrongEmphasisSpan.class, (store, editable, input, span, spanStart, spanTextLength) -> { | ||||
|                 .handleMarkdownSpan(StrongEmphasisSpan.class, (store, editable, input, span, spanStart, spanTextLength) -> { | ||||
|                     editable.setSpan( | ||||
|                             store.get(StrongEmphasisSpan.class), | ||||
|                             spanStart, | ||||
| @ -172,7 +178,7 @@ public class EditorActivity extends Activity { | ||||
|                             Spanned.SPAN_EXCLUSIVE_EXCLUSIVE | ||||
|                     ); | ||||
|                 }) | ||||
|                 .include(EmphasisSpan.class, (store, editable, input, span, spanStart, spanTextLength) -> { | ||||
|                 .handleMarkdownSpan(EmphasisSpan.class, (store, editable, input, span, spanStart, spanTextLength) -> { | ||||
|                     editable.setSpan( | ||||
|                             store.get(EmphasisSpan.class), | ||||
|                             spanStart, | ||||
| @ -180,7 +186,7 @@ public class EditorActivity extends Activity { | ||||
|                             Spanned.SPAN_EXCLUSIVE_EXCLUSIVE | ||||
|                     ); | ||||
|                 }) | ||||
|                 .include(StrikethroughSpan.class, (store, editable, input, span, spanStart, spanTextLength) -> { | ||||
|                 .handleMarkdownSpan(StrikethroughSpan.class, (store, editable, input, span, spanStart, spanTextLength) -> { | ||||
|                     editable.setSpan( | ||||
|                             store.get(StrikethroughSpan.class), | ||||
|                             spanStart, | ||||
| @ -188,7 +194,7 @@ public class EditorActivity extends Activity { | ||||
|                             Spanned.SPAN_EXCLUSIVE_EXCLUSIVE | ||||
|                     ); | ||||
|                 }) | ||||
|                 .include(CodeSpan.class, (store, editable, input, span, spanStart, spanTextLength) -> { | ||||
|                 .handleMarkdownSpan(CodeSpan.class, (store, editable, input, span, spanStart, spanTextLength) -> { | ||||
|                     // we do not add offset here because markwon (by default) adds spaces | ||||
|                     // around inline code | ||||
|                     editable.setSpan( | ||||
| @ -198,7 +204,7 @@ public class EditorActivity extends Activity { | ||||
|                             Spanned.SPAN_EXCLUSIVE_EXCLUSIVE | ||||
|                     ); | ||||
|                 }) | ||||
|                 .include(CodeBlockSpan.class, (store, editable, input, span, spanStart, spanTextLength) -> { | ||||
|                 .handleMarkdownSpan(CodeBlockSpan.class, (store, editable, input, span, spanStart, spanTextLength) -> { | ||||
|                     // we do not handle indented code blocks here | ||||
|                     if (input.charAt(spanStart) == '`') { | ||||
|                         final int firstLineEnd = input.indexOf('\n', spanStart); | ||||
| @ -214,7 +220,7 @@ public class EditorActivity extends Activity { | ||||
|                         ); | ||||
|                     } | ||||
|                 }) | ||||
|                 .include(BlockQuoteSpan.class, (store, editable, input, span, spanStart, spanTextLength) -> { | ||||
|                 .handleMarkdownSpan(BlockQuoteSpan.class, (store, editable, input, span, spanStart, spanTextLength) -> { | ||||
|                     editable.setSpan( | ||||
|                             store.get(BlockQuoteSpan.class), | ||||
|                             spanStart, | ||||
| @ -222,7 +228,7 @@ public class EditorActivity extends Activity { | ||||
|                             Spanned.SPAN_EXCLUSIVE_EXCLUSIVE | ||||
|                     ); | ||||
|                 }) | ||||
|                 .include(LinkSpan.class, (store, editable, input, span, spanStart, spanTextLength) -> { | ||||
|                 .handleMarkdownSpan(LinkSpan.class, (store, editable, input, span, spanStart, spanTextLength) -> { | ||||
|                     editable.setSpan( | ||||
|                             store.get(EditLinkSpan.class), | ||||
|                             // add underline only for link text | ||||
| @ -235,6 +241,81 @@ public class EditorActivity extends Activity { | ||||
|                 .build(); | ||||
|     } | ||||
| 
 | ||||
|     private void initBottomBar() { | ||||
|         // all except block-quote wraps if have selection, or inserts at current cursor position | ||||
| 
 | ||||
|         final Button bold = findViewById(R.id.bold); | ||||
|         final Button italic = findViewById(R.id.italic); | ||||
|         final Button strike = findViewById(R.id.strike); | ||||
|         final Button quote = findViewById(R.id.quote); | ||||
|         final Button code = findViewById(R.id.code); | ||||
| 
 | ||||
|         addSpan(bold, new StrongEmphasisSpan()); | ||||
|         addSpan(italic, new EmphasisSpan()); | ||||
|         addSpan(strike, new StrikethroughSpan()); | ||||
| 
 | ||||
|         bold.setOnClickListener(new InsertOrWrapClickListener(editText, "**")); | ||||
|         italic.setOnClickListener(new InsertOrWrapClickListener(editText, "_")); | ||||
|         strike.setOnClickListener(new InsertOrWrapClickListener(editText, "~~")); | ||||
|         code.setOnClickListener(new InsertOrWrapClickListener(editText, "`")); | ||||
| 
 | ||||
|         quote.setOnClickListener(v -> { | ||||
|             final int start = editText.getSelectionStart(); | ||||
|             final int end = editText.getSelectionEnd(); | ||||
|             if (start == end) { | ||||
|                 editText.getText().insert(start, "> "); | ||||
|             } else { | ||||
|                 // wrap the whole selected area in a quote | ||||
|                 final List<Integer> newLines = new ArrayList<>(3); | ||||
|                 newLines.add(start); | ||||
| 
 | ||||
|                 final String text = editText.getText().subSequence(start, end).toString(); | ||||
|                 int index = text.indexOf('\n'); | ||||
|                 while (index != -1) { | ||||
|                     newLines.add(start + index); | ||||
|                     index = text.indexOf('\n', index + 1); | ||||
|                 } | ||||
| 
 | ||||
|                 for (int i = newLines.size() - 1; i >= 0; i--) { | ||||
|                     editText.getText().insert(newLines.get(i), "> "); | ||||
|                 } | ||||
|             } | ||||
|         }); | ||||
|     } | ||||
| 
 | ||||
|     private static void addSpan(@NonNull TextView textView, Object... spans) { | ||||
|         final SpannableStringBuilder builder = new SpannableStringBuilder(textView.getText()); | ||||
|         final int end = builder.length(); | ||||
|         for (Object span : spans) { | ||||
|             builder.setSpan(span, 0, end, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); | ||||
|         } | ||||
|         textView.setText(builder); | ||||
|     } | ||||
| 
 | ||||
|     private static class InsertOrWrapClickListener implements View.OnClickListener { | ||||
| 
 | ||||
|         private final EditText editText; | ||||
|         private final String text; | ||||
| 
 | ||||
|         InsertOrWrapClickListener(@NonNull EditText editText, @NonNull String text) { | ||||
|             this.editText = editText; | ||||
|             this.text = text; | ||||
|         } | ||||
| 
 | ||||
|         @Override | ||||
|         public void onClick(View v) { | ||||
|             final int start = editText.getSelectionStart(); | ||||
|             final int end = editText.getSelectionEnd(); | ||||
|             if (start == end) { | ||||
|                 // insert at current position | ||||
|                 editText.getText().insert(start, text); | ||||
|             } else { | ||||
|                 editText.getText().insert(end, text); | ||||
|                 editText.getText().insert(start, text); | ||||
|             } | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     private static class CustomPunctuationSpan extends ForegroundColorSpan { | ||||
|         CustomPunctuationSpan() { | ||||
|             super(0xFFFF0000); // RED | ||||
|  | ||||
| @ -1,17 +1,72 @@ | ||||
| <?xml version="1.0" encoding="utf-8"?> | ||||
| <FrameLayout xmlns:android="http://schemas.android.com/apk/res/android" | ||||
| <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" | ||||
|     android:layout_width="match_parent" | ||||
|     android:layout_height="match_parent" | ||||
|     android:clipToPadding="false" | ||||
|     android:orientation="vertical" | ||||
|     android:padding="8dip"> | ||||
| 
 | ||||
|     <EditText | ||||
|         android:id="@+id/edit_text" | ||||
|     <FrameLayout | ||||
|         android:layout_width="match_parent" | ||||
|         android:layout_height="0px" | ||||
|         android:layout_weight="1"> | ||||
| 
 | ||||
|         <EditText | ||||
|             android:id="@+id/edit_text" | ||||
|             android:layout_width="match_parent" | ||||
|             android:layout_height="wrap_content" | ||||
|             android:autofillHints="none" | ||||
|             android:hint="Markdown..." | ||||
|             android:inputType="text|textLongMessage|textMultiLine" | ||||
|             android:maxLines="100" /> | ||||
| 
 | ||||
|     </FrameLayout> | ||||
| 
 | ||||
|     <LinearLayout | ||||
|         android:layout_width="match_parent" | ||||
|         android:layout_height="wrap_content" | ||||
|         android:autofillHints="none" | ||||
|         android:hint="Markdown..." | ||||
|         android:inputType="text|textLongMessage|textMultiLine" | ||||
|         android:maxLines="100" /> | ||||
|         android:orientation="horizontal"> | ||||
| 
 | ||||
| </FrameLayout> | ||||
|         <Button | ||||
|             android:id="@+id/bold" | ||||
|             android:layout_width="0px" | ||||
|             android:layout_height="wrap_content" | ||||
|             android:layout_weight="1" | ||||
|             android:text="B" | ||||
|             android:typeface="monospace" /> | ||||
| 
 | ||||
|         <Button | ||||
|             android:id="@+id/italic" | ||||
|             android:layout_width="0px" | ||||
|             android:layout_height="wrap_content" | ||||
|             android:layout_weight="1" | ||||
|             android:text="I" | ||||
|             android:typeface="monospace" /> | ||||
| 
 | ||||
|         <Button | ||||
|             android:id="@+id/strike" | ||||
|             android:layout_width="0px" | ||||
|             android:layout_height="wrap_content" | ||||
|             android:layout_weight="1" | ||||
|             android:text="S" | ||||
|             android:typeface="monospace" /> | ||||
| 
 | ||||
|         <Button | ||||
|             android:id="@+id/quote" | ||||
|             android:layout_width="0px" | ||||
|             android:layout_height="wrap_content" | ||||
|             android:layout_weight="1" | ||||
|             android:text=">" | ||||
|             android:typeface="monospace" /> | ||||
| 
 | ||||
|         <Button | ||||
|             android:id="@+id/code" | ||||
|             android:layout_width="0px" | ||||
|             android:layout_height="wrap_content" | ||||
|             android:layout_weight="1" | ||||
|             android:text="`" | ||||
|             android:typeface="monospace" /> | ||||
| 
 | ||||
|     </LinearLayout> | ||||
| 
 | ||||
| </LinearLayout> | ||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user
	 Dimitry Ivanov
						Dimitry Ivanov