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_.
|
||||
:::
|
@ -4,3 +4,10 @@ Markdown editor for Android based on `Markwon`.
|
||||
|
||||
Main principle: _difference_ between input text and rendered markdown is considered to be
|
||||
_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