Editor implementation
This commit is contained in:
parent
870733ee2a
commit
8768e8a33c
@ -1,5 +1,10 @@
|
|||||||
# Changelog
|
# 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
|
# 4.1.2
|
||||||
* Do not re-use RenderProps when creating a new visitor (fixes [#171])
|
* Do not re-use RenderProps when creating a new visitor (fixes [#171])
|
||||||
|
|
||||||
|
@ -24,10 +24,17 @@ android {
|
|||||||
proguardFile 'proguard.pro'
|
proguardFile 'proguard.pro'
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
compileOptions {
|
||||||
|
targetCompatibility '1.8'
|
||||||
|
sourceCompatibility '1.8'
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
|
|
||||||
|
implementation project(':markwon-editor')
|
||||||
|
|
||||||
implementation project(':markwon-core')
|
implementation project(':markwon-core')
|
||||||
implementation project(':markwon-ext-strikethrough')
|
implementation project(':markwon-ext-strikethrough')
|
||||||
implementation project(':markwon-ext-tables')
|
implementation project(':markwon-ext-tables')
|
||||||
|
@ -53,6 +53,11 @@
|
|||||||
|
|
||||||
</activity>
|
</activity>
|
||||||
|
|
||||||
|
<activity
|
||||||
|
android:name=".edit.EditActivity"
|
||||||
|
android:exported="true"
|
||||||
|
android:windowSoftInputMode="adjustResize" />
|
||||||
|
|
||||||
</application>
|
</application>
|
||||||
|
|
||||||
</manifest>
|
</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
|
@NonNull
|
||||||
public abstract List<? extends MarkwonPlugin> getPlugins();
|
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
|
* Interface to set text on a TextView. Primary goal is to give a way to use PrecomputedText
|
||||||
* functionality
|
* functionality
|
||||||
@ -141,7 +144,7 @@ public abstract class Markwon {
|
|||||||
* @see PrecomputedTextSetterCompat
|
* @see PrecomputedTextSetterCompat
|
||||||
* @since 4.1.0
|
* @since 4.1.0
|
||||||
*/
|
*/
|
||||||
public interface TextSetter {
|
public interface TextSetter {
|
||||||
/**
|
/**
|
||||||
* @param textView TextView
|
* @param textView TextView
|
||||||
* @param markdown prepared markdown
|
* @param markdown prepared markdown
|
||||||
@ -155,7 +158,7 @@ public interface TextSetter {
|
|||||||
@NonNull Spanned markdown,
|
@NonNull Spanned markdown,
|
||||||
@NonNull TextView.BufferType bufferType,
|
@NonNull TextView.BufferType bufferType,
|
||||||
@NonNull Runnable onComplete);
|
@NonNull Runnable onComplete);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Builder for {@link Markwon}.
|
* Builder for {@link Markwon}.
|
||||||
|
@ -113,6 +113,7 @@ class MarkwonBuilderImpl implements Markwon.Builder {
|
|||||||
textSetter,
|
textSetter,
|
||||||
parserBuilder.build(),
|
parserBuilder.build(),
|
||||||
visitorFactory,
|
visitorFactory,
|
||||||
|
configuration,
|
||||||
Collections.unmodifiableList(plugins)
|
Collections.unmodifiableList(plugins)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -21,6 +21,7 @@ class MarkwonImpl extends Markwon {
|
|||||||
private final TextView.BufferType bufferType;
|
private final TextView.BufferType bufferType;
|
||||||
private final Parser parser;
|
private final Parser parser;
|
||||||
private final MarkwonVisitorFactory visitorFactory; // @since 4.1.1
|
private final MarkwonVisitorFactory visitorFactory; // @since 4.1.1
|
||||||
|
private final MarkwonConfiguration configuration;
|
||||||
private final List<MarkwonPlugin> plugins;
|
private final List<MarkwonPlugin> plugins;
|
||||||
|
|
||||||
// @since 4.1.0
|
// @since 4.1.0
|
||||||
@ -32,11 +33,13 @@ class MarkwonImpl extends Markwon {
|
|||||||
@Nullable TextSetter textSetter,
|
@Nullable TextSetter textSetter,
|
||||||
@NonNull Parser parser,
|
@NonNull Parser parser,
|
||||||
@NonNull MarkwonVisitorFactory visitorFactory,
|
@NonNull MarkwonVisitorFactory visitorFactory,
|
||||||
|
@NonNull MarkwonConfiguration configuration,
|
||||||
@NonNull List<MarkwonPlugin> plugins) {
|
@NonNull List<MarkwonPlugin> plugins) {
|
||||||
this.bufferType = bufferType;
|
this.bufferType = bufferType;
|
||||||
this.textSetter = textSetter;
|
this.textSetter = textSetter;
|
||||||
this.parser = parser;
|
this.parser = parser;
|
||||||
this.visitorFactory = visitorFactory;
|
this.visitorFactory = visitorFactory;
|
||||||
|
this.configuration = configuration;
|
||||||
this.plugins = plugins;
|
this.plugins = plugins;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -154,4 +157,10 @@ class MarkwonImpl extends Markwon {
|
|||||||
public List<? extends MarkwonPlugin> getPlugins() {
|
public List<? extends MarkwonPlugin> getPlugins() {
|
||||||
return Collections.unmodifiableList(plugins);
|
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 static class LinkifyTextAddedListener implements CorePlugin.OnTextAddedListener {
|
||||||
|
|
||||||
private final int mask;
|
private final int mask;
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
rootProject.name = 'MarkwonProject'
|
rootProject.name = 'MarkwonProject'
|
||||||
include ':app', ':sample',
|
include ':app', ':sample',
|
||||||
':markwon-core',
|
':markwon-core',
|
||||||
|
':markwon-editor',
|
||||||
':markwon-ext-latex',
|
':markwon-ext-latex',
|
||||||
':markwon-ext-strikethrough',
|
':markwon-ext-strikethrough',
|
||||||
':markwon-ext-tables',
|
':markwon-ext-tables',
|
||||||
|
Loading…
x
Reference in New Issue
Block a user