Add editor sample
This commit is contained in:
parent
8768e8a33c
commit
f1e750b305
@ -53,11 +53,6 @@
|
||||
|
||||
</activity>
|
||||
|
||||
<activity
|
||||
android:name=".edit.EditActivity"
|
||||
android:exported="true"
|
||||
android:windowSoftInputMode="adjustResize" />
|
||||
|
||||
</application>
|
||||
|
||||
</manifest>
|
@ -1,185 +0,0 @@
|
||||
package io.noties.markwon.app.edit;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.os.Bundle;
|
||||
import android.text.Editable;
|
||||
import android.text.Spanned;
|
||||
import android.text.TextPaint;
|
||||
import android.text.style.ForegroundColorSpan;
|
||||
import android.text.style.MetricAffectingSpan;
|
||||
import android.text.style.UnderlineSpan;
|
||||
import android.widget.EditText;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import java.util.concurrent.Executors;
|
||||
|
||||
import io.noties.debug.AndroidLogDebugOutput;
|
||||
import io.noties.debug.Debug;
|
||||
import io.noties.markwon.Markwon;
|
||||
import io.noties.markwon.app.R;
|
||||
import io.noties.markwon.core.MarkwonTheme;
|
||||
import io.noties.markwon.core.spans.BlockQuoteSpan;
|
||||
import io.noties.markwon.core.spans.CodeBlockSpan;
|
||||
import io.noties.markwon.core.spans.CodeSpan;
|
||||
import io.noties.markwon.core.spans.LinkSpan;
|
||||
import io.noties.markwon.core.spans.StrongEmphasisSpan;
|
||||
import io.noties.markwon.editor.MarkwonEditor;
|
||||
import io.noties.markwon.editor.MarkwonEditorTextWatcher;
|
||||
|
||||
public class EditActivity extends Activity {
|
||||
|
||||
static {
|
||||
Debug.init(new AndroidLogDebugOutput(true));
|
||||
}
|
||||
//
|
||||
// private static final String S = "**bold** it seems to **work** for now, new lines are cool to certain extend **yo**!\n" +
|
||||
// "\n" +
|
||||
// "> quote!\n" +
|
||||
// "> > nested quote!\n" +
|
||||
// "\n" +
|
||||
// "**bold** man, make it bold!\n" +
|
||||
// "\n" +
|
||||
// "# Head\n" +
|
||||
// "## Head\n" +
|
||||
// "\n" +
|
||||
// "man, **crazy** thing called love.... **work**, **work** **work** man, super weird,\n" +
|
||||
// "\n" +
|
||||
// "`code`, yeah and code doesn't work\n" +
|
||||
// "\n" +
|
||||
// "* one\n" +
|
||||
// "* two\n" +
|
||||
// "* three\n" +
|
||||
// "* * hey!\n" +
|
||||
// " * super hey\n" +
|
||||
// "\n" +
|
||||
// "it does seem good **bold**, now shifted... **bold** man, now restored **bold** *em* sd\n" +
|
||||
// "\n" +
|
||||
// "[link](#) is it?  hey! **bold**\n" +
|
||||
// "\n";
|
||||
|
||||
@Override
|
||||
public void onCreate(@Nullable Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
setContentView(R.layout.activity_edit);
|
||||
|
||||
final EditText editText = findViewById(R.id.edit_text);
|
||||
|
||||
final Markwon markwon = Markwon.create(this);
|
||||
final MarkwonTheme theme = markwon.configuration().theme();
|
||||
|
||||
final MarkwonEditor editor = MarkwonEditor.builder(markwon)
|
||||
.withPunctuationSpan(MarkdownPunctuationSpan.class, MarkdownPunctuationSpan::new)
|
||||
.includeEditSpan(Bold.class, Bold::new)
|
||||
.includeEditSpan(CodeSpan.class, () -> new CodeSpan(theme))
|
||||
.includeEditSpan(UnderlineSpan.class, UnderlineSpan::new)
|
||||
.includeEditSpan(CodeBlockSpan.class, () -> new CodeBlockSpan(theme))
|
||||
.includeEditSpan(BlockQuoteSpan.class, () -> new BlockQuoteSpan(theme))
|
||||
.withEditSpanHandlerFor(StrongEmphasisSpan.class, new MarkwonEditor.EditSpanHandler<StrongEmphasisSpan>() {
|
||||
@Override
|
||||
public void handle(@NonNull MarkwonEditor.SpanStore store, @NonNull Editable editable, @NonNull String input, @NonNull StrongEmphasisSpan span, int spanStart, int spanTextLength) {
|
||||
editable.setSpan(
|
||||
store.get(Bold.class),
|
||||
spanStart,
|
||||
// we know that strong emphasis is delimited with 2 characters on both sides
|
||||
spanStart + spanTextLength + 4,
|
||||
Spanned.SPAN_EXCLUSIVE_EXCLUSIVE
|
||||
);
|
||||
}
|
||||
})
|
||||
.withEditSpanHandlerFor(LinkSpan.class, new MarkwonEditor.EditSpanHandler<LinkSpan>() {
|
||||
@Override
|
||||
public void handle(@NonNull MarkwonEditor.SpanStore store, @NonNull Editable editable, @NonNull String input, @NonNull LinkSpan span, int spanStart, int spanTextLength) {
|
||||
editable.setSpan(
|
||||
store.get(UnderlineSpan.class),
|
||||
// add underline only for link text
|
||||
spanStart + 1,
|
||||
spanStart + 1 + spanTextLength,
|
||||
Spanned.SPAN_EXCLUSIVE_EXCLUSIVE
|
||||
);
|
||||
}
|
||||
})
|
||||
.withEditSpanHandlerFor(CodeSpan.class, new MarkwonEditor.EditSpanHandler<CodeSpan>() {
|
||||
@Override
|
||||
public void handle(@NonNull MarkwonEditor.SpanStore store, @NonNull Editable editable, @NonNull String input, @NonNull CodeSpan span, int spanStart, int spanTextLength) {
|
||||
editable.setSpan(
|
||||
store.get(CodeSpan.class),
|
||||
spanStart,
|
||||
spanStart + spanTextLength,
|
||||
Spanned.SPAN_EXCLUSIVE_EXCLUSIVE
|
||||
);
|
||||
}
|
||||
})
|
||||
.withEditSpanHandlerFor(CodeBlockSpan.class, new MarkwonEditor.EditSpanHandler<CodeBlockSpan>() {
|
||||
@Override
|
||||
public void handle(@NonNull MarkwonEditor.SpanStore store, @NonNull Editable editable, @NonNull String input, @NonNull CodeBlockSpan span, int spanStart, int spanTextLength) {
|
||||
// if starts with backticks -> count them and then add everything until the line end
|
||||
if (input.charAt(spanStart) == '`') {
|
||||
final int firstLineEnd = input.indexOf('\n', spanStart);
|
||||
if (firstLineEnd == -1) return;
|
||||
int lastLineEnd = input.indexOf('\n', spanStart + (firstLineEnd - spanStart) + spanTextLength + 1);
|
||||
if (lastLineEnd == -1) lastLineEnd = input.length();
|
||||
|
||||
editable.setSpan(
|
||||
store.get(CodeBlockSpan.class),
|
||||
spanStart,
|
||||
lastLineEnd,
|
||||
Spanned.SPAN_EXCLUSIVE_EXCLUSIVE
|
||||
);
|
||||
} else {
|
||||
// todo: just everything until the end
|
||||
editable.setSpan(
|
||||
store.get(CodeBlockSpan.class),
|
||||
spanStart,
|
||||
spanStart + spanTextLength,
|
||||
Spanned.SPAN_EXCLUSIVE_EXCLUSIVE
|
||||
);
|
||||
}
|
||||
}
|
||||
})
|
||||
.withEditSpanHandlerFor(BlockQuoteSpan.class, new MarkwonEditor.EditSpanHandler<BlockQuoteSpan>() {
|
||||
@Override
|
||||
public void handle(@NonNull MarkwonEditor.SpanStore store, @NonNull Editable editable, @NonNull String input, @NonNull BlockQuoteSpan span, int spanStart, int spanTextLength) {
|
||||
editable.setSpan(
|
||||
store.get(BlockQuoteSpan.class),
|
||||
spanStart,
|
||||
spanStart + spanTextLength,
|
||||
Spanned.SPAN_EXCLUSIVE_EXCLUSIVE
|
||||
);
|
||||
}
|
||||
})
|
||||
.build();
|
||||
|
||||
editText.addTextChangedListener(MarkwonEditorTextWatcher.withPreRender(
|
||||
editor,
|
||||
Executors.newCachedThreadPool(),
|
||||
editText));
|
||||
}
|
||||
|
||||
private static class MarkdownPunctuationSpan extends ForegroundColorSpan {
|
||||
MarkdownPunctuationSpan() {
|
||||
super(0xFFFF0000);
|
||||
}
|
||||
}
|
||||
|
||||
private static class Bold extends MetricAffectingSpan {
|
||||
public Bold() {
|
||||
super();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void updateDrawState(TextPaint tp) {
|
||||
update(tp);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void updateMeasureState(@NonNull TextPaint textPaint) {
|
||||
update(textPaint);
|
||||
}
|
||||
|
||||
private void update(@NonNull TextPaint paint) {
|
||||
paint.setFakeBoldText(true);
|
||||
}
|
||||
}
|
||||
}
|
File diff suppressed because it is too large
Load Diff
@ -8,16 +8,38 @@ import androidx.annotation.Nullable;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
class EditSpanHandlerBuilder {
|
||||
/**
|
||||
* @since 4.2.0-SNAPSHOT
|
||||
*/
|
||||
public class EditSpanHandlerBuilder {
|
||||
|
||||
private final Map<Class<?>, MarkwonEditor.EditSpanHandler> map = new HashMap<>(3);
|
||||
public interface EditSpanHandlerTyped<T> {
|
||||
void handle(
|
||||
@NonNull MarkwonEditor.EditSpanStore store,
|
||||
@NonNull Editable editable,
|
||||
@NonNull String input,
|
||||
@NonNull T span,
|
||||
int spanStart,
|
||||
int spanTextLength);
|
||||
}
|
||||
|
||||
<T> void include(@NonNull Class<T> type, @NonNull MarkwonEditor.EditSpanHandler<T> handler) {
|
||||
@NonNull
|
||||
public static EditSpanHandlerBuilder create() {
|
||||
return new EditSpanHandlerBuilder();
|
||||
}
|
||||
|
||||
private final Map<Class<?>, EditSpanHandlerTyped> map = new HashMap<>(3);
|
||||
|
||||
@NonNull
|
||||
public <T> EditSpanHandlerBuilder include(
|
||||
@NonNull Class<T> type,
|
||||
@NonNull EditSpanHandlerTyped<T> handler) {
|
||||
map.put(type, handler);
|
||||
return this;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
MarkwonEditor.EditSpanHandler build() {
|
||||
public MarkwonEditor.EditSpanHandler build() {
|
||||
if (map.size() == 0) {
|
||||
return null;
|
||||
}
|
||||
@ -26,15 +48,21 @@ class EditSpanHandlerBuilder {
|
||||
|
||||
private static class EditSpanHandlerImpl implements MarkwonEditor.EditSpanHandler {
|
||||
|
||||
private final Map<Class<?>, MarkwonEditor.EditSpanHandler> map;
|
||||
private final Map<Class<?>, EditSpanHandlerTyped> map;
|
||||
|
||||
EditSpanHandlerImpl(@NonNull Map<Class<?>, MarkwonEditor.EditSpanHandler> map) {
|
||||
EditSpanHandlerImpl(@NonNull Map<Class<?>, EditSpanHandlerTyped> map) {
|
||||
this.map = map;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void handle(@NonNull MarkwonEditor.SpanStore store, @NonNull Editable editable, @NonNull String input, @NonNull Object span, int spanStart, int spanTextLength) {
|
||||
final MarkwonEditor.EditSpanHandler handler = map.get(span.getClass());
|
||||
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,29 +3,31 @@ 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;
|
||||
|
||||
import io.noties.markwon.Markwon;
|
||||
|
||||
// todo: if multiple spans are used via factory... only the first one is delivered to edit-span-handler
|
||||
// does it though? yeah... only the first one and then break... deliver all?
|
||||
|
||||
// todo: how to reuse existing spanFactories? to obtain a value they require render-props....
|
||||
// maybe.. mock them? plus, spanFactory can return multiple spans
|
||||
|
||||
// todo: we can actually create punctuation span with reasonable defaults to be used by default
|
||||
// todo: as we do text-diff, then images, latex and tables won't be handled... they will be treated as punctuation as-whole..
|
||||
|
||||
/**
|
||||
* @see #builder(Markwon)
|
||||
* @see #create(Markwon)
|
||||
* @see #process(Editable)
|
||||
* @see #preRender(Editable, PreRenderResultListener)
|
||||
* @since 4.2.0-SNAPSHOT
|
||||
*/
|
||||
public abstract class MarkwonEditor {
|
||||
|
||||
public interface SpanStore {
|
||||
public interface EditSpanStore {
|
||||
|
||||
/**
|
||||
* If a span of specified type was not registered with {@link Builder#includeEditSpan(Class, SpanFactory)}
|
||||
* If a span of specified type was not registered with {@link Builder#includeEditSpan(Class, EditSpanFactory)}
|
||||
* then an exception is raised.
|
||||
*
|
||||
* @param type of a span to obtain
|
||||
@ -35,34 +37,68 @@ public abstract class MarkwonEditor {
|
||||
<T> T get(Class<T> type);
|
||||
}
|
||||
|
||||
public interface SpanFactory<T> {
|
||||
public interface EditSpanFactory<T> {
|
||||
@NonNull
|
||||
T create();
|
||||
}
|
||||
|
||||
public interface EditSpanHandler<T> {
|
||||
/**
|
||||
* Interface to handle _original_ span that is present in rendered markdown. Can be useful
|
||||
* to add specific spans for EditText (for example, make text bold to better indicate
|
||||
* strong emphasis used in markdown input).
|
||||
*
|
||||
* @see Builder#withEditSpanHandler(EditSpanHandler)
|
||||
* @see EditSpanHandlerBuilder
|
||||
*/
|
||||
public interface EditSpanHandler {
|
||||
void handle(
|
||||
@NonNull SpanStore store,
|
||||
@NonNull EditSpanStore store,
|
||||
@NonNull Editable editable,
|
||||
@NonNull String input,
|
||||
@NonNull T span,
|
||||
@NonNull Object span,
|
||||
int spanStart,
|
||||
int spanTextLength);
|
||||
}
|
||||
|
||||
public interface PreRenderResult {
|
||||
|
||||
// With which pre-render method was called as input
|
||||
/**
|
||||
* @return Editable instance for which result was calculated. This must not be
|
||||
* actual Editable of EditText
|
||||
*/
|
||||
@NonNull
|
||||
Editable resultEditable();
|
||||
|
||||
/**
|
||||
* Dispatch pre-rendering result to EditText
|
||||
*
|
||||
* @param editable to dispatch result to
|
||||
*/
|
||||
void dispatchTo(@NonNull Editable editable);
|
||||
}
|
||||
|
||||
/**
|
||||
* @see #preRender(Editable, PreRenderResultListener)
|
||||
*/
|
||||
public interface PreRenderResultListener {
|
||||
void onPreRenderResult(@NonNull PreRenderResult result);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates default instance of {@link MarkwonEditor}. By default it will handle only
|
||||
* punctuation spans (highlight markdown punctuation and nothing more).
|
||||
*
|
||||
* @see #builder(Markwon)
|
||||
*/
|
||||
@NonNull
|
||||
public static MarkwonEditor create(@NonNull Markwon markwon) {
|
||||
return builder(markwon).build();
|
||||
}
|
||||
|
||||
/**
|
||||
* @see #create(Markwon)
|
||||
* @see Builder
|
||||
*/
|
||||
@NonNull
|
||||
public static Builder builder(@NonNull Markwon markwon) {
|
||||
return new Builder(markwon);
|
||||
@ -78,9 +114,12 @@ public abstract class MarkwonEditor {
|
||||
public abstract void process(@NonNull Editable editable);
|
||||
|
||||
/**
|
||||
* Pre-render highlight result. Can be useful to create highlight information on a different
|
||||
* thread.
|
||||
* <p>
|
||||
* Please note that currently only `setSpan` and `removeSpan` actions will be recorded (and thus dispatched).
|
||||
* make sure you use only these methods in your {@link EditSpanHandler}, or implement the required
|
||||
* functionality in some other way.
|
||||
* Make sure you use only these methods in your {@link EditSpanHandler}, or implement the required
|
||||
* functionality some other way.
|
||||
*
|
||||
* @param editable to process and pre-render
|
||||
* @param preRenderListener listener to be notified when pre-render result will be ready
|
||||
@ -88,40 +127,40 @@ public abstract class MarkwonEditor {
|
||||
*/
|
||||
public abstract void preRender(@NonNull Editable editable, @NonNull PreRenderResultListener preRenderListener);
|
||||
|
||||
|
||||
public static class Builder {
|
||||
|
||||
private final Markwon markwon;
|
||||
private final EditSpanHandlerBuilder editSpanHandlerBuilder = new EditSpanHandlerBuilder();
|
||||
|
||||
private Class<?> punctuationSpanType;
|
||||
private Map<Class<?>, SpanFactory> spans = new HashMap<>(3);
|
||||
private Map<Class<?>, EditSpanFactory> spans = new HashMap<>(3);
|
||||
private EditSpanHandler editSpanHandler;
|
||||
|
||||
Builder(@NonNull Markwon markwon) {
|
||||
this.markwon = markwon;
|
||||
}
|
||||
|
||||
/**
|
||||
* The only required argument which will make {@link MarkwonEditor} apply specified span only
|
||||
* to markdown punctuation
|
||||
* Specify which punctuation span will be used.
|
||||
*
|
||||
* @param type of the span
|
||||
* @param factory to create a new instance of the span
|
||||
*/
|
||||
@NonNull
|
||||
public <T> Builder withPunctuationSpan(@NonNull Class<T> type, @NonNull SpanFactory<T> factory) {
|
||||
public <T> Builder withPunctuationSpan(@NonNull Class<T> type, @NonNull EditSpanFactory<T> factory) {
|
||||
this.punctuationSpanType = type;
|
||||
this.spans.put(type, factory);
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Include specific span that will be used in highlighting. It is important to understand
|
||||
* Include additional span handling that is used in highlighting. It is important to understand
|
||||
* that it is not the span that is used by Markwon, but instead your own span that you
|
||||
* apply in a custom {@link EditSpanHandler} specified by {@link #withEditSpanHandlerFor(Class, EditSpanHandler)}.
|
||||
* apply in a custom {@link EditSpanHandler} specified by {@link #withEditSpanHandler(EditSpanHandler)}.
|
||||
* You can apply a Markwon bundled span (or any other) but it must be still explicitly
|
||||
* included by this method.
|
||||
* <p>
|
||||
* The span will be exposed via {@link SpanStore} in your custom {@link EditSpanHandler}.
|
||||
* The span will be exposed via {@link EditSpanStore} in your custom {@link EditSpanHandler}.
|
||||
* If you do not use a custom {@link EditSpanHandler} you do not need to specify any span here.
|
||||
*
|
||||
* @param type of a span to include
|
||||
@ -130,31 +169,50 @@ public abstract class MarkwonEditor {
|
||||
@NonNull
|
||||
public <T> Builder includeEditSpan(
|
||||
@NonNull Class<T> type,
|
||||
@NonNull SpanFactory<T> factory) {
|
||||
@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 <T> Builder withEditSpanHandlerFor(@NonNull Class<T> type, @NonNull EditSpanHandler<T> editSpanHandler) {
|
||||
this.editSpanHandlerBuilder.include(type, editSpanHandler);
|
||||
public Builder withEditSpanHandler(@Nullable EditSpanHandler editSpanHandler) {
|
||||
this.editSpanHandler = editSpanHandler;
|
||||
return this;
|
||||
}
|
||||
|
||||
@NonNull
|
||||
public MarkwonEditor build() {
|
||||
|
||||
final Class<?> punctuationSpanType = this.punctuationSpanType;
|
||||
Class<?> punctuationSpanType = this.punctuationSpanType;
|
||||
if (punctuationSpanType == null) {
|
||||
throw new IllegalStateException("Punctuation span type is required, " +
|
||||
"add with Builder#withPunctuationSpan method");
|
||||
withPunctuationSpan(PunctuationSpan.class, new EditSpanFactory<PunctuationSpan>() {
|
||||
@NonNull
|
||||
@Override
|
||||
public PunctuationSpan create() {
|
||||
return new PunctuationSpan();
|
||||
}
|
||||
});
|
||||
punctuationSpanType = this.punctuationSpanType;
|
||||
}
|
||||
|
||||
// if we have no editSpanHandler, but spans are registered -> throw an error
|
||||
if (spans.size() > 1 && editSpanHandler == null) {
|
||||
throw new IllegalStateException("There is no need to include edit spans " +
|
||||
"when you do not use custom EditSpanHandler");
|
||||
}
|
||||
|
||||
return new MarkwonEditorImpl(
|
||||
markwon,
|
||||
spans,
|
||||
punctuationSpanType,
|
||||
editSpanHandlerBuilder.build());
|
||||
editSpanHandler);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -20,7 +20,7 @@ import io.noties.markwon.editor.diff_match_patch.Diff;
|
||||
class MarkwonEditorImpl extends MarkwonEditor {
|
||||
|
||||
private final Markwon markwon;
|
||||
private final Map<Class<?>, SpanFactory> spans;
|
||||
private final Map<Class<?>, EditSpanFactory> spans;
|
||||
private final Class<?> punctuationSpanType;
|
||||
|
||||
@Nullable
|
||||
@ -28,7 +28,7 @@ class MarkwonEditorImpl extends MarkwonEditor {
|
||||
|
||||
MarkwonEditorImpl(
|
||||
@NonNull Markwon markwon,
|
||||
@NonNull Map<Class<?>, SpanFactory> spans,
|
||||
@NonNull Map<Class<?>, EditSpanFactory> spans,
|
||||
@NonNull Class<?> punctuationSpanType,
|
||||
@Nullable EditSpanHandler editSpanHandler) {
|
||||
this.markwon = markwon;
|
||||
@ -47,7 +47,7 @@ class MarkwonEditorImpl extends MarkwonEditor {
|
||||
final EditSpanHandler editSpanHandler = this.editSpanHandler;
|
||||
final boolean hasAdditionalSpans = editSpanHandler != null;
|
||||
|
||||
final SpanStoreImpl store = new SpanStoreImpl(editable, spans);
|
||||
final EditSpanStoreImpl store = new EditSpanStoreImpl(editable, spans);
|
||||
try {
|
||||
|
||||
final List<Diff> diffs = diff_match_patch.diff_main(input, markdown);
|
||||
@ -81,6 +81,9 @@ class MarkwonEditorImpl extends MarkwonEditor {
|
||||
span,
|
||||
start,
|
||||
renderedMarkdown.getSpanEnd(span) - markdownLength);
|
||||
// NB, we do not break here in case of SpanFactory
|
||||
// returns multiple spans for a markdown node, this way
|
||||
// we will handle all of them
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -153,13 +156,13 @@ class MarkwonEditorImpl extends MarkwonEditor {
|
||||
return map;
|
||||
}
|
||||
|
||||
static class SpanStoreImpl implements SpanStore {
|
||||
static class EditSpanStoreImpl implements EditSpanStore {
|
||||
|
||||
private final Spannable spannable;
|
||||
private final Map<Class<?>, SpanFactory> spans;
|
||||
private final Map<Class<?>, EditSpanFactory> spans;
|
||||
private final Map<Class<?>, List<Object>> map;
|
||||
|
||||
SpanStoreImpl(@NonNull Spannable spannable, @NonNull Map<Class<?>, SpanFactory> spans) {
|
||||
EditSpanStoreImpl(@NonNull Spannable spannable, @NonNull Map<Class<?>, EditSpanFactory> spans) {
|
||||
this.spannable = spannable;
|
||||
this.spans = spans;
|
||||
this.map = extractSpans(spannable, spans.keySet());
|
||||
@ -175,7 +178,7 @@ class MarkwonEditorImpl extends MarkwonEditor {
|
||||
if (list != null && list.size() > 0) {
|
||||
span = list.remove(0);
|
||||
} else {
|
||||
final SpanFactory spanFactory = spans.get(type);
|
||||
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");
|
||||
|
@ -17,6 +17,8 @@ import java.util.concurrent.Future;
|
||||
*
|
||||
* @see MarkwonEditor#process(Editable)
|
||||
* @see MarkwonEditor#preRender(Editable, MarkwonEditor.PreRenderResultListener)
|
||||
* @see #withProcess(MarkwonEditor)
|
||||
* @see #withPreRender(MarkwonEditor, ExecutorService, EditText)
|
||||
* @since 4.2.0-SNAPSHOT
|
||||
*/
|
||||
public abstract class MarkwonEditorTextWatcher implements TextWatcher {
|
||||
@ -54,7 +56,7 @@ public abstract class MarkwonEditorTextWatcher implements TextWatcher {
|
||||
|
||||
private boolean selfChange;
|
||||
|
||||
public WithProcess(@NonNull MarkwonEditor editor) {
|
||||
WithProcess(@NonNull MarkwonEditor editor) {
|
||||
this.editor = editor;
|
||||
}
|
||||
|
||||
@ -84,6 +86,8 @@ public abstract class MarkwonEditorTextWatcher implements TextWatcher {
|
||||
|
||||
private Future<?> future;
|
||||
|
||||
private boolean selfChange;
|
||||
|
||||
WithPreRender(
|
||||
@NonNull MarkwonEditor editor,
|
||||
@NonNull ExecutorService executorService,
|
||||
@ -107,6 +111,10 @@ public abstract class MarkwonEditorTextWatcher implements TextWatcher {
|
||||
@Override
|
||||
public void afterTextChanged(final Editable s) {
|
||||
|
||||
if (selfChange) {
|
||||
return;
|
||||
}
|
||||
|
||||
// todo: maybe checking hash is not so performant?
|
||||
// what if we create a atomic reference and use it (with tag applied to editText)?
|
||||
|
||||
@ -121,12 +129,17 @@ public abstract class MarkwonEditorTextWatcher implements TextWatcher {
|
||||
@Override
|
||||
public void onPreRenderResult(@NonNull final MarkwonEditor.PreRenderResult result) {
|
||||
if (editText != null) {
|
||||
final int key = result.resultEditable().toString().hashCode();
|
||||
final int key = key(result.resultEditable());
|
||||
editText.post(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
if (key == editText.getText().toString().hashCode()) {
|
||||
result.dispatchTo(editText.getText());
|
||||
if (key == key(editText.getText())) {
|
||||
selfChange = true;
|
||||
try {
|
||||
result.dispatchTo(editText.getText());
|
||||
} finally {
|
||||
selfChange = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
@ -136,5 +149,12 @@ public abstract class MarkwonEditorTextWatcher implements TextWatcher {
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
static int key(@NonNull Editable editable) {
|
||||
// toString is important here, as using #hashCode directly
|
||||
// would also check for spans (and some spans can be added/removed). This is why
|
||||
// we are checking for exact match of text
|
||||
return editable.toString().hashCode();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -0,0 +1,17 @@
|
||||
package io.noties.markwon.editor;
|
||||
|
||||
import android.text.TextPaint;
|
||||
import android.text.style.CharacterStyle;
|
||||
|
||||
import io.noties.markwon.utils.ColorUtils;
|
||||
|
||||
class PunctuationSpan extends CharacterStyle {
|
||||
|
||||
private static final int DEF_PUNCTUATION_ALPHA = 75;
|
||||
|
||||
@Override
|
||||
public void updateDrawState(TextPaint tp) {
|
||||
final int color = ColorUtils.applyAlpha(tp.getColor(), DEF_PUNCTUATION_ALPHA);
|
||||
tp.setColor(color);
|
||||
}
|
||||
}
|
@ -34,6 +34,7 @@ android {
|
||||
dependencies {
|
||||
|
||||
implementation project(':markwon-core')
|
||||
implementation project(':markwon-editor')
|
||||
implementation project(':markwon-ext-latex')
|
||||
implementation project(':markwon-ext-strikethrough')
|
||||
implementation project(':markwon-ext-tables')
|
||||
|
@ -28,6 +28,7 @@
|
||||
<activity android:name=".simpleext.SimpleExtActivity" />
|
||||
<activity android:name=".customextension2.CustomExtensionActivity2" />
|
||||
<activity android:name=".precomputed.PrecomputedActivity" />
|
||||
<activity android:name=".editor.EditorActivity" />
|
||||
|
||||
</application>
|
||||
|
||||
|
@ -22,6 +22,7 @@ import io.noties.markwon.sample.basicplugins.BasicPluginsActivity;
|
||||
import io.noties.markwon.sample.core.CoreActivity;
|
||||
import io.noties.markwon.sample.customextension.CustomExtensionActivity;
|
||||
import io.noties.markwon.sample.customextension2.CustomExtensionActivity2;
|
||||
import io.noties.markwon.sample.editor.EditorActivity;
|
||||
import io.noties.markwon.sample.html.HtmlActivity;
|
||||
import io.noties.markwon.sample.latex.LatexActivity;
|
||||
import io.noties.markwon.sample.precomputed.PrecomputedActivity;
|
||||
@ -117,6 +118,10 @@ public class MainActivity extends Activity {
|
||||
activity = PrecomputedActivity.class;
|
||||
break;
|
||||
|
||||
case EDITOR:
|
||||
activity = EditorActivity.class;
|
||||
break;
|
||||
|
||||
default:
|
||||
throw new IllegalStateException("No Activity is associated with sample-item: " + item);
|
||||
}
|
||||
|
@ -21,7 +21,9 @@ public enum Sample {
|
||||
|
||||
CUSTOM_EXTENSION_2(R.string.sample_custom_extension_2),
|
||||
|
||||
PRECOMPUTED_TEXT(R.string.sample_precomputed_text);
|
||||
PRECOMPUTED_TEXT(R.string.sample_precomputed_text),
|
||||
|
||||
EDITOR(R.string.sample_editor);
|
||||
|
||||
private final int textResId;
|
||||
|
||||
|
@ -0,0 +1,271 @@
|
||||
package io.noties.markwon.sample.editor;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.os.Bundle;
|
||||
import android.text.Editable;
|
||||
import android.text.Spanned;
|
||||
import android.text.TextPaint;
|
||||
import android.text.style.CharacterStyle;
|
||||
import android.text.style.ForegroundColorSpan;
|
||||
import android.text.style.MetricAffectingSpan;
|
||||
import android.text.style.StrikethroughSpan;
|
||||
import android.widget.EditText;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import java.util.concurrent.Executors;
|
||||
|
||||
import io.noties.markwon.Markwon;
|
||||
import io.noties.markwon.core.MarkwonTheme;
|
||||
import io.noties.markwon.core.spans.BlockQuoteSpan;
|
||||
import io.noties.markwon.core.spans.CodeBlockSpan;
|
||||
import io.noties.markwon.core.spans.CodeSpan;
|
||||
import io.noties.markwon.core.spans.EmphasisSpan;
|
||||
import io.noties.markwon.core.spans.LinkSpan;
|
||||
import io.noties.markwon.core.spans.StrongEmphasisSpan;
|
||||
import io.noties.markwon.editor.EditSpanHandlerBuilder;
|
||||
import io.noties.markwon.editor.MarkwonEditor;
|
||||
import io.noties.markwon.editor.MarkwonEditorTextWatcher;
|
||||
import io.noties.markwon.ext.strikethrough.StrikethroughPlugin;
|
||||
import io.noties.markwon.sample.R;
|
||||
|
||||
public class EditorActivity extends Activity {
|
||||
|
||||
private EditText editText;
|
||||
|
||||
@Override
|
||||
public void onCreate(@Nullable Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
setContentView(R.layout.activity_editor);
|
||||
|
||||
this.editText = findViewById(R.id.edit_text);
|
||||
|
||||
|
||||
// simple_process();
|
||||
|
||||
// simple_pre_render();
|
||||
|
||||
// custom_punctuation_span();
|
||||
|
||||
// additional_edit_span();
|
||||
|
||||
// additional_plugins();
|
||||
|
||||
multiple_edit_spans();
|
||||
}
|
||||
|
||||
private void simple_process() {
|
||||
// Process highlight in-place (right after text has changed)
|
||||
|
||||
// obtain Markwon instance
|
||||
final Markwon markwon = Markwon.create(this);
|
||||
|
||||
// create editor
|
||||
final MarkwonEditor editor = MarkwonEditor.create(markwon);
|
||||
|
||||
// set edit listener
|
||||
editText.addTextChangedListener(MarkwonEditorTextWatcher.withProcess(editor));
|
||||
}
|
||||
|
||||
private void simple_pre_render() {
|
||||
// Process highlight in background thread
|
||||
|
||||
final Markwon markwon = Markwon.create(this);
|
||||
final MarkwonEditor editor = MarkwonEditor.create(markwon);
|
||||
|
||||
editText.addTextChangedListener(MarkwonEditorTextWatcher.withPreRender(
|
||||
editor,
|
||||
Executors.newCachedThreadPool(),
|
||||
editText));
|
||||
}
|
||||
|
||||
private void custom_punctuation_span() {
|
||||
// Use own punctuation span
|
||||
|
||||
final MarkwonEditor editor = MarkwonEditor.builder(Markwon.create(this))
|
||||
.withPunctuationSpan(CustomPunctuationSpan.class, CustomPunctuationSpan::new)
|
||||
.build();
|
||||
|
||||
editText.addTextChangedListener(MarkwonEditorTextWatcher.withProcess(editor));
|
||||
}
|
||||
|
||||
private void additional_edit_span() {
|
||||
// An additional span is used to highlight strong-emphasis
|
||||
|
||||
final MarkwonEditor editor = MarkwonEditor.builder(Markwon.create(this))
|
||||
// This is required for edit-span cache
|
||||
// We could use Markwon `StrongEmphasisSpan` here, but I use a different
|
||||
// one to indicate that those are completely unrelated spans and must be
|
||||
// treated differently.
|
||||
.includeEditSpan(Bold.class, Bold::new)
|
||||
.withEditSpanHandler(new MarkwonEditor.EditSpanHandler() {
|
||||
@Override
|
||||
public void handle(
|
||||
@NonNull MarkwonEditor.EditSpanStore store,
|
||||
@NonNull Editable editable,
|
||||
@NonNull String input,
|
||||
@NonNull Object span,
|
||||
int spanStart,
|
||||
int spanTextLength) {
|
||||
if (span instanceof StrongEmphasisSpan) {
|
||||
editable.setSpan(
|
||||
// `includeEditSpan(Bold.class, Bold::new)` ensured that we have
|
||||
// a span here to use (either reuse existing or create a new one)
|
||||
store.get(Bold.class),
|
||||
spanStart,
|
||||
// we know that strong emphasis is delimited with 2 characters on both sides
|
||||
spanStart + spanTextLength + 4,
|
||||
Spanned.SPAN_EXCLUSIVE_EXCLUSIVE
|
||||
);
|
||||
}
|
||||
}
|
||||
})
|
||||
.build();
|
||||
|
||||
editText.addTextChangedListener(MarkwonEditorTextWatcher.withProcess(editor));
|
||||
}
|
||||
|
||||
private void additional_plugins() {
|
||||
// As highlight works based on text-diff, everything that is present in input,
|
||||
// but missing in resulting markdown is considered to be punctuation, this is why
|
||||
// additional plugins do not need special handling
|
||||
|
||||
final Markwon markwon = Markwon.builder(this)
|
||||
.usePlugin(StrikethroughPlugin.create())
|
||||
.build();
|
||||
|
||||
final MarkwonEditor editor = MarkwonEditor.create(markwon);
|
||||
|
||||
editText.addTextChangedListener(MarkwonEditorTextWatcher.withProcess(editor));
|
||||
}
|
||||
|
||||
private void multiple_edit_spans() {
|
||||
|
||||
final Markwon markwon = Markwon.builder(this)
|
||||
.usePlugin(StrikethroughPlugin.create())
|
||||
.build();
|
||||
final MarkwonTheme theme = markwon.configuration().theme();
|
||||
|
||||
final MarkwonEditor editor = MarkwonEditor.builder(markwon)
|
||||
.includeEditSpan(StrongEmphasisSpan.class, StrongEmphasisSpan::new)
|
||||
.includeEditSpan(EmphasisSpan.class, EmphasisSpan::new)
|
||||
.includeEditSpan(StrikethroughSpan.class, StrikethroughSpan::new)
|
||||
.includeEditSpan(CodeSpan.class, () -> new CodeSpan(theme))
|
||||
.includeEditSpan(CodeBlockSpan.class, () -> new CodeBlockSpan(theme))
|
||||
.includeEditSpan(BlockQuoteSpan.class, () -> new BlockQuoteSpan(theme))
|
||||
.includeEditSpan(EditLinkSpan.class, EditLinkSpan::new)
|
||||
.withEditSpanHandler(createEditSpanHandler())
|
||||
.build();
|
||||
|
||||
editText.addTextChangedListener(MarkwonEditorTextWatcher.withProcess(editor));
|
||||
}
|
||||
|
||||
private static MarkwonEditor.EditSpanHandler createEditSpanHandler() {
|
||||
// Please note that here we specify spans THAT ARE USED IN MARKDOWN
|
||||
return EditSpanHandlerBuilder.create()
|
||||
.include(StrongEmphasisSpan.class, (store, editable, input, span, spanStart, spanTextLength) -> {
|
||||
editable.setSpan(
|
||||
store.get(StrongEmphasisSpan.class),
|
||||
spanStart,
|
||||
spanStart + spanTextLength + 4,
|
||||
Spanned.SPAN_EXCLUSIVE_EXCLUSIVE
|
||||
);
|
||||
})
|
||||
.include(EmphasisSpan.class, (store, editable, input, span, spanStart, spanTextLength) -> {
|
||||
editable.setSpan(
|
||||
store.get(EmphasisSpan.class),
|
||||
spanStart,
|
||||
spanStart + spanTextLength + 2,
|
||||
Spanned.SPAN_EXCLUSIVE_EXCLUSIVE
|
||||
);
|
||||
})
|
||||
.include(StrikethroughSpan.class, (store, editable, input, span, spanStart, spanTextLength) -> {
|
||||
editable.setSpan(
|
||||
store.get(StrikethroughSpan.class),
|
||||
spanStart,
|
||||
spanStart + spanTextLength + 4,
|
||||
Spanned.SPAN_EXCLUSIVE_EXCLUSIVE
|
||||
);
|
||||
})
|
||||
.include(CodeSpan.class, (store, editable, input, span, spanStart, spanTextLength) -> {
|
||||
// we do not add offset here because markwon (by default) adds spaces
|
||||
// around inline code
|
||||
editable.setSpan(
|
||||
store.get(CodeSpan.class),
|
||||
spanStart,
|
||||
spanStart + spanTextLength,
|
||||
Spanned.SPAN_EXCLUSIVE_EXCLUSIVE
|
||||
);
|
||||
})
|
||||
.include(CodeBlockSpan.class, (store, editable, input, span, spanStart, spanTextLength) -> {
|
||||
// we do not handle indented code blocks here
|
||||
if (input.charAt(spanStart) == '`') {
|
||||
final int firstLineEnd = input.indexOf('\n', spanStart);
|
||||
if (firstLineEnd == -1) return;
|
||||
int lastLineEnd = input.indexOf('\n', spanStart + (firstLineEnd - spanStart) + spanTextLength + 1);
|
||||
if (lastLineEnd == -1) lastLineEnd = input.length();
|
||||
|
||||
editable.setSpan(
|
||||
store.get(CodeBlockSpan.class),
|
||||
spanStart,
|
||||
lastLineEnd,
|
||||
Spanned.SPAN_EXCLUSIVE_EXCLUSIVE
|
||||
);
|
||||
}
|
||||
})
|
||||
.include(BlockQuoteSpan.class, (store, editable, input, span, spanStart, spanTextLength) -> {
|
||||
editable.setSpan(
|
||||
store.get(BlockQuoteSpan.class),
|
||||
spanStart,
|
||||
spanStart + spanTextLength,
|
||||
Spanned.SPAN_EXCLUSIVE_EXCLUSIVE
|
||||
);
|
||||
})
|
||||
.include(LinkSpan.class, (store, editable, input, span, spanStart, spanTextLength) -> {
|
||||
editable.setSpan(
|
||||
store.get(EditLinkSpan.class),
|
||||
// add underline only for link text
|
||||
spanStart + 1,
|
||||
spanStart + 1 + spanTextLength,
|
||||
Spanned.SPAN_EXCLUSIVE_EXCLUSIVE
|
||||
);
|
||||
})
|
||||
// returns nullable type
|
||||
.build();
|
||||
}
|
||||
|
||||
private static class CustomPunctuationSpan extends ForegroundColorSpan {
|
||||
CustomPunctuationSpan() {
|
||||
super(0xFFFF0000); // RED
|
||||
}
|
||||
}
|
||||
|
||||
private static class Bold extends MetricAffectingSpan {
|
||||
public Bold() {
|
||||
super();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void updateDrawState(TextPaint tp) {
|
||||
update(tp);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void updateMeasureState(@NonNull TextPaint textPaint) {
|
||||
update(textPaint);
|
||||
}
|
||||
|
||||
private void update(@NonNull TextPaint paint) {
|
||||
paint.setFakeBoldText(true);
|
||||
}
|
||||
}
|
||||
|
||||
private static class EditLinkSpan extends CharacterStyle {
|
||||
@Override
|
||||
public void updateDrawState(TextPaint tp) {
|
||||
tp.setColor(tp.linkColor);
|
||||
tp.setUnderlineText(true);
|
||||
}
|
||||
}
|
||||
}
|
@ -2,6 +2,7 @@
|
||||
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:clipToPadding="false"
|
||||
android:padding="8dip">
|
||||
|
||||
<EditText
|
||||
@ -9,8 +10,8 @@
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:autofillHints="none"
|
||||
android:hint="Message..."
|
||||
android:maxLines="100"
|
||||
android:inputType="text|textLongMessage|textMultiLine" />
|
||||
android:hint="Markdown..."
|
||||
android:inputType="text|textLongMessage|textMultiLine"
|
||||
android:maxLines="100" />
|
||||
|
||||
</FrameLayout>
|
@ -25,4 +25,6 @@
|
||||
|
||||
<string name="sample_precomputed_text"># \# PrecomputedText\n\nUsage of TextSetter and PrecomputedTextCompat</string>
|
||||
|
||||
<string name="sample_editor"># \# Editor\n\n`MarkwonEditor` sample usage to highlight user input in EditText</string>
|
||||
|
||||
</resources>
|
Loading…
x
Reference in New Issue
Block a user