Add editor sample

This commit is contained in:
Dimitry Ivanov 2019-11-07 14:02:07 +03:00
parent 8768e8a33c
commit f1e750b305
15 changed files with 460 additions and 2712 deletions

View File

@ -53,11 +53,6 @@
</activity> </activity>
<activity
android:name=".edit.EditActivity"
android:exported="true"
android:windowSoftInputMode="adjustResize" />
</application> </application>
</manifest> </manifest>

View File

@ -1,185 +0,0 @@
package io.noties.markwon.app.edit;
import android.app.Activity;
import android.os.Bundle;
import android.text.Editable;
import android.text.Spanned;
import android.text.TextPaint;
import android.text.style.ForegroundColorSpan;
import android.text.style.MetricAffectingSpan;
import android.text.style.UnderlineSpan;
import android.widget.EditText;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import java.util.concurrent.Executors;
import io.noties.debug.AndroidLogDebugOutput;
import io.noties.debug.Debug;
import io.noties.markwon.Markwon;
import io.noties.markwon.app.R;
import io.noties.markwon.core.MarkwonTheme;
import io.noties.markwon.core.spans.BlockQuoteSpan;
import io.noties.markwon.core.spans.CodeBlockSpan;
import io.noties.markwon.core.spans.CodeSpan;
import io.noties.markwon.core.spans.LinkSpan;
import io.noties.markwon.core.spans.StrongEmphasisSpan;
import io.noties.markwon.editor.MarkwonEditor;
import io.noties.markwon.editor.MarkwonEditorTextWatcher;
public class EditActivity extends Activity {
static {
Debug.init(new AndroidLogDebugOutput(true));
}
//
// private static final String S = "**bold** it seems to **work** for now, new lines are cool to certain extend **yo**!\n" +
// "\n" +
// "> quote!\n" +
// "> > nested quote!\n" +
// "\n" +
// "**bold** man, make it bold!\n" +
// "\n" +
// "# Head\n" +
// "## Head\n" +
// "\n" +
// "man, **crazy** thing called love.... **work**, **work** **work** man, super weird,\n" +
// "\n" +
// "`code`, yeah and code doesn't work\n" +
// "\n" +
// "* one\n" +
// "* two\n" +
// "* three\n" +
// "* * hey!\n" +
// " * super hey\n" +
// "\n" +
// "it does seem good **bold**, now shifted... **bold** man, now restored **bold** *em* sd\n" +
// "\n" +
// "[link](#) is it? ![image](./png) hey! **bold**\n" +
// "\n";
@Override
public void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_edit);
final EditText editText = findViewById(R.id.edit_text);
final Markwon markwon = Markwon.create(this);
final MarkwonTheme theme = markwon.configuration().theme();
final MarkwonEditor editor = MarkwonEditor.builder(markwon)
.withPunctuationSpan(MarkdownPunctuationSpan.class, MarkdownPunctuationSpan::new)
.includeEditSpan(Bold.class, Bold::new)
.includeEditSpan(CodeSpan.class, () -> new CodeSpan(theme))
.includeEditSpan(UnderlineSpan.class, UnderlineSpan::new)
.includeEditSpan(CodeBlockSpan.class, () -> new CodeBlockSpan(theme))
.includeEditSpan(BlockQuoteSpan.class, () -> new BlockQuoteSpan(theme))
.withEditSpanHandlerFor(StrongEmphasisSpan.class, new MarkwonEditor.EditSpanHandler<StrongEmphasisSpan>() {
@Override
public void handle(@NonNull MarkwonEditor.SpanStore store, @NonNull Editable editable, @NonNull String input, @NonNull StrongEmphasisSpan span, int spanStart, int spanTextLength) {
editable.setSpan(
store.get(Bold.class),
spanStart,
// we know that strong emphasis is delimited with 2 characters on both sides
spanStart + spanTextLength + 4,
Spanned.SPAN_EXCLUSIVE_EXCLUSIVE
);
}
})
.withEditSpanHandlerFor(LinkSpan.class, new MarkwonEditor.EditSpanHandler<LinkSpan>() {
@Override
public void handle(@NonNull MarkwonEditor.SpanStore store, @NonNull Editable editable, @NonNull String input, @NonNull LinkSpan span, int spanStart, int spanTextLength) {
editable.setSpan(
store.get(UnderlineSpan.class),
// add underline only for link text
spanStart + 1,
spanStart + 1 + spanTextLength,
Spanned.SPAN_EXCLUSIVE_EXCLUSIVE
);
}
})
.withEditSpanHandlerFor(CodeSpan.class, new MarkwonEditor.EditSpanHandler<CodeSpan>() {
@Override
public void handle(@NonNull MarkwonEditor.SpanStore store, @NonNull Editable editable, @NonNull String input, @NonNull CodeSpan span, int spanStart, int spanTextLength) {
editable.setSpan(
store.get(CodeSpan.class),
spanStart,
spanStart + spanTextLength,
Spanned.SPAN_EXCLUSIVE_EXCLUSIVE
);
}
})
.withEditSpanHandlerFor(CodeBlockSpan.class, new MarkwonEditor.EditSpanHandler<CodeBlockSpan>() {
@Override
public void handle(@NonNull MarkwonEditor.SpanStore store, @NonNull Editable editable, @NonNull String input, @NonNull CodeBlockSpan span, int spanStart, int spanTextLength) {
// if starts with backticks -> count them and then add everything until the line end
if (input.charAt(spanStart) == '`') {
final int firstLineEnd = input.indexOf('\n', spanStart);
if (firstLineEnd == -1) return;
int lastLineEnd = input.indexOf('\n', spanStart + (firstLineEnd - spanStart) + spanTextLength + 1);
if (lastLineEnd == -1) lastLineEnd = input.length();
editable.setSpan(
store.get(CodeBlockSpan.class),
spanStart,
lastLineEnd,
Spanned.SPAN_EXCLUSIVE_EXCLUSIVE
);
} else {
// todo: just everything until the end
editable.setSpan(
store.get(CodeBlockSpan.class),
spanStart,
spanStart + spanTextLength,
Spanned.SPAN_EXCLUSIVE_EXCLUSIVE
);
}
}
})
.withEditSpanHandlerFor(BlockQuoteSpan.class, new MarkwonEditor.EditSpanHandler<BlockQuoteSpan>() {
@Override
public void handle(@NonNull MarkwonEditor.SpanStore store, @NonNull Editable editable, @NonNull String input, @NonNull BlockQuoteSpan span, int spanStart, int spanTextLength) {
editable.setSpan(
store.get(BlockQuoteSpan.class),
spanStart,
spanStart + spanTextLength,
Spanned.SPAN_EXCLUSIVE_EXCLUSIVE
);
}
})
.build();
editText.addTextChangedListener(MarkwonEditorTextWatcher.withPreRender(
editor,
Executors.newCachedThreadPool(),
editText));
}
private static class MarkdownPunctuationSpan extends ForegroundColorSpan {
MarkdownPunctuationSpan() {
super(0xFFFF0000);
}
}
private static class Bold extends MetricAffectingSpan {
public Bold() {
super();
}
@Override
public void updateDrawState(TextPaint tp) {
update(tp);
}
@Override
public void updateMeasureState(@NonNull TextPaint textPaint) {
update(textPaint);
}
private void update(@NonNull TextPaint paint) {
paint.setFakeBoldText(true);
}
}
}

View File

@ -8,16 +8,38 @@ import androidx.annotation.Nullable;
import java.util.HashMap; import java.util.HashMap;
import java.util.Map; import java.util.Map;
class EditSpanHandlerBuilder { /**
* @since 4.2.0-SNAPSHOT
*/
public class EditSpanHandlerBuilder {
private final Map<Class<?>, MarkwonEditor.EditSpanHandler> map = new HashMap<>(3); public interface EditSpanHandlerTyped<T> {
void handle(
@NonNull MarkwonEditor.EditSpanStore store,
@NonNull Editable editable,
@NonNull String input,
@NonNull T span,
int spanStart,
int spanTextLength);
}
<T> void include(@NonNull Class<T> type, @NonNull MarkwonEditor.EditSpanHandler<T> handler) { @NonNull
public static EditSpanHandlerBuilder create() {
return new EditSpanHandlerBuilder();
}
private final Map<Class<?>, EditSpanHandlerTyped> map = new HashMap<>(3);
@NonNull
public <T> EditSpanHandlerBuilder include(
@NonNull Class<T> type,
@NonNull EditSpanHandlerTyped<T> handler) {
map.put(type, handler); map.put(type, handler);
return this;
} }
@Nullable @Nullable
MarkwonEditor.EditSpanHandler build() { public MarkwonEditor.EditSpanHandler build() {
if (map.size() == 0) { if (map.size() == 0) {
return null; return null;
} }
@ -26,15 +48,21 @@ class EditSpanHandlerBuilder {
private static class EditSpanHandlerImpl implements MarkwonEditor.EditSpanHandler { private static class EditSpanHandlerImpl implements MarkwonEditor.EditSpanHandler {
private final Map<Class<?>, MarkwonEditor.EditSpanHandler> map; private final Map<Class<?>, EditSpanHandlerTyped> map;
EditSpanHandlerImpl(@NonNull Map<Class<?>, MarkwonEditor.EditSpanHandler> map) { EditSpanHandlerImpl(@NonNull Map<Class<?>, EditSpanHandlerTyped> map) {
this.map = map; this.map = map;
} }
@Override @Override
public void handle(@NonNull MarkwonEditor.SpanStore store, @NonNull Editable editable, @NonNull String input, @NonNull Object span, int spanStart, int spanTextLength) { public void handle(
final MarkwonEditor.EditSpanHandler handler = map.get(span.getClass()); @NonNull MarkwonEditor.EditSpanStore store,
@NonNull Editable editable,
@NonNull String input,
@NonNull Object span,
int spanStart,
int spanTextLength) {
final EditSpanHandlerTyped handler = map.get(span.getClass());
if (handler != null) { if (handler != null) {
//noinspection unchecked //noinspection unchecked
handler.handle(store, editable, input, span, spanStart, spanTextLength); handler.handle(store, editable, input, span, spanStart, spanTextLength);

View File

@ -3,29 +3,31 @@ package io.noties.markwon.editor;
import android.text.Editable; import android.text.Editable;
import androidx.annotation.NonNull; import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import java.util.HashMap; import java.util.HashMap;
import java.util.Map; import java.util.Map;
import io.noties.markwon.Markwon; import io.noties.markwon.Markwon;
// todo: if multiple spans are used via factory... only the first one is delivered to edit-span-handler
// does it though? yeah... only the first one and then break... deliver all?
// todo: how to reuse existing spanFactories? to obtain a value they require render-props.... // todo: how to reuse existing spanFactories? to obtain a value they require render-props....
// maybe.. mock them? plus, spanFactory can return multiple spans // maybe.. mock them? plus, spanFactory can return multiple spans
// todo: we can actually create punctuation span with reasonable defaults to be used by default // 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)
* @see #process(Editable)
* @see #preRender(Editable, PreRenderResultListener)
* @since 4.2.0-SNAPSHOT * @since 4.2.0-SNAPSHOT
*/ */
public abstract class MarkwonEditor { public abstract class MarkwonEditor {
public interface SpanStore { public interface EditSpanStore {
/** /**
* If a span of specified type was not registered with {@link Builder#includeEditSpan(Class, SpanFactory)} * If a span of specified type was not registered with {@link Builder#includeEditSpan(Class, EditSpanFactory)}
* then an exception is raised. * then an exception is raised.
* *
* @param type of a span to obtain * @param type of a span to obtain
@ -35,34 +37,68 @@ public abstract class MarkwonEditor {
<T> T get(Class<T> type); <T> T get(Class<T> type);
} }
public interface SpanFactory<T> { public interface EditSpanFactory<T> {
@NonNull @NonNull
T create(); T create();
} }
public interface EditSpanHandler<T> { /**
* Interface to handle _original_ span that is present in rendered markdown. Can be useful
* to add specific spans for EditText (for example, make text bold to better indicate
* strong emphasis used in markdown input).
*
* @see Builder#withEditSpanHandler(EditSpanHandler)
* @see EditSpanHandlerBuilder
*/
public interface EditSpanHandler {
void handle( void handle(
@NonNull SpanStore store, @NonNull EditSpanStore store,
@NonNull Editable editable, @NonNull Editable editable,
@NonNull String input, @NonNull String input,
@NonNull T span, @NonNull Object span,
int spanStart, int spanStart,
int spanTextLength); int spanTextLength);
} }
public interface PreRenderResult { public interface PreRenderResult {
// With which pre-render method was called as input /**
* @return Editable instance for which result was calculated. This must not be
* actual Editable of EditText
*/
@NonNull @NonNull
Editable resultEditable(); Editable resultEditable();
/**
* Dispatch pre-rendering result to EditText
*
* @param editable to dispatch result to
*/
void dispatchTo(@NonNull Editable editable); void dispatchTo(@NonNull Editable editable);
} }
/**
* @see #preRender(Editable, PreRenderResultListener)
*/
public interface PreRenderResultListener { public interface PreRenderResultListener {
void onPreRenderResult(@NonNull PreRenderResult result); void onPreRenderResult(@NonNull PreRenderResult result);
} }
/**
* Creates default instance of {@link MarkwonEditor}. By default it will handle only
* punctuation spans (highlight markdown punctuation and nothing more).
*
* @see #builder(Markwon)
*/
@NonNull
public static MarkwonEditor create(@NonNull Markwon markwon) {
return builder(markwon).build();
}
/**
* @see #create(Markwon)
* @see Builder
*/
@NonNull @NonNull
public static Builder builder(@NonNull Markwon markwon) { public static Builder builder(@NonNull Markwon markwon) {
return new Builder(markwon); return new Builder(markwon);
@ -78,9 +114,12 @@ public abstract class MarkwonEditor {
public abstract void process(@NonNull Editable editable); public abstract void process(@NonNull Editable editable);
/** /**
* Pre-render highlight result. Can be useful to create highlight information on a different
* thread.
* <p>
* Please note that currently only `setSpan` and `removeSpan` actions will be recorded (and thus dispatched). * Please note that currently only `setSpan` and `removeSpan` actions will be recorded (and thus dispatched).
* make sure you use only these methods in your {@link EditSpanHandler}, or implement the required * Make sure you use only these methods in your {@link EditSpanHandler}, or implement the required
* functionality in some other way. * functionality some other way.
* *
* @param editable to process and pre-render * @param editable to process and pre-render
* @param preRenderListener listener to be notified when pre-render result will be ready * @param preRenderListener listener to be notified when pre-render result will be ready
@ -88,40 +127,40 @@ public abstract class MarkwonEditor {
*/ */
public abstract void preRender(@NonNull Editable editable, @NonNull PreRenderResultListener preRenderListener); public abstract void preRender(@NonNull Editable editable, @NonNull PreRenderResultListener preRenderListener);
public static class Builder { public static class Builder {
private final Markwon markwon; private final Markwon markwon;
private final EditSpanHandlerBuilder editSpanHandlerBuilder = new EditSpanHandlerBuilder();
private Class<?> punctuationSpanType; private Class<?> punctuationSpanType;
private Map<Class<?>, SpanFactory> spans = new HashMap<>(3); private Map<Class<?>, EditSpanFactory> spans = new HashMap<>(3);
private EditSpanHandler editSpanHandler;
Builder(@NonNull Markwon markwon) { Builder(@NonNull Markwon markwon) {
this.markwon = markwon; this.markwon = markwon;
} }
/** /**
* The only required argument which will make {@link MarkwonEditor} apply specified span only * Specify which punctuation span will be used.
* to markdown punctuation
* *
* @param type of the span * @param type of the span
* @param factory to create a new instance of the span * @param factory to create a new instance of the span
*/ */
@NonNull @NonNull
public <T> Builder withPunctuationSpan(@NonNull Class<T> type, @NonNull SpanFactory<T> factory) { public <T> Builder withPunctuationSpan(@NonNull Class<T> type, @NonNull EditSpanFactory<T> factory) {
this.punctuationSpanType = type; this.punctuationSpanType = type;
this.spans.put(type, factory); this.spans.put(type, factory);
return this; return this;
} }
/** /**
* Include specific span that will be used in highlighting. It is important to understand * Include additional span handling that is used in highlighting. It is important to understand
* that it is not the span that is used by Markwon, but instead your own span that you * that it is not the span that is used by Markwon, but instead your own span that you
* apply in a custom {@link EditSpanHandler} specified by {@link #withEditSpanHandlerFor(Class, EditSpanHandler)}. * apply in a custom {@link EditSpanHandler} specified by {@link #withEditSpanHandler(EditSpanHandler)}.
* You can apply a Markwon bundled span (or any other) but it must be still explicitly * You can apply a Markwon bundled span (or any other) but it must be still explicitly
* included by this method. * included by this method.
* <p> * <p>
* The span will be exposed via {@link SpanStore} in your custom {@link EditSpanHandler}. * The span will be exposed via {@link EditSpanStore} in your custom {@link EditSpanHandler}.
* If you do not use a custom {@link EditSpanHandler} you do not need to specify any span here. * If you do not use a custom {@link EditSpanHandler} you do not need to specify any span here.
* *
* @param type of a span to include * @param type of a span to include
@ -130,31 +169,50 @@ public abstract class MarkwonEditor {
@NonNull @NonNull
public <T> Builder includeEditSpan( public <T> Builder includeEditSpan(
@NonNull Class<T> type, @NonNull Class<T> type,
@NonNull SpanFactory<T> factory) { @NonNull EditSpanFactory<T> factory) {
this.spans.put(type, factory); this.spans.put(type, factory);
return this; return this;
} }
/**
* Additional handling of markdown spans.
*
* @param editSpanHandler handler for additional highlight spans
* @see EditSpanHandler
* @see EditSpanHandlerBuilder
*/
@NonNull @NonNull
public <T> Builder withEditSpanHandlerFor(@NonNull Class<T> type, @NonNull EditSpanHandler<T> editSpanHandler) { public Builder withEditSpanHandler(@Nullable EditSpanHandler editSpanHandler) {
this.editSpanHandlerBuilder.include(type, editSpanHandler); this.editSpanHandler = editSpanHandler;
return this; return this;
} }
@NonNull @NonNull
public MarkwonEditor build() { public MarkwonEditor build() {
final Class<?> punctuationSpanType = this.punctuationSpanType; Class<?> punctuationSpanType = this.punctuationSpanType;
if (punctuationSpanType == null) { if (punctuationSpanType == null) {
throw new IllegalStateException("Punctuation span type is required, " + withPunctuationSpan(PunctuationSpan.class, new EditSpanFactory<PunctuationSpan>() {
"add with Builder#withPunctuationSpan method"); @NonNull
@Override
public PunctuationSpan create() {
return new PunctuationSpan();
}
});
punctuationSpanType = this.punctuationSpanType;
}
// if we have no editSpanHandler, but spans are registered -> throw an error
if (spans.size() > 1 && editSpanHandler == null) {
throw new IllegalStateException("There is no need to include edit spans " +
"when you do not use custom EditSpanHandler");
} }
return new MarkwonEditorImpl( return new MarkwonEditorImpl(
markwon, markwon,
spans, spans,
punctuationSpanType, punctuationSpanType,
editSpanHandlerBuilder.build()); editSpanHandler);
} }
} }
} }

View File

@ -20,7 +20,7 @@ import io.noties.markwon.editor.diff_match_patch.Diff;
class MarkwonEditorImpl extends MarkwonEditor { class MarkwonEditorImpl extends MarkwonEditor {
private final Markwon markwon; private final Markwon markwon;
private final Map<Class<?>, SpanFactory> spans; private final Map<Class<?>, EditSpanFactory> spans;
private final Class<?> punctuationSpanType; private final Class<?> punctuationSpanType;
@Nullable @Nullable
@ -28,7 +28,7 @@ class MarkwonEditorImpl extends MarkwonEditor {
MarkwonEditorImpl( MarkwonEditorImpl(
@NonNull Markwon markwon, @NonNull Markwon markwon,
@NonNull Map<Class<?>, SpanFactory> spans, @NonNull Map<Class<?>, EditSpanFactory> spans,
@NonNull Class<?> punctuationSpanType, @NonNull Class<?> punctuationSpanType,
@Nullable EditSpanHandler editSpanHandler) { @Nullable EditSpanHandler editSpanHandler) {
this.markwon = markwon; this.markwon = markwon;
@ -47,7 +47,7 @@ class MarkwonEditorImpl extends MarkwonEditor {
final EditSpanHandler editSpanHandler = this.editSpanHandler; final EditSpanHandler editSpanHandler = this.editSpanHandler;
final boolean hasAdditionalSpans = editSpanHandler != null; final boolean hasAdditionalSpans = editSpanHandler != null;
final SpanStoreImpl store = new SpanStoreImpl(editable, spans); final EditSpanStoreImpl store = new EditSpanStoreImpl(editable, spans);
try { try {
final List<Diff> diffs = diff_match_patch.diff_main(input, markdown); final List<Diff> diffs = diff_match_patch.diff_main(input, markdown);
@ -81,6 +81,9 @@ class MarkwonEditorImpl extends MarkwonEditor {
span, span,
start, start,
renderedMarkdown.getSpanEnd(span) - markdownLength); renderedMarkdown.getSpanEnd(span) - markdownLength);
// NB, we do not break here in case of SpanFactory
// returns multiple spans for a markdown node, this way
// we will handle all of them
} }
} }
} }
@ -153,13 +156,13 @@ class MarkwonEditorImpl extends MarkwonEditor {
return map; return map;
} }
static class SpanStoreImpl implements SpanStore { static class EditSpanStoreImpl implements EditSpanStore {
private final Spannable spannable; private final Spannable spannable;
private final Map<Class<?>, SpanFactory> spans; private final Map<Class<?>, EditSpanFactory> spans;
private final Map<Class<?>, List<Object>> map; private final Map<Class<?>, List<Object>> map;
SpanStoreImpl(@NonNull Spannable spannable, @NonNull Map<Class<?>, SpanFactory> spans) { EditSpanStoreImpl(@NonNull Spannable spannable, @NonNull Map<Class<?>, EditSpanFactory> spans) {
this.spannable = spannable; this.spannable = spannable;
this.spans = spans; this.spans = spans;
this.map = extractSpans(spannable, spans.keySet()); this.map = extractSpans(spannable, spans.keySet());
@ -175,7 +178,7 @@ class MarkwonEditorImpl extends MarkwonEditor {
if (list != null && list.size() > 0) { if (list != null && list.size() > 0) {
span = list.remove(0); span = list.remove(0);
} else { } else {
final SpanFactory spanFactory = spans.get(type); final EditSpanFactory spanFactory = spans.get(type);
if (spanFactory == null) { if (spanFactory == null) {
throw new IllegalStateException("Requested type `" + type.getName() + "` was " + throw new IllegalStateException("Requested type `" + type.getName() + "` was " +
"not registered, use Builder#includeEditSpan method to register"); "not registered, use Builder#includeEditSpan method to register");

View File

@ -17,6 +17,8 @@ import java.util.concurrent.Future;
* *
* @see MarkwonEditor#process(Editable) * @see MarkwonEditor#process(Editable)
* @see MarkwonEditor#preRender(Editable, MarkwonEditor.PreRenderResultListener) * @see MarkwonEditor#preRender(Editable, MarkwonEditor.PreRenderResultListener)
* @see #withProcess(MarkwonEditor)
* @see #withPreRender(MarkwonEditor, ExecutorService, EditText)
* @since 4.2.0-SNAPSHOT * @since 4.2.0-SNAPSHOT
*/ */
public abstract class MarkwonEditorTextWatcher implements TextWatcher { public abstract class MarkwonEditorTextWatcher implements TextWatcher {
@ -54,7 +56,7 @@ public abstract class MarkwonEditorTextWatcher implements TextWatcher {
private boolean selfChange; private boolean selfChange;
public WithProcess(@NonNull MarkwonEditor editor) { WithProcess(@NonNull MarkwonEditor editor) {
this.editor = editor; this.editor = editor;
} }
@ -84,6 +86,8 @@ public abstract class MarkwonEditorTextWatcher implements TextWatcher {
private Future<?> future; private Future<?> future;
private boolean selfChange;
WithPreRender( WithPreRender(
@NonNull MarkwonEditor editor, @NonNull MarkwonEditor editor,
@NonNull ExecutorService executorService, @NonNull ExecutorService executorService,
@ -107,6 +111,10 @@ public abstract class MarkwonEditorTextWatcher implements TextWatcher {
@Override @Override
public void afterTextChanged(final Editable s) { public void afterTextChanged(final Editable s) {
if (selfChange) {
return;
}
// todo: maybe checking hash is not so performant? // todo: maybe checking hash is not so performant?
// what if we create a atomic reference and use it (with tag applied to editText)? // what if we create a atomic reference and use it (with tag applied to editText)?
@ -121,12 +129,17 @@ public abstract class MarkwonEditorTextWatcher implements TextWatcher {
@Override @Override
public void onPreRenderResult(@NonNull final MarkwonEditor.PreRenderResult result) { public void onPreRenderResult(@NonNull final MarkwonEditor.PreRenderResult result) {
if (editText != null) { if (editText != null) {
final int key = result.resultEditable().toString().hashCode(); final int key = key(result.resultEditable());
editText.post(new Runnable() { editText.post(new Runnable() {
@Override @Override
public void run() { public void run() {
if (key == editText.getText().toString().hashCode()) { if (key == key(editText.getText())) {
result.dispatchTo(editText.getText()); selfChange = true;
try {
result.dispatchTo(editText.getText());
} finally {
selfChange = false;
}
} }
} }
}); });
@ -136,5 +149,12 @@ public abstract class MarkwonEditorTextWatcher implements TextWatcher {
} }
}); });
} }
static int key(@NonNull Editable editable) {
// toString is important here, as using #hashCode directly
// would also check for spans (and some spans can be added/removed). This is why
// we are checking for exact match of text
return editable.toString().hashCode();
}
} }
} }

View File

@ -0,0 +1,17 @@
package io.noties.markwon.editor;
import android.text.TextPaint;
import android.text.style.CharacterStyle;
import io.noties.markwon.utils.ColorUtils;
class PunctuationSpan extends CharacterStyle {
private static final int DEF_PUNCTUATION_ALPHA = 75;
@Override
public void updateDrawState(TextPaint tp) {
final int color = ColorUtils.applyAlpha(tp.getColor(), DEF_PUNCTUATION_ALPHA);
tp.setColor(color);
}
}

View File

@ -34,6 +34,7 @@ android {
dependencies { dependencies {
implementation project(':markwon-core') implementation project(':markwon-core')
implementation project(':markwon-editor')
implementation project(':markwon-ext-latex') implementation project(':markwon-ext-latex')
implementation project(':markwon-ext-strikethrough') implementation project(':markwon-ext-strikethrough')
implementation project(':markwon-ext-tables') implementation project(':markwon-ext-tables')

View File

@ -28,6 +28,7 @@
<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" />
</application> </application>

View File

@ -22,6 +22,7 @@ import io.noties.markwon.sample.basicplugins.BasicPluginsActivity;
import io.noties.markwon.sample.core.CoreActivity; import io.noties.markwon.sample.core.CoreActivity;
import io.noties.markwon.sample.customextension.CustomExtensionActivity; import io.noties.markwon.sample.customextension.CustomExtensionActivity;
import io.noties.markwon.sample.customextension2.CustomExtensionActivity2; import io.noties.markwon.sample.customextension2.CustomExtensionActivity2;
import io.noties.markwon.sample.editor.EditorActivity;
import io.noties.markwon.sample.html.HtmlActivity; import io.noties.markwon.sample.html.HtmlActivity;
import io.noties.markwon.sample.latex.LatexActivity; import io.noties.markwon.sample.latex.LatexActivity;
import io.noties.markwon.sample.precomputed.PrecomputedActivity; import io.noties.markwon.sample.precomputed.PrecomputedActivity;
@ -117,6 +118,10 @@ public class MainActivity extends Activity {
activity = PrecomputedActivity.class; activity = PrecomputedActivity.class;
break; break;
case EDITOR:
activity = EditorActivity.class;
break;
default: default:
throw new IllegalStateException("No Activity is associated with sample-item: " + item); throw new IllegalStateException("No Activity is associated with sample-item: " + item);
} }

View File

@ -21,7 +21,9 @@ public enum Sample {
CUSTOM_EXTENSION_2(R.string.sample_custom_extension_2), CUSTOM_EXTENSION_2(R.string.sample_custom_extension_2),
PRECOMPUTED_TEXT(R.string.sample_precomputed_text); PRECOMPUTED_TEXT(R.string.sample_precomputed_text),
EDITOR(R.string.sample_editor);
private final int textResId; private final int textResId;

View File

@ -0,0 +1,271 @@
package io.noties.markwon.sample.editor;
import android.app.Activity;
import android.os.Bundle;
import android.text.Editable;
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.widget.EditText;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import java.util.concurrent.Executors;
import io.noties.markwon.Markwon;
import io.noties.markwon.core.MarkwonTheme;
import io.noties.markwon.core.spans.BlockQuoteSpan;
import io.noties.markwon.core.spans.CodeBlockSpan;
import io.noties.markwon.core.spans.CodeSpan;
import io.noties.markwon.core.spans.EmphasisSpan;
import io.noties.markwon.core.spans.LinkSpan;
import io.noties.markwon.core.spans.StrongEmphasisSpan;
import io.noties.markwon.editor.EditSpanHandlerBuilder;
import io.noties.markwon.editor.MarkwonEditor;
import io.noties.markwon.editor.MarkwonEditorTextWatcher;
import io.noties.markwon.ext.strikethrough.StrikethroughPlugin;
import io.noties.markwon.sample.R;
public class EditorActivity extends Activity {
private EditText editText;
@Override
public void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_editor);
this.editText = findViewById(R.id.edit_text);
// simple_process();
// simple_pre_render();
// custom_punctuation_span();
// additional_edit_span();
// additional_plugins();
multiple_edit_spans();
}
private void simple_process() {
// Process highlight in-place (right after text has changed)
// obtain Markwon instance
final Markwon markwon = Markwon.create(this);
// create editor
final MarkwonEditor editor = MarkwonEditor.create(markwon);
// set edit listener
editText.addTextChangedListener(MarkwonEditorTextWatcher.withProcess(editor));
}
private void simple_pre_render() {
// Process highlight in background thread
final Markwon markwon = Markwon.create(this);
final MarkwonEditor editor = MarkwonEditor.create(markwon);
editText.addTextChangedListener(MarkwonEditorTextWatcher.withPreRender(
editor,
Executors.newCachedThreadPool(),
editText));
}
private void custom_punctuation_span() {
// Use own punctuation span
final MarkwonEditor editor = MarkwonEditor.builder(Markwon.create(this))
.withPunctuationSpan(CustomPunctuationSpan.class, CustomPunctuationSpan::new)
.build();
editText.addTextChangedListener(MarkwonEditorTextWatcher.withProcess(editor));
}
private void additional_edit_span() {
// An additional span is used to highlight strong-emphasis
final MarkwonEditor editor = MarkwonEditor.builder(Markwon.create(this))
// This is required for edit-span cache
// We could use Markwon `StrongEmphasisSpan` here, but I use a different
// one to indicate that those are completely unrelated spans and must be
// treated differently.
.includeEditSpan(Bold.class, Bold::new)
.withEditSpanHandler(new MarkwonEditor.EditSpanHandler() {
@Override
public void handle(
@NonNull MarkwonEditor.EditSpanStore store,
@NonNull Editable editable,
@NonNull String input,
@NonNull Object span,
int spanStart,
int spanTextLength) {
if (span instanceof StrongEmphasisSpan) {
editable.setSpan(
// `includeEditSpan(Bold.class, Bold::new)` ensured that we have
// a span here to use (either reuse existing or create a new one)
store.get(Bold.class),
spanStart,
// we know that strong emphasis is delimited with 2 characters on both sides
spanStart + spanTextLength + 4,
Spanned.SPAN_EXCLUSIVE_EXCLUSIVE
);
}
}
})
.build();
editText.addTextChangedListener(MarkwonEditorTextWatcher.withProcess(editor));
}
private void additional_plugins() {
// As highlight works based on text-diff, everything that is present in input,
// but missing in resulting markdown is considered to be punctuation, this is why
// additional plugins do not need special handling
final Markwon markwon = Markwon.builder(this)
.usePlugin(StrikethroughPlugin.create())
.build();
final MarkwonEditor editor = MarkwonEditor.create(markwon);
editText.addTextChangedListener(MarkwonEditorTextWatcher.withProcess(editor));
}
private void multiple_edit_spans() {
final Markwon markwon = Markwon.builder(this)
.usePlugin(StrikethroughPlugin.create())
.build();
final MarkwonTheme theme = markwon.configuration().theme();
final MarkwonEditor editor = MarkwonEditor.builder(markwon)
.includeEditSpan(StrongEmphasisSpan.class, StrongEmphasisSpan::new)
.includeEditSpan(EmphasisSpan.class, EmphasisSpan::new)
.includeEditSpan(StrikethroughSpan.class, StrikethroughSpan::new)
.includeEditSpan(CodeSpan.class, () -> new CodeSpan(theme))
.includeEditSpan(CodeBlockSpan.class, () -> new CodeBlockSpan(theme))
.includeEditSpan(BlockQuoteSpan.class, () -> new BlockQuoteSpan(theme))
.includeEditSpan(EditLinkSpan.class, EditLinkSpan::new)
.withEditSpanHandler(createEditSpanHandler())
.build();
editText.addTextChangedListener(MarkwonEditorTextWatcher.withProcess(editor));
}
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) -> {
editable.setSpan(
store.get(StrongEmphasisSpan.class),
spanStart,
spanStart + spanTextLength + 4,
Spanned.SPAN_EXCLUSIVE_EXCLUSIVE
);
})
.include(EmphasisSpan.class, (store, editable, input, span, spanStart, spanTextLength) -> {
editable.setSpan(
store.get(EmphasisSpan.class),
spanStart,
spanStart + spanTextLength + 2,
Spanned.SPAN_EXCLUSIVE_EXCLUSIVE
);
})
.include(StrikethroughSpan.class, (store, editable, input, span, spanStart, spanTextLength) -> {
editable.setSpan(
store.get(StrikethroughSpan.class),
spanStart,
spanStart + spanTextLength + 4,
Spanned.SPAN_EXCLUSIVE_EXCLUSIVE
);
})
.include(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(
store.get(CodeSpan.class),
spanStart,
spanStart + spanTextLength,
Spanned.SPAN_EXCLUSIVE_EXCLUSIVE
);
})
.include(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);
if (firstLineEnd == -1) return;
int lastLineEnd = input.indexOf('\n', spanStart + (firstLineEnd - spanStart) + spanTextLength + 1);
if (lastLineEnd == -1) lastLineEnd = input.length();
editable.setSpan(
store.get(CodeBlockSpan.class),
spanStart,
lastLineEnd,
Spanned.SPAN_EXCLUSIVE_EXCLUSIVE
);
}
})
.include(BlockQuoteSpan.class, (store, editable, input, span, spanStart, spanTextLength) -> {
editable.setSpan(
store.get(BlockQuoteSpan.class),
spanStart,
spanStart + spanTextLength,
Spanned.SPAN_EXCLUSIVE_EXCLUSIVE
);
})
.include(LinkSpan.class, (store, editable, input, span, spanStart, spanTextLength) -> {
editable.setSpan(
store.get(EditLinkSpan.class),
// add underline only for link text
spanStart + 1,
spanStart + 1 + spanTextLength,
Spanned.SPAN_EXCLUSIVE_EXCLUSIVE
);
})
// returns nullable type
.build();
}
private static class CustomPunctuationSpan extends ForegroundColorSpan {
CustomPunctuationSpan() {
super(0xFFFF0000); // RED
}
}
private static class Bold extends MetricAffectingSpan {
public Bold() {
super();
}
@Override
public void updateDrawState(TextPaint tp) {
update(tp);
}
@Override
public void updateMeasureState(@NonNull TextPaint textPaint) {
update(textPaint);
}
private void update(@NonNull TextPaint paint) {
paint.setFakeBoldText(true);
}
}
private static class EditLinkSpan extends CharacterStyle {
@Override
public void updateDrawState(TextPaint tp) {
tp.setColor(tp.linkColor);
tp.setUnderlineText(true);
}
}
}

View File

@ -2,6 +2,7 @@
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android" <FrameLayout 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:padding="8dip"> android:padding="8dip">
<EditText <EditText
@ -9,8 +10,8 @@
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:autofillHints="none" android:autofillHints="none"
android:hint="Message..." android:hint="Markdown..."
android:maxLines="100" android:inputType="text|textLongMessage|textMultiLine"
android:inputType="text|textLongMessage|textMultiLine" /> android:maxLines="100" />
</FrameLayout> </FrameLayout>

View File

@ -25,4 +25,6 @@
<string name="sample_precomputed_text"># \# PrecomputedText\n\nUsage of TextSetter and PrecomputedTextCompat</string> <string name="sample_precomputed_text"># \# PrecomputedText\n\nUsage of TextSetter and PrecomputedTextCompat</string>
<string name="sample_editor"># \# Editor\n\n`MarkwonEditor` sample usage to highlight user input in EditText</string>
</resources> </resources>