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:
|
with:
|
||||||
java-version: 1.8
|
java-version: 1.8
|
||||||
- name: Build with Gradle
|
- name: Build with Gradle
|
||||||
run: ./gradlew build
|
run: ./gradlew build -Prelease
|
||||||
|
|
||||||
deploy:
|
deploy:
|
||||||
needs: build
|
needs: build
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
|
|
||||||
// this is a generated file, do not modify. To update it run 'collectArtifacts.js' script
|
// 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 };
|
export { artifacts };
|
||||||
|
@ -95,6 +95,7 @@ module.exports = {
|
|||||||
'/docs/v4/core/text-setter.md'
|
'/docs/v4/core/text-setter.md'
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
'/docs/v4/editor/',
|
||||||
'/docs/v4/ext-latex/',
|
'/docs/v4/ext-latex/',
|
||||||
'/docs/v4/ext-strikethrough/',
|
'/docs/v4/ext-strikethrough/',
|
||||||
'/docs/v4/ext-tables/',
|
'/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**,
|
listed in <Link name="commonmark-spec" /> are supported (including support for **inlined/block HTML code**,
|
||||||
**markdown tables**, **images** and **syntax highlight**).
|
**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
|
## Supported markdown features
|
||||||
|
|
||||||
* Emphasis (`*`, `_`)
|
* 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_.
|
||||||
|
:::
|
@ -4,3 +4,10 @@ Markdown editor for Android based on `Markwon`.
|
|||||||
|
|
||||||
Main principle: _difference_ between input text and rendered markdown is considered to be
|
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);
|
private final Map<Class<?>, EditSpanHandlerTyped> map = new HashMap<>(3);
|
||||||
|
|
||||||
@NonNull
|
@NonNull
|
||||||
public <T> EditSpanHandlerBuilder include(
|
public <T> EditSpanHandlerBuilder handleMarkdownSpan(
|
||||||
@NonNull Class<T> type,
|
@NonNull Class<T> type,
|
||||||
@NonNull EditSpanHandlerTyped<T> handler) {
|
@NonNull EditSpanHandlerTyped<T> handler) {
|
||||||
map.put(type, handler);
|
map.put(type, handler);
|
||||||
|
@ -10,11 +10,6 @@ import java.util.Map;
|
|||||||
|
|
||||||
import io.noties.markwon.Markwon;
|
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 #builder(Markwon)
|
||||||
* @see #create(Markwon)
|
* @see #create(Markwon)
|
||||||
@ -24,6 +19,9 @@ import io.noties.markwon.Markwon;
|
|||||||
*/
|
*/
|
||||||
public abstract class MarkwonEditor {
|
public abstract class MarkwonEditor {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Represents cache of spans that are used during highlight
|
||||||
|
*/
|
||||||
public interface EditSpanStore {
|
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=".simpleext.SimpleExtActivity" />
|
||||||
<activity android:name=".customextension2.CustomExtensionActivity2" />
|
<activity android:name=".customextension2.CustomExtensionActivity2" />
|
||||||
<activity android:name=".precomputed.PrecomputedActivity" />
|
<activity android:name=".precomputed.PrecomputedActivity" />
|
||||||
<activity android:name=".editor.EditorActivity" />
|
|
||||||
|
<activity
|
||||||
|
android:name=".editor.EditorActivity"
|
||||||
|
android:windowSoftInputMode="adjustResize" />
|
||||||
|
|
||||||
</application>
|
</application>
|
||||||
|
|
||||||
|
@ -3,17 +3,23 @@ package io.noties.markwon.sample.editor;
|
|||||||
import android.app.Activity;
|
import android.app.Activity;
|
||||||
import android.os.Bundle;
|
import android.os.Bundle;
|
||||||
import android.text.Editable;
|
import android.text.Editable;
|
||||||
|
import android.text.SpannableStringBuilder;
|
||||||
import android.text.Spanned;
|
import android.text.Spanned;
|
||||||
import android.text.TextPaint;
|
import android.text.TextPaint;
|
||||||
import android.text.style.CharacterStyle;
|
import android.text.style.CharacterStyle;
|
||||||
import android.text.style.ForegroundColorSpan;
|
import android.text.style.ForegroundColorSpan;
|
||||||
import android.text.style.MetricAffectingSpan;
|
import android.text.style.MetricAffectingSpan;
|
||||||
import android.text.style.StrikethroughSpan;
|
import android.text.style.StrikethroughSpan;
|
||||||
|
import android.view.View;
|
||||||
|
import android.widget.Button;
|
||||||
import android.widget.EditText;
|
import android.widget.EditText;
|
||||||
|
import android.widget.TextView;
|
||||||
|
|
||||||
import androidx.annotation.NonNull;
|
import androidx.annotation.NonNull;
|
||||||
import androidx.annotation.Nullable;
|
import androidx.annotation.Nullable;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
import java.util.concurrent.Executors;
|
import java.util.concurrent.Executors;
|
||||||
|
|
||||||
import io.noties.markwon.Markwon;
|
import io.noties.markwon.Markwon;
|
||||||
@ -40,7 +46,7 @@ public class EditorActivity extends Activity {
|
|||||||
setContentView(R.layout.activity_editor);
|
setContentView(R.layout.activity_editor);
|
||||||
|
|
||||||
this.editText = findViewById(R.id.edit_text);
|
this.editText = findViewById(R.id.edit_text);
|
||||||
|
initBottomBar();
|
||||||
|
|
||||||
// simple_process();
|
// simple_process();
|
||||||
|
|
||||||
@ -164,7 +170,7 @@ public class EditorActivity extends Activity {
|
|||||||
private static MarkwonEditor.EditSpanHandler createEditSpanHandler() {
|
private static MarkwonEditor.EditSpanHandler createEditSpanHandler() {
|
||||||
// Please note that here we specify spans THAT ARE USED IN MARKDOWN
|
// Please note that here we specify spans THAT ARE USED IN MARKDOWN
|
||||||
return EditSpanHandlerBuilder.create()
|
return EditSpanHandlerBuilder.create()
|
||||||
.include(StrongEmphasisSpan.class, (store, editable, input, span, spanStart, spanTextLength) -> {
|
.handleMarkdownSpan(StrongEmphasisSpan.class, (store, editable, input, span, spanStart, spanTextLength) -> {
|
||||||
editable.setSpan(
|
editable.setSpan(
|
||||||
store.get(StrongEmphasisSpan.class),
|
store.get(StrongEmphasisSpan.class),
|
||||||
spanStart,
|
spanStart,
|
||||||
@ -172,7 +178,7 @@ public class EditorActivity extends Activity {
|
|||||||
Spanned.SPAN_EXCLUSIVE_EXCLUSIVE
|
Spanned.SPAN_EXCLUSIVE_EXCLUSIVE
|
||||||
);
|
);
|
||||||
})
|
})
|
||||||
.include(EmphasisSpan.class, (store, editable, input, span, spanStart, spanTextLength) -> {
|
.handleMarkdownSpan(EmphasisSpan.class, (store, editable, input, span, spanStart, spanTextLength) -> {
|
||||||
editable.setSpan(
|
editable.setSpan(
|
||||||
store.get(EmphasisSpan.class),
|
store.get(EmphasisSpan.class),
|
||||||
spanStart,
|
spanStart,
|
||||||
@ -180,7 +186,7 @@ public class EditorActivity extends Activity {
|
|||||||
Spanned.SPAN_EXCLUSIVE_EXCLUSIVE
|
Spanned.SPAN_EXCLUSIVE_EXCLUSIVE
|
||||||
);
|
);
|
||||||
})
|
})
|
||||||
.include(StrikethroughSpan.class, (store, editable, input, span, spanStart, spanTextLength) -> {
|
.handleMarkdownSpan(StrikethroughSpan.class, (store, editable, input, span, spanStart, spanTextLength) -> {
|
||||||
editable.setSpan(
|
editable.setSpan(
|
||||||
store.get(StrikethroughSpan.class),
|
store.get(StrikethroughSpan.class),
|
||||||
spanStart,
|
spanStart,
|
||||||
@ -188,7 +194,7 @@ public class EditorActivity extends Activity {
|
|||||||
Spanned.SPAN_EXCLUSIVE_EXCLUSIVE
|
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
|
// we do not add offset here because markwon (by default) adds spaces
|
||||||
// around inline code
|
// around inline code
|
||||||
editable.setSpan(
|
editable.setSpan(
|
||||||
@ -198,7 +204,7 @@ public class EditorActivity extends Activity {
|
|||||||
Spanned.SPAN_EXCLUSIVE_EXCLUSIVE
|
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
|
// we do not handle indented code blocks here
|
||||||
if (input.charAt(spanStart) == '`') {
|
if (input.charAt(spanStart) == '`') {
|
||||||
final int firstLineEnd = input.indexOf('\n', 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(
|
editable.setSpan(
|
||||||
store.get(BlockQuoteSpan.class),
|
store.get(BlockQuoteSpan.class),
|
||||||
spanStart,
|
spanStart,
|
||||||
@ -222,7 +228,7 @@ public class EditorActivity extends Activity {
|
|||||||
Spanned.SPAN_EXCLUSIVE_EXCLUSIVE
|
Spanned.SPAN_EXCLUSIVE_EXCLUSIVE
|
||||||
);
|
);
|
||||||
})
|
})
|
||||||
.include(LinkSpan.class, (store, editable, input, span, spanStart, spanTextLength) -> {
|
.handleMarkdownSpan(LinkSpan.class, (store, editable, input, span, spanStart, spanTextLength) -> {
|
||||||
editable.setSpan(
|
editable.setSpan(
|
||||||
store.get(EditLinkSpan.class),
|
store.get(EditLinkSpan.class),
|
||||||
// add underline only for link text
|
// add underline only for link text
|
||||||
@ -235,6 +241,81 @@ public class EditorActivity extends Activity {
|
|||||||
.build();
|
.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 {
|
private static class CustomPunctuationSpan extends ForegroundColorSpan {
|
||||||
CustomPunctuationSpan() {
|
CustomPunctuationSpan() {
|
||||||
super(0xFFFF0000); // RED
|
super(0xFFFF0000); // RED
|
||||||
|
@ -1,17 +1,72 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?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_width="match_parent"
|
||||||
android:layout_height="match_parent"
|
android:layout_height="match_parent"
|
||||||
android:clipToPadding="false"
|
android:clipToPadding="false"
|
||||||
|
android:orientation="vertical"
|
||||||
android:padding="8dip">
|
android:padding="8dip">
|
||||||
|
|
||||||
<EditText
|
<FrameLayout
|
||||||
android:id="@+id/edit_text"
|
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_width="match_parent"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:autofillHints="none"
|
android:orientation="horizontal">
|
||||||
android:hint="Markdown..."
|
|
||||||
android:inputType="text|textLongMessage|textMultiLine"
|
|
||||||
android:maxLines="100" />
|
|
||||||
|
|
||||||
</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