Add editor sample
This commit is contained in:
parent
8768e8a33c
commit
f1e750b305
@ -53,11 +53,6 @@
|
|||||||
|
|
||||||
</activity>
|
</activity>
|
||||||
|
|
||||||
<activity
|
|
||||||
android:name=".edit.EditActivity"
|
|
||||||
android:exported="true"
|
|
||||||
android:windowSoftInputMode="adjustResize" />
|
|
||||||
|
|
||||||
</application>
|
</application>
|
||||||
|
|
||||||
</manifest>
|
</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.HashMap;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
|
||||||
class EditSpanHandlerBuilder {
|
/**
|
||||||
|
* @since 4.2.0-SNAPSHOT
|
||||||
|
*/
|
||||||
|
public class EditSpanHandlerBuilder {
|
||||||
|
|
||||||
private final Map<Class<?>, MarkwonEditor.EditSpanHandler> map = new HashMap<>(3);
|
public interface EditSpanHandlerTyped<T> {
|
||||||
|
void handle(
|
||||||
|
@NonNull MarkwonEditor.EditSpanStore store,
|
||||||
|
@NonNull Editable editable,
|
||||||
|
@NonNull String input,
|
||||||
|
@NonNull T span,
|
||||||
|
int spanStart,
|
||||||
|
int spanTextLength);
|
||||||
|
}
|
||||||
|
|
||||||
<T> void include(@NonNull Class<T> type, @NonNull MarkwonEditor.EditSpanHandler<T> handler) {
|
@NonNull
|
||||||
|
public static EditSpanHandlerBuilder create() {
|
||||||
|
return new EditSpanHandlerBuilder();
|
||||||
|
}
|
||||||
|
|
||||||
|
private final Map<Class<?>, EditSpanHandlerTyped> map = new HashMap<>(3);
|
||||||
|
|
||||||
|
@NonNull
|
||||||
|
public <T> EditSpanHandlerBuilder include(
|
||||||
|
@NonNull Class<T> type,
|
||||||
|
@NonNull EditSpanHandlerTyped<T> handler) {
|
||||||
map.put(type, handler);
|
map.put(type, handler);
|
||||||
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Nullable
|
@Nullable
|
||||||
MarkwonEditor.EditSpanHandler build() {
|
public MarkwonEditor.EditSpanHandler build() {
|
||||||
if (map.size() == 0) {
|
if (map.size() == 0) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
@ -26,15 +48,21 @@ class EditSpanHandlerBuilder {
|
|||||||
|
|
||||||
private static class EditSpanHandlerImpl implements MarkwonEditor.EditSpanHandler {
|
private static class EditSpanHandlerImpl implements MarkwonEditor.EditSpanHandler {
|
||||||
|
|
||||||
private final Map<Class<?>, MarkwonEditor.EditSpanHandler> map;
|
private final Map<Class<?>, EditSpanHandlerTyped> map;
|
||||||
|
|
||||||
EditSpanHandlerImpl(@NonNull Map<Class<?>, MarkwonEditor.EditSpanHandler> map) {
|
EditSpanHandlerImpl(@NonNull Map<Class<?>, EditSpanHandlerTyped> map) {
|
||||||
this.map = map;
|
this.map = map;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void handle(@NonNull MarkwonEditor.SpanStore store, @NonNull Editable editable, @NonNull String input, @NonNull Object span, int spanStart, int spanTextLength) {
|
public void handle(
|
||||||
final MarkwonEditor.EditSpanHandler handler = map.get(span.getClass());
|
@NonNull MarkwonEditor.EditSpanStore store,
|
||||||
|
@NonNull Editable editable,
|
||||||
|
@NonNull String input,
|
||||||
|
@NonNull Object span,
|
||||||
|
int spanStart,
|
||||||
|
int spanTextLength) {
|
||||||
|
final EditSpanHandlerTyped handler = map.get(span.getClass());
|
||||||
if (handler != null) {
|
if (handler != null) {
|
||||||
//noinspection unchecked
|
//noinspection unchecked
|
||||||
handler.handle(store, editable, input, span, spanStart, spanTextLength);
|
handler.handle(store, editable, input, span, spanStart, spanTextLength);
|
||||||
|
@ -3,29 +3,31 @@ package io.noties.markwon.editor;
|
|||||||
import android.text.Editable;
|
import android.text.Editable;
|
||||||
|
|
||||||
import androidx.annotation.NonNull;
|
import androidx.annotation.NonNull;
|
||||||
|
import androidx.annotation.Nullable;
|
||||||
|
|
||||||
import java.util.HashMap;
|
import java.util.HashMap;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
|
||||||
import io.noties.markwon.Markwon;
|
import io.noties.markwon.Markwon;
|
||||||
|
|
||||||
// todo: if multiple spans are used via factory... only the first one is delivered to edit-span-handler
|
|
||||||
// does it though? yeah... only the first one and then break... deliver all?
|
|
||||||
|
|
||||||
// todo: how to reuse existing spanFactories? to obtain a value they require render-props....
|
// todo: how to reuse existing spanFactories? to obtain a value they require render-props....
|
||||||
// maybe.. mock them? plus, spanFactory can return multiple spans
|
// maybe.. mock them? plus, spanFactory can return multiple spans
|
||||||
|
|
||||||
// todo: we can actually create punctuation span with reasonable defaults to be used by default
|
// todo: as we do text-diff, then images, latex and tables won't be handled... they will be treated as punctuation as-whole..
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
* @see #builder(Markwon)
|
||||||
|
* @see #create(Markwon)
|
||||||
|
* @see #process(Editable)
|
||||||
|
* @see #preRender(Editable, PreRenderResultListener)
|
||||||
* @since 4.2.0-SNAPSHOT
|
* @since 4.2.0-SNAPSHOT
|
||||||
*/
|
*/
|
||||||
public abstract class MarkwonEditor {
|
public abstract class MarkwonEditor {
|
||||||
|
|
||||||
public interface SpanStore {
|
public interface EditSpanStore {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* If a span of specified type was not registered with {@link Builder#includeEditSpan(Class, SpanFactory)}
|
* If a span of specified type was not registered with {@link Builder#includeEditSpan(Class, EditSpanFactory)}
|
||||||
* then an exception is raised.
|
* then an exception is raised.
|
||||||
*
|
*
|
||||||
* @param type of a span to obtain
|
* @param type of a span to obtain
|
||||||
@ -35,34 +37,68 @@ public abstract class MarkwonEditor {
|
|||||||
<T> T get(Class<T> type);
|
<T> T get(Class<T> type);
|
||||||
}
|
}
|
||||||
|
|
||||||
public interface SpanFactory<T> {
|
public interface EditSpanFactory<T> {
|
||||||
@NonNull
|
@NonNull
|
||||||
T create();
|
T create();
|
||||||
}
|
}
|
||||||
|
|
||||||
public interface EditSpanHandler<T> {
|
/**
|
||||||
|
* Interface to handle _original_ span that is present in rendered markdown. Can be useful
|
||||||
|
* to add specific spans for EditText (for example, make text bold to better indicate
|
||||||
|
* strong emphasis used in markdown input).
|
||||||
|
*
|
||||||
|
* @see Builder#withEditSpanHandler(EditSpanHandler)
|
||||||
|
* @see EditSpanHandlerBuilder
|
||||||
|
*/
|
||||||
|
public interface EditSpanHandler {
|
||||||
void handle(
|
void handle(
|
||||||
@NonNull SpanStore store,
|
@NonNull EditSpanStore store,
|
||||||
@NonNull Editable editable,
|
@NonNull Editable editable,
|
||||||
@NonNull String input,
|
@NonNull String input,
|
||||||
@NonNull T span,
|
@NonNull Object span,
|
||||||
int spanStart,
|
int spanStart,
|
||||||
int spanTextLength);
|
int spanTextLength);
|
||||||
}
|
}
|
||||||
|
|
||||||
public interface PreRenderResult {
|
public interface PreRenderResult {
|
||||||
|
|
||||||
// With which pre-render method was called as input
|
/**
|
||||||
|
* @return Editable instance for which result was calculated. This must not be
|
||||||
|
* actual Editable of EditText
|
||||||
|
*/
|
||||||
@NonNull
|
@NonNull
|
||||||
Editable resultEditable();
|
Editable resultEditable();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Dispatch pre-rendering result to EditText
|
||||||
|
*
|
||||||
|
* @param editable to dispatch result to
|
||||||
|
*/
|
||||||
void dispatchTo(@NonNull Editable editable);
|
void dispatchTo(@NonNull Editable editable);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @see #preRender(Editable, PreRenderResultListener)
|
||||||
|
*/
|
||||||
public interface PreRenderResultListener {
|
public interface PreRenderResultListener {
|
||||||
void onPreRenderResult(@NonNull PreRenderResult result);
|
void onPreRenderResult(@NonNull PreRenderResult result);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates default instance of {@link MarkwonEditor}. By default it will handle only
|
||||||
|
* punctuation spans (highlight markdown punctuation and nothing more).
|
||||||
|
*
|
||||||
|
* @see #builder(Markwon)
|
||||||
|
*/
|
||||||
|
@NonNull
|
||||||
|
public static MarkwonEditor create(@NonNull Markwon markwon) {
|
||||||
|
return builder(markwon).build();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @see #create(Markwon)
|
||||||
|
* @see Builder
|
||||||
|
*/
|
||||||
@NonNull
|
@NonNull
|
||||||
public static Builder builder(@NonNull Markwon markwon) {
|
public static Builder builder(@NonNull Markwon markwon) {
|
||||||
return new Builder(markwon);
|
return new Builder(markwon);
|
||||||
@ -78,9 +114,12 @@ public abstract class MarkwonEditor {
|
|||||||
public abstract void process(@NonNull Editable editable);
|
public abstract void process(@NonNull Editable editable);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
* Pre-render highlight result. Can be useful to create highlight information on a different
|
||||||
|
* thread.
|
||||||
|
* <p>
|
||||||
* Please note that currently only `setSpan` and `removeSpan` actions will be recorded (and thus dispatched).
|
* Please note that currently only `setSpan` and `removeSpan` actions will be recorded (and thus dispatched).
|
||||||
* make sure you use only these methods in your {@link EditSpanHandler}, or implement the required
|
* Make sure you use only these methods in your {@link EditSpanHandler}, or implement the required
|
||||||
* functionality in some other way.
|
* functionality some other way.
|
||||||
*
|
*
|
||||||
* @param editable to process and pre-render
|
* @param editable to process and pre-render
|
||||||
* @param preRenderListener listener to be notified when pre-render result will be ready
|
* @param preRenderListener listener to be notified when pre-render result will be ready
|
||||||
@ -88,40 +127,40 @@ public abstract class MarkwonEditor {
|
|||||||
*/
|
*/
|
||||||
public abstract void preRender(@NonNull Editable editable, @NonNull PreRenderResultListener preRenderListener);
|
public abstract void preRender(@NonNull Editable editable, @NonNull PreRenderResultListener preRenderListener);
|
||||||
|
|
||||||
|
|
||||||
public static class Builder {
|
public static class Builder {
|
||||||
|
|
||||||
private final Markwon markwon;
|
private final Markwon markwon;
|
||||||
private final EditSpanHandlerBuilder editSpanHandlerBuilder = new EditSpanHandlerBuilder();
|
|
||||||
|
|
||||||
private Class<?> punctuationSpanType;
|
private Class<?> punctuationSpanType;
|
||||||
private Map<Class<?>, SpanFactory> spans = new HashMap<>(3);
|
private Map<Class<?>, EditSpanFactory> spans = new HashMap<>(3);
|
||||||
|
private EditSpanHandler editSpanHandler;
|
||||||
|
|
||||||
Builder(@NonNull Markwon markwon) {
|
Builder(@NonNull Markwon markwon) {
|
||||||
this.markwon = markwon;
|
this.markwon = markwon;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The only required argument which will make {@link MarkwonEditor} apply specified span only
|
* Specify which punctuation span will be used.
|
||||||
* to markdown punctuation
|
|
||||||
*
|
*
|
||||||
* @param type of the span
|
* @param type of the span
|
||||||
* @param factory to create a new instance of the span
|
* @param factory to create a new instance of the span
|
||||||
*/
|
*/
|
||||||
@NonNull
|
@NonNull
|
||||||
public <T> Builder withPunctuationSpan(@NonNull Class<T> type, @NonNull SpanFactory<T> factory) {
|
public <T> Builder withPunctuationSpan(@NonNull Class<T> type, @NonNull EditSpanFactory<T> factory) {
|
||||||
this.punctuationSpanType = type;
|
this.punctuationSpanType = type;
|
||||||
this.spans.put(type, factory);
|
this.spans.put(type, factory);
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Include specific span that will be used in highlighting. It is important to understand
|
* Include additional span handling that is used in highlighting. It is important to understand
|
||||||
* that it is not the span that is used by Markwon, but instead your own span that you
|
* that it is not the span that is used by Markwon, but instead your own span that you
|
||||||
* apply in a custom {@link EditSpanHandler} specified by {@link #withEditSpanHandlerFor(Class, EditSpanHandler)}.
|
* apply in a custom {@link EditSpanHandler} specified by {@link #withEditSpanHandler(EditSpanHandler)}.
|
||||||
* You can apply a Markwon bundled span (or any other) but it must be still explicitly
|
* You can apply a Markwon bundled span (or any other) but it must be still explicitly
|
||||||
* included by this method.
|
* included by this method.
|
||||||
* <p>
|
* <p>
|
||||||
* The span will be exposed via {@link SpanStore} in your custom {@link EditSpanHandler}.
|
* The span will be exposed via {@link EditSpanStore} in your custom {@link EditSpanHandler}.
|
||||||
* If you do not use a custom {@link EditSpanHandler} you do not need to specify any span here.
|
* If you do not use a custom {@link EditSpanHandler} you do not need to specify any span here.
|
||||||
*
|
*
|
||||||
* @param type of a span to include
|
* @param type of a span to include
|
||||||
@ -130,31 +169,50 @@ public abstract class MarkwonEditor {
|
|||||||
@NonNull
|
@NonNull
|
||||||
public <T> Builder includeEditSpan(
|
public <T> Builder includeEditSpan(
|
||||||
@NonNull Class<T> type,
|
@NonNull Class<T> type,
|
||||||
@NonNull SpanFactory<T> factory) {
|
@NonNull EditSpanFactory<T> factory) {
|
||||||
this.spans.put(type, factory);
|
this.spans.put(type, factory);
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Additional handling of markdown spans.
|
||||||
|
*
|
||||||
|
* @param editSpanHandler handler for additional highlight spans
|
||||||
|
* @see EditSpanHandler
|
||||||
|
* @see EditSpanHandlerBuilder
|
||||||
|
*/
|
||||||
@NonNull
|
@NonNull
|
||||||
public <T> Builder withEditSpanHandlerFor(@NonNull Class<T> type, @NonNull EditSpanHandler<T> editSpanHandler) {
|
public Builder withEditSpanHandler(@Nullable EditSpanHandler editSpanHandler) {
|
||||||
this.editSpanHandlerBuilder.include(type, editSpanHandler);
|
this.editSpanHandler = editSpanHandler;
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
@NonNull
|
@NonNull
|
||||||
public MarkwonEditor build() {
|
public MarkwonEditor build() {
|
||||||
|
|
||||||
final Class<?> punctuationSpanType = this.punctuationSpanType;
|
Class<?> punctuationSpanType = this.punctuationSpanType;
|
||||||
if (punctuationSpanType == null) {
|
if (punctuationSpanType == null) {
|
||||||
throw new IllegalStateException("Punctuation span type is required, " +
|
withPunctuationSpan(PunctuationSpan.class, new EditSpanFactory<PunctuationSpan>() {
|
||||||
"add with Builder#withPunctuationSpan method");
|
@NonNull
|
||||||
|
@Override
|
||||||
|
public PunctuationSpan create() {
|
||||||
|
return new PunctuationSpan();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
punctuationSpanType = this.punctuationSpanType;
|
||||||
|
}
|
||||||
|
|
||||||
|
// if we have no editSpanHandler, but spans are registered -> throw an error
|
||||||
|
if (spans.size() > 1 && editSpanHandler == null) {
|
||||||
|
throw new IllegalStateException("There is no need to include edit spans " +
|
||||||
|
"when you do not use custom EditSpanHandler");
|
||||||
}
|
}
|
||||||
|
|
||||||
return new MarkwonEditorImpl(
|
return new MarkwonEditorImpl(
|
||||||
markwon,
|
markwon,
|
||||||
spans,
|
spans,
|
||||||
punctuationSpanType,
|
punctuationSpanType,
|
||||||
editSpanHandlerBuilder.build());
|
editSpanHandler);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -20,7 +20,7 @@ import io.noties.markwon.editor.diff_match_patch.Diff;
|
|||||||
class MarkwonEditorImpl extends MarkwonEditor {
|
class MarkwonEditorImpl extends MarkwonEditor {
|
||||||
|
|
||||||
private final Markwon markwon;
|
private final Markwon markwon;
|
||||||
private final Map<Class<?>, SpanFactory> spans;
|
private final Map<Class<?>, EditSpanFactory> spans;
|
||||||
private final Class<?> punctuationSpanType;
|
private final Class<?> punctuationSpanType;
|
||||||
|
|
||||||
@Nullable
|
@Nullable
|
||||||
@ -28,7 +28,7 @@ class MarkwonEditorImpl extends MarkwonEditor {
|
|||||||
|
|
||||||
MarkwonEditorImpl(
|
MarkwonEditorImpl(
|
||||||
@NonNull Markwon markwon,
|
@NonNull Markwon markwon,
|
||||||
@NonNull Map<Class<?>, SpanFactory> spans,
|
@NonNull Map<Class<?>, EditSpanFactory> spans,
|
||||||
@NonNull Class<?> punctuationSpanType,
|
@NonNull Class<?> punctuationSpanType,
|
||||||
@Nullable EditSpanHandler editSpanHandler) {
|
@Nullable EditSpanHandler editSpanHandler) {
|
||||||
this.markwon = markwon;
|
this.markwon = markwon;
|
||||||
@ -47,7 +47,7 @@ class MarkwonEditorImpl extends MarkwonEditor {
|
|||||||
final EditSpanHandler editSpanHandler = this.editSpanHandler;
|
final EditSpanHandler editSpanHandler = this.editSpanHandler;
|
||||||
final boolean hasAdditionalSpans = editSpanHandler != null;
|
final boolean hasAdditionalSpans = editSpanHandler != null;
|
||||||
|
|
||||||
final SpanStoreImpl store = new SpanStoreImpl(editable, spans);
|
final EditSpanStoreImpl store = new EditSpanStoreImpl(editable, spans);
|
||||||
try {
|
try {
|
||||||
|
|
||||||
final List<Diff> diffs = diff_match_patch.diff_main(input, markdown);
|
final List<Diff> diffs = diff_match_patch.diff_main(input, markdown);
|
||||||
@ -81,6 +81,9 @@ class MarkwonEditorImpl extends MarkwonEditor {
|
|||||||
span,
|
span,
|
||||||
start,
|
start,
|
||||||
renderedMarkdown.getSpanEnd(span) - markdownLength);
|
renderedMarkdown.getSpanEnd(span) - markdownLength);
|
||||||
|
// NB, we do not break here in case of SpanFactory
|
||||||
|
// returns multiple spans for a markdown node, this way
|
||||||
|
// we will handle all of them
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -153,13 +156,13 @@ class MarkwonEditorImpl extends MarkwonEditor {
|
|||||||
return map;
|
return map;
|
||||||
}
|
}
|
||||||
|
|
||||||
static class SpanStoreImpl implements SpanStore {
|
static class EditSpanStoreImpl implements EditSpanStore {
|
||||||
|
|
||||||
private final Spannable spannable;
|
private final Spannable spannable;
|
||||||
private final Map<Class<?>, SpanFactory> spans;
|
private final Map<Class<?>, EditSpanFactory> spans;
|
||||||
private final Map<Class<?>, List<Object>> map;
|
private final Map<Class<?>, List<Object>> map;
|
||||||
|
|
||||||
SpanStoreImpl(@NonNull Spannable spannable, @NonNull Map<Class<?>, SpanFactory> spans) {
|
EditSpanStoreImpl(@NonNull Spannable spannable, @NonNull Map<Class<?>, EditSpanFactory> spans) {
|
||||||
this.spannable = spannable;
|
this.spannable = spannable;
|
||||||
this.spans = spans;
|
this.spans = spans;
|
||||||
this.map = extractSpans(spannable, spans.keySet());
|
this.map = extractSpans(spannable, spans.keySet());
|
||||||
@ -175,7 +178,7 @@ class MarkwonEditorImpl extends MarkwonEditor {
|
|||||||
if (list != null && list.size() > 0) {
|
if (list != null && list.size() > 0) {
|
||||||
span = list.remove(0);
|
span = list.remove(0);
|
||||||
} else {
|
} else {
|
||||||
final SpanFactory spanFactory = spans.get(type);
|
final EditSpanFactory spanFactory = spans.get(type);
|
||||||
if (spanFactory == null) {
|
if (spanFactory == null) {
|
||||||
throw new IllegalStateException("Requested type `" + type.getName() + "` was " +
|
throw new IllegalStateException("Requested type `" + type.getName() + "` was " +
|
||||||
"not registered, use Builder#includeEditSpan method to register");
|
"not registered, use Builder#includeEditSpan method to register");
|
||||||
|
@ -17,6 +17,8 @@ import java.util.concurrent.Future;
|
|||||||
*
|
*
|
||||||
* @see MarkwonEditor#process(Editable)
|
* @see MarkwonEditor#process(Editable)
|
||||||
* @see MarkwonEditor#preRender(Editable, MarkwonEditor.PreRenderResultListener)
|
* @see MarkwonEditor#preRender(Editable, MarkwonEditor.PreRenderResultListener)
|
||||||
|
* @see #withProcess(MarkwonEditor)
|
||||||
|
* @see #withPreRender(MarkwonEditor, ExecutorService, EditText)
|
||||||
* @since 4.2.0-SNAPSHOT
|
* @since 4.2.0-SNAPSHOT
|
||||||
*/
|
*/
|
||||||
public abstract class MarkwonEditorTextWatcher implements TextWatcher {
|
public abstract class MarkwonEditorTextWatcher implements TextWatcher {
|
||||||
@ -54,7 +56,7 @@ public abstract class MarkwonEditorTextWatcher implements TextWatcher {
|
|||||||
|
|
||||||
private boolean selfChange;
|
private boolean selfChange;
|
||||||
|
|
||||||
public WithProcess(@NonNull MarkwonEditor editor) {
|
WithProcess(@NonNull MarkwonEditor editor) {
|
||||||
this.editor = editor;
|
this.editor = editor;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -84,6 +86,8 @@ public abstract class MarkwonEditorTextWatcher implements TextWatcher {
|
|||||||
|
|
||||||
private Future<?> future;
|
private Future<?> future;
|
||||||
|
|
||||||
|
private boolean selfChange;
|
||||||
|
|
||||||
WithPreRender(
|
WithPreRender(
|
||||||
@NonNull MarkwonEditor editor,
|
@NonNull MarkwonEditor editor,
|
||||||
@NonNull ExecutorService executorService,
|
@NonNull ExecutorService executorService,
|
||||||
@ -107,6 +111,10 @@ public abstract class MarkwonEditorTextWatcher implements TextWatcher {
|
|||||||
@Override
|
@Override
|
||||||
public void afterTextChanged(final Editable s) {
|
public void afterTextChanged(final Editable s) {
|
||||||
|
|
||||||
|
if (selfChange) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
// todo: maybe checking hash is not so performant?
|
// todo: maybe checking hash is not so performant?
|
||||||
// what if we create a atomic reference and use it (with tag applied to editText)?
|
// what if we create a atomic reference and use it (with tag applied to editText)?
|
||||||
|
|
||||||
@ -121,12 +129,17 @@ public abstract class MarkwonEditorTextWatcher implements TextWatcher {
|
|||||||
@Override
|
@Override
|
||||||
public void onPreRenderResult(@NonNull final MarkwonEditor.PreRenderResult result) {
|
public void onPreRenderResult(@NonNull final MarkwonEditor.PreRenderResult result) {
|
||||||
if (editText != null) {
|
if (editText != null) {
|
||||||
final int key = result.resultEditable().toString().hashCode();
|
final int key = key(result.resultEditable());
|
||||||
editText.post(new Runnable() {
|
editText.post(new Runnable() {
|
||||||
@Override
|
@Override
|
||||||
public void run() {
|
public void run() {
|
||||||
if (key == editText.getText().toString().hashCode()) {
|
if (key == key(editText.getText())) {
|
||||||
result.dispatchTo(editText.getText());
|
selfChange = true;
|
||||||
|
try {
|
||||||
|
result.dispatchTo(editText.getText());
|
||||||
|
} finally {
|
||||||
|
selfChange = false;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@ -136,5 +149,12 @@ public abstract class MarkwonEditorTextWatcher implements TextWatcher {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static int key(@NonNull Editable editable) {
|
||||||
|
// toString is important here, as using #hashCode directly
|
||||||
|
// would also check for spans (and some spans can be added/removed). This is why
|
||||||
|
// we are checking for exact match of text
|
||||||
|
return editable.toString().hashCode();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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 {
|
dependencies {
|
||||||
|
|
||||||
implementation project(':markwon-core')
|
implementation project(':markwon-core')
|
||||||
|
implementation project(':markwon-editor')
|
||||||
implementation project(':markwon-ext-latex')
|
implementation project(':markwon-ext-latex')
|
||||||
implementation project(':markwon-ext-strikethrough')
|
implementation project(':markwon-ext-strikethrough')
|
||||||
implementation project(':markwon-ext-tables')
|
implementation project(':markwon-ext-tables')
|
||||||
|
@ -28,6 +28,7 @@
|
|||||||
<activity android:name=".simpleext.SimpleExtActivity" />
|
<activity android:name=".simpleext.SimpleExtActivity" />
|
||||||
<activity android:name=".customextension2.CustomExtensionActivity2" />
|
<activity android:name=".customextension2.CustomExtensionActivity2" />
|
||||||
<activity android:name=".precomputed.PrecomputedActivity" />
|
<activity android:name=".precomputed.PrecomputedActivity" />
|
||||||
|
<activity android:name=".editor.EditorActivity" />
|
||||||
|
|
||||||
</application>
|
</application>
|
||||||
|
|
||||||
|
@ -22,6 +22,7 @@ import io.noties.markwon.sample.basicplugins.BasicPluginsActivity;
|
|||||||
import io.noties.markwon.sample.core.CoreActivity;
|
import io.noties.markwon.sample.core.CoreActivity;
|
||||||
import io.noties.markwon.sample.customextension.CustomExtensionActivity;
|
import io.noties.markwon.sample.customextension.CustomExtensionActivity;
|
||||||
import io.noties.markwon.sample.customextension2.CustomExtensionActivity2;
|
import io.noties.markwon.sample.customextension2.CustomExtensionActivity2;
|
||||||
|
import io.noties.markwon.sample.editor.EditorActivity;
|
||||||
import io.noties.markwon.sample.html.HtmlActivity;
|
import io.noties.markwon.sample.html.HtmlActivity;
|
||||||
import io.noties.markwon.sample.latex.LatexActivity;
|
import io.noties.markwon.sample.latex.LatexActivity;
|
||||||
import io.noties.markwon.sample.precomputed.PrecomputedActivity;
|
import io.noties.markwon.sample.precomputed.PrecomputedActivity;
|
||||||
@ -117,6 +118,10 @@ public class MainActivity extends Activity {
|
|||||||
activity = PrecomputedActivity.class;
|
activity = PrecomputedActivity.class;
|
||||||
break;
|
break;
|
||||||
|
|
||||||
|
case EDITOR:
|
||||||
|
activity = EditorActivity.class;
|
||||||
|
break;
|
||||||
|
|
||||||
default:
|
default:
|
||||||
throw new IllegalStateException("No Activity is associated with sample-item: " + item);
|
throw new IllegalStateException("No Activity is associated with sample-item: " + item);
|
||||||
}
|
}
|
||||||
|
@ -21,7 +21,9 @@ public enum Sample {
|
|||||||
|
|
||||||
CUSTOM_EXTENSION_2(R.string.sample_custom_extension_2),
|
CUSTOM_EXTENSION_2(R.string.sample_custom_extension_2),
|
||||||
|
|
||||||
PRECOMPUTED_TEXT(R.string.sample_precomputed_text);
|
PRECOMPUTED_TEXT(R.string.sample_precomputed_text),
|
||||||
|
|
||||||
|
EDITOR(R.string.sample_editor);
|
||||||
|
|
||||||
private final int textResId;
|
private final int textResId;
|
||||||
|
|
||||||
|
@ -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"
|
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="match_parent"
|
android:layout_height="match_parent"
|
||||||
|
android:clipToPadding="false"
|
||||||
android:padding="8dip">
|
android:padding="8dip">
|
||||||
|
|
||||||
<EditText
|
<EditText
|
||||||
@ -9,8 +10,8 @@
|
|||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:autofillHints="none"
|
android:autofillHints="none"
|
||||||
android:hint="Message..."
|
android:hint="Markdown..."
|
||||||
android:maxLines="100"
|
android:inputType="text|textLongMessage|textMultiLine"
|
||||||
android:inputType="text|textLongMessage|textMultiLine" />
|
android:maxLines="100" />
|
||||||
|
|
||||||
</FrameLayout>
|
</FrameLayout>
|
@ -25,4 +25,6 @@
|
|||||||
|
|
||||||
<string name="sample_precomputed_text"># \# PrecomputedText\n\nUsage of TextSetter and PrecomputedTextCompat</string>
|
<string name="sample_precomputed_text"># \# PrecomputedText\n\nUsage of TextSetter and PrecomputedTextCompat</string>
|
||||||
|
|
||||||
|
<string name="sample_editor"># \# Editor\n\n`MarkwonEditor` sample usage to highlight user input in EditText</string>
|
||||||
|
|
||||||
</resources>
|
</resources>
|
Loading…
x
Reference in New Issue
Block a user