Editor implementation
This commit is contained in:
parent
870733ee2a
commit
8768e8a33c
@ -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])
|
||||
|
||||
|
@ -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')
|
||||
|
@ -53,6 +53,11 @@
|
||||
|
||||
</activity>
|
||||
|
||||
<activity
|
||||
android:name=".edit.EditActivity"
|
||||
android:exported="true"
|
||||
android:windowSoftInputMode="adjustResize" />
|
||||
|
||||
</application>
|
||||
|
||||
</manifest>
|
185
app/src/main/java/io/noties/markwon/app/edit/EditActivity.java
Normal file
185
app/src/main/java/io/noties/markwon/app/edit/EditActivity.java
Normal 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?  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);
|
||||
}
|
||||
}
|
||||
}
|
2471
app/src/main/java/io/noties/markwon/app/edit/diff_match_patch.java
Normal file
2471
app/src/main/java/io/noties/markwon/app/edit/diff_match_patch.java
Normal file
File diff suppressed because it is too large
Load Diff
16
app/src/main/res/layout/activity_edit.xml
Normal file
16
app/src/main/res/layout/activity_edit.xml
Normal 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>
|
@ -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
|
||||
@ -141,21 +144,21 @@ public abstract class Markwon {
|
||||
* @see PrecomputedTextSetterCompat
|
||||
* @since 4.1.0
|
||||
*/
|
||||
public interface TextSetter {
|
||||
/**
|
||||
* @param textView TextView
|
||||
* @param markdown prepared markdown
|
||||
* @param bufferType BufferType specified when building {@link Markwon} instance
|
||||
* via {@link Builder#bufferType(TextView.BufferType)}
|
||||
* @param onComplete action to run when set-text is finished (required to call in order
|
||||
* to execute {@link MarkwonPlugin#afterSetText(TextView)})
|
||||
*/
|
||||
void setText(
|
||||
@NonNull TextView textView,
|
||||
@NonNull Spanned markdown,
|
||||
@NonNull TextView.BufferType bufferType,
|
||||
@NonNull Runnable onComplete);
|
||||
}
|
||||
public interface TextSetter {
|
||||
/**
|
||||
* @param textView TextView
|
||||
* @param markdown prepared markdown
|
||||
* @param bufferType BufferType specified when building {@link Markwon} instance
|
||||
* via {@link Builder#bufferType(TextView.BufferType)}
|
||||
* @param onComplete action to run when set-text is finished (required to call in order
|
||||
* to execute {@link MarkwonPlugin#afterSetText(TextView)})
|
||||
*/
|
||||
void setText(
|
||||
@NonNull TextView textView,
|
||||
@NonNull Spanned markdown,
|
||||
@NonNull TextView.BufferType bufferType,
|
||||
@NonNull Runnable onComplete);
|
||||
}
|
||||
|
||||
/**
|
||||
* Builder for {@link Markwon}.
|
||||
|
@ -113,6 +113,7 @@ class MarkwonBuilderImpl implements Markwon.Builder {
|
||||
textSetter,
|
||||
parserBuilder.build(),
|
||||
visitorFactory,
|
||||
configuration,
|
||||
Collections.unmodifiableList(plugins)
|
||||
);
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
@ -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
6
markwon-editor/README.md
Normal 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_.
|
32
markwon-editor/build.gradle
Normal file
32
markwon-editor/build.gradle
Normal 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)
|
4
markwon-editor/gradle.properties
Normal file
4
markwon-editor/gradle.properties
Normal file
@ -0,0 +1,4 @@
|
||||
POM_NAME=Editor
|
||||
POM_ARTIFACT_ID=editor
|
||||
POM_DESCRIPTION=Markdown editor based on Markwon
|
||||
POM_PACKAGING=aar
|
1
markwon-editor/src/main/AndroidManifest.xml
Normal file
1
markwon-editor/src/main/AndroidManifest.xml
Normal file
@ -0,0 +1 @@
|
||||
<manifest package="io.noties.markwon.editor" />
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -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());
|
||||
}
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
@ -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
@ -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;
|
||||
|
@ -1,6 +1,7 @@
|
||||
rootProject.name = 'MarkwonProject'
|
||||
include ':app', ':sample',
|
||||
':markwon-core',
|
||||
':markwon-editor',
|
||||
':markwon-ext-latex',
|
||||
':markwon-ext-strikethrough',
|
||||
':markwon-ext-tables',
|
||||
|
Loading…
x
Reference in New Issue
Block a user