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.
+ *
+ * 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 Builder includeEditSpan(
+ @NonNull Class type,
+ @NonNull SpanFactory factory) {
+ this.spans.put(type, factory);
+ return this;
+ }
+
+ @NonNull
+ public Builder withEditSpanHandlerFor(@NonNull Class type, @NonNull EditSpanHandler 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());
+ }
+ }
+}
diff --git a/markwon-editor/src/main/java/io/noties/markwon/editor/MarkwonEditorImpl.java b/markwon-editor/src/main/java/io/noties/markwon/editor/MarkwonEditorImpl.java
new file mode 100644
index 00000000..d0067b5e
--- /dev/null
+++ b/markwon-editor/src/main/java/io/noties/markwon/editor/MarkwonEditorImpl.java
@@ -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, SpanFactory> spans;
+ private final Class> punctuationSpanType;
+
+ @Nullable
+ private final EditSpanHandler editSpanHandler;
+
+ MarkwonEditorImpl(
+ @NonNull Markwon markwon,
+ @NonNull Map, 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 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, List