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