Welcome to mirror list, hosted at ThFree Co, Russian Federation.

github.com/stefan-niedermann/nextcloud-notes.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorStefan Niedermann <info@niedermann.it>2021-06-13 16:20:29 +0300
committerStefan Niedermann <info@niedermann.it>2021-06-13 16:20:29 +0300
commitf57603d4726b09573c29cc825e6d963674ef7a44 (patch)
tree455936e24fe4220ed38ebcede738336e723fcadb
parente1b6e8b785ef2c7ab7c3105ff856805a84a03cca (diff)
Refactor links in checkboxes and add unit tests
Signed-off-by: Stefan Niedermann <info@niedermann.it>
-rw-r--r--markdown/build.gradle3
-rw-r--r--markdown/src/main/java/it/niedermann/android/markdown/markwon/plugins/ToggleableTaskListPlugin.java86
-rw-r--r--markdown/src/test/java/it/niedermann/android/markdown/markwon/plugins/ToggleableTaskListPluginTest.java200
3 files changed, 253 insertions, 36 deletions
diff --git a/markdown/build.gradle b/markdown/build.gradle
index 91d8d437..a9107750 100644
--- a/markdown/build.gradle
+++ b/markdown/build.gradle
@@ -57,6 +57,9 @@ dependencies {
coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:1.1.5'
+ testImplementation 'androidx.test:core:1.3.0'
+ testImplementation 'androidx.arch.core:core-testing:2.1.0'
testImplementation 'junit:junit:4.13.2'
+ testImplementation 'org.mockito:mockito-core:3.11.0'
testImplementation 'org.robolectric:robolectric:4.5.1'
} \ No newline at end of file
diff --git a/markdown/src/main/java/it/niedermann/android/markdown/markwon/plugins/ToggleableTaskListPlugin.java b/markdown/src/main/java/it/niedermann/android/markdown/markwon/plugins/ToggleableTaskListPlugin.java
index b43c5711..77411392 100644
--- a/markdown/src/main/java/it/niedermann/android/markdown/markwon/plugins/ToggleableTaskListPlugin.java
+++ b/markdown/src/main/java/it/niedermann/android/markdown/markwon/plugins/ToggleableTaskListPlugin.java
@@ -3,8 +3,10 @@ package it.niedermann.android.markdown.markwon.plugins;
import android.text.Spannable;
import android.text.style.ClickableSpan;
import android.util.Range;
+import android.widget.TextView;
import androidx.annotation.NonNull;
+import androidx.annotation.VisibleForTesting;
import org.commonmark.node.AbstractVisitor;
import org.commonmark.node.Block;
@@ -15,7 +17,6 @@ import org.commonmark.node.SoftLineBreak;
import org.commonmark.node.Text;
import java.util.ArrayList;
-import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.concurrent.atomic.AtomicBoolean;
@@ -29,6 +30,7 @@ import io.noties.markwon.SpannableBuilder;
import io.noties.markwon.ext.tasklist.TaskListItem;
import io.noties.markwon.ext.tasklist.TaskListProps;
import io.noties.markwon.ext.tasklist.TaskListSpan;
+import it.niedermann.android.markdown.MarkdownUtil;
import it.niedermann.android.markdown.markwon.span.ToggleTaskListSpan;
/**
@@ -103,81 +105,92 @@ public class ToggleableTaskListPlugin extends AbstractMarkwonPlugin {
});
}
+
+ /**
+ * Adds for each {@link ToggleMarkerSpan} and actual {@link ToggleTaskListSpan}s respecting existing {@link ClickableSpan}s.
+ */
@Override
public void afterRender(@NonNull Node node, @NonNull MarkwonVisitor visitor) {
super.afterRender(node, visitor);
- final Spannable spanned = visitor.builder().spannableStringBuilder();
- final List<ToggleMarkerSpan> markerSpans = getSortedToggleMarkerSpans(spanned, visitor.builder());
+ final List<SpannableBuilder.Span> markerSpans = getSortedToggleMarkerSpans(visitor.builder());
- replaceMarkerSpans(markerSpans, spanned, visitor.builder());
- }
-
- /**
- * Converts all {@link ToggleMarkerSpan}s to actual {@link ToggleTaskListSpan}s respecting existing {@link ClickableSpan}s.
- */
- private void replaceMarkerSpans(@NonNull List<ToggleMarkerSpan> markerSpans, @NonNull Spannable spanned, @NonNull SpannableBuilder builder) {
for (int position = 0; position < markerSpans.size(); position++) {
- final ToggleMarkerSpan markerSpan = markerSpans.get(position);
- final int start = spanned.getSpanStart(markerSpan);
- final int end = spanned.getSpanEnd(markerSpan);
- final List<Range<Integer>> freeRanges = findFreeRanges(spanned, start, end);
+ final SpannableBuilder.Span markerSpan = markerSpans.get(position);
+ final int start = markerSpan.start;
+ final int end = markerSpan.end;
+ final List<Range<Integer>> freeRanges = findFreeRanges(visitor.builder(), start, end);
for (Range<Integer> freeRange : freeRanges) {
- builder.setSpan(
- new ToggleTaskListSpan(enabled, toggleListener, markerSpan.getTaskListSpan(), position),
+ visitor.builder().setSpan(
+ new ToggleTaskListSpan(enabled, toggleListener, ((ToggleMarkerSpan) markerSpan.what).getTaskListSpan(), position),
freeRange.getLower(), freeRange.getUpper());
}
- spanned.removeSpan(markerSpan);
}
}
/**
- * @return a {@link List} of {@link ToggleMarkerSpan}s, sorted ascending by the span start.
+ * Removes {@link ToggleMarkerSpan}s from {@param textView}.
*/
- @NonNull
- private static List<ToggleMarkerSpan> getSortedToggleMarkerSpans(@NonNull Spannable spanned, @NonNull SpannableBuilder builder) {
- return builder.getSpans(0, builder.length())
- .stream()
- .filter(span -> span.what instanceof ToggleMarkerSpan)
- .map(span -> ((ToggleMarkerSpan) span.what))
- .sorted((o1, o2) -> spanned.getSpanStart(o1) - spanned.getSpanStart(o2))
- .collect(Collectors.toList());
+ @Override
+ public void afterSetText(@NonNull TextView textView) {
+ super.afterSetText(textView);
+ final Spannable spannable = MarkdownUtil.getContentAsSpannable(textView);
+ for (ToggleMarkerSpan span : spannable.getSpans(0, spannable.length(), ToggleMarkerSpan.class)) {
+ spannable.removeSpan(span);
+ }
+ textView.setText(spannable);
}
/**
* @return a {@link List} of {@link Range}s in the given {@param spanned} from {@param start} to {@param end} which is <strong>not</strong> taken for a {@link ClickableSpan}.
*/
@NonNull
- private static List<Range<Integer>> findFreeRanges(@NonNull Spannable spanned, int start, int end) {
+ private static List<Range<Integer>> findFreeRanges(@NonNull SpannableBuilder builder, int start, int end) {
final List<Range<Integer>> freeRanges;
- final List<ClickableSpan> clickableSpans = getClickableSpans(spanned, start, end);
+ final List<SpannableBuilder.Span> clickableSpans = getClickableSpans(builder, start, end);
if (clickableSpans.size() > 0) {
freeRanges = new ArrayList<>(clickableSpans.size());
int from = start;
- for (ClickableSpan clickableSpan : clickableSpans) {
- final int clickableStart = spanned.getSpanStart(clickableSpan);
- final int clickableEnd = spanned.getSpanEnd(clickableSpan);
+ for (SpannableBuilder.Span clickableSpan : clickableSpans) {
+ final int clickableStart = clickableSpan.start;
+ final int clickableEnd = clickableSpan.end;
if (from != clickableStart) {
freeRanges.add(new Range<>(from, clickableStart));
}
from = clickableEnd;
}
if (clickableSpans.size() > 0) {
- final int lastUpperBlocker = spanned.getSpanEnd(clickableSpans.get(clickableSpans.size() - 1));
+ final int lastUpperBlocker = clickableSpans.get(clickableSpans.size() - 1).end;
if (lastUpperBlocker < end) {
freeRanges.add(new Range<>(lastUpperBlocker, end));
}
}
+ } else if (start == end) {
+ freeRanges = Collections.emptyList();
} else {
freeRanges = Collections.singletonList(new Range<>(start, end));
}
return freeRanges;
}
+ /**
+ * @return a {@link List} of {@link ToggleMarkerSpan}s, sorted ascending by the span start.
+ */
@NonNull
- private static List<ClickableSpan> getClickableSpans(@NonNull Spannable spanned, int start, int end) {
- return Arrays.stream(spanned.getSpans(start, end, ClickableSpan.class))
- .sorted((o1, o2) -> spanned.getSpanStart(o1) - spanned.getSpanStart(o2))
+ private static List<SpannableBuilder.Span> getSortedToggleMarkerSpans(@NonNull SpannableBuilder builder) {
+ return builder.getSpans(0, builder.length())
+ .stream()
+ .filter(span -> span.what instanceof ToggleMarkerSpan)
+ .sorted((o1, o2) -> o1.start - o2.start)
+ .collect(Collectors.toList());
+ }
+
+ @NonNull
+ private static List<SpannableBuilder.Span> getClickableSpans(@NonNull SpannableBuilder builder, int start, int end) {
+ return builder.getSpans(start, end)
+ .stream()
+ .filter(span -> span.what instanceof ClickableSpan)
+ .sorted((o1, o2) -> o1.start - o2.start)
.collect(Collectors.toList());
}
@@ -229,7 +242,8 @@ public class ToggleableTaskListPlugin extends AbstractMarkwonPlugin {
/**
* Helper class which holds an {@link TaskListSpan} but does not include the range of child {@link TaskListSpan}s.
*/
- private static final class ToggleMarkerSpan {
+ @VisibleForTesting
+ static final class ToggleMarkerSpan {
@NonNull
private final TaskListSpan taskListSpan;
diff --git a/markdown/src/test/java/it/niedermann/android/markdown/markwon/plugins/ToggleableTaskListPluginTest.java b/markdown/src/test/java/it/niedermann/android/markdown/markwon/plugins/ToggleableTaskListPluginTest.java
new file mode 100644
index 00000000..823e1e13
--- /dev/null
+++ b/markdown/src/test/java/it/niedermann/android/markdown/markwon/plugins/ToggleableTaskListPluginTest.java
@@ -0,0 +1,200 @@
+package it.niedermann.android.markdown.markwon.plugins;
+
+import android.text.Editable;
+import android.text.Spannable;
+import android.text.SpannableStringBuilder;
+import android.text.Spanned;
+import android.text.style.ClickableSpan;
+import android.text.style.URLSpan;
+import android.util.Range;
+import android.widget.TextView;
+
+import androidx.test.core.app.ApplicationProvider;
+
+import junit.framework.TestCase;
+
+import org.commonmark.node.Node;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.robolectric.RobolectricTestRunner;
+
+import java.lang.reflect.Constructor;
+import java.lang.reflect.InvocationTargetException;
+import java.lang.reflect.Method;
+import java.util.Collections;
+import java.util.List;
+import java.util.stream.Collectors;
+
+import io.noties.markwon.MarkwonVisitor;
+import io.noties.markwon.SpannableBuilder;
+import io.noties.markwon.ext.tasklist.TaskListSpan;
+import it.niedermann.android.markdown.markwon.span.InterceptedURLSpan;
+import it.niedermann.android.markdown.markwon.span.ToggleTaskListSpan;
+
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+
+@RunWith(RobolectricTestRunner.class)
+public class ToggleableTaskListPluginTest extends TestCase {
+
+ @Test
+ public void testAfterRender() throws IllegalAccessException, InvocationTargetException, InstantiationException, NoSuchMethodException {
+ final Node node = mock(Node.class);
+ final MarkwonVisitor visitor = mock(MarkwonVisitor.class);
+
+ final Constructor<ToggleableTaskListPlugin.ToggleMarkerSpan> markerSpanConstructor = ToggleableTaskListPlugin.ToggleMarkerSpan.class.getDeclaredConstructor(TaskListSpan.class);
+ markerSpanConstructor.setAccessible(true);
+
+ final SpannableBuilder builder = new SpannableBuilder("Lorem Ipsum Dolor \nSit Amet");
+ builder.setSpan(markerSpanConstructor.newInstance(mock(TaskListSpan.class)), 0, 6);
+ builder.setSpan(new URLSpan(""), 6, 11);
+ builder.setSpan(markerSpanConstructor.newInstance(mock(TaskListSpan.class)), 11, 19);
+ builder.setSpan(new InterceptedURLSpan(Collections.emptyList(), ""), 19, 22);
+ builder.setSpan(markerSpanConstructor.newInstance(mock(TaskListSpan.class)), 22, 27);
+
+ when(visitor.builder()).thenReturn(builder);
+
+ final ToggleableTaskListPlugin plugin = new ToggleableTaskListPlugin((i, b) -> {
+ // Do nothing...
+ });
+ plugin.afterRender(node, visitor);
+
+ // We ignore marker spans in this test. They will be removed in another step
+ final List<SpannableBuilder.Span> spans = builder.getSpans(0, builder.length())
+ .stream()
+ .filter(span -> span.what.getClass() != ToggleableTaskListPlugin.ToggleMarkerSpan.class)
+ .sorted((o1, o2) -> o1.start - o2.start)
+ .collect(Collectors.toList());
+
+ assertEquals(5, spans.size());
+ assertEquals(ToggleTaskListSpan.class, spans.get(0).what.getClass());
+ assertEquals(0, spans.get(0).start);
+ assertEquals(6, spans.get(0).end);
+ assertEquals(URLSpan.class, spans.get(1).what.getClass());
+ assertEquals(6, spans.get(1).start);
+ assertEquals(11, spans.get(1).end);
+ assertEquals(ToggleTaskListSpan.class, spans.get(2).what.getClass());
+ assertEquals(11, spans.get(2).start);
+ assertEquals(19, spans.get(2).end);
+ assertEquals(InterceptedURLSpan.class, spans.get(3).what.getClass());
+ assertEquals(19, spans.get(3).start);
+ assertEquals(22, spans.get(3).end);
+ assertEquals(ToggleTaskListSpan.class, spans.get(4).what.getClass());
+ assertEquals(22, spans.get(4).start);
+ assertEquals(27, spans.get(4).end);
+ }
+
+ @Test
+ public void testAfterSetText() throws IllegalAccessException, InvocationTargetException, InstantiationException, NoSuchMethodException {
+ final Constructor<ToggleableTaskListPlugin.ToggleMarkerSpan> markerSpanConstructor = ToggleableTaskListPlugin.ToggleMarkerSpan.class.getDeclaredConstructor(TaskListSpan.class);
+ markerSpanConstructor.setAccessible(true);
+
+ final Editable editable = new SpannableStringBuilder("Lorem Ipsum Dolor \nSit Amet");
+ editable.setSpan(markerSpanConstructor.newInstance(mock(TaskListSpan.class)), 0, 6, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
+ editable.setSpan(new URLSpan(""), 6, 11, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
+ editable.setSpan(markerSpanConstructor.newInstance(mock(TaskListSpan.class)), 11, 19, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
+ editable.setSpan(new InterceptedURLSpan(Collections.emptyList(), ""), 19, 22, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
+ editable.setSpan(markerSpanConstructor.newInstance(mock(TaskListSpan.class)), 22, 27, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
+
+ final TextView textView = new TextView(ApplicationProvider.getApplicationContext());
+ textView.setText(editable);
+
+ assertEquals(3, ((Spanned) textView.getText()).getSpans(0, textView.getText().length(), ToggleableTaskListPlugin.ToggleMarkerSpan.class).length);
+
+ final ToggleableTaskListPlugin plugin = new ToggleableTaskListPlugin((i, b) -> {
+ // Do nothing...
+ });
+ plugin.afterSetText(textView);
+
+ assertEquals(0, ((Spanned) textView.getText()).getSpans(0, textView.getText().length(), ToggleableTaskListPlugin.ToggleMarkerSpan.class).length);
+ }
+
+ @Test
+ @SuppressWarnings({"unchecked", "ConstantConditions"})
+ public void testGetClickableSpans() throws NoSuchMethodException, InvocationTargetException, IllegalAccessException {
+ final Method m = ToggleableTaskListPlugin.class.getDeclaredMethod("getClickableSpans", SpannableBuilder.class, int.class, int.class);
+ m.setAccessible(true);
+
+ final Object firstClickableSpan = new URLSpan("");
+ final Object secondClickableSpan = new InterceptedURLSpan(Collections.emptyList(), "");
+ final SpannableBuilder spannable = new SpannableBuilder("Lorem Ipsum Dolor \nSit Amet");
+ spannable.setSpan(firstClickableSpan, 6, 11, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
+ spannable.setSpan(secondClickableSpan, 19, 22, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
+
+ List<SpannableBuilder.Span> clickableSpans;
+
+ clickableSpans = (List<SpannableBuilder.Span>) m.invoke(null, spannable, 0, 0);
+ assertEquals(0, clickableSpans.size());
+
+ clickableSpans = (List<SpannableBuilder.Span>) m.invoke(null, spannable, spannable.length() - 1, spannable.length() - 1);
+ assertEquals(0, clickableSpans.size());
+
+ clickableSpans = (List<SpannableBuilder.Span>) m.invoke(null, spannable, 0, 5);
+ assertEquals(0, clickableSpans.size());
+
+ clickableSpans = (List<SpannableBuilder.Span>) m.invoke(null, spannable, 0, spannable.length());
+ assertEquals(2, clickableSpans.size());
+ assertEquals(firstClickableSpan, clickableSpans.get(0).what);
+ assertEquals(secondClickableSpan, clickableSpans.get(1).what);
+
+ clickableSpans = (List<SpannableBuilder.Span>) m.invoke(null, spannable, 0, 17);
+ assertEquals(1, clickableSpans.size());
+ assertEquals(firstClickableSpan, clickableSpans.get(0).what);
+
+ clickableSpans = (List<SpannableBuilder.Span>) m.invoke(null, spannable, 12, 22);
+ assertEquals(1, clickableSpans.size());
+ assertEquals(secondClickableSpan, clickableSpans.get(0).what);
+
+ clickableSpans = (List<SpannableBuilder.Span>) m.invoke(null, spannable, 9, 20);
+ assertEquals(2, clickableSpans.size());
+ assertEquals(firstClickableSpan, clickableSpans.get(0).what);
+ assertEquals(secondClickableSpan, clickableSpans.get(1).what);
+ }
+
+ @Test
+ @SuppressWarnings({"unchecked", "ConstantConditions"})
+ public void testFindFreeRanges() throws NoSuchMethodException, InvocationTargetException, IllegalAccessException {
+ final Method m = ToggleableTaskListPlugin.class.getDeclaredMethod("findFreeRanges", SpannableBuilder.class, int.class, int.class);
+ m.setAccessible(true);
+
+ final Object firstClickableSpan = new URLSpan("");
+ final Object secondClickableSpan = new InterceptedURLSpan(Collections.emptyList(), "");
+ final SpannableBuilder spannable = new SpannableBuilder("Lorem Ipsum Dolor \nSit Amet");
+ spannable.setSpan(firstClickableSpan, 6, 11, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
+ spannable.setSpan(secondClickableSpan, 19, 22, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
+
+ List<Range<Integer>> freeRanges;
+
+ freeRanges = (List<Range<Integer>>) m.invoke(null, spannable, 0, 0);
+ assertEquals(0, freeRanges.size());
+
+ freeRanges = (List<Range<Integer>>) m.invoke(null, spannable, spannable.length() - 1, spannable.length() - 1);
+ assertEquals(0, freeRanges.size());
+
+ freeRanges = (List<Range<Integer>>) m.invoke(null, spannable, 0, 6);
+ assertEquals(1, freeRanges.size());
+ assertEquals(0, (int) freeRanges.get(0).getLower());
+ assertEquals(6, (int) freeRanges.get(0).getUpper());
+
+ freeRanges = (List<Range<Integer>>) m.invoke(null, spannable, 0, 6);
+ assertEquals(1, freeRanges.size());
+ assertEquals(0, (int) freeRanges.get(0).getLower());
+ assertEquals(6, (int) freeRanges.get(0).getUpper());
+
+ freeRanges = (List<Range<Integer>>) m.invoke(null, spannable, 3, 15);
+ assertEquals(2, freeRanges.size());
+ assertEquals(3, (int) freeRanges.get(0).getLower());
+ assertEquals(6, (int) freeRanges.get(0).getUpper());
+ assertEquals(11, (int) freeRanges.get(1).getLower());
+ assertEquals(15, (int) freeRanges.get(1).getUpper());
+
+ freeRanges = (List<Range<Integer>>) m.invoke(null, spannable, 0, spannable.length());
+ assertEquals(3, freeRanges.size());
+ assertEquals(0, (int) freeRanges.get(0).getLower());
+ assertEquals(6, (int) freeRanges.get(0).getUpper());
+ assertEquals(11, (int) freeRanges.get(1).getLower());
+ assertEquals(19, (int) freeRanges.get(1).getUpper());
+ assertEquals(22, (int) freeRanges.get(2).getLower());
+ assertEquals(27, (int) freeRanges.get(2).getUpper());
+ }
+}