Editor implementation

This commit is contained in:
Dimitry Ivanov 2019-11-07 00:27:41 +03:00
parent 870733ee2a
commit 8768e8a33c
21 changed files with 5842 additions and 15 deletions

View File

@ -1,5 +1,10 @@
# Changelog
# 4.2.0-SNAPSHOT
* `MarkwonEditor` to highlight markdown input whilst editing (new module: `markwon-editor`)
* `Markwon#configuration` method to expose `MarkwonConfiguration` via public API
* `HeadingSpan#getLevel` getter
# 4.1.2
* Do not re-use RenderProps when creating a new visitor (fixes [#171])

View File

@ -24,10 +24,17 @@ android {
proguardFile 'proguard.pro'
}
}
compileOptions {
targetCompatibility '1.8'
sourceCompatibility '1.8'
}
}
dependencies {
implementation project(':markwon-editor')
implementation project(':markwon-core')
implementation project(':markwon-ext-strikethrough')
implementation project(':markwon-ext-tables')

View File

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

View File

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

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,16 @@
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:padding="8dip">
<EditText
android:id="@+id/edit_text"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:autofillHints="none"
android:hint="Message..."
android:maxLines="100"
android:inputType="text|textLongMessage|textMultiLine" />
</FrameLayout>

View File

@ -134,6 +134,9 @@ public abstract class Markwon {
@NonNull
public abstract List<? extends MarkwonPlugin> getPlugins();
@NonNull
public abstract MarkwonConfiguration configuration();
/**
* Interface to set text on a TextView. Primary goal is to give a way to use PrecomputedText
* functionality

View File

@ -113,6 +113,7 @@ class MarkwonBuilderImpl implements Markwon.Builder {
textSetter,
parserBuilder.build(),
visitorFactory,
configuration,
Collections.unmodifiableList(plugins)
);
}

View File

@ -21,6 +21,7 @@ class MarkwonImpl extends Markwon {
private final TextView.BufferType bufferType;
private final Parser parser;
private final MarkwonVisitorFactory visitorFactory; // @since 4.1.1
private final MarkwonConfiguration configuration;
private final List<MarkwonPlugin> plugins;
// @since 4.1.0
@ -32,11 +33,13 @@ class MarkwonImpl extends Markwon {
@Nullable TextSetter textSetter,
@NonNull Parser parser,
@NonNull MarkwonVisitorFactory visitorFactory,
@NonNull MarkwonConfiguration configuration,
@NonNull List<MarkwonPlugin> plugins) {
this.bufferType = bufferType;
this.textSetter = textSetter;
this.parser = parser;
this.visitorFactory = visitorFactory;
this.configuration = configuration;
this.plugins = plugins;
}
@ -154,4 +157,10 @@ class MarkwonImpl extends Markwon {
public List<? extends MarkwonPlugin> getPlugins() {
return Collections.unmodifiableList(plugins);
}
@NonNull
@Override
public MarkwonConfiguration configuration() {
return configuration;
}
}

View File

@ -77,4 +77,11 @@ public class HeadingSpan extends MetricAffectingSpan implements LeadingMarginSpa
}
}
}
/**
* @since 4.2.0-SNAPSHOT
*/
public int getLevel() {
return level;
}
}

6
markwon-editor/README.md Normal file
View File

@ -0,0 +1,6 @@
# Editor
Markdown editor for Android based on `Markwon`.
Main principle: _difference_ between input text and rendered markdown is considered to be
_punctuation_.

View File

@ -0,0 +1,32 @@
apply plugin: 'com.android.library'
android {
compileSdkVersion config['compile-sdk']
buildToolsVersion config['build-tools']
defaultConfig {
minSdkVersion config['min-sdk']
targetSdkVersion config['target-sdk']
versionCode 1
versionName version
}
}
dependencies {
api project(':markwon-core')
deps['test'].with {
testImplementation project(':markwon-test-span')
testImplementation it['junit']
testImplementation it['robolectric']
testImplementation it['mockito']
testImplementation it['commons-io']
}
}
registerArtifact(this)

View File

@ -0,0 +1,4 @@
POM_NAME=Editor
POM_ARTIFACT_ID=editor
POM_DESCRIPTION=Markdown editor based on Markwon
POM_PACKAGING=aar

View File

@ -0,0 +1 @@
<manifest package="io.noties.markwon.editor" />

View File

@ -0,0 +1,44 @@
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;
class EditSpanHandlerBuilder {
private final Map<Class<?>, MarkwonEditor.EditSpanHandler> map = new HashMap<>(3);
<T> void include(@NonNull Class<T> type, @NonNull MarkwonEditor.EditSpanHandler<T> handler) {
map.put(type, handler);
}
@Nullable
MarkwonEditor.EditSpanHandler build() {
if (map.size() == 0) {
return null;
}
return new EditSpanHandlerImpl(map);
}
private static class EditSpanHandlerImpl implements MarkwonEditor.EditSpanHandler {
private final Map<Class<?>, MarkwonEditor.EditSpanHandler> map;
EditSpanHandlerImpl(@NonNull Map<Class<?>, MarkwonEditor.EditSpanHandler> 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());
if (handler != null) {
//noinspection unchecked
handler.handle(store, editable, input, span, spanStart, spanTextLength);
}
}
}
}

View File

@ -0,0 +1,160 @@
package io.noties.markwon.editor;
import android.text.Editable;
import androidx.annotation.NonNull;
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
/**
* @since 4.2.0-SNAPSHOT
*/
public abstract class MarkwonEditor {
public interface SpanStore {
/**
* If a span of specified type was not registered with {@link Builder#includeEditSpan(Class, SpanFactory)}
* 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 SpanFactory<T> {
@NonNull
T create();
}
public interface EditSpanHandler<T> {
void handle(
@NonNull SpanStore store,
@NonNull Editable editable,
@NonNull String input,
@NonNull T span,
int spanStart,
int spanTextLength);
}
public interface PreRenderResult {
// With which pre-render method was called as input
@NonNull
Editable resultEditable();
void dispatchTo(@NonNull Editable editable);
}
public interface PreRenderResultListener {
void onPreRenderResult(@NonNull PreRenderResult result);
}
@NonNull
public static Builder builder(@NonNull Markwon markwon) {
return new Builder(markwon);
}
/**
* Synchronous method that processes supplied Editable in-place. If you wish to move this job
* to another thread consider using {@link #preRender(Editable, PreRenderResultListener)}
*
* @param editable to process
* @see #preRender(Editable, PreRenderResultListener)
*/
public abstract void process(@NonNull Editable editable);
/**
* 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.
*
* @param editable to process and pre-render
* @param preRenderListener listener to be notified when pre-render result will be ready
* @see #process(Editable)
*/
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);
Builder(@NonNull Markwon markwon) {
this.markwon = markwon;
}
/**
* The only required argument which will make {@link MarkwonEditor} apply specified span only
* to markdown punctuation
*
* @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) {
this.punctuationSpanType = type;
this.spans.put(type, factory);
return this;
}
/**
* Include specific span that will be 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)}.
* 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}.
* 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 SpanFactory<T> factory) {
this.spans.put(type, factory);
return this;
}
@NonNull
public <T> Builder withEditSpanHandlerFor(@NonNull Class<T> type, @NonNull EditSpanHandler<T> editSpanHandler) {
this.editSpanHandlerBuilder.include(type, editSpanHandler);
return this;
}
@NonNull
public MarkwonEditor build() {
final Class<?> punctuationSpanType = this.punctuationSpanType;
if (punctuationSpanType == null) {
throw new IllegalStateException("Punctuation span type is required, " +
"add with Builder#withPunctuationSpan method");
}
return new MarkwonEditorImpl(
markwon,
spans,
punctuationSpanType,
editSpanHandlerBuilder.build());
}
}
}

View File

@ -0,0 +1,237 @@
package io.noties.markwon.editor;
import android.text.Editable;
import android.text.Spannable;
import android.text.SpannableStringBuilder;
import android.text.Spanned;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import io.noties.markwon.Markwon;
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 Class<?> punctuationSpanType;
@Nullable
private final EditSpanHandler editSpanHandler;
MarkwonEditorImpl(
@NonNull Markwon markwon,
@NonNull Map<Class<?>, SpanFactory> spans,
@NonNull Class<?> punctuationSpanType,
@Nullable EditSpanHandler editSpanHandler) {
this.markwon = markwon;
this.spans = spans;
this.punctuationSpanType = punctuationSpanType;
this.editSpanHandler = editSpanHandler;
}
@Override
public void process(@NonNull Editable editable) {
final String input = editable.toString();
final Spanned renderedMarkdown = markwon.toMarkdown(input);
final String markdown = renderedMarkdown.toString();
final EditSpanHandler editSpanHandler = this.editSpanHandler;
final boolean hasAdditionalSpans = editSpanHandler != null;
final SpanStoreImpl store = new SpanStoreImpl(editable, spans);
try {
final List<Diff> diffs = diff_match_patch.diff_main(input, markdown);
int inputLength = 0;
int markdownLength = 0;
for (Diff diff : diffs) {
switch (diff.operation) {
case DELETE:
final int start = inputLength;
inputLength += diff.text.length();
editable.setSpan(
store.get(punctuationSpanType),
start,
inputLength,
Spanned.SPAN_EXCLUSIVE_EXCLUSIVE
);
if (hasAdditionalSpans) {
final Object[] spans = renderedMarkdown.getSpans(markdownLength, markdownLength + 1, Object.class);
for (Object span : spans) {
if (markdownLength == renderedMarkdown.getSpanStart(span)) {
editSpanHandler.handle(
store,
editable,
input,
span,
start,
renderedMarkdown.getSpanEnd(span) - markdownLength);
}
}
}
break;
case INSERT:
markdownLength += diff.text.length();
break;
case EQUAL:
final int length = diff.text.length();
inputLength += length;
markdownLength += length;
break;
default:
throw new IllegalStateException();
}
}
} finally {
store.removeUnused();
}
}
@Override
public void preRender(@NonNull final Editable editable, @NonNull PreRenderResultListener listener) {
final RecordingSpannableStringBuilder builder = new RecordingSpannableStringBuilder(editable);
process(builder);
listener.onPreRenderResult(new PreRenderResult() {
@NonNull
@Override
public Editable resultEditable() {
// if they are the same, they should be equals then (what about additional spans?? like cursor? it should not interfere....)
return builder;
}
@Override
public void dispatchTo(@NonNull Editable e) {
for (Span span : builder.applied) {
e.setSpan(span.what, span.start, span.end, span.flags);
}
for (Object span : builder.removed) {
e.removeSpan(span);
}
}
});
}
@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 SpanStoreImpl implements SpanStore {
private final Spannable spannable;
private final Map<Class<?>, SpanFactory> spans;
private final Map<Class<?>, List<Object>> map;
SpanStoreImpl(@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(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 Builder#includeEditSpan method to register");
}
span = spanFactory.create();
}
//noinspection unchecked
return (T) span;
}
void removeUnused() {
for (List<Object> spans : map.values()) {
if (spans != null
&& spans.size() > 0) {
for (Object span : spans) {
spannable.removeSpan(span);
}
}
}
}
}
private static class Span {
final Object what;
final int start;
final int end;
final int flags;
Span(Object what, int start, int end, int flags) {
this.what = what;
this.start = start;
this.end = end;
this.flags = flags;
}
}
private static class RecordingSpannableStringBuilder extends SpannableStringBuilder {
final List<Span> applied = new ArrayList<>(3);
final List<Object> removed = new ArrayList<>(0);
RecordingSpannableStringBuilder(CharSequence text) {
super(text);
}
@Override
public void setSpan(Object what, int start, int end, int flags) {
super.setSpan(what, start, end, flags);
applied.add(new Span(what, start, end, flags));
}
@Override
public void removeSpan(Object what) {
super.removeSpan(what);
removed.add(what);
}
}
}

View File

@ -0,0 +1,140 @@
package io.noties.markwon.editor;
import android.text.Editable;
import android.text.TextWatcher;
import android.view.View;
import android.widget.EditText;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Future;
/**
* Implementation of TextWatcher that uses {@link MarkwonEditor#process(Editable)} method
* to apply markdown highlighting right after text changes.
*
* @see MarkwonEditor#process(Editable)
* @see MarkwonEditor#preRender(Editable, MarkwonEditor.PreRenderResultListener)
* @since 4.2.0-SNAPSHOT
*/
public abstract class MarkwonEditorTextWatcher implements TextWatcher {
@NonNull
public static MarkwonEditorTextWatcher withProcess(@NonNull MarkwonEditor editor) {
return new WithProcess(editor);
}
@NonNull
public static MarkwonEditorTextWatcher withPreRender(
@NonNull MarkwonEditor editor,
@NonNull ExecutorService executorService,
@NonNull EditText editText) {
return new WithPreRender(editor, executorService, editText);
}
@Override
public abstract void afterTextChanged(Editable s);
@Override
public void beforeTextChanged(CharSequence s, int start, int count, int after) {
}
@Override
public void onTextChanged(CharSequence s, int start, int before, int count) {
}
static class WithProcess extends MarkwonEditorTextWatcher {
private final MarkwonEditor editor;
private boolean selfChange;
public WithProcess(@NonNull MarkwonEditor editor) {
this.editor = editor;
}
@Override
public void afterTextChanged(Editable s) {
if (selfChange) {
return;
}
selfChange = true;
try {
editor.process(s);
} finally {
selfChange = false;
}
}
}
static class WithPreRender extends MarkwonEditorTextWatcher {
private final MarkwonEditor editor;
private final ExecutorService executorService;
@Nullable
private EditText editText;
private Future<?> future;
WithPreRender(
@NonNull MarkwonEditor editor,
@NonNull ExecutorService executorService,
@NonNull EditText editText) {
this.editor = editor;
this.executorService = executorService;
this.editText = editText;
this.editText.addOnAttachStateChangeListener(new View.OnAttachStateChangeListener() {
@Override
public void onViewAttachedToWindow(View v) {
}
@Override
public void onViewDetachedFromWindow(View v) {
WithPreRender.this.editText = null;
}
});
}
@Override
public void afterTextChanged(final Editable s) {
// todo: maybe checking hash is not so performant?
// what if we create a atomic reference and use it (with tag applied to editText)?
if (future != null) {
future.cancel(true);
}
future = executorService.submit(new Runnable() {
@Override
public void run() {
editor.preRender(s, new MarkwonEditor.PreRenderResultListener() {
@Override
public void onPreRenderResult(@NonNull final MarkwonEditor.PreRenderResult result) {
if (editText != null) {
final int key = result.resultEditable().toString().hashCode();
editText.post(new Runnable() {
@Override
public void run() {
if (key == editText.getText().toString().hashCode()) {
result.dispatchTo(editText.getText());
}
}
});
}
}
});
}
});
}
}
}

File diff suppressed because it is too large Load Diff

View File

@ -52,6 +52,7 @@ public class LinkifyPlugin extends AbstractMarkwonPlugin {
});
}
// todo: thread safety (builder is reused)
private static class LinkifyTextAddedListener implements CorePlugin.OnTextAddedListener {
private final int mask;

View File

@ -1,6 +1,7 @@
rootProject.name = 'MarkwonProject'
include ':app', ':sample',
':markwon-core',
':markwon-editor',
':markwon-ext-latex',
':markwon-ext-strikethrough',
':markwon-ext-tables',