Refactor markdown module (#1025)

Refactor markdown module
This commit is contained in:
Niedermann IT-Dienstleistungen 2021-01-06 11:56:06 +01:00 committed by GitHub
parent 82fd1b02b4
commit 43432c9a84
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
16 changed files with 506 additions and 656 deletions

View file

@ -21,12 +21,11 @@ import java.lang.reflect.Method;
import java.util.HashMap;
import java.util.Map;
import it.niedermann.android.markdown.markwon.MarkwonMarkdownUtil;
import it.niedermann.android.markdown.markwon.model.EListType;
import it.niedermann.android.markdown.markwon.span.SearchSpan;
import it.niedermann.android.markdown.model.EListType;
import it.niedermann.android.markdown.model.SearchSpan;
@RunWith(AndroidJUnit4.class)
public class MarkwonMarkdownUtilTest extends TestCase {
public class MarkdownUtilTest extends TestCase {
@Test
public void testGetStartOfLine() {
@ -42,7 +41,7 @@ public class MarkwonMarkdownUtilTest extends TestCase {
);
for (int i = 0; i < test.length(); i++) {
int startOfLine = MarkwonMarkdownUtil.getStartOfLine(test, i);
int startOfLine = MarkdownUtil.getStartOfLine(test, i);
if (i <= 11) {
assertEquals(0, startOfLine);
} else if (i <= 12) {
@ -73,7 +72,7 @@ public class MarkwonMarkdownUtilTest extends TestCase {
"\n"; // line 78 - 79
for (int i = 0; i < test.length(); i++) {
int endOfLine = MarkwonMarkdownUtil.getEndOfLine(test, i);
int endOfLine = MarkdownUtil.getEndOfLine(test, i);
if (i <= 11) {
assertEquals(11, endOfLine);
} else if (i <= 12) {
@ -159,7 +158,7 @@ public class MarkwonMarkdownUtilTest extends TestCase {
lines.put("*[]", false);
lines.put("+[]", false);
lines.forEach((key, value) -> assertEquals(value, (Boolean) MarkwonMarkdownUtil.lineStartsWithCheckbox(key)));
lines.forEach((key, value) -> assertEquals(value, (Boolean) MarkdownUtil.lineStartsWithCheckbox(key)));
}
@Test
@ -168,121 +167,121 @@ public class MarkwonMarkdownUtilTest extends TestCase {
// Add italic
builder = new SpannableStringBuilder("Lorem ipsum dolor sit amet.");
assertEquals(13, MarkwonMarkdownUtil.togglePunctuation(builder, 6, 11, "*"));
assertEquals(13, MarkdownUtil.togglePunctuation(builder, 6, 11, "*"));
assertEquals("Lorem *ipsum* dolor sit amet.", builder.toString());
// Remove italic
builder = new SpannableStringBuilder("Lorem *ipsum* dolor sit amet.");
assertEquals(11, MarkwonMarkdownUtil.togglePunctuation(builder, 7, 12, "*"));
assertEquals(11, MarkdownUtil.togglePunctuation(builder, 7, 12, "*"));
assertEquals("Lorem ipsum dolor sit amet.", builder.toString());
// Add bold
builder = new SpannableStringBuilder("Lorem ipsum dolor sit amet.");
assertEquals(15, MarkwonMarkdownUtil.togglePunctuation(builder, 6, 11, "**"));
assertEquals(15, MarkdownUtil.togglePunctuation(builder, 6, 11, "**"));
assertEquals("Lorem **ipsum** dolor sit amet.", builder.toString());
// Remove bold
builder = new SpannableStringBuilder("Lorem **ipsum** dolor sit amet.");
assertEquals(11, MarkwonMarkdownUtil.togglePunctuation(builder, 8, 13, "**"));
assertEquals(11, MarkdownUtil.togglePunctuation(builder, 8, 13, "**"));
assertEquals("Lorem ipsum dolor sit amet.", builder.toString());
// Add strike
builder = new SpannableStringBuilder("Lorem ipsum dolor sit amet.");
assertEquals(15, MarkwonMarkdownUtil.togglePunctuation(builder, 6, 11, "~~"));
assertEquals(15, MarkdownUtil.togglePunctuation(builder, 6, 11, "~~"));
assertEquals("Lorem ~~ipsum~~ dolor sit amet.", builder.toString());
// Remove strike
builder = new SpannableStringBuilder("Lorem ~~ipsum~~ dolor sit amet.");
assertEquals(11, MarkwonMarkdownUtil.togglePunctuation(builder, 8, 13, "~~"));
assertEquals(11, MarkdownUtil.togglePunctuation(builder, 8, 13, "~~"));
assertEquals("Lorem ipsum dolor sit amet.", builder.toString());
// Add italic at first position
builder = new SpannableStringBuilder("Lorem ipsum dolor sit amet.");
assertEquals(7, MarkwonMarkdownUtil.togglePunctuation(builder, 0, 5, "*"));
assertEquals(7, MarkdownUtil.togglePunctuation(builder, 0, 5, "*"));
assertEquals("*Lorem* ipsum dolor sit amet.", builder.toString());
// Remove italic from first position
builder = new SpannableStringBuilder("*Lorem* ipsum dolor sit amet.");
assertEquals(5, MarkwonMarkdownUtil.togglePunctuation(builder, 1, 6, "*"));
assertEquals(5, MarkdownUtil.togglePunctuation(builder, 1, 6, "*"));
assertEquals("Lorem ipsum dolor sit amet.", builder.toString());
// Add italic at last position
builder = new SpannableStringBuilder("Lorem ipsum dolor sit amet.");
assertEquals(29, MarkwonMarkdownUtil.togglePunctuation(builder, 22, 27, "*"));
assertEquals(29, MarkdownUtil.togglePunctuation(builder, 22, 27, "*"));
assertEquals("Lorem ipsum dolor sit *amet.*", builder.toString());
// Remove italic from last position
builder = new SpannableStringBuilder("Lorem ipsum dolor sit *amet.*");
assertEquals(27, MarkwonMarkdownUtil.togglePunctuation(builder, 23, 28, "*"));
assertEquals(27, MarkdownUtil.togglePunctuation(builder, 23, 28, "*"));
assertEquals("Lorem ipsum dolor sit amet.", builder.toString());
// Text is not directly surrounded by punctuation but contains it
// Do nothing when the same punctuation is contained only one time
builder = new SpannableStringBuilder("Lorem *ipsum dolor sit amet.");
assertEquals(28, MarkwonMarkdownUtil.togglePunctuation(builder, 0, 28, "*"));
assertEquals(28, MarkdownUtil.togglePunctuation(builder, 0, 28, "*"));
assertEquals("Lorem *ipsum dolor sit amet.", builder.toString());
// Do nothing when the same punctuation is contained only one time
builder = new SpannableStringBuilder("Lorem **ipsum dolor sit amet.");
assertEquals(29, MarkwonMarkdownUtil.togglePunctuation(builder, 0, 29, "**"));
assertEquals(29, MarkdownUtil.togglePunctuation(builder, 0, 29, "**"));
assertEquals("Lorem **ipsum dolor sit amet.", builder.toString());
// Remove containing punctuation
builder = new SpannableStringBuilder("Lorem *ipsum* dolor sit amet.");
assertEquals(11, MarkwonMarkdownUtil.togglePunctuation(builder, 6, 13, "*"));
assertEquals(11, MarkdownUtil.togglePunctuation(builder, 6, 13, "*"));
assertEquals("Lorem ipsum dolor sit amet.", builder.toString());
// Remove containing punctuation
builder = new SpannableStringBuilder("Lorem *ipsum* dolor sit amet.");
assertEquals(27, MarkwonMarkdownUtil.togglePunctuation(builder, 0, 29, "*"));
assertEquals(27, MarkdownUtil.togglePunctuation(builder, 0, 29, "*"));
assertEquals("Lorem ipsum dolor sit amet.", builder.toString());
// Remove multiple containing punctuations
builder = new SpannableStringBuilder("Lorem *ipsum* dolor *sit* amet.");
assertEquals(27, MarkwonMarkdownUtil.togglePunctuation(builder, 0, 31, "*"));
assertEquals(27, MarkdownUtil.togglePunctuation(builder, 0, 31, "*"));
assertEquals("Lorem ipsum dolor sit amet.", builder.toString());
// Special use-case: toggle from italic to bold and back
// TODO Toggle italic on bold text
// builder = new SpannableStringBuilder("Lorem **ipsum** dolor sit amet.");
// assertEquals(17, MarkwonMarkdownUtil.togglePunctuation(builder, 8, 13, "*"));
// assertEquals(17, MarkdownUtil.togglePunctuation(builder, 8, 13, "*"));
// assertEquals("Lorem ***ipsum*** dolor sit amet.", builder.toString());
// TODO Toggle bold on italic text
// builder = new SpannableStringBuilder("Lorem *ipsum* dolor sit amet.");
// assertEquals(17, MarkwonMarkdownUtil.togglePunctuation(builder, 7, 12, "**"));
// assertEquals(17, MarkdownUtil.togglePunctuation(builder, 7, 12, "**"));
// assertEquals("Lorem ***ipsum*** dolor sit amet.", builder.toString());
// TODO Toggle bold to italic
// builder = new SpannableStringBuilder("Lorem **ipsum** dolor sit amet.");
// assertEquals(33, MarkwonMarkdownUtil.togglePunctuation(builder, 0, 31, "*"));
// assertEquals(33, MarkdownUtil.togglePunctuation(builder, 0, 31, "*"));
// assertEquals("Lorem ***ipsum*** dolor sit amet.", builder.toString());
// TODO Toggle multiple bold parts to italic
// builder = new SpannableStringBuilder("Lorem **ipsum** dolor **sit** amet.");
// assertEquals(38, MarkwonMarkdownUtil.togglePunctuation(builder, 0, 34, "*"));
// assertEquals(38, MarkdownUtil.togglePunctuation(builder, 0, 34, "*"));
// assertEquals("Lorem ***ipsum*** dolor ***sit*** amet.", builder.toString());
// TODO Toggle italic and bold to bold
// builder = new SpannableStringBuilder("Lorem ***ipsum*** dolor sit amet.");
// assertEquals(13, MarkwonMarkdownUtil.togglePunctuation(builder, 0, 14, "*"));
// assertEquals(13, MarkdownUtil.togglePunctuation(builder, 0, 14, "*"));
// assertEquals("Lorem **ipsum** dolor sit amet.", builder.toString());
// TODO Toggle italic and bold to italic
// builder = new SpannableStringBuilder("Lorem ***ipsum*** dolor sit amet.");
// assertEquals(12, MarkwonMarkdownUtil.togglePunctuation(builder, 9, 14, "**"));
// assertEquals(12, MarkdownUtil.togglePunctuation(builder, 9, 14, "**"));
// assertEquals("Lorem *ipsum* dolor sit amet.", builder.toString());
// TODO Toggle multiple italic and bold to bold
// builder = new SpannableStringBuilder("Lorem ***ipsum*** dolor ***sit*** amet.");
// assertEquals(34, MarkwonMarkdownUtil.togglePunctuation(builder, 0, 38, "*"));
// assertEquals(34, MarkdownUtil.togglePunctuation(builder, 0, 38, "*"));
// assertEquals("Lorem **ipsum** dolor **sit** amet.", builder.toString());
// TODO Toggle multiple italic and bold to italic
// builder = new SpannableStringBuilder("Lorem ***ipsum*** dolor ***sit*** amet.");
// assertEquals(30, MarkwonMarkdownUtil.togglePunctuation(builder, 0, 38, "**"));
// assertEquals(30, MarkdownUtil.togglePunctuation(builder, 0, 38, "**"));
// assertEquals("Lorem *ipsum* dolor *sit* amet.", builder.toString());
}
@ -292,59 +291,59 @@ public class MarkwonMarkdownUtilTest extends TestCase {
// Add link without clipboardUrl to normal text
builder = new SpannableStringBuilder("Lorem ipsum dolor sit amet.");
assertEquals(14, MarkwonMarkdownUtil.insertLink(builder, 6, 11, null));
assertEquals(14, MarkdownUtil.insertLink(builder, 6, 11, null));
assertEquals("Lorem [ipsum]() dolor sit amet.", builder.toString());
// Add link without clipboardUrl to url
builder = new SpannableStringBuilder("Lorem https://example.com dolor sit amet.");
assertEquals(7, MarkwonMarkdownUtil.insertLink(builder, 6, 25, null));
assertEquals(7, MarkdownUtil.insertLink(builder, 6, 25, null));
assertEquals("Lorem [](https://example.com) dolor sit amet.", builder.toString());
// TODO Add link without clipboardUrl to empty selection before space character
// builder = new SpannableStringBuilder("Lorem ipsum dolor sit amet.");
// assertEquals(13, MarkwonMarkdownUtil.insertLink(builder, 11, 11, null));
// assertEquals(13, MarkdownUtil.insertLink(builder, 11, 11, null));
// assertEquals("Lorem ipsum []() dolor sit amet.", builder.toString());
// TODO Add link without clipboardUrl to empty selection after space character
// builder = new SpannableStringBuilder("Lorem ipsum dolor sit amet.");
// assertEquals(13, MarkwonMarkdownUtil.insertLink(builder, 12, 12, null));
// assertEquals(13, MarkdownUtil.insertLink(builder, 12, 12, null));
// assertEquals("Lorem ipsum []() dolor sit amet.", builder.toString());
// TODO Add link without clipboardUrl to empty selection in word
// builder = new SpannableStringBuilder("Lorem ipsum dolor sit amet.");
// assertEquals(20, MarkwonMarkdownUtil.insertLink(builder, 14, 14, null));
// assertEquals(20, MarkdownUtil.insertLink(builder, 14, 14, null));
// assertEquals("Lorem ipsum [dolor]() sit amet.", builder.toString());
// Add link with clipboardUrl to normal text
builder = new SpannableStringBuilder("Lorem ipsum dolor sit amet.");
assertEquals(33, MarkwonMarkdownUtil.insertLink(builder, 6, 11, "https://example.com"));
assertEquals(33, MarkdownUtil.insertLink(builder, 6, 11, "https://example.com"));
assertEquals("Lorem [ipsum](https://example.com) dolor sit amet.", builder.toString());
// Add link with clipboardUrl to url
builder = new SpannableStringBuilder("Lorem https://example.com dolor sit amet.");
assertEquals(46, MarkwonMarkdownUtil.insertLink(builder, 6, 25, "https://example.de"));
assertEquals(46, MarkdownUtil.insertLink(builder, 6, 25, "https://example.de"));
assertEquals("Lorem [https://example.com](https://example.de) dolor sit amet.", builder.toString());
// TODO Add link with clipboardUrl to empty selection before space character
// builder = new SpannableStringBuilder("Lorem ipsum dolor sit amet.");
// assertEquals(13, MarkwonMarkdownUtil.insertLink(builder, 11, 11, "https://example.de"));
// assertEquals(13, MarkdownUtil.insertLink(builder, 11, 11, "https://example.de"));
// assertEquals("Lorem ipsum []("https://example.de") dolor sit amet.", builder.toString());
// TODO Add link with clipboardUrl to empty selection after space character
// builder = new SpannableStringBuilder("Lorem ipsum dolor sit amet.");
// assertEquals(13, MarkwonMarkdownUtil.insertLink(builder, 12, 12, "https://example.de"));
// assertEquals(13, MarkdownUtil.insertLink(builder, 12, 12, "https://example.de"));
// assertEquals("Lorem ipsum []("https://example.de") dolor sit amet.", builder.toString());
// TODO Add link with clipboardUrl to empty selection in word
// builder = new SpannableStringBuilder("Lorem ipsum dolor sit amet.");
// assertEquals(18, MarkwonMarkdownUtil.insertLink(builder, 14, 14, "https://example.de"));
// assertEquals(18, MarkdownUtil.insertLink(builder, 14, 14, "https://example.de"));
// assertEquals("Lorem ipsum [dolor]("https://example.de") sit amet.", builder.toString());
}
@Test
public void testRemoveContainingPunctuation() {
try {
final Method m = MarkwonMarkdownUtil.class.getDeclaredMethod("removeContainingPunctuation", Editable.class, int.class, int.class, String.class);
final Method m = MarkdownUtil.class.getDeclaredMethod("removeContainingPunctuation", Editable.class, int.class, int.class, String.class);
m.setAccessible(true);
Editable builder;
@ -384,7 +383,7 @@ public class MarkwonMarkdownUtilTest extends TestCase {
@SuppressWarnings("ConstantConditions")
public void testSelectionIsSurroundedByPunctuation() {
try {
final Method m = MarkwonMarkdownUtil.class.getDeclaredMethod("selectionIsSurroundedByPunctuation", CharSequence.class, int.class, int.class, String.class);
final Method m = MarkdownUtil.class.getDeclaredMethod("selectionIsSurroundedByPunctuation", CharSequence.class, int.class, int.class, String.class);
m.setAccessible(true);
assertTrue((Boolean) m.invoke(null, "*Lorem ipsum*", 1, 12, "*"));
assertTrue((Boolean) m.invoke(null, "**Lorem ipsum**", 2, 13, "*"));
@ -402,7 +401,7 @@ public class MarkwonMarkdownUtilTest extends TestCase {
@SuppressWarnings("ConstantConditions")
public void testGetContainedPunctuationCount() {
try {
final Method m = MarkwonMarkdownUtil.class.getDeclaredMethod("getContainedPunctuationCount", CharSequence.class, int.class, int.class, String.class);
final Method m = MarkdownUtil.class.getDeclaredMethod("getContainedPunctuationCount", CharSequence.class, int.class, int.class, String.class);
m.setAccessible(true);
assertEquals(0, (int) m.invoke(null, "*Lorem ipsum*", 1, 12, "*"));
assertEquals(1, (int) m.invoke(null, "*Lorem ipsum*", 1, 13, "*"));
@ -419,7 +418,7 @@ public class MarkwonMarkdownUtilTest extends TestCase {
@SuppressWarnings("ConstantConditions")
public void testSelectionIsInLink() {
try {
final Method m = MarkwonMarkdownUtil.class.getDeclaredMethod("selectionIsInLink", CharSequence.class, int.class, int.class);
final Method m = MarkdownUtil.class.getDeclaredMethod("selectionIsInLink", CharSequence.class, int.class, int.class);
m.setAccessible(true);
assertTrue((Boolean) m.invoke(null, "Lorem [ipsum](https://example.com) dolor sit amet.", 7, 12));
@ -457,35 +456,35 @@ public class MarkwonMarkdownUtilTest extends TestCase {
@Test
public void testGetListItemIfIsEmpty() {
assertEquals("- ", MarkwonMarkdownUtil.getListItemIfIsEmpty("- "));
assertEquals("+ ", MarkwonMarkdownUtil.getListItemIfIsEmpty("+ "));
assertEquals("* ", MarkwonMarkdownUtil.getListItemIfIsEmpty("* "));
assertEquals("1. ", MarkwonMarkdownUtil.getListItemIfIsEmpty("1. "));
assertEquals("- ", MarkdownUtil.getListItemIfIsEmpty("- "));
assertEquals("+ ", MarkdownUtil.getListItemIfIsEmpty("+ "));
assertEquals("* ", MarkdownUtil.getListItemIfIsEmpty("* "));
assertEquals("1. ", MarkdownUtil.getListItemIfIsEmpty("1. "));
assertNull(MarkwonMarkdownUtil.getListItemIfIsEmpty("- Test"));
assertNull(MarkwonMarkdownUtil.getListItemIfIsEmpty("+ Test"));
assertNull(MarkwonMarkdownUtil.getListItemIfIsEmpty("* Test"));
assertNull(MarkwonMarkdownUtil.getListItemIfIsEmpty("1. s"));
assertNull(MarkwonMarkdownUtil.getListItemIfIsEmpty("1. "));
assertNull(MarkdownUtil.getListItemIfIsEmpty("- Test"));
assertNull(MarkdownUtil.getListItemIfIsEmpty("+ Test"));
assertNull(MarkdownUtil.getListItemIfIsEmpty("* Test"));
assertNull(MarkdownUtil.getListItemIfIsEmpty("1. s"));
assertNull(MarkdownUtil.getListItemIfIsEmpty("1. "));
}
@Test
public void testLineStartsWithOrderedList() {
assertEquals(1, MarkwonMarkdownUtil.getOrderedListNumber("1. Test"));
assertEquals(2, MarkwonMarkdownUtil.getOrderedListNumber("2. Test"));
assertEquals(3, MarkwonMarkdownUtil.getOrderedListNumber("3. Test"));
assertEquals(10, MarkwonMarkdownUtil.getOrderedListNumber("10. Test"));
assertEquals(11, MarkwonMarkdownUtil.getOrderedListNumber("11. Test"));
assertEquals(12, MarkwonMarkdownUtil.getOrderedListNumber("12. Test"));
assertEquals(1, MarkwonMarkdownUtil.getOrderedListNumber("1. 1"));
assertEquals(1, MarkwonMarkdownUtil.getOrderedListNumber("1. Test 1"));
assertEquals(1, MarkdownUtil.getOrderedListNumber("1. Test"));
assertEquals(2, MarkdownUtil.getOrderedListNumber("2. Test"));
assertEquals(3, MarkdownUtil.getOrderedListNumber("3. Test"));
assertEquals(10, MarkdownUtil.getOrderedListNumber("10. Test"));
assertEquals(11, MarkdownUtil.getOrderedListNumber("11. Test"));
assertEquals(12, MarkdownUtil.getOrderedListNumber("12. Test"));
assertEquals(1, MarkdownUtil.getOrderedListNumber("1. 1"));
assertEquals(1, MarkdownUtil.getOrderedListNumber("1. Test 1"));
assertEquals(-1, MarkwonMarkdownUtil.getOrderedListNumber(""));
assertEquals(-1, MarkwonMarkdownUtil.getOrderedListNumber("1."));
assertEquals(-1, MarkwonMarkdownUtil.getOrderedListNumber("1. "));
assertEquals(-1, MarkwonMarkdownUtil.getOrderedListNumber("11. "));
assertEquals(-1, MarkwonMarkdownUtil.getOrderedListNumber("-1. Test"));
assertEquals(-1, MarkwonMarkdownUtil.getOrderedListNumber(" 1. Test"));
assertEquals(-1, MarkdownUtil.getOrderedListNumber(""));
assertEquals(-1, MarkdownUtil.getOrderedListNumber("1."));
assertEquals(-1, MarkdownUtil.getOrderedListNumber("1. "));
assertEquals(-1, MarkdownUtil.getOrderedListNumber("11. "));
assertEquals(-1, MarkdownUtil.getOrderedListNumber("-1. Test"));
assertEquals(-1, MarkdownUtil.getOrderedListNumber(" 1. Test"));
}
@Test
@ -493,19 +492,19 @@ public class MarkwonMarkdownUtilTest extends TestCase {
for (EListType listType : EListType.values()) {
final String origin_1 = listType.checkboxUnchecked + " Item";
final String expected_1 = listType.checkboxChecked + " Item";
assertEquals(expected_1, MarkwonMarkdownUtil.setCheckboxStatus(origin_1, 0, true));
assertEquals(expected_1, MarkdownUtil.setCheckboxStatus(origin_1, 0, true));
final String origin_2 = listType.checkboxChecked + " Item";
final String expected_2 = listType.checkboxChecked + " Item";
assertEquals(expected_2, MarkwonMarkdownUtil.setCheckboxStatus(origin_2, 0, true));
assertEquals(expected_2, MarkdownUtil.setCheckboxStatus(origin_2, 0, true));
final String origin_3 = listType.checkboxChecked + " Item";
final String expected_3 = listType.checkboxChecked + " Item";
assertEquals(expected_3, MarkwonMarkdownUtil.setCheckboxStatus(origin_3, -1, true));
assertEquals(expected_3, MarkdownUtil.setCheckboxStatus(origin_3, -1, true));
final String origin_4 = listType.checkboxChecked + " Item";
final String expected_4 = listType.checkboxChecked + " Item";
assertEquals(expected_4, MarkwonMarkdownUtil.setCheckboxStatus(origin_4, 3, true));
assertEquals(expected_4, MarkdownUtil.setCheckboxStatus(origin_4, 3, true));
final String origin_5 = "" +
listType.checkboxChecked + " Item\n" +
@ -513,7 +512,7 @@ public class MarkwonMarkdownUtilTest extends TestCase {
final String expected_5 = "" +
listType.checkboxChecked + " Item\n" +
listType.checkboxUnchecked + " Item";
assertEquals(expected_5, MarkwonMarkdownUtil.setCheckboxStatus(origin_5, 1, false));
assertEquals(expected_5, MarkdownUtil.setCheckboxStatus(origin_5, 1, false));
// Checkboxes in fenced code block aren't rendered by Markwon and therefore don't count to the checkbox index
final String origin_6 = "" +
@ -528,7 +527,7 @@ public class MarkwonMarkdownUtilTest extends TestCase {
listType.checkboxUnchecked + " Item\n" +
"```\n" +
listType.checkboxChecked + " Item";
assertEquals(expected_6, MarkwonMarkdownUtil.setCheckboxStatus(origin_6, 1, true));
assertEquals(expected_6, MarkdownUtil.setCheckboxStatus(origin_6, 1, true));
// Checkbox in partial nested fenced code block does not count as rendered checkbox
final String origin_7 = "" +
@ -545,7 +544,7 @@ public class MarkwonMarkdownUtilTest extends TestCase {
listType.checkboxUnchecked + " Item\n" +
"````\n" +
listType.checkboxChecked + " Item";
assertEquals(expected_7, MarkwonMarkdownUtil.setCheckboxStatus(origin_7, 1, true));
assertEquals(expected_7, MarkdownUtil.setCheckboxStatus(origin_7, 1, true));
// Checkbox in complete nested fenced code block does not count as rendered checkbox
final String origin_8 = "" +
@ -564,7 +563,7 @@ public class MarkwonMarkdownUtilTest extends TestCase {
"```\n" +
"````\n" +
listType.checkboxChecked + " Item";
assertEquals(expected_8, MarkwonMarkdownUtil.setCheckboxStatus(origin_8, 1, true));
assertEquals(expected_8, MarkdownUtil.setCheckboxStatus(origin_8, 1, true));
// If checkbox has no content, it doesn't get rendered by Markwon and therefore can not be checked
final String origin_9 = "" +
@ -585,30 +584,30 @@ public class MarkwonMarkdownUtilTest extends TestCase {
"````\n" +
listType.checkboxUnchecked + " \n" +
listType.checkboxChecked + " Item";
assertEquals(expected_9, MarkwonMarkdownUtil.setCheckboxStatus(origin_9, 1, true));
assertEquals(expected_9, MarkdownUtil.setCheckboxStatus(origin_9, 1, true));
}
}
@Test
public void testRemoveSpans() {
try {
final Method removeSpans = MarkwonMarkdownUtil.class.getDeclaredMethod("removeSpans", Spannable.class, Class.class);
final Method removeSpans = MarkdownUtil.class.getDeclaredMethod("removeSpans", Spannable.class, Class.class);
removeSpans.setAccessible(true);
final Context context = ApplicationProvider.getApplicationContext();
final Editable editable_1 = new SpannableStringBuilder("Lorem Ipsum dolor sit amet");
editable_1.setSpan(new SearchSpan(context, Color.RED, false), 0, 5, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
editable_1.setSpan(new SearchSpan(Color.RED, Color.GRAY, false, false), 0, 5, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
editable_1.setSpan(new ForegroundColorSpan(Color.BLUE), 6, 11, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
editable_1.setSpan(new SearchSpan(context, Color.GREEN, true), 12, 17, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
editable_1.setSpan(new SearchSpan(Color.BLUE, Color.GREEN, true, false), 12, 17, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
removeSpans.invoke(null, editable_1, SearchSpan.class);
assertEquals(0, editable_1.getSpans(0, editable_1.length(), SearchSpan.class).length);
assertEquals(1, editable_1.getSpans(0, editable_1.length(), ForegroundColorSpan.class).length);
final Editable editable_2 = new SpannableStringBuilder("Lorem Ipsum dolor sit amet");
editable_2.setSpan(new SearchSpan(context, Color.RED, false), 0, 5, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
editable_2.setSpan(new SearchSpan(Color.GRAY, Color.RED, false, true), 0, 5, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
editable_2.setSpan(new ForegroundColorSpan(Color.BLUE), 2, 7, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
editable_2.setSpan(new SearchSpan(context, Color.GREEN, true), 3, 9, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
editable_2.setSpan(new SearchSpan(Color.BLUE, Color.GREEN, true, false), 3, 9, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
removeSpans.invoke(null, editable_2, SearchSpan.class);
assertEquals(0, editable_2.getSpans(0, editable_2.length(), SearchSpan.class).length);
assertEquals(1, editable_2.getSpans(0, editable_2.length(), ForegroundColorSpan.class).length);

View file

@ -1,25 +1,45 @@
package it.niedermann.android.markdown;
import android.content.Context;
import android.text.Editable;
import android.text.Spannable;
import android.text.SpannableString;
import android.text.Spanned;
import android.text.TextUtils;
import android.util.Log;
import android.widget.RemoteViews.RemoteView;
import android.widget.TextView;
import androidx.annotation.ColorInt;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.core.text.HtmlCompat;
import com.yydcdut.markdown.MarkdownProcessor;
import com.yydcdut.markdown.syntax.text.TextFactory;
import com.yydcdut.rxmarkdown.RxMarkdown;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import io.noties.markwon.Markwon;
import it.niedermann.android.markdown.model.SearchSpan;
import it.niedermann.android.markdown.model.EListType;
public class MarkdownUtil {
private static final String TAG = MarkdownUtil.class.getSimpleName();
private static final String MD_IMAGE_WITH_EMPTY_DESCRIPTION = "![](";
private static final String MD_IMAGE_WITH_SPACE_DESCRIPTION = "![ ](";
private static final String[] MD_IMAGE_WITH_EMPTY_DESCRIPTION_ARRAY = new String[]{MD_IMAGE_WITH_EMPTY_DESCRIPTION};
private static final String[] MD_IMAGE_WITH_SPACE_DESCRIPTION_ARRAY = new String[]{MD_IMAGE_WITH_SPACE_DESCRIPTION};
private static final Pattern PATTERN_CODE_FENCE = Pattern.compile("^(`{3,})");
private static final Pattern PATTERN_ORDERED_LIST_ITEM = Pattern.compile("^(\\d+).\\s.+$");
private static final Pattern PATTERN_ORDERED_LIST_ITEM_EMPTY = Pattern.compile("^(\\d+).\\s$");
private static final Pattern PATTERN_MARKDOWN_LINK = Pattern.compile("\\[(.+)?]\\(([^ ]+?)?( \"(.+)\")?\\)");
private MarkdownUtil() {
// Util class
}
@ -58,4 +78,263 @@ public class MarkdownUtil {
return markdownProcessor.parse(text);
}
public static int getStartOfLine(@NonNull CharSequence s, int cursorPosition) {
int startOfLine = cursorPosition;
while (startOfLine > 0 && s.charAt(startOfLine - 1) != '\n') {
startOfLine--;
}
return startOfLine;
}
public static int getEndOfLine(@NonNull CharSequence s, int cursorPosition) {
int nextLinebreak = s.toString().indexOf('\n', cursorPosition);
if (nextLinebreak > -1) {
return nextLinebreak;
}
return cursorPosition;
}
public static String getListItemIfIsEmpty(@NonNull String line) {
for (EListType listType : EListType.values()) {
if (line.equals(listType.checkboxUncheckedWithTrailingSpace)) {
return listType.checkboxUncheckedWithTrailingSpace;
} else if (line.equals(listType.listSymbolWithTrailingSpace)) {
return listType.listSymbolWithTrailingSpace;
}
}
final Matcher matcher = PATTERN_ORDERED_LIST_ITEM_EMPTY.matcher(line);
if (matcher.find()) {
return matcher.group();
}
return null;
}
public static CharSequence setCheckboxStatus(@NonNull String markdownString, int targetCheckboxIndex, boolean newCheckedState) {
final String[] lines = markdownString.split("\n");
int checkboxIndex = 0;
boolean isInFencedCodeBlock = false;
int fencedCodeBlockSigns = 0;
for (int i = 0; i < lines.length; i++) {
final Matcher matcher = PATTERN_CODE_FENCE.matcher(lines[i]);
if (matcher.find()) {
final String fence = matcher.group(1);
if (fence != null) {
int currentFencedCodeBlockSigns = fence.length();
if (isInFencedCodeBlock) {
if (currentFencedCodeBlockSigns == fencedCodeBlockSigns) {
isInFencedCodeBlock = false;
fencedCodeBlockSigns = 0;
}
} else {
isInFencedCodeBlock = true;
fencedCodeBlockSigns = currentFencedCodeBlockSigns;
}
}
}
if (!isInFencedCodeBlock) {
if (lineStartsWithCheckbox(lines[i]) && lines[i].trim().length() > EListType.DASH.checkboxChecked.length()) {
if (checkboxIndex == targetCheckboxIndex) {
final int indexOfStartingBracket = lines[i].indexOf("[");
final String toggledLine = lines[i].substring(0, indexOfStartingBracket + 1) +
(newCheckedState ? 'x' : ' ') +
lines[i].substring(indexOfStartingBracket + 2);
lines[i] = toggledLine;
break;
}
checkboxIndex++;
}
}
}
return TextUtils.join("\n", lines);
}
public static boolean lineStartsWithCheckbox(@NonNull String line) {
for (EListType listType : EListType.values()) {
if (lineStartsWithCheckbox(line, listType)) {
return true;
}
}
return false;
}
public static boolean lineStartsWithCheckbox(@NonNull String line, @NonNull EListType listType) {
final String trimmedLine = line.trim();
return (trimmedLine.startsWith(listType.checkboxUnchecked) || trimmedLine.startsWith(listType.checkboxChecked));
}
/**
* @return the number of the ordered list item if the line is an ordered list, otherwise -1.
*/
public static int getOrderedListNumber(@NonNull String line) {
final Matcher matcher = PATTERN_ORDERED_LIST_ITEM.matcher(line);
if (matcher.find()) {
final String groupNumber = matcher.group(1);
if (groupNumber != null) {
try {
return Integer.parseInt(groupNumber);
} catch (NumberFormatException e) {
return -1;
}
}
}
return -1;
}
/**
* Modifies the {@param editable} and adds the given {@param punctuation} from
* {@param selectionStart} to {@param selectionEnd} or removes the {@param punctuation} in case
* it already is around the selected part.
*
* @return the new cursor position
*/
public static int togglePunctuation(@NonNull Editable editable, int selectionStart, int selectionEnd, @NonNull String punctuation) {
switch (punctuation) {
case "**":
case "__":
case "*":
case "_":
case "~~": {
final boolean selectionIsSurroundedByPunctuation = selectionIsSurroundedByPunctuation(editable, selectionStart, selectionEnd, punctuation);
if (selectionIsSurroundedByPunctuation) {
editable.delete(selectionEnd, selectionEnd + punctuation.length());
editable.delete(selectionStart - punctuation.length(), selectionStart);
return selectionEnd - punctuation.length();
} else {
final int containedPunctuationCount = getContainedPunctuationCount(editable, selectionStart, selectionEnd, punctuation);
if (containedPunctuationCount == 0) {
editable.insert(selectionEnd, punctuation);
editable.insert(selectionStart, punctuation);
return selectionEnd + punctuation.length() * 2;
} else if (containedPunctuationCount % 2 > 0) {
return selectionEnd;
} else {
removeContainingPunctuation(editable, selectionStart, selectionEnd, punctuation);
return selectionEnd - containedPunctuationCount * punctuation.length();
}
}
}
default:
throw new UnsupportedOperationException("This kind of punctuation is not yet supported: " + punctuation);
}
}
/**
* Inserts a link into the given {@param editable} from {@param selectionStart} to {@param selectionEnd} and uses the {@param clipboardUrl} if available.
*
* @return the new cursor position
*/
public static int insertLink(@NonNull Editable editable, int selectionStart, int selectionEnd, @Nullable String clipboardUrl) {
if (selectionStart == selectionEnd) {
editable.insert(selectionStart, "[](" + (clipboardUrl == null ? "" : clipboardUrl) + ")");
return selectionStart + 1;
} else {
final boolean textToFormatIsLink = TextUtils.indexOf(editable.subSequence(selectionStart, selectionEnd), "http") == 0;
if (textToFormatIsLink) {
if (clipboardUrl == null) {
editable.insert(selectionEnd, ")");
editable.insert(selectionStart, "[](");
} else {
editable.insert(selectionEnd, "](" + clipboardUrl + ")");
editable.insert(selectionStart, "[");
selectionEnd += clipboardUrl.length();
}
} else {
if (clipboardUrl == null) {
editable.insert(selectionEnd, "]()");
} else {
editable.insert(selectionEnd, "](" + clipboardUrl + ")");
selectionEnd += clipboardUrl.length();
}
editable.insert(selectionStart, "[");
}
return textToFormatIsLink && clipboardUrl == null
? selectionStart + 1
: selectionEnd + 3;
}
}
/**
* @return whether or not the selection of {@param text} from {@param start} to {@param end} is
* surrounded or not by the given {@param punctuation}.
*/
private static boolean selectionIsSurroundedByPunctuation(@NonNull CharSequence text, int start, int end, @NonNull String punctuation) {
if (text.length() < end + punctuation.length()) {
return false;
}
if (start - punctuation.length() < 0 || end + punctuation.length() > text.length()) {
return false;
}
return punctuation.contentEquals(text.subSequence(start - punctuation.length(), start))
&& punctuation.contentEquals(text.subSequence(end, end + punctuation.length()));
}
private static int getContainedPunctuationCount(@NonNull CharSequence text, int start, int end, @NonNull String punctuation) {
final Matcher matcher = Pattern.compile(Pattern.quote(punctuation)).matcher(text.subSequence(start, end));
int counter = 0;
while (matcher.find()) {
counter++;
}
return counter;
}
private static void removeContainingPunctuation(@NonNull Editable editable, int start, int end, @NonNull String punctuation) {
final Matcher matcher = Pattern.compile(Pattern.quote(punctuation)).matcher(editable.subSequence(start, end));
int countDeletedPunctuations = 0;
while (matcher.find()) {
editable.delete(start + matcher.start() - countDeletedPunctuations * punctuation.length(), start + matcher.end() - countDeletedPunctuations * punctuation.length());
countDeletedPunctuations++;
}
}
public static boolean selectionIsInLink(@NonNull CharSequence text, int start, int end) {
final Matcher matcher = PATTERN_MARKDOWN_LINK.matcher(text);
while (matcher.find()) {
if ((start >= matcher.start() && start < matcher.end()) || (end > matcher.start() && end <= matcher.end())) {
return true;
}
}
return false;
}
public static void searchAndColor(@NonNull Spannable editable, @Nullable CharSequence searchText,@Nullable Integer current, @ColorInt int mainColor, @ColorInt int highlightColor, boolean darkTheme) {
if (searchText != null) {
final Matcher m = Pattern
.compile(searchText.toString(), Pattern.CASE_INSENSITIVE | Pattern.LITERAL)
.matcher(editable);
int i = 1;
while (m.find()) {
int start = m.start();
int end = m.end();
editable.setSpan(new SearchSpan(mainColor, highlightColor, (current != null && i == current), darkTheme), start, end, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
i++;
}
}
}
/**
* Removes all spans of {@param spanType} from {@param spannable}.
*/
public static <T> void removeSpans(@NonNull Spannable spannable, @SuppressWarnings("SameParameterValue") Class<T> spanType) {
for (T span : spannable.getSpans(0, spannable.length(), spanType)) {
spannable.removeSpan(span);
}
}
/**
* @return When the content of the {@param textView} is already of type {@link Spannable}, it will cast and return it directly.
* Otherwise it will create a new {@link SpannableString} from the content, set this as new content of the {@param textView} and return it.
*/
public static Spannable getContentAsSpannable(@NonNull TextView textView) {
final CharSequence content = textView.getText();
if (content.getClass() == SpannableString.class || content instanceof Spannable) {
return (Spannable) content;
} else {
Log.w(TAG, "Expected " + TextView.class.getSimpleName() + " content to be of type " + Spannable.class.getSimpleName() + ", but was of type " + content.getClass() + ". Search highlighting will be not performant.");
final Spannable spannableContent = new SpannableString(content);
textView.setText(spannableContent, TextView.BufferType.SPANNABLE);
return spannableContent;
}
}
}

View file

@ -18,6 +18,10 @@ import io.noties.markwon.Markwon;
import io.noties.markwon.editor.MarkwonEditor;
import io.noties.markwon.editor.handler.EmphasisEditHandler;
import io.noties.markwon.editor.handler.StrongEmphasisEditHandler;
import io.noties.markwon.ext.strikethrough.StrikethroughPlugin;
import io.noties.markwon.image.ImagesPlugin;
import io.noties.markwon.inlineparser.MarkwonInlineParserPlugin;
import io.noties.markwon.simple.ext.SimpleExtPlugin;
import it.niedermann.android.markdown.MarkdownEditor;
import it.niedermann.android.markdown.markwon.format.ContextBasedFormattingCallback;
import it.niedermann.android.markdown.markwon.format.ContextBasedRangeFormattingCallback;
@ -26,6 +30,8 @@ import it.niedermann.android.markdown.markwon.handler.CodeBlockEditHandler;
import it.niedermann.android.markdown.markwon.handler.CodeEditHandler;
import it.niedermann.android.markdown.markwon.handler.HeadingEditHandler;
import it.niedermann.android.markdown.markwon.handler.StrikethroughEditHandler;
import it.niedermann.android.markdown.markwon.plugins.SearchHighlightPlugin;
import it.niedermann.android.markdown.markwon.plugins.ThemePlugin;
import it.niedermann.android.markdown.markwon.textwatcher.CombinedTextWatcher;
import it.niedermann.android.markdown.markwon.textwatcher.SearchHighlightTextWatcher;
@ -46,16 +52,10 @@ public class MarkwonMarkdownEditor extends AppCompatEditText implements Markdown
public MarkwonMarkdownEditor(@NonNull Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
final Markwon markwon = MarkwonMarkdownUtil.initMarkwonEditor(context).build();
final MarkwonEditor editor = MarkwonEditor.builder(markwon)
.useEditHandler(new EmphasisEditHandler())
.useEditHandler(new StrongEmphasisEditHandler())
.useEditHandler(new StrikethroughEditHandler())
.useEditHandler(new CodeEditHandler())
.useEditHandler(new CodeBlockEditHandler())
.useEditHandler(new BlockQuoteEditHandler())
.useEditHandler(new HeadingEditHandler())
.build();
final Markwon markwon = createMarkwonBuilder(context).build();
final MarkwonEditor editor = createMarkwonEditorBuilder(markwon).build();
combinedWatcher = new CombinedTextWatcher(editor, this);
addTextChangedListener(combinedWatcher);
setCustomSelectionActionModeCallback(new ContextBasedRangeFormattingCallback(this));
@ -64,6 +64,27 @@ public class MarkwonMarkdownEditor extends AppCompatEditText implements Markdown
}
}
private static Markwon.Builder createMarkwonBuilder(@NonNull Context context) {
return Markwon.builder(context)
.usePlugin(ThemePlugin.create(context))
.usePlugin(StrikethroughPlugin.create())
.usePlugin(SimpleExtPlugin.create())
.usePlugin(ImagesPlugin.create())
.usePlugin(MarkwonInlineParserPlugin.create())
.usePlugin(SearchHighlightPlugin.create(context));
}
private static MarkwonEditor.Builder createMarkwonEditorBuilder(@NonNull Markwon markwon) {
return MarkwonEditor.builder(markwon)
.useEditHandler(new EmphasisEditHandler())
.useEditHandler(new StrongEmphasisEditHandler())
.useEditHandler(new StrikethroughEditHandler())
.useEditHandler(new CodeEditHandler())
.useEditHandler(new CodeBlockEditHandler())
.useEditHandler(new BlockQuoteEditHandler())
.useEditHandler(new HeadingEditHandler());
}
@Override
public void setSearchColor(@ColorInt int color) {
final SearchHighlightTextWatcher searchHighlightTextWatcher = combinedWatcher.get(SearchHighlightTextWatcher.class);

View file

@ -2,344 +2,17 @@ package it.niedermann.android.markdown.markwon;
import android.content.Context;
import android.content.res.Configuration;
import android.text.Editable;
import android.text.Spannable;
import android.text.SpannableString;
import android.text.TextUtils;
import android.util.Log;
import android.widget.TextView;
import androidx.annotation.ColorInt;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.RestrictTo;
import java.util.Map;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import io.noties.markwon.Markwon;
import io.noties.markwon.ext.strikethrough.StrikethroughPlugin;
import io.noties.markwon.ext.tables.TablePlugin;
import io.noties.markwon.ext.tasklist.TaskListPlugin;
import io.noties.markwon.image.ImagesPlugin;
import io.noties.markwon.inlineparser.MarkwonInlineParserPlugin;
import io.noties.markwon.linkify.LinkifyPlugin;
import io.noties.markwon.simple.ext.SimpleExtPlugin;
import io.noties.markwon.syntax.Prism4jTheme;
import io.noties.markwon.syntax.Prism4jThemeDarkula;
import io.noties.markwon.syntax.Prism4jThemeDefault;
import io.noties.markwon.syntax.SyntaxHighlightPlugin;
import io.noties.prism4j.Prism4j;
import io.noties.prism4j.annotations.PrismBundle;
import it.niedermann.android.markdown.markwon.model.EListType;
import it.niedermann.android.markdown.markwon.plugins.LinkClickInterceptorPlugin;
import it.niedermann.android.markdown.markwon.plugins.NextcloudMentionsPlugin;
import it.niedermann.android.markdown.markwon.plugins.SearchHighlightPlugin;
import it.niedermann.android.markdown.markwon.plugins.ThemePlugin;
import it.niedermann.android.markdown.markwon.span.SearchSpan;
@RestrictTo(value = RestrictTo.Scope.LIBRARY)
@PrismBundle(includeAll = true, grammarLocatorClassName = ".MarkwonGrammarLocator")
public class MarkwonMarkdownUtil {
private static final String TAG = MarkwonMarkdownUtil.class.getSimpleName();
private static final Pattern PATTERN_CODE_FENCE = Pattern.compile("^(`{3,})");
private static final Pattern PATTERN_ORDERED_LIST_ITEM = Pattern.compile("^(\\d+).\\s.+$");
private static final Pattern PATTERN_ORDERED_LIST_ITEM_EMPTY = Pattern.compile("^(\\d+).\\s$");
private static final Pattern PATTERN_MARKDOWN_LINK = Pattern.compile("\\[(.+)?]\\(([^ ]+?)?( \"(.+)\")?\\)");
private MarkwonMarkdownUtil() {
// Util class
}
public static Markwon.Builder initMarkwonEditor(@NonNull Context context) {
return Markwon.builder(context)
.usePlugin(ThemePlugin.create(context))
.usePlugin(StrikethroughPlugin.create())
.usePlugin(SimpleExtPlugin.create())
.usePlugin(ImagesPlugin.create())
.usePlugin(MarkwonInlineParserPlugin.create())
.usePlugin(SearchHighlightPlugin.create(context));
}
public static Markwon.Builder initMarkwonViewer(@NonNull Context context) {
final Prism4j prism4j = new Prism4j(new MarkwonGrammarLocator());
final Prism4jTheme prism4jTheme = isDarkThemeActive(context)
? Prism4jThemeDarkula.create()
: Prism4jThemeDefault.create();
return initMarkwonEditor(context)
.usePlugin(TablePlugin.create(context))
.usePlugin(TaskListPlugin.create(context))
.usePlugin(LinkifyPlugin.create(true))
.usePlugin(LinkClickInterceptorPlugin.create())
.usePlugin(ImagesPlugin.create())
.usePlugin(SyntaxHighlightPlugin.create(prism4j, prism4jTheme));
}
public static Markwon.Builder initMarkwonViewer(@NonNull Context context, @NonNull Map<String, String> mentions) {
return initMarkwonViewer(context)
.usePlugin(NextcloudMentionsPlugin.create(context, mentions));
}
private static boolean isDarkThemeActive(@NonNull Context context) {
public static boolean isDarkThemeActive(@NonNull Context context) {
final int uiMode = context.getResources().getConfiguration().uiMode;
return (uiMode & Configuration.UI_MODE_NIGHT_MASK) == Configuration.UI_MODE_NIGHT_YES;
}
public static int getStartOfLine(@NonNull CharSequence s, int cursorPosition) {
int startOfLine = cursorPosition;
while (startOfLine > 0 && s.charAt(startOfLine - 1) != '\n') {
startOfLine--;
}
return startOfLine;
}
public static int getEndOfLine(@NonNull CharSequence s, int cursorPosition) {
int nextLinebreak = s.toString().indexOf('\n', cursorPosition);
if (nextLinebreak > -1) {
return nextLinebreak;
}
return cursorPosition;
}
public static String getListItemIfIsEmpty(@NonNull String line) {
for (EListType listType : EListType.values()) {
if (line.equals(listType.checkboxUncheckedWithTrailingSpace)) {
return listType.checkboxUncheckedWithTrailingSpace;
} else if (line.equals(listType.listSymbolWithTrailingSpace)) {
return listType.listSymbolWithTrailingSpace;
}
}
final Matcher matcher = PATTERN_ORDERED_LIST_ITEM_EMPTY.matcher(line);
if (matcher.find()) {
return matcher.group();
}
return null;
}
public static CharSequence setCheckboxStatus(@NonNull String markdownString, int targetCheckboxIndex, boolean newCheckedState) {
final String[] lines = markdownString.split("\n");
int checkboxIndex = 0;
boolean isInFencedCodeBlock = false;
int fencedCodeBlockSigns = 0;
for (int i = 0; i < lines.length; i++) {
final Matcher matcher = PATTERN_CODE_FENCE.matcher(lines[i]);
if (matcher.find()) {
final String fence = matcher.group(1);
if (fence != null) {
int currentFencedCodeBlockSigns = fence.length();
if (isInFencedCodeBlock) {
if (currentFencedCodeBlockSigns == fencedCodeBlockSigns) {
isInFencedCodeBlock = false;
fencedCodeBlockSigns = 0;
}
} else {
isInFencedCodeBlock = true;
fencedCodeBlockSigns = currentFencedCodeBlockSigns;
}
}
}
if (!isInFencedCodeBlock) {
if (lineStartsWithCheckbox(lines[i]) && lines[i].trim().length() > EListType.DASH.checkboxChecked.length()) {
if (checkboxIndex == targetCheckboxIndex) {
final int indexOfStartingBracket = lines[i].indexOf("[");
final String toggledLine = lines[i].substring(0, indexOfStartingBracket + 1) +
(newCheckedState ? 'x' : ' ') +
lines[i].substring(indexOfStartingBracket + 2);
lines[i] = toggledLine;
break;
}
checkboxIndex++;
}
}
}
return TextUtils.join("\n", lines);
}
public static boolean lineStartsWithCheckbox(@NonNull String line) {
for (EListType listType : EListType.values()) {
if (lineStartsWithCheckbox(line, listType)) {
return true;
}
}
return false;
}
public static boolean lineStartsWithCheckbox(@NonNull String line, @NonNull EListType listType) {
final String trimmedLine = line.trim();
return (trimmedLine.startsWith(listType.checkboxUnchecked) || trimmedLine.startsWith(listType.checkboxChecked));
}
/**
* @return the number of the ordered list item if the line is an ordered list, otherwise -1.
*/
public static int getOrderedListNumber(@NonNull String line) {
final Matcher matcher = PATTERN_ORDERED_LIST_ITEM.matcher(line);
if (matcher.find()) {
final String groupNumber = matcher.group(1);
if (groupNumber != null) {
try {
return Integer.parseInt(groupNumber);
} catch (NumberFormatException e) {
return -1;
}
}
}
return -1;
}
/**
* Modifies the {@param editable} and adds the given {@param punctuation} from
* {@param selectionStart} to {@param selectionEnd} or removes the {@param punctuation} in case
* it already is around the selected part.
*
* @return the new cursor position
*/
public static int togglePunctuation(@NonNull Editable editable, int selectionStart, int selectionEnd, @NonNull String punctuation) {
switch (punctuation) {
case "**":
case "__":
case "*":
case "_":
case "~~": {
final boolean selectionIsSurroundedByPunctuation = selectionIsSurroundedByPunctuation(editable, selectionStart, selectionEnd, punctuation);
if (selectionIsSurroundedByPunctuation) {
editable.delete(selectionEnd, selectionEnd + punctuation.length());
editable.delete(selectionStart - punctuation.length(), selectionStart);
return selectionEnd - punctuation.length();
} else {
final int containedPunctuationCount = getContainedPunctuationCount(editable, selectionStart, selectionEnd, punctuation);
if (containedPunctuationCount == 0) {
editable.insert(selectionEnd, punctuation);
editable.insert(selectionStart, punctuation);
return selectionEnd + punctuation.length() * 2;
} else if (containedPunctuationCount % 2 > 0) {
return selectionEnd;
} else {
removeContainingPunctuation(editable, selectionStart, selectionEnd, punctuation);
return selectionEnd - containedPunctuationCount * punctuation.length();
}
}
}
default:
throw new UnsupportedOperationException("This kind of punctuation is not yet supported: " + punctuation);
}
}
/**
* Inserts a link into the given {@param editable} from {@param selectionStart} to {@param selectionEnd} and uses the {@param clipboardUrl} if available.
*
* @return the new cursor position
*/
public static int insertLink(@NonNull Editable editable, int selectionStart, int selectionEnd, @Nullable String clipboardUrl) {
if (selectionStart == selectionEnd) {
editable.insert(selectionStart, "[](" + (clipboardUrl == null ? "" : clipboardUrl) + ")");
return selectionStart + 1;
} else {
final boolean textToFormatIsLink = TextUtils.indexOf(editable.subSequence(selectionStart, selectionEnd), "http") == 0;
if (textToFormatIsLink) {
if (clipboardUrl == null) {
editable.insert(selectionEnd, ")");
editable.insert(selectionStart, "[](");
} else {
editable.insert(selectionEnd, "](" + clipboardUrl + ")");
editable.insert(selectionStart, "[");
selectionEnd += clipboardUrl.length();
}
} else {
if (clipboardUrl == null) {
editable.insert(selectionEnd, "]()");
} else {
editable.insert(selectionEnd, "](" + clipboardUrl + ")");
selectionEnd += clipboardUrl.length();
}
editable.insert(selectionStart, "[");
}
return textToFormatIsLink && clipboardUrl == null
? selectionStart + 1
: selectionEnd + 3;
}
}
/**
* @return whether or not the selection of {@param text} from {@param start} to {@param end} is
* surrounded or not by the given {@param punctuation}.
*/
private static boolean selectionIsSurroundedByPunctuation(@NonNull CharSequence text, int start, int end, @NonNull String punctuation) {
if (text.length() < end + punctuation.length()) {
return false;
}
if (start - punctuation.length() < 0 || end + punctuation.length() > text.length()) {
return false;
}
return punctuation.contentEquals(text.subSequence(start - punctuation.length(), start))
&& punctuation.contentEquals(text.subSequence(end, end + punctuation.length()));
}
private static int getContainedPunctuationCount(@NonNull CharSequence text, int start, int end, @NonNull String punctuation) {
final Matcher matcher = Pattern.compile(Pattern.quote(punctuation)).matcher(text.subSequence(start, end));
int counter = 0;
while (matcher.find()) {
counter++;
}
return counter;
}
private static void removeContainingPunctuation(@NonNull Editable editable, int start, int end, @NonNull String punctuation) {
final Matcher matcher = Pattern.compile(Pattern.quote(punctuation)).matcher(editable.subSequence(start, end));
int countDeletedPunctuations = 0;
while (matcher.find()) {
editable.delete(start + matcher.start() - countDeletedPunctuations * punctuation.length(), start + matcher.end() - countDeletedPunctuations * punctuation.length());
countDeletedPunctuations++;
}
}
public static boolean selectionIsInLink(@NonNull CharSequence text, int start, int end) {
final Matcher matcher = PATTERN_MARKDOWN_LINK.matcher(text);
while (matcher.find()) {
if ((start >= matcher.start() && start < matcher.end()) || (end > matcher.start() && end <= matcher.end())) {
return true;
}
}
return false;
}
public static void searchAndColor(@NonNull Spannable editable, @Nullable CharSequence searchText, @NonNull Context context, @Nullable Integer current, @ColorInt int mainColor) {
if (searchText != null) {
final Matcher m = Pattern
.compile(searchText.toString(), Pattern.CASE_INSENSITIVE | Pattern.LITERAL)
.matcher(editable);
int i = 1;
while (m.find()) {
int start = m.start();
int end = m.end();
editable.setSpan(new SearchSpan(context, mainColor, (current != null && i == current)), start, end, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
i++;
}
}
}
public static <T> void removeSpans(@NonNull Spannable spannable, @SuppressWarnings("SameParameterValue") Class<T> spanType) {
for (T span : spannable.getSpans(0, spannable.length(), spanType)) {
spannable.removeSpan(span);
}
}
/**
* @return When the content of the {@param textView} is already of type {@link Spannable}, it will cast and return it directly.
* Otherwise it will create a new {@link SpannableString} from the content, set this as new content of the {@param textView} and return it.
*/
public static Spannable getContentAsSpannable(@NonNull TextView textView) {
final CharSequence content = textView.getText();
if (content.getClass() == SpannableString.class || content instanceof Spannable) {
return (Spannable) content;
} else {
Log.w(TAG, "Expected " + TextView.class.getSimpleName() + " content to be of type " + Spannable.class.getSimpleName() + ", but was of type " + content.getClass() + ". Search highlighting will be not performant.");
final Spannable spannableContent = new SpannableString(content);
textView.setText(spannableContent, TextView.BufferType.SPANNABLE);
return spannableContent;
}
}
}

View file

@ -1,6 +1,7 @@
package it.niedermann.android.markdown.markwon;
import android.content.Context;
import android.content.res.Configuration;
import android.text.TextUtils;
import android.util.AttributeSet;
import android.util.Log;
@ -19,18 +20,36 @@ import java.util.function.Function;
import io.noties.markwon.Markwon;
import io.noties.markwon.MarkwonPlugin;
import io.noties.markwon.ext.strikethrough.StrikethroughPlugin;
import io.noties.markwon.ext.tables.TablePlugin;
import io.noties.markwon.ext.tasklist.TaskListPlugin;
import io.noties.markwon.image.ImagesPlugin;
import io.noties.markwon.inlineparser.MarkwonInlineParserPlugin;
import io.noties.markwon.linkify.LinkifyPlugin;
import io.noties.markwon.simple.ext.SimpleExtPlugin;
import io.noties.markwon.syntax.Prism4jTheme;
import io.noties.markwon.syntax.Prism4jThemeDarkula;
import io.noties.markwon.syntax.Prism4jThemeDefault;
import io.noties.markwon.syntax.SyntaxHighlightPlugin;
import io.noties.prism4j.Prism4j;
import io.noties.prism4j.annotations.PrismBundle;
import it.niedermann.android.markdown.MarkdownEditor;
import it.niedermann.android.markdown.MarkdownUtil;
import it.niedermann.android.markdown.markwon.plugins.LinkClickInterceptorPlugin;
import it.niedermann.android.markdown.markwon.plugins.NextcloudMentionsPlugin;
import it.niedermann.android.markdown.markwon.plugins.SearchHighlightPlugin;
import it.niedermann.android.markdown.markwon.plugins.ThemePlugin;
import it.niedermann.android.markdown.markwon.plugins.ToggleableTaskListPlugin;
import static androidx.lifecycle.Transformations.distinctUntilChanged;
import static it.niedermann.android.markdown.markwon.MarkwonMarkdownUtil.initMarkwonViewer;
@PrismBundle(includeAll = true, grammarLocatorClassName = ".MarkwonGrammarLocator")
public class MarkwonMarkdownViewer extends AppCompatTextView implements MarkdownEditor {
private static final String TAG = MarkwonMarkdownViewer.class.getSimpleName();
private static final Prism4j prism4j = new Prism4j(new MarkwonGrammarLocator());
private Markwon markwon;
private final MutableLiveData<CharSequence> unrenderedText$ = new MutableLiveData<>();
@ -46,27 +65,60 @@ public class MarkwonMarkdownViewer extends AppCompatTextView implements Markdown
public MarkwonMarkdownViewer(@NonNull Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
this.markwon = initMarkwonViewer(context)
this.markwon = createMarkwonBuilder(context).build();
this.renderService = Executors.newSingleThreadExecutor();
}
private Markwon.Builder createMarkwonBuilder(@NonNull Context context) {
final Prism4jTheme prism4jTheme = MarkwonMarkdownUtil.isDarkThemeActive(context)
? Prism4jThemeDarkula.create()
: Prism4jThemeDefault.create();
return Markwon.builder(context)
.usePlugin(ThemePlugin.create(context))
.usePlugin(StrikethroughPlugin.create())
.usePlugin(SimpleExtPlugin.create())
.usePlugin(ImagesPlugin.create())
.usePlugin(MarkwonInlineParserPlugin.create())
.usePlugin(SearchHighlightPlugin.create(context))
.usePlugin(TablePlugin.create(context))
.usePlugin(TaskListPlugin.create(context))
.usePlugin(LinkifyPlugin.create(true))
.usePlugin(LinkClickInterceptorPlugin.create())
.usePlugin(ImagesPlugin.create())
.usePlugin(SyntaxHighlightPlugin.create(prism4j, prism4jTheme))
.usePlugin(new ToggleableTaskListPlugin((toggledCheckboxPosition, newCheckedState) -> {
final CharSequence oldUnrenderedText = unrenderedText$.getValue();
if (oldUnrenderedText == null) {
throw new IllegalStateException("Checkbox #" + toggledCheckboxPosition + ", but unrenderedText$ value is null.");
}
final CharSequence newUnrenderedText = MarkwonMarkdownUtil.setCheckboxStatus(oldUnrenderedText.toString(), toggledCheckboxPosition, newCheckedState);
final CharSequence newUnrenderedText = MarkdownUtil.setCheckboxStatus(oldUnrenderedText.toString(), toggledCheckboxPosition, newCheckedState);
this.setMarkdownString(newUnrenderedText);
}))
.build();
this.renderService = Executors.newSingleThreadExecutor();
}));
}
public Markwon.Builder createMarkwonBuilder(@NonNull Context context, @NonNull Map<String, String> mentions) {
return createMarkwonBuilder(context)
.usePlugin(NextcloudMentionsPlugin.create(context, mentions));
}
@Override
public void registerOnLinkClickCallback(@NonNull Function<String, Boolean> callback) {
this.markwon.getPlugin(LinkClickInterceptorPlugin.class).registerOnLinkClickCallback(callback);
final LinkClickInterceptorPlugin plugin = this.markwon.getPlugin(LinkClickInterceptorPlugin.class);
if (plugin == null) {
Log.w(TAG, "Tried to register callback, but " + LinkClickInterceptorPlugin.class.getSimpleName() + " is not a registered " + MarkwonPlugin.class.getSimpleName() + ".");
} else {
plugin.registerOnLinkClickCallback(callback);
}
}
@Override
public void setEnabled(boolean enabled) {
this.markwon.getPlugin(ToggleableTaskListPlugin.class).setEnabled(enabled);
final ToggleableTaskListPlugin plugin = this.markwon.getPlugin(ToggleableTaskListPlugin.class);
if (plugin == null) {
Log.w(TAG, "Tried to set enabled state for " + ToggleableTaskListPlugin.class.getSimpleName() + ", but " + ToggleableTaskListPlugin.class.getSimpleName() + " is not a registered " + MarkwonPlugin.class.getSimpleName() + ".");
} else {
plugin.setEnabled(enabled);
}
}
@Override
@ -104,7 +156,7 @@ public class MarkwonMarkdownViewer extends AppCompatTextView implements Markdown
@Override
public void setMarkdownString(CharSequence text, @NonNull Map<String, String> mentions) {
this.markwon = initMarkwonViewer(getContext(), mentions).build();
this.markwon = createMarkwonBuilder(getContext(), mentions).build();
setMarkdownString(text);
}

View file

@ -8,13 +8,13 @@ import android.view.MenuItem;
import it.niedermann.android.markdown.R;
import it.niedermann.android.markdown.markwon.MarkwonMarkdownEditor;
import it.niedermann.android.markdown.markwon.MarkwonMarkdownUtil;
import it.niedermann.android.markdown.markwon.model.EListType;
import it.niedermann.android.markdown.MarkdownUtil;
import it.niedermann.android.markdown.model.EListType;
import it.niedermann.android.util.ClipboardUtil;
import static it.niedermann.android.markdown.markwon.MarkwonMarkdownUtil.getEndOfLine;
import static it.niedermann.android.markdown.markwon.MarkwonMarkdownUtil.getStartOfLine;
import static it.niedermann.android.markdown.markwon.MarkwonMarkdownUtil.lineStartsWithCheckbox;
import static it.niedermann.android.markdown.MarkdownUtil.getEndOfLine;
import static it.niedermann.android.markdown.MarkdownUtil.getStartOfLine;
import static it.niedermann.android.markdown.MarkdownUtil.lineStartsWithCheckbox;
public class ContextBasedFormattingCallback implements ActionMode.Callback {
@ -65,7 +65,7 @@ public class ContextBasedFormattingCallback implements ActionMode.Callback {
editText.setSelection(cursorPosition + EListType.DASH.checkboxUncheckedWithTrailingSpace.length());
return true;
} else if (itemId == R.id.link) {
final int newSelection = MarkwonMarkdownUtil.insertLink(editable, cursorPosition, cursorPosition, ClipboardUtil.INSTANCE.getClipboardURLorNull(editText.getContext()));
final int newSelection = MarkdownUtil.insertLink(editable, cursorPosition, cursorPosition, ClipboardUtil.INSTANCE.getClipboardURLorNull(editText.getContext()));
editText.setMarkdownStringModel(editable);
editText.setSelection(newSelection);
return true;

View file

@ -12,7 +12,7 @@ import android.view.MenuItem;
import it.niedermann.android.markdown.R;
import it.niedermann.android.markdown.markwon.MarkwonMarkdownEditor;
import it.niedermann.android.markdown.markwon.MarkwonMarkdownUtil;
import it.niedermann.android.markdown.MarkdownUtil;
import it.niedermann.android.util.ClipboardUtil;
public class ContextBasedRangeFormattingCallback implements ActionMode.Callback {
@ -55,7 +55,7 @@ public class ContextBasedRangeFormattingCallback implements ActionMode.Callback
final int selectionStart = editText.getSelectionStart();
final int selectionEnd = editText.getSelectionEnd();
if (selectionStart >= 0 && selectionStart <= text.length()) {
if (MarkwonMarkdownUtil.selectionIsInLink(text, selectionStart, selectionEnd)) {
if (MarkdownUtil.selectionIsInLink(text, selectionStart, selectionEnd)) {
menu.findItem(R.id.link).setVisible(false);
Log.i(TAG, "Hide link menu item because the selection is already within a link.");
}
@ -75,17 +75,17 @@ public class ContextBasedRangeFormattingCallback implements ActionMode.Callback
final int end = editText.getSelectionEnd();
if (itemId == R.id.bold) {
final int newSelection = MarkwonMarkdownUtil.togglePunctuation(editable, start, end, "**");
final int newSelection = MarkdownUtil.togglePunctuation(editable, start, end, "**");
editText.setMarkdownStringModel(editable);
editText.setSelection(newSelection);
return true;
} else if (itemId == R.id.italic) {
final int newSelection = MarkwonMarkdownUtil.togglePunctuation(editable, start, end, "*");
final int newSelection = MarkdownUtil.togglePunctuation(editable, start, end, "*");
editText.setMarkdownStringModel(editable);
editText.setSelection(newSelection);
return true;
} else if (itemId == R.id.link) {
final int newSelection = MarkwonMarkdownUtil.insertLink(editable, start, end, ClipboardUtil.INSTANCE.getClipboardURLorNull(editText.getContext()));
final int newSelection = MarkdownUtil.insertLink(editable, start, end, ClipboardUtil.INSTANCE.getClipboardURLorNull(editText.getContext()));
editText.setMarkdownStringModel(editable);
editText.setSelection(newSelection);
return true;

View file

@ -15,7 +15,7 @@ import io.noties.markwon.MarkwonPlugin;
import it.niedermann.android.markdown.markwon.span.InterceptedURLSpan;
import static android.text.Spanned.SPAN_EXCLUSIVE_EXCLUSIVE;
import static it.niedermann.android.markdown.markwon.MarkwonMarkdownUtil.getContentAsSpannable;
import static it.niedermann.android.markdown.MarkdownUtil.getContentAsSpannable;
public class LinkClickInterceptorPlugin extends AbstractMarkwonPlugin {

View file

@ -12,21 +12,28 @@ import androidx.core.content.ContextCompat;
import io.noties.markwon.AbstractMarkwonPlugin;
import io.noties.markwon.MarkwonPlugin;
import it.niedermann.android.markdown.MarkdownUtil;
import it.niedermann.android.markdown.R;
import it.niedermann.android.markdown.markwon.MarkwonMarkdownUtil;
import it.niedermann.android.markdown.markwon.span.SearchSpan;
import it.niedermann.android.markdown.model.SearchSpan;
import static it.niedermann.android.markdown.markwon.MarkwonMarkdownUtil.getContentAsSpannable;
import static it.niedermann.android.markdown.MarkdownUtil.getContentAsSpannable;
public class SearchHighlightPlugin extends AbstractMarkwonPlugin {
@Nullable
private CharSequence searchText = null;
private Integer current;
@ColorInt
private int color;
@ColorInt
private final int highlightColor;
private final boolean darkTheme;
public SearchHighlightPlugin(@NonNull Context context) {
color = ContextCompat.getColor(context, R.color.search_color);
this.color = ContextCompat.getColor(context, R.color.search_color);
this.highlightColor = ContextCompat.getColor(context, R.color.bg_highlighted);
this.darkTheme = MarkwonMarkdownUtil.isDarkThemeActive(context);
}
public static MarkwonPlugin create(@NonNull Context context) {
@ -35,7 +42,7 @@ public class SearchHighlightPlugin extends AbstractMarkwonPlugin {
public void setSearchText(@Nullable CharSequence searchText, @Nullable Integer current, @NonNull TextView textView) {
this.current = current;
MarkwonMarkdownUtil.removeSpans(getContentAsSpannable(textView), SearchSpan.class);
MarkdownUtil.removeSpans(getContentAsSpannable(textView), SearchSpan.class);
if (TextUtils.isEmpty(searchText)) {
this.searchText = null;
} else {
@ -54,7 +61,7 @@ public class SearchHighlightPlugin extends AbstractMarkwonPlugin {
super.afterSetText(textView);
if (this.searchText != null) {
final Spannable spannable = getContentAsSpannable(textView);
MarkwonMarkdownUtil.searchAndColor(spannable, searchText, textView.getContext(), current, color);
MarkdownUtil.searchAndColor(spannable, searchText, current, color, highlightColor, darkTheme);
}
}
}

View file

@ -6,12 +6,12 @@ import android.text.TextWatcher;
import androidx.annotation.NonNull;
import it.niedermann.android.markdown.markwon.MarkwonMarkdownEditor;
import it.niedermann.android.markdown.markwon.model.EListType;
import it.niedermann.android.markdown.model.EListType;
import static it.niedermann.android.markdown.markwon.MarkwonMarkdownUtil.getListItemIfIsEmpty;
import static it.niedermann.android.markdown.markwon.MarkwonMarkdownUtil.getOrderedListNumber;
import static it.niedermann.android.markdown.markwon.MarkwonMarkdownUtil.getStartOfLine;
import static it.niedermann.android.markdown.markwon.MarkwonMarkdownUtil.lineStartsWithCheckbox;
import static it.niedermann.android.markdown.MarkdownUtil.getListItemIfIsEmpty;
import static it.niedermann.android.markdown.MarkdownUtil.getOrderedListNumber;
import static it.niedermann.android.markdown.MarkdownUtil.getStartOfLine;
import static it.niedermann.android.markdown.MarkdownUtil.lineStartsWithCheckbox;
/**
* Automatically continues lists and checkbox lists when pressing enter

View file

@ -1,5 +1,6 @@
package it.niedermann.android.markdown.markwon.textwatcher;
import android.content.Context;
import android.text.Editable;
import android.text.TextUtils;
import android.text.TextWatcher;
@ -9,10 +10,11 @@ import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.core.content.ContextCompat;
import it.niedermann.android.markdown.MarkdownUtil;
import it.niedermann.android.markdown.R;
import it.niedermann.android.markdown.markwon.MarkwonMarkdownEditor;
import it.niedermann.android.markdown.markwon.MarkwonMarkdownUtil;
import it.niedermann.android.markdown.markwon.span.SearchSpan;
import it.niedermann.android.markdown.model.SearchSpan;
public class SearchHighlightTextWatcher extends InterceptorTextWatcher {
@ -20,19 +22,29 @@ public class SearchHighlightTextWatcher extends InterceptorTextWatcher {
@Nullable
private CharSequence searchText;
private Integer current;
@ColorInt
private int color;
@ColorInt
private final int highlightColor;
private final boolean darkTheme;
public SearchHighlightTextWatcher(@NonNull TextWatcher originalWatcher, @NonNull MarkwonMarkdownEditor editText) {
super(originalWatcher);
this.editText = editText;
this.color = ContextCompat.getColor(editText.getContext(), R.color.search_color);
final Context context = editText.getContext();
this.color = ContextCompat.getColor(context, R.color.search_color);
this.highlightColor = ContextCompat.getColor(context, R.color.bg_highlighted);
this.darkTheme = MarkwonMarkdownUtil.isDarkThemeActive(context);
}
public void setSearchText(@Nullable CharSequence searchText, @Nullable Integer current) {
this.current = current;
if (TextUtils.isEmpty(searchText)) {
this.searchText = null;
MarkwonMarkdownUtil.removeSpans(editText.getText(), SearchSpan.class);
final Editable text = editText.getText();
if (text != null) {
MarkdownUtil.removeSpans(text, SearchSpan.class);
}
} else {
this.searchText = searchText;
afterTextChanged(editText.getText());
@ -48,8 +60,8 @@ public class SearchHighlightTextWatcher extends InterceptorTextWatcher {
public void afterTextChanged(Editable s) {
originalWatcher.afterTextChanged(s);
if (searchText != null) {
MarkwonMarkdownUtil.removeSpans(s, SearchSpan.class);
MarkwonMarkdownUtil.searchAndColor(s, searchText, editText.getContext(), current, color);
MarkdownUtil.removeSpans(s, SearchSpan.class);
MarkdownUtil.searchAndColor(s, searchText, current, color, highlightColor, darkTheme);
}
}
}

View file

@ -1,4 +1,4 @@
package it.niedermann.android.markdown.markwon.model;
package it.niedermann.android.markdown.model;
public enum EListType {
STAR('*'),

View file

@ -1,7 +1,5 @@
package it.niedermann.android.markdown.markwon.span;
package it.niedermann.android.markdown.model;
import android.content.Context;
import android.content.res.Configuration;
import android.graphics.Color;
import android.text.TextPaint;
import android.text.style.MetricAffectingSpan;
@ -9,30 +7,28 @@ import android.text.style.MetricAffectingSpan;
import androidx.annotation.ColorInt;
import androidx.annotation.NonNull;
import it.niedermann.android.markdown.R;
import it.niedermann.android.util.ColorUtil;
public class SearchSpan extends MetricAffectingSpan {
private final boolean current;
@NonNull
Context context;
@ColorInt
private final int mainColor;
@ColorInt
private final int highlightColor;
private final boolean darkTheme;
public SearchSpan(@NonNull Context context, @ColorInt int mainColor, boolean current) {
this.context = context;
public SearchSpan(@ColorInt int mainColor, @ColorInt int highlightColor, boolean current, boolean darkTheme) {
this.mainColor = mainColor;
this.current = current;
this.highlightColor = context.getResources().getColor(R.color.bg_highlighted);
this.highlightColor = highlightColor;
this.darkTheme = darkTheme;
}
@Override
public void updateDrawState(TextPaint tp) {
if (current) {
if (isDarkThemeActive(context)) {
if (darkTheme) {
if (ColorUtil.INSTANCE.isColorDark(mainColor)) {
tp.bgColor = Color.WHITE;
tp.setColor(mainColor);
@ -58,7 +54,7 @@ public class SearchSpan extends MetricAffectingSpan {
if (ColorUtil.INSTANCE.getContrastRatio(mainColor, highlightColor) > 3d) {
tp.setColor(mainColor);
} else {
if (isDarkThemeActive(context)) {
if (darkTheme) {
tp.setColor(Color.WHITE);
} else {
tp.setColor(Color.BLACK);
@ -72,9 +68,4 @@ public class SearchSpan extends MetricAffectingSpan {
public void updateMeasureState(@NonNull TextPaint tp) {
tp.setFakeBoldText(true);
}
private static boolean isDarkThemeActive(Context context) {
int uiMode = context.getResources().getConfiguration().uiMode;
return (uiMode & Configuration.UI_MODE_NIGHT_MASK) == Configuration.UI_MODE_NIGHT_YES;
}
}

View file

@ -1,72 +0,0 @@
package it.niedermann.android.markdown.rxmarkdown;
import android.content.Context;
import android.text.Editable;
import android.text.TextWatcher;
import android.util.AttributeSet;
import androidx.lifecycle.LiveData;
import androidx.lifecycle.MutableLiveData;
import com.yydcdut.markdown.MarkdownEditText;
import com.yydcdut.markdown.MarkdownProcessor;
import com.yydcdut.markdown.syntax.edit.EditFactory;
import it.niedermann.android.markdown.MarkdownEditor;
import static androidx.lifecycle.Transformations.distinctUntilChanged;
@Deprecated
public class RxMarkdownEditor extends MarkdownEditText implements MarkdownEditor {
private final MutableLiveData<CharSequence> unrenderedText$ = new MutableLiveData<>();
private MarkdownProcessor markdownProcessor;
public RxMarkdownEditor(Context context) {
super(context);
init(context);
}
public RxMarkdownEditor(Context context, AttributeSet attrs) {
super(context, attrs);
init(context);
}
public RxMarkdownEditor(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
init(context);
}
private void init(Context context) {
markdownProcessor = new MarkdownProcessor(context);
markdownProcessor.config(RxMarkdownUtil.getMarkDownConfiguration(context).build());
markdownProcessor.factory(EditFactory.create());
markdownProcessor.live(this);
addTextChangedListener(new TextWatcher() {
@Override
public void beforeTextChanged(CharSequence s, int start, int count, int after) {
}
@Override
public void onTextChanged(CharSequence s, int start, int before, int count) {
unrenderedText$.setValue(s.toString());
}
@Override
public void afterTextChanged(Editable s) {
}
});
}
@Override
public void setMarkdownString(CharSequence text) {
setText(markdownProcessor.parse(text));
}
@Override
public LiveData<CharSequence> getMarkdownString() {
return distinctUntilChanged(unrenderedText$);
}
}

View file

@ -1,44 +0,0 @@
package it.niedermann.android.markdown.rxmarkdown;
import android.content.Context;
import androidx.annotation.RestrictTo;
import com.yydcdut.rxmarkdown.RxMDConfiguration.Builder;
/**
* Created by stefan on 07.12.16.
*/
@Deprecated
@RestrictTo(value = RestrictTo.Scope.LIBRARY)
public class RxMarkdownUtil {
private RxMarkdownUtil() {
}
/**
* Ensures every instance of RxMD uses the same configuration
*
* @param context Context
* @return RxMDConfiguration
*/
public static Builder getMarkDownConfiguration(Context context) {
return new Builder(context)
.setHeader2RelativeSize(1.35f)
.setHeader3RelativeSize(1.25f)
.setHeader4RelativeSize(1.15f)
.setHeader5RelativeSize(1.1f)
.setHeader6RelativeSize(1.05f)
.setHorizontalRulesHeight(2);
}
public static Builder getMarkDownConfiguration(Context context, Boolean darkTheme) {
return new Builder(context)
.setHeader2RelativeSize(1.35f)
.setHeader3RelativeSize(1.25f)
.setHeader4RelativeSize(1.15f)
.setHeader5RelativeSize(1.1f)
.setHeader6RelativeSize(1.05f)
.setHorizontalRulesHeight(2);
}
}

View file

@ -1,68 +0,0 @@
package it.niedermann.android.markdown.rxmarkdown;
import android.content.Context;
import android.util.AttributeSet;
import androidx.annotation.NonNull;
import androidx.lifecycle.LiveData;
import androidx.lifecycle.MutableLiveData;
import com.nextcloud.android.sso.exceptions.NextcloudFilesAppAccountNotFoundException;
import com.nextcloud.android.sso.exceptions.NoCurrentAccountSelectedException;
import com.nextcloud.android.sso.helper.SingleAccountHelper;
import com.yydcdut.markdown.MarkdownProcessor;
import com.yydcdut.markdown.MarkdownTextView;
import com.yydcdut.markdown.syntax.text.TextFactory;
import java.util.Map;
import it.niedermann.android.markdown.MarkdownEditor;
import static it.niedermann.android.markdown.MentionUtil.setupMentions;
@Deprecated
public class RxMarkdownViewer extends MarkdownTextView implements MarkdownEditor {
private MarkdownProcessor markdownProcessor;
public RxMarkdownViewer(Context context) {
super(context);
init(context);
}
public RxMarkdownViewer(Context context, AttributeSet attrs) {
super(context, attrs);
init(context);
}
public RxMarkdownViewer(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
init(context);
}
private void init(Context context) {
markdownProcessor = new MarkdownProcessor(context);
markdownProcessor.config(RxMarkdownUtil.getMarkDownConfiguration(context).build());
markdownProcessor.factory(TextFactory.create());
}
@Override
public void setMarkdownString(CharSequence text) {
setText(markdownProcessor.parse(text));
}
@Override
public LiveData<CharSequence> getMarkdownString() {
return new MutableLiveData<>();
}
@Override
public void setMarkdownString(CharSequence text, @NonNull Map<String, String> mentions) {
try {
setMarkdownString(text);
setupMentions(SingleAccountHelper.getCurrentSingleSignOnAccount(getContext()), mentions, this);
} catch (NextcloudFilesAppAccountNotFoundException | NoCurrentAccountSelectedException e) {
e.printStackTrace();
}
}
}