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