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