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 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;
|
||||||
@ -20,44 +19,8 @@ import io.noties.markwon.Markwon;
|
|||||||
public abstract class MarkwonEditor {
|
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 {
|
public interface PreRenderResult {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -116,7 +79,7 @@ public abstract class MarkwonEditor {
|
|||||||
* thread.
|
* thread.
|
||||||
* <p>
|
* <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 EditHandler}, or implement the required
|
||||||
* functionality some other way.
|
* functionality some other way.
|
||||||
*
|
*
|
||||||
* @param editable to process and pre-render
|
* @param editable to process and pre-render
|
||||||
@ -129,15 +92,22 @@ public abstract class MarkwonEditor {
|
|||||||
public static class Builder {
|
public static class Builder {
|
||||||
|
|
||||||
private final Markwon markwon;
|
private final Markwon markwon;
|
||||||
|
private final PersistedSpans.Provider persistedSpansProvider = PersistedSpans.provider();
|
||||||
|
private final Map<Class<?>, EditHandler> editHandlers = new HashMap<>(0);
|
||||||
|
|
||||||
private Class<?> punctuationSpanType;
|
private Class<?> punctuationSpanType;
|
||||||
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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@NonNull
|
||||||
|
public <T> Builder useEditHandler(@NonNull EditHandler<T> handler) {
|
||||||
|
this.editHandlers.put(handler.markdownSpanType(), handler);
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Specify which punctuation span will be used.
|
* 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
|
* @param factory to create a new instance of the span
|
||||||
*/
|
*/
|
||||||
@NonNull
|
@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.punctuationSpanType = type;
|
||||||
this.spans.put(type, factory);
|
this.persistedSpansProvider.persistSpan(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;
|
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -190,7 +126,7 @@ public abstract class MarkwonEditor {
|
|||||||
|
|
||||||
Class<?> punctuationSpanType = this.punctuationSpanType;
|
Class<?> punctuationSpanType = this.punctuationSpanType;
|
||||||
if (punctuationSpanType == null) {
|
if (punctuationSpanType == null) {
|
||||||
withPunctuationSpan(PunctuationSpan.class, new EditSpanFactory<PunctuationSpan>() {
|
punctuationSpan(PunctuationSpan.class, new PersistedSpans.SpanFactory<PunctuationSpan>() {
|
||||||
@NonNull
|
@NonNull
|
||||||
@Override
|
@Override
|
||||||
public PunctuationSpan create() {
|
public PunctuationSpan create() {
|
||||||
@ -200,17 +136,54 @@ public abstract class MarkwonEditor {
|
|||||||
punctuationSpanType = this.punctuationSpanType;
|
punctuationSpanType = this.punctuationSpanType;
|
||||||
}
|
}
|
||||||
|
|
||||||
// if we have no editSpanHandler, but spans are registered -> throw an error
|
for (EditHandler handler : editHandlers.values()) {
|
||||||
if (spans.size() > 1 && editSpanHandler == null) {
|
handler.init(markwon);
|
||||||
throw new IllegalStateException("There is no need to include edit spans " +
|
handler.configurePersistedSpans(persistedSpansProvider);
|
||||||
"when you do not use custom EditSpanHandler");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
final SpansHandler spansHandler = editHandlers.size() == 0
|
||||||
|
? null
|
||||||
|
: new SpansHandlerImpl(editHandlers);
|
||||||
|
|
||||||
return new MarkwonEditorImpl(
|
return new MarkwonEditorImpl(
|
||||||
markwon,
|
markwon,
|
||||||
spans,
|
persistedSpansProvider,
|
||||||
punctuationSpanType,
|
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 androidx.annotation.Nullable;
|
||||||
|
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.Collection;
|
|
||||||
import java.util.HashMap;
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Map;
|
|
||||||
|
|
||||||
import io.noties.markwon.Markwon;
|
import io.noties.markwon.Markwon;
|
||||||
import io.noties.markwon.editor.diff_match_patch.Diff;
|
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 {
|
class MarkwonEditorImpl extends MarkwonEditor {
|
||||||
|
|
||||||
private final Markwon markwon;
|
private final Markwon markwon;
|
||||||
private final Map<Class<?>, EditSpanFactory> spans;
|
private final PersistedSpans.Provider persistedSpansProvider;
|
||||||
private final Class<?> punctuationSpanType;
|
private final Class<?> punctuationSpanType;
|
||||||
|
|
||||||
@Nullable
|
@Nullable
|
||||||
private final EditSpanHandler editSpanHandler;
|
private final SpansHandler spansHandler;
|
||||||
|
|
||||||
MarkwonEditorImpl(
|
MarkwonEditorImpl(
|
||||||
@NonNull Markwon markwon,
|
@NonNull Markwon markwon,
|
||||||
@NonNull Map<Class<?>, EditSpanFactory> spans,
|
@NonNull PersistedSpans.Provider persistedSpansProvider,
|
||||||
@NonNull Class<?> punctuationSpanType,
|
@NonNull Class<?> punctuationSpanType,
|
||||||
@Nullable EditSpanHandler editSpanHandler) {
|
@Nullable SpansHandler spansHandler) {
|
||||||
this.markwon = markwon;
|
this.markwon = markwon;
|
||||||
this.spans = spans;
|
this.persistedSpansProvider = persistedSpansProvider;
|
||||||
this.punctuationSpanType = punctuationSpanType;
|
this.punctuationSpanType = punctuationSpanType;
|
||||||
this.editSpanHandler = editSpanHandler;
|
this.spansHandler = spansHandler;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@ -49,10 +46,10 @@ class MarkwonEditorImpl extends MarkwonEditor {
|
|||||||
|
|
||||||
final String markdown = renderedMarkdown.toString();
|
final String markdown = renderedMarkdown.toString();
|
||||||
|
|
||||||
final EditSpanHandler editSpanHandler = this.editSpanHandler;
|
final SpansHandler spansHandler = this.spansHandler;
|
||||||
final boolean hasAdditionalSpans = editSpanHandler != null;
|
final boolean hasAdditionalSpans = spansHandler != null;
|
||||||
|
|
||||||
final EditSpanStoreImpl store = new EditSpanStoreImpl(editable, spans);
|
final PersistedSpans persistedSpans = persistedSpansProvider.provide(editable);
|
||||||
try {
|
try {
|
||||||
|
|
||||||
final List<Diff> diffs = diff_match_patch.diff_main(input, markdown);
|
final List<Diff> diffs = diff_match_patch.diff_main(input, markdown);
|
||||||
@ -68,8 +65,9 @@ class MarkwonEditorImpl extends MarkwonEditor {
|
|||||||
|
|
||||||
final int start = inputLength;
|
final int start = inputLength;
|
||||||
inputLength += diff.text.length();
|
inputLength += diff.text.length();
|
||||||
|
|
||||||
editable.setSpan(
|
editable.setSpan(
|
||||||
store.get(punctuationSpanType),
|
persistedSpans.get(punctuationSpanType),
|
||||||
start,
|
start,
|
||||||
inputLength,
|
inputLength,
|
||||||
Spanned.SPAN_EXCLUSIVE_EXCLUSIVE
|
Spanned.SPAN_EXCLUSIVE_EXCLUSIVE
|
||||||
@ -84,8 +82,8 @@ class MarkwonEditorImpl extends MarkwonEditor {
|
|||||||
for (Object span : spans) {
|
for (Object span : spans) {
|
||||||
if (markdownLength == renderedMarkdown.getSpanStart(span)) {
|
if (markdownLength == renderedMarkdown.getSpanStart(span)) {
|
||||||
|
|
||||||
editSpanHandler.handle(
|
spansHandler.handle(
|
||||||
store,
|
persistedSpans,
|
||||||
editable,
|
editable,
|
||||||
input,
|
input,
|
||||||
span,
|
span,
|
||||||
@ -126,8 +124,8 @@ class MarkwonEditorImpl extends MarkwonEditor {
|
|||||||
final int end = renderedMarkdown.getSpanEnd(span);
|
final int end = renderedMarkdown.getSpanEnd(span);
|
||||||
if (end <= markdownLength) {
|
if (end <= markdownLength) {
|
||||||
|
|
||||||
editSpanHandler.handle(
|
spansHandler.handle(
|
||||||
store,
|
persistedSpans,
|
||||||
editable,
|
editable,
|
||||||
input,
|
input,
|
||||||
span,
|
span,
|
||||||
@ -149,7 +147,7 @@ class MarkwonEditorImpl extends MarkwonEditor {
|
|||||||
}
|
}
|
||||||
|
|
||||||
} finally {
|
} 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 {
|
private static class Span {
|
||||||
final Object what;
|
final Object what;
|
||||||
final int start;
|
final int start;
|
||||||
|
@ -129,26 +129,43 @@ public abstract class MarkwonEditorTextWatcher implements TextWatcher {
|
|||||||
future = executorService.submit(new Runnable() {
|
future = executorService.submit(new Runnable() {
|
||||||
@Override
|
@Override
|
||||||
public void run() {
|
public void run() {
|
||||||
editor.preRender(s, new MarkwonEditor.PreRenderResultListener() {
|
try {
|
||||||
@Override
|
editor.preRender(s, new MarkwonEditor.PreRenderResultListener() {
|
||||||
public void onPreRenderResult(@NonNull final MarkwonEditor.PreRenderResult result) {
|
@Override
|
||||||
if (editText != null) {
|
public void onPreRenderResult(@NonNull final MarkwonEditor.PreRenderResult result) {
|
||||||
editText.post(new Runnable() {
|
final EditText et = editText;
|
||||||
@Override
|
if (et != null) {
|
||||||
public void run() {
|
et.post(new Runnable() {
|
||||||
if (key == generator) {
|
@Override
|
||||||
selfChange = true;
|
public void run() {
|
||||||
try {
|
if (key == generator) {
|
||||||
result.dispatchTo(editText.getText());
|
final EditText et = editText;
|
||||||
} finally {
|
if (et != null) {
|
||||||
selfChange = false;
|
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;
|
package io.noties.markwon.editor;
|
||||||
|
|
||||||
|
import android.text.Spanned;
|
||||||
|
|
||||||
import androidx.annotation.NonNull;
|
import androidx.annotation.NonNull;
|
||||||
import androidx.annotation.Nullable;
|
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
|
* @since 4.2.0-SNAPSHOT
|
||||||
*/
|
*/
|
||||||
public abstract class MarkwonEditorUtils {
|
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 {
|
public interface Match {
|
||||||
|
|
||||||
@NonNull
|
@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;
|
package io.noties.markwon.editor;
|
||||||
|
|
||||||
import android.text.SpannableStringBuilder;
|
import android.text.SpannableStringBuilder;
|
||||||
import android.text.Spanned;
|
|
||||||
|
|
||||||
import androidx.annotation.NonNull;
|
|
||||||
|
|
||||||
import org.junit.Test;
|
import org.junit.Test;
|
||||||
import org.junit.runner.RunWith;
|
import org.junit.runner.RunWith;
|
||||||
@ -11,135 +8,14 @@ import org.robolectric.RobolectricTestRunner;
|
|||||||
import org.robolectric.RuntimeEnvironment;
|
import org.robolectric.RuntimeEnvironment;
|
||||||
import org.robolectric.annotation.Config;
|
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.Markwon;
|
||||||
import io.noties.markwon.editor.MarkwonEditorImpl.EditSpanStoreImpl;
|
|
||||||
|
|
||||||
import static org.junit.Assert.assertEquals;
|
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)
|
@RunWith(RobolectricTestRunner.class)
|
||||||
@Config(manifest = Config.NONE)
|
@Config(manifest = Config.NONE)
|
||||||
public class MarkwonEditorImplTest {
|
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
|
@Test
|
||||||
public void process() {
|
public void process() {
|
||||||
// create markwon
|
// create markwon
|
||||||
|
@ -27,19 +27,4 @@ public class MarkwonEditorTest {
|
|||||||
fail(t.getMessage());
|
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.PreRenderResult;
|
||||||
import io.noties.markwon.editor.MarkwonEditor.PreRenderResultListener;
|
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.any;
|
||||||
import static org.mockito.ArgumentMatchers.eq;
|
import static org.mockito.ArgumentMatchers.eq;
|
||||||
|
import static org.mockito.Mockito.RETURNS_MOCKS;
|
||||||
import static org.mockito.Mockito.doAnswer;
|
import static org.mockito.Mockito.doAnswer;
|
||||||
import static org.mockito.Mockito.mock;
|
import static org.mockito.Mockito.mock;
|
||||||
import static org.mockito.Mockito.times;
|
import static org.mockito.Mockito.times;
|
||||||
@ -88,7 +91,49 @@ public class MarkwonEditorTextWatcherTest {
|
|||||||
|
|
||||||
listener.onPreRenderResult(result);
|
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));
|
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;
|
package io.noties.markwon.editor;
|
||||||
|
|
||||||
|
import android.text.SpannableStringBuilder;
|
||||||
|
|
||||||
import androidx.annotation.NonNull;
|
import androidx.annotation.NonNull;
|
||||||
import androidx.annotation.Nullable;
|
import androidx.annotation.Nullable;
|
||||||
|
|
||||||
@ -8,19 +10,55 @@ import org.junit.runner.RunWith;
|
|||||||
import org.robolectric.RobolectricTestRunner;
|
import org.robolectric.RobolectricTestRunner;
|
||||||
import org.robolectric.annotation.Config;
|
import org.robolectric.annotation.Config;
|
||||||
|
|
||||||
|
import java.util.Arrays;
|
||||||
|
import java.util.List;
|
||||||
import java.util.Locale;
|
import java.util.Locale;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
import io.noties.markwon.editor.MarkwonEditorUtils.Match;
|
import io.noties.markwon.editor.MarkwonEditorUtils.Match;
|
||||||
|
|
||||||
import static io.noties.markwon.editor.MarkwonEditorUtils.findDelimited;
|
import static io.noties.markwon.editor.MarkwonEditorUtils.findDelimited;
|
||||||
|
import static io.noties.markwon.editor.SpannableUtils.append;
|
||||||
import static java.lang.String.format;
|
import static java.lang.String.format;
|
||||||
import static org.junit.Assert.assertEquals;
|
import static org.junit.Assert.assertEquals;
|
||||||
import static org.junit.Assert.assertNotNull;
|
import static org.junit.Assert.assertNotNull;
|
||||||
|
import static org.junit.Assert.assertNull;
|
||||||
|
|
||||||
@RunWith(RobolectricTestRunner.class)
|
@RunWith(RobolectricTestRunner.class)
|
||||||
@Config(manifest = Config.NONE)
|
@Config(manifest = Config.NONE)
|
||||||
public class MarkwonEditorUtilsTest {
|
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
|
@Test
|
||||||
public void delimited_single() {
|
public void delimited_single() {
|
||||||
final String input = "**bold**";
|
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.Spanned;
|
||||||
import android.text.TextPaint;
|
import android.text.TextPaint;
|
||||||
import android.text.method.LinkMovementMethod;
|
import android.text.method.LinkMovementMethod;
|
||||||
import android.text.style.ClickableSpan;
|
|
||||||
import android.text.style.ForegroundColorSpan;
|
import android.text.style.ForegroundColorSpan;
|
||||||
import android.text.style.MetricAffectingSpan;
|
import android.text.style.MetricAffectingSpan;
|
||||||
import android.text.style.StrikethroughSpan;
|
import android.text.style.StrikethroughSpan;
|
||||||
@ -19,21 +18,23 @@ import android.widget.TextView;
|
|||||||
import androidx.annotation.NonNull;
|
import androidx.annotation.NonNull;
|
||||||
import androidx.annotation.Nullable;
|
import androidx.annotation.Nullable;
|
||||||
|
|
||||||
|
import org.commonmark.parser.Parser;
|
||||||
|
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.concurrent.Executors;
|
import java.util.concurrent.Executors;
|
||||||
|
|
||||||
|
import io.noties.markwon.AbstractMarkwonPlugin;
|
||||||
import io.noties.markwon.Markwon;
|
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.EmphasisSpan;
|
||||||
import io.noties.markwon.core.spans.LinkSpan;
|
|
||||||
import io.noties.markwon.core.spans.StrongEmphasisSpan;
|
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.MarkwonEditor;
|
||||||
import io.noties.markwon.editor.MarkwonEditorTextWatcher;
|
import io.noties.markwon.editor.MarkwonEditorTextWatcher;
|
||||||
import io.noties.markwon.editor.MarkwonEditorUtils;
|
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.ext.strikethrough.StrikethroughPlugin;
|
||||||
import io.noties.markwon.linkify.LinkifyPlugin;
|
import io.noties.markwon.linkify.LinkifyPlugin;
|
||||||
import io.noties.markwon.sample.R;
|
import io.noties.markwon.sample.R;
|
||||||
@ -92,7 +93,7 @@ public class EditorActivity extends Activity {
|
|||||||
// Use own punctuation span
|
// Use own punctuation span
|
||||||
|
|
||||||
final MarkwonEditor editor = MarkwonEditor.builder(Markwon.create(this))
|
final MarkwonEditor editor = MarkwonEditor.builder(Markwon.create(this))
|
||||||
.withPunctuationSpan(CustomPunctuationSpan.class, CustomPunctuationSpan::new)
|
.punctuationSpan(CustomPunctuationSpan.class, CustomPunctuationSpan::new)
|
||||||
.build();
|
.build();
|
||||||
|
|
||||||
editText.addTextChangedListener(MarkwonEditorTextWatcher.withProcess(editor));
|
editText.addTextChangedListener(MarkwonEditorTextWatcher.withProcess(editor));
|
||||||
@ -102,32 +103,46 @@ public class EditorActivity extends Activity {
|
|||||||
// An additional span is used to highlight strong-emphasis
|
// An additional span is used to highlight strong-emphasis
|
||||||
|
|
||||||
final MarkwonEditor editor = MarkwonEditor.builder(Markwon.create(this))
|
final MarkwonEditor editor = MarkwonEditor.builder(Markwon.create(this))
|
||||||
// This is required for edit-span cache
|
.useEditHandler(new AbstractEditHandler<StrongEmphasisSpan>() {
|
||||||
// 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
|
@Override
|
||||||
public void handle(
|
public void configurePersistedSpans(@NonNull PersistedSpans.Builder builder) {
|
||||||
@NonNull MarkwonEditor.EditSpanStore store,
|
// 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 Editable editable,
|
||||||
@NonNull String input,
|
@NonNull String input,
|
||||||
@NonNull Object span,
|
@NonNull StrongEmphasisSpan span,
|
||||||
int spanStart,
|
int spanStart,
|
||||||
int spanTextLength) {
|
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(
|
editable.setSpan(
|
||||||
// `includeEditSpan(Bold.class, Bold::new)` ensured that we have
|
persistedSpans.get(Bold.class),
|
||||||
// a span here to use (either reuse existing or create a new one)
|
match.start(),
|
||||||
store.get(Bold.class),
|
match.end(),
|
||||||
spanStart,
|
|
||||||
// we know that strong emphasis is delimited with 2 characters on both sides
|
|
||||||
spanStart + spanTextLength + 4,
|
|
||||||
Spanned.SPAN_EXCLUSIVE_EXCLUSIVE
|
Spanned.SPAN_EXCLUSIVE_EXCLUSIVE
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@NonNull
|
||||||
|
@Override
|
||||||
|
public Class<StrongEmphasisSpan> markdownSpanType() {
|
||||||
|
return StrongEmphasisSpan.class;
|
||||||
|
}
|
||||||
})
|
})
|
||||||
.build();
|
.build();
|
||||||
|
|
||||||
@ -156,20 +171,24 @@ public class EditorActivity extends Activity {
|
|||||||
final Markwon markwon = Markwon.builder(this)
|
final Markwon markwon = Markwon.builder(this)
|
||||||
.usePlugin(StrikethroughPlugin.create())
|
.usePlugin(StrikethroughPlugin.create())
|
||||||
.usePlugin(LinkifyPlugin.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();
|
.build();
|
||||||
|
|
||||||
final MarkwonTheme theme = markwon.configuration().theme();
|
final LinkEditHandler.OnClick onClick = (widget, link) -> markwon.configuration().linkResolver().resolve(widget, link);
|
||||||
|
|
||||||
final EditLinkSpan.OnClick onClick = (widget, link) -> markwon.configuration().linkResolver().resolve(widget, link);
|
|
||||||
|
|
||||||
final MarkwonEditor editor = MarkwonEditor.builder(markwon)
|
final MarkwonEditor editor = MarkwonEditor.builder(markwon)
|
||||||
.includeEditSpan(StrongEmphasisSpan.class, StrongEmphasisSpan::new)
|
.useEditHandler(new EmphasisEditHandler())
|
||||||
.includeEditSpan(EmphasisSpan.class, EmphasisSpan::new)
|
.useEditHandler(new StrongEmphasisEditHandler())
|
||||||
.includeEditSpan(StrikethroughSpan.class, StrikethroughSpan::new)
|
.useEditHandler(new StrikethroughEditHandler())
|
||||||
.includeEditSpan(CodeSpan.class, () -> new CodeSpan(theme))
|
.useEditHandler(new CodeEditHandler())
|
||||||
.includeEditSpan(BlockQuoteSpan.class, () -> new BlockQuoteSpan(theme))
|
.useEditHandler(new BlockQuoteEditHandler())
|
||||||
.includeEditSpan(EditLinkSpan.class, () -> new EditLinkSpan(onClick))
|
.useEditHandler(new LinkEditHandler(onClick))
|
||||||
.withEditSpanHandler(createEditSpanHandler())
|
|
||||||
.build();
|
.build();
|
||||||
|
|
||||||
// editText.addTextChangedListener(MarkwonEditorTextWatcher.withProcess(editor));
|
// editText.addTextChangedListener(MarkwonEditorTextWatcher.withProcess(editor));
|
||||||
@ -177,100 +196,6 @@ public class EditorActivity extends Activity {
|
|||||||
editor, Executors.newSingleThreadExecutor(), editText));
|
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() {
|
private void initBottomBar() {
|
||||||
// all except block-quote wraps if have selection, or inserts at current cursor position
|
// 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);
|
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