Proper delimiters matching and autolinking support
This commit is contained in:
		
							parent
							
								
									5e3ace0c29
								
							
						
					
					
						commit
						681a7f68d7
					
				| @ -5,6 +5,8 @@ | ||||
| * `Markwon#configuration` method to expose `MarkwonConfiguration` via public API | ||||
| * `HeadingSpan#getLevel` getter | ||||
| * Add `SvgPictureMediaDecoder` in `image` module to deal with SVG without dimensions ([#165]) | ||||
| * `LinkSpan#getLink` method | ||||
| * `LinkifyPlugin` applies link span that is configured by `Markwon` (obtain via span factory) | ||||
| 
 | ||||
| [#165]: https://github.com/noties/Markwon/issues/165 | ||||
| 
 | ||||
|  | ||||
| @ -31,7 +31,15 @@ public class LinkSpan extends URLSpan { | ||||
|     } | ||||
| 
 | ||||
|     @Override | ||||
|     public void updateDrawState(TextPaint ds) { | ||||
|     public void updateDrawState(@NonNull TextPaint ds) { | ||||
|         theme.applyLinkStyle(ds); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * @since 4.2.0-SNAPSHOT | ||||
|      */ | ||||
|     @NonNull | ||||
|     public String getLink() { | ||||
|         return link; | ||||
|     } | ||||
| } | ||||
|  | ||||
| @ -41,7 +41,12 @@ class MarkwonEditorImpl extends MarkwonEditor { | ||||
|     public void process(@NonNull Editable editable) { | ||||
| 
 | ||||
|         final String input = editable.toString(); | ||||
|         final Spanned renderedMarkdown = markwon.toMarkdown(input); | ||||
| 
 | ||||
|         // NB, we cast to Spannable here without prior checks | ||||
|         //  if by some occasion Markwon stops returning here a Spannable our tests will catch that | ||||
|         //  (we need Spannable in order to remove processed spans, so they do not appear multiple times) | ||||
|         final Spannable renderedMarkdown = (Spannable) markwon.toMarkdown(input); | ||||
| 
 | ||||
|         final String markdown = renderedMarkdown.toString(); | ||||
| 
 | ||||
|         final EditSpanHandler editSpanHandler = this.editSpanHandler; | ||||
| @ -71,9 +76,14 @@ class MarkwonEditorImpl extends MarkwonEditor { | ||||
|                         ); | ||||
| 
 | ||||
|                         if (hasAdditionalSpans) { | ||||
|                             // obtain spans for a single character of renderedMarkdown | ||||
|                             //  editable here should return all spans that are contained in specified | ||||
|                             //  region. Later we match if span starts at current position | ||||
|                             //  and notify additional span handler about it | ||||
|                             final Object[] spans = renderedMarkdown.getSpans(markdownLength, markdownLength + 1, Object.class); | ||||
|                             for (Object span : spans) { | ||||
|                                 if (markdownLength == renderedMarkdown.getSpanStart(span)) { | ||||
| 
 | ||||
|                                     editSpanHandler.handle( | ||||
|                                             store, | ||||
|                                             editable, | ||||
| @ -84,19 +94,53 @@ class MarkwonEditorImpl extends MarkwonEditor { | ||||
|                                     // NB, we do not break here in case of SpanFactory | ||||
|                                     // returns multiple spans for a markdown node, this way | ||||
|                                     // we will handle all of them | ||||
| 
 | ||||
|                                     // It is important to remove span after we have processed it | ||||
|                                     //  as we process them in 2 places: here and in EQUAL | ||||
|                                     renderedMarkdown.removeSpan(span); | ||||
|                                 } | ||||
|                             } | ||||
|                         } | ||||
|                         break; | ||||
| 
 | ||||
|                     case INSERT: | ||||
|                         // no special handling here, but still we must advance the markdownLength | ||||
|                         markdownLength += diff.text.length(); | ||||
|                         break; | ||||
| 
 | ||||
|                     case EQUAL: | ||||
|                         final int length = diff.text.length(); | ||||
|                         final int inputStart = inputLength; | ||||
|                         final int markdownStart = markdownLength; | ||||
|                         inputLength += length; | ||||
|                         markdownLength += length; | ||||
| 
 | ||||
|                         // it is possible that there are spans for the text that is the same | ||||
|                         //  for example, if some links were _autolinked_ (text is the same, | ||||
|                         //  but there is an additional URLSpan) | ||||
|                         if (hasAdditionalSpans) { | ||||
|                             final Object[] spans = renderedMarkdown.getSpans(markdownStart, markdownLength, Object.class); | ||||
|                             for (Object span : spans) { | ||||
|                                 final int spanStart = renderedMarkdown.getSpanStart(span); | ||||
|                                 if (spanStart >= markdownStart) { | ||||
|                                     final int end = renderedMarkdown.getSpanEnd(span); | ||||
|                                     if (end <= markdownLength) { | ||||
| 
 | ||||
|                                         editSpanHandler.handle( | ||||
|                                                 store, | ||||
|                                                 editable, | ||||
|                                                 input, | ||||
|                                                 span, | ||||
|                                                 // shift span to input position (can be different from the text itself) | ||||
|                                                 inputStart + (spanStart - markdownStart), | ||||
|                                                 end - spanStart | ||||
|                                         ); | ||||
| 
 | ||||
|                                         renderedMarkdown.removeSpan(span); | ||||
|                                     } | ||||
|                                 } | ||||
|                             } | ||||
|                         } | ||||
|                         break; | ||||
| 
 | ||||
|                     default: | ||||
|  | ||||
| @ -81,6 +81,10 @@ public abstract class MarkwonEditorTextWatcher implements TextWatcher { | ||||
|         private final MarkwonEditor editor; | ||||
|         private final ExecutorService executorService; | ||||
| 
 | ||||
|         // As we operate on a single thread (main) we are fine with a regular int | ||||
|         //  for marking current _generation_ | ||||
|         private int generator; | ||||
| 
 | ||||
|         @Nullable | ||||
|         private EditText editText; | ||||
| 
 | ||||
| @ -115,8 +119,8 @@ public abstract class MarkwonEditorTextWatcher implements TextWatcher { | ||||
|                 return; | ||||
|             } | ||||
| 
 | ||||
|             // todo: maybe checking hash is not so performant? | ||||
|             //   what if we create a atomic reference and use it (with tag applied to editText)? | ||||
|             // both will be the same here (generator incremented and key assigned incremented value) | ||||
|             final int key = ++this.generator; | ||||
| 
 | ||||
|             if (future != null) { | ||||
|                 future.cancel(true); | ||||
| @ -129,11 +133,10 @@ public abstract class MarkwonEditorTextWatcher implements TextWatcher { | ||||
|                         @Override | ||||
|                         public void onPreRenderResult(@NonNull final MarkwonEditor.PreRenderResult result) { | ||||
|                             if (editText != null) { | ||||
|                                 final int key = key(result.resultEditable()); | ||||
|                                 editText.post(new Runnable() { | ||||
|                                     @Override | ||||
|                                     public void run() { | ||||
|                                         if (key == key(editText.getText())) { | ||||
|                                         if (key == generator) { | ||||
|                                             selfChange = true; | ||||
|                                             try { | ||||
|                                                 result.dispatchTo(editText.getText()); | ||||
| @ -149,12 +152,5 @@ public abstract class MarkwonEditorTextWatcher implements TextWatcher { | ||||
|                 } | ||||
|             }); | ||||
|         } | ||||
| 
 | ||||
|         static int key(@NonNull Editable editable) { | ||||
|             // toString is important here, as using #hashCode directly | ||||
|             // would also check for spans (and some spans can be added/removed). This is why | ||||
|             // we are checking for exact match of text | ||||
|             return editable.toString().hashCode(); | ||||
|         } | ||||
|     } | ||||
| } | ||||
|  | ||||
| @ -0,0 +1,152 @@ | ||||
| package io.noties.markwon.editor; | ||||
| 
 | ||||
| import androidx.annotation.NonNull; | ||||
| import androidx.annotation.Nullable; | ||||
| 
 | ||||
| /** | ||||
|  * @since 4.2.0-SNAPSHOT | ||||
|  */ | ||||
| public abstract class MarkwonEditorUtils { | ||||
| 
 | ||||
|     public interface Match { | ||||
| 
 | ||||
|         @NonNull | ||||
|         String delimiter(); | ||||
| 
 | ||||
|         int start(); | ||||
| 
 | ||||
|         int end(); | ||||
|     } | ||||
| 
 | ||||
|     @Nullable | ||||
|     public static Match findDelimited(@NonNull String input, int startFrom, @NonNull String delimiter) { | ||||
|         final int start = input.indexOf(delimiter, startFrom); | ||||
|         if (start > -1) { | ||||
|             final int length = delimiter.length(); | ||||
|             final int end = input.indexOf(delimiter, start + length); | ||||
|             if (end > -1) { | ||||
|                 return new MatchImpl(delimiter, start, end + length); | ||||
|             } | ||||
|         } | ||||
|         return null; | ||||
|     } | ||||
| 
 | ||||
|     @Nullable | ||||
|     public static Match findDelimited( | ||||
|             @NonNull String input, | ||||
|             int start, | ||||
|             @NonNull String delimiter1, | ||||
|             @NonNull String delimiter2) { | ||||
| 
 | ||||
|         final int l1 = delimiter1.length(); | ||||
|         final int l2 = delimiter2.length(); | ||||
| 
 | ||||
|         final char c1 = delimiter1.charAt(0); | ||||
|         final char c2 = delimiter2.charAt(0); | ||||
| 
 | ||||
|         char c; | ||||
|         char previousC = 0; | ||||
| 
 | ||||
|         Match match; | ||||
| 
 | ||||
|         for (int i = start, length = input.length(); i < length; i++) { | ||||
|             c = input.charAt(i); | ||||
| 
 | ||||
|             // if this char is the same as previous (and we obviously have no match) -> skip | ||||
|             if (c == previousC) { | ||||
|                 continue; | ||||
|             } | ||||
| 
 | ||||
|             if (c == c1) { | ||||
|                 match = matchDelimiter(input, i, length, delimiter1, l1); | ||||
|                 if (match != null) { | ||||
|                     return match; | ||||
|                 } | ||||
|             } else if (c == c2) { | ||||
|                 match = matchDelimiter(input, i, length, delimiter2, l2); | ||||
|                 if (match != null) { | ||||
|                     return match; | ||||
|                 } | ||||
|             } | ||||
| 
 | ||||
|             previousC = c; | ||||
|         } | ||||
| 
 | ||||
|         return null; | ||||
|     } | ||||
| 
 | ||||
|     // This method assumes that first char is matched already | ||||
|     @Nullable | ||||
|     private static Match matchDelimiter( | ||||
|             @NonNull String input, | ||||
|             int start, | ||||
|             int length, | ||||
|             @NonNull String delimiter, | ||||
|             int delimiterLength) { | ||||
| 
 | ||||
|         if (start + delimiterLength < length) { | ||||
| 
 | ||||
|             boolean result = true; | ||||
| 
 | ||||
|             for (int i = 1; i < delimiterLength; i++) { | ||||
|                 if (input.charAt(start + i) != delimiter.charAt(i)) { | ||||
|                     result = false; | ||||
|                     break; | ||||
|                 } | ||||
|             } | ||||
| 
 | ||||
|             if (result) { | ||||
|                 // find end | ||||
|                 final int end = input.indexOf(delimiter, start + delimiterLength); | ||||
|                 // it's important to check if match has content | ||||
|                 if (end > -1 && (end - start) > delimiterLength) { | ||||
|                     return new MatchImpl(delimiter, start, end + delimiterLength); | ||||
|                 } | ||||
|             } | ||||
|         } | ||||
| 
 | ||||
|         return null; | ||||
|     } | ||||
| 
 | ||||
|     private MarkwonEditorUtils() { | ||||
|     } | ||||
| 
 | ||||
|     private static class MatchImpl implements Match { | ||||
| 
 | ||||
|         private final String delimiter; | ||||
|         private final int start; | ||||
|         private final int end; | ||||
| 
 | ||||
|         MatchImpl(@NonNull String delimiter, int start, int end) { | ||||
|             this.delimiter = delimiter; | ||||
|             this.start = start; | ||||
|             this.end = end; | ||||
|         } | ||||
| 
 | ||||
|         @NonNull | ||||
|         @Override | ||||
|         public String delimiter() { | ||||
|             return delimiter; | ||||
|         } | ||||
| 
 | ||||
|         @Override | ||||
|         public int start() { | ||||
|             return start; | ||||
|         } | ||||
| 
 | ||||
|         @Override | ||||
|         public int end() { | ||||
|             return end; | ||||
|         } | ||||
| 
 | ||||
|         @Override | ||||
|         @NonNull | ||||
|         public String toString() { | ||||
|             return "MatchImpl{" + | ||||
|                     "delimiter='" + delimiter + '\'' + | ||||
|                     ", start=" + start + | ||||
|                     ", end=" + end + | ||||
|                     '}'; | ||||
|         } | ||||
|     } | ||||
| } | ||||
| @ -0,0 +1,71 @@ | ||||
| package io.noties.markwon.editor; | ||||
| 
 | ||||
| import androidx.annotation.NonNull; | ||||
| import androidx.annotation.Nullable; | ||||
| 
 | ||||
| import org.junit.Test; | ||||
| import org.junit.runner.RunWith; | ||||
| import org.robolectric.RobolectricTestRunner; | ||||
| import org.robolectric.annotation.Config; | ||||
| 
 | ||||
| import java.util.Locale; | ||||
| 
 | ||||
| import io.noties.markwon.editor.MarkwonEditorUtils.Match; | ||||
| 
 | ||||
| import static io.noties.markwon.editor.MarkwonEditorUtils.findDelimited; | ||||
| import static java.lang.String.format; | ||||
| import static org.junit.Assert.assertEquals; | ||||
| import static org.junit.Assert.assertNotNull; | ||||
| 
 | ||||
| @RunWith(RobolectricTestRunner.class) | ||||
| @Config(manifest = Config.NONE) | ||||
| public class MarkwonEditorUtilsTest { | ||||
| 
 | ||||
|     @Test | ||||
|     public void delimited_single() { | ||||
|         final String input = "**bold**"; | ||||
|         final Match match = findDelimited(input, 0, "**"); | ||||
|         assertMatched(input, match, "**", 0, input.length()); | ||||
|     } | ||||
| 
 | ||||
|     @Test | ||||
|     public void delimited_multiple() { | ||||
|         final String input = "**bold**"; | ||||
|         final Match match = findDelimited(input, 0, "**", "__"); | ||||
|         assertMatched(input, match, "**", 0, input.length()); | ||||
|     } | ||||
| 
 | ||||
|     @Test | ||||
|     public void delimited_em() { | ||||
|         // for example we will try to match `*` or `_` and our implementation will find first | ||||
|         final String input = "**_em_**"; // problematic for em... | ||||
|         final Match match = findDelimited(input, 0, "_", "*"); | ||||
|         assertMatched(input, match, "_", 2, 6); | ||||
|     } | ||||
| 
 | ||||
|     @Test | ||||
|     public void delimited_bold_em_strike() { | ||||
|         final String input = "**_~~dude~~_**"; | ||||
| 
 | ||||
|         final Match bold = findDelimited(input, 0, "**", "__"); | ||||
|         final Match em = findDelimited(input, 0, "*", "_"); | ||||
|         final Match strike = findDelimited(input, 0, "~~"); | ||||
| 
 | ||||
|         assertMatched(input, bold, "**", 0, input.length()); | ||||
|         assertMatched(input, em, "_", 2, 12); | ||||
|         assertMatched(input, strike, "~~", 3, 11); | ||||
|     } | ||||
| 
 | ||||
|     private static void assertMatched( | ||||
|             @NonNull String input, | ||||
|             @Nullable Match match, | ||||
|             @NonNull String delimiter, | ||||
|             int start, | ||||
|             int end) { | ||||
|         assertNotNull(format(Locale.ROOT, "delimiter: '%s', input: '%s'", delimiter, input), match); | ||||
|         final String m = format(Locale.ROOT, "input: '%s', match: %s", input, match); | ||||
|         assertEquals(m, delimiter, match.delimiter()); | ||||
|         assertEquals(m, start, match.start()); | ||||
|         assertEquals(m, end, match.end()); | ||||
|     } | ||||
| } | ||||
| @ -1,18 +1,24 @@ | ||||
| package io.noties.markwon.linkify; | ||||
| 
 | ||||
| import android.text.SpannableStringBuilder; | ||||
| import android.text.style.URLSpan; | ||||
| import android.text.util.Linkify; | ||||
| 
 | ||||
| import androidx.annotation.IntDef; | ||||
| import androidx.annotation.NonNull; | ||||
| 
 | ||||
| import org.commonmark.node.Link; | ||||
| 
 | ||||
| import java.lang.annotation.Retention; | ||||
| import java.lang.annotation.RetentionPolicy; | ||||
| 
 | ||||
| import io.noties.markwon.AbstractMarkwonPlugin; | ||||
| import io.noties.markwon.MarkwonVisitor; | ||||
| import io.noties.markwon.RenderProps; | ||||
| import io.noties.markwon.SpanFactory; | ||||
| import io.noties.markwon.SpannableBuilder; | ||||
| import io.noties.markwon.core.CorePlugin; | ||||
| import io.noties.markwon.core.CoreProps; | ||||
| 
 | ||||
| public class LinkifyPlugin extends AbstractMarkwonPlugin { | ||||
| 
 | ||||
| @ -66,6 +72,13 @@ public class LinkifyPlugin extends AbstractMarkwonPlugin { | ||||
|         @Override | ||||
|         public void onTextAdded(@NonNull MarkwonVisitor visitor, @NonNull String text, int start) { | ||||
| 
 | ||||
|             // @since 4.2.0-SNAPSHOT obtain span factory for links | ||||
|             //  we will be using the link that is used by markdown (instead of directly applying URLSpan) | ||||
|             final SpanFactory spanFactory = visitor.configuration().spansFactory().get(Link.class); | ||||
|             if (spanFactory == null) { | ||||
|                 return; | ||||
|             } | ||||
| 
 | ||||
|             // clear previous state | ||||
|             builder.clear(); | ||||
|             builder.clearSpans(); | ||||
| @ -74,16 +87,22 @@ public class LinkifyPlugin extends AbstractMarkwonPlugin { | ||||
|             builder.append(text); | ||||
| 
 | ||||
|             if (Linkify.addLinks(builder, mask)) { | ||||
|                 final Object[] spans = builder.getSpans(0, builder.length(), Object.class); | ||||
|                 // target URL span specifically | ||||
|                 final URLSpan[] spans = builder.getSpans(0, builder.length(), URLSpan.class); | ||||
|                 if (spans != null | ||||
|                         && spans.length > 0) { | ||||
| 
 | ||||
|                     final RenderProps renderProps = visitor.renderProps(); | ||||
|                     final SpannableBuilder spannableBuilder = visitor.builder(); | ||||
|                     for (Object span : spans) { | ||||
|                         spannableBuilder.setSpan( | ||||
|                                 span, | ||||
| 
 | ||||
|                     for (URLSpan span : spans) { | ||||
|                         CoreProps.LINK_DESTINATION.set(renderProps, span.getURL()); | ||||
|                         SpannableBuilder.setSpans( | ||||
|                                 spannableBuilder, | ||||
|                                 spanFactory.getSpans(visitor.configuration(), renderProps), | ||||
|                                 start + builder.getSpanStart(span), | ||||
|                                 start + builder.getSpanEnd(span), | ||||
|                                 builder.getSpanFlags(span)); | ||||
|                                 start + builder.getSpanEnd(span) | ||||
|                         ); | ||||
|                     } | ||||
|                 } | ||||
|             } | ||||
|  | ||||
| @ -41,10 +41,11 @@ dependencies { | ||||
|     implementation project(':markwon-ext-tasklist') | ||||
|     implementation project(':markwon-html') | ||||
|     implementation project(':markwon-image') | ||||
|     implementation project(':markwon-syntax-highlight') | ||||
|     implementation project(':markwon-linkify') | ||||
|     implementation project(':markwon-recycler') | ||||
|     implementation project(':markwon-recycler-table') | ||||
|     implementation project(':markwon-simple-ext') | ||||
|     implementation project(':markwon-syntax-highlight') | ||||
| 
 | ||||
|     implementation project(':markwon-image-picasso') | ||||
| 
 | ||||
|  | ||||
| @ -6,7 +6,8 @@ import android.text.Editable; | ||||
| import android.text.SpannableStringBuilder; | ||||
| import android.text.Spanned; | ||||
| import android.text.TextPaint; | ||||
| import android.text.style.CharacterStyle; | ||||
| import android.text.method.LinkMovementMethod; | ||||
| import android.text.style.ClickableSpan; | ||||
| import android.text.style.ForegroundColorSpan; | ||||
| import android.text.style.MetricAffectingSpan; | ||||
| import android.text.style.StrikethroughSpan; | ||||
| @ -25,7 +26,6 @@ import java.util.concurrent.Executors; | ||||
| import io.noties.markwon.Markwon; | ||||
| 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.EmphasisSpan; | ||||
| import io.noties.markwon.core.spans.LinkSpan; | ||||
| @ -33,6 +33,7 @@ import io.noties.markwon.core.spans.StrongEmphasisSpan; | ||||
| import io.noties.markwon.editor.EditSpanHandlerBuilder; | ||||
| import io.noties.markwon.editor.MarkwonEditor; | ||||
| import io.noties.markwon.editor.MarkwonEditorTextWatcher; | ||||
| import io.noties.markwon.editor.MarkwonEditorUtils; | ||||
| import io.noties.markwon.ext.strikethrough.StrikethroughPlugin; | ||||
| import io.noties.markwon.sample.R; | ||||
| 
 | ||||
| @ -148,79 +149,91 @@ public class EditorActivity extends Activity { | ||||
| 
 | ||||
|     private void multiple_edit_spans() { | ||||
| 
 | ||||
|         // for links to be clickable | ||||
|         editText.setMovementMethod(LinkMovementMethod.getInstance()); | ||||
| 
 | ||||
|         final Markwon markwon = Markwon.builder(this) | ||||
|                 .usePlugin(StrikethroughPlugin.create()) | ||||
| //                .usePlugin(LinkifyPlugin.create()) | ||||
|                 .build(); | ||||
| 
 | ||||
|         final MarkwonTheme theme = markwon.configuration().theme(); | ||||
| 
 | ||||
|         final EditLinkSpan.OnClick onClick = (widget, link) -> markwon.configuration().linkResolver().resolve(widget, link); | ||||
| 
 | ||||
|         final MarkwonEditor editor = MarkwonEditor.builder(markwon) | ||||
|                 .includeEditSpan(StrongEmphasisSpan.class, StrongEmphasisSpan::new) | ||||
|                 .includeEditSpan(EmphasisSpan.class, EmphasisSpan::new) | ||||
|                 .includeEditSpan(StrikethroughSpan.class, StrikethroughSpan::new) | ||||
|                 .includeEditSpan(CodeSpan.class, () -> new CodeSpan(theme)) | ||||
|                 .includeEditSpan(CodeBlockSpan.class, () -> new CodeBlockSpan(theme)) | ||||
|                 .includeEditSpan(BlockQuoteSpan.class, () -> new BlockQuoteSpan(theme)) | ||||
|                 .includeEditSpan(EditLinkSpan.class, EditLinkSpan::new) | ||||
|                 .includeEditSpan(EditLinkSpan.class, () -> new EditLinkSpan(onClick)) | ||||
|                 .withEditSpanHandler(createEditSpanHandler()) | ||||
|                 .build(); | ||||
| 
 | ||||
|         editText.addTextChangedListener(MarkwonEditorTextWatcher.withProcess(editor)); | ||||
| //        editText.addTextChangedListener(MarkwonEditorTextWatcher.withProcess(editor)); | ||||
|         editText.addTextChangedListener(MarkwonEditorTextWatcher.withPreRender( | ||||
|                 editor, Executors.newSingleThreadExecutor(), editText)); | ||||
|     } | ||||
| 
 | ||||
|     private static MarkwonEditor.EditSpanHandler createEditSpanHandler() { | ||||
|         // Please note that here we specify spans THAT ARE USED IN MARKDOWN | ||||
|         return EditSpanHandlerBuilder.create() | ||||
|                 .handleMarkdownSpan(StrongEmphasisSpan.class, (store, editable, input, span, spanStart, spanTextLength) -> { | ||||
|                     editable.setSpan( | ||||
|                             store.get(StrongEmphasisSpan.class), | ||||
|                             spanStart, | ||||
|                             spanStart + spanTextLength + 4, | ||||
|                             Spanned.SPAN_EXCLUSIVE_EXCLUSIVE | ||||
|                     ); | ||||
|                     // inline spans can delimit other inline spans, | ||||
|                     //  for example: `**_~~hey~~_**`, this is why we must additionally find delimiter used | ||||
|                     //  and its actual start/end positions | ||||
|                     final MarkwonEditorUtils.Match match = | ||||
|                             MarkwonEditorUtils.findDelimited(input, spanStart, "**", "__"); | ||||
|                     if (match != null) { | ||||
|                         editable.setSpan( | ||||
|                                 store.get(StrongEmphasisSpan.class), | ||||
|                                 match.start(), | ||||
|                                 match.end(), | ||||
|                                 Spanned.SPAN_EXCLUSIVE_EXCLUSIVE | ||||
|                         ); | ||||
|                     } | ||||
|                 }) | ||||
|                 .handleMarkdownSpan(EmphasisSpan.class, (store, editable, input, span, spanStart, spanTextLength) -> { | ||||
|                     editable.setSpan( | ||||
|                             store.get(EmphasisSpan.class), | ||||
|                             spanStart, | ||||
|                             spanStart + spanTextLength + 2, | ||||
|                             Spanned.SPAN_EXCLUSIVE_EXCLUSIVE | ||||
|                     ); | ||||
|                     final MarkwonEditorUtils.Match match = | ||||
|                             MarkwonEditorUtils.findDelimited(input, spanStart, "*", "_"); | ||||
|                     if (match != null) { | ||||
|                         editable.setSpan( | ||||
|                                 store.get(EmphasisSpan.class), | ||||
|                                 match.start(), | ||||
|                                 match.end(), | ||||
|                                 Spanned.SPAN_EXCLUSIVE_EXCLUSIVE | ||||
|                         ); | ||||
|                     } | ||||
|                 }) | ||||
|                 .handleMarkdownSpan(StrikethroughSpan.class, (store, editable, input, span, spanStart, spanTextLength) -> { | ||||
|                     editable.setSpan( | ||||
|                             store.get(StrikethroughSpan.class), | ||||
|                             spanStart, | ||||
|                             spanStart + spanTextLength + 4, | ||||
|                             Spanned.SPAN_EXCLUSIVE_EXCLUSIVE | ||||
|                     ); | ||||
|                     final MarkwonEditorUtils.Match match = | ||||
|                             MarkwonEditorUtils.findDelimited(input, spanStart, "~~"); | ||||
|                     if (match != null) { | ||||
|                         editable.setSpan( | ||||
|                                 store.get(StrikethroughSpan.class), | ||||
|                                 match.start(), | ||||
|                                 match.end(), | ||||
|                                 Spanned.SPAN_EXCLUSIVE_EXCLUSIVE | ||||
|                         ); | ||||
|                     } | ||||
|                 }) | ||||
|                 .handleMarkdownSpan(CodeSpan.class, (store, editable, input, span, spanStart, spanTextLength) -> { | ||||
|                     // we do not add offset here because markwon (by default) adds spaces | ||||
|                     // around inline code | ||||
|                     editable.setSpan( | ||||
|                             store.get(CodeSpan.class), | ||||
|                             spanStart, | ||||
|                             spanStart + spanTextLength, | ||||
|                             Spanned.SPAN_EXCLUSIVE_EXCLUSIVE | ||||
|                     ); | ||||
|                 }) | ||||
|                 .handleMarkdownSpan(CodeBlockSpan.class, (store, editable, input, span, spanStart, spanTextLength) -> { | ||||
|                     // we do not handle indented code blocks here | ||||
|                     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(); | ||||
| 
 | ||||
|                     final MarkwonEditorUtils.Match match = | ||||
|                             MarkwonEditorUtils.findDelimited(input, spanStart, "`"); | ||||
|                     if (match != null) { | ||||
|                         editable.setSpan( | ||||
|                                 store.get(CodeBlockSpan.class), | ||||
|                                 spanStart, | ||||
|                                 lastLineEnd, | ||||
|                                 store.get(CodeSpan.class), | ||||
|                                 match.start(), | ||||
|                                 match.end(), | ||||
|                                 Spanned.SPAN_EXCLUSIVE_EXCLUSIVE | ||||
|                         ); | ||||
|                     } | ||||
|                 }) | ||||
|                 .handleMarkdownSpan(BlockQuoteSpan.class, (store, editable, input, span, spanStart, spanTextLength) -> { | ||||
|                     // todo: here we should actually find a proper ending of a block quote... | ||||
|                     editable.setSpan( | ||||
|                             store.get(BlockQuoteSpan.class), | ||||
|                             spanStart, | ||||
| @ -229,11 +242,27 @@ public class EditorActivity extends Activity { | ||||
|                     ); | ||||
|                 }) | ||||
|                 .handleMarkdownSpan(LinkSpan.class, (store, editable, input, span, spanStart, spanTextLength) -> { | ||||
| 
 | ||||
|                     final EditLinkSpan editLinkSpan = store.get(EditLinkSpan.class); | ||||
|                     editLinkSpan.link = span.getLink(); | ||||
| 
 | ||||
|                     final int s; | ||||
|                     final int e; | ||||
| 
 | ||||
|                     // markdown link vs. autolink | ||||
|                     if ('[' == input.charAt(spanStart)) { | ||||
|                         s = spanStart + 1; | ||||
|                         e = spanStart + 1 + spanTextLength; | ||||
|                     } else { | ||||
|                         s = spanStart; | ||||
|                         e = spanStart + spanTextLength; | ||||
|                     } | ||||
| 
 | ||||
|                     editable.setSpan( | ||||
|                             store.get(EditLinkSpan.class), | ||||
|                             editLinkSpan, | ||||
|                             // add underline only for link text | ||||
|                             spanStart + 1, | ||||
|                             spanStart + 1 + spanTextLength, | ||||
|                             s, | ||||
|                             e, | ||||
|                             Spanned.SPAN_EXCLUSIVE_EXCLUSIVE | ||||
|                     ); | ||||
|                 }) | ||||
| @ -260,8 +289,14 @@ public class EditorActivity extends Activity { | ||||
|         code.setOnClickListener(new InsertOrWrapClickListener(editText, "`")); | ||||
| 
 | ||||
|         quote.setOnClickListener(v -> { | ||||
| 
 | ||||
|             final int start = editText.getSelectionStart(); | ||||
|             final int end = editText.getSelectionEnd(); | ||||
| 
 | ||||
|             if (start < 0) { | ||||
|                 return; | ||||
|             } | ||||
| 
 | ||||
|             if (start == end) { | ||||
|                 editText.getText().insert(start, "> "); | ||||
|             } else { | ||||
| @ -306,6 +341,11 @@ public class EditorActivity extends Activity { | ||||
|         public void onClick(View v) { | ||||
|             final int start = editText.getSelectionStart(); | ||||
|             final int end = editText.getSelectionEnd(); | ||||
| 
 | ||||
|             if (start < 0) { | ||||
|                 return; | ||||
|             } | ||||
| 
 | ||||
|             if (start == end) { | ||||
|                 // insert at current position | ||||
|                 editText.getText().insert(start, text); | ||||
| @ -342,11 +382,25 @@ public class EditorActivity extends Activity { | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     private static class EditLinkSpan extends CharacterStyle { | ||||
|     private static class EditLinkSpan extends ClickableSpan { | ||||
| 
 | ||||
|         interface OnClick { | ||||
|             void onClick(@NonNull View widget, @NonNull String link); | ||||
|         } | ||||
| 
 | ||||
|         private final OnClick onClick; | ||||
| 
 | ||||
|         String link; | ||||
| 
 | ||||
|         EditLinkSpan(@NonNull OnClick onClick) { | ||||
|             this.onClick = onClick; | ||||
|         } | ||||
| 
 | ||||
|         @Override | ||||
|         public void updateDrawState(TextPaint tp) { | ||||
|             tp.setColor(tp.linkColor); | ||||
|             tp.setUnderlineText(true); | ||||
|         public void onClick(@NonNull View widget) { | ||||
|             if (link != null) { | ||||
|                 onClick.onClick(widget, link); | ||||
|             } | ||||
|         } | ||||
|     } | ||||
| } | ||||
|  | ||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user
	 Dimitry Ivanov
						Dimitry Ivanov