diff --git a/markdown/src/androidTest/java/it/niedermann/android/markdown/MarkwonMarkdownUtilTest.java b/markdown/src/androidTest/java/it/niedermann/android/markdown/MarkdownUtilTest.java similarity index 77% rename from markdown/src/androidTest/java/it/niedermann/android/markdown/MarkwonMarkdownUtilTest.java rename to markdown/src/androidTest/java/it/niedermann/android/markdown/MarkdownUtilTest.java index dbbcc4e0..4fa3d2b9 100644 --- a/markdown/src/androidTest/java/it/niedermann/android/markdown/MarkwonMarkdownUtilTest.java +++ b/markdown/src/androidTest/java/it/niedermann/android/markdown/MarkdownUtilTest.java @@ -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); diff --git a/markdown/src/main/java/it/niedermann/android/markdown/MarkdownUtil.java b/markdown/src/main/java/it/niedermann/android/markdown/MarkdownUtil.java index b6d04b7d..e6d2c90e 100644 --- a/markdown/src/main/java/it/niedermann/android/markdown/MarkdownUtil.java +++ b/markdown/src/main/java/it/niedermann/android/markdown/MarkdownUtil.java @@ -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 void removeSpans(@NonNull Spannable spannable, @SuppressWarnings("SameParameterValue") Class 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; + } + } } diff --git a/markdown/src/main/java/it/niedermann/android/markdown/markwon/MarkwonMarkdownEditor.java b/markdown/src/main/java/it/niedermann/android/markdown/markwon/MarkwonMarkdownEditor.java index 1100da77..11d98e4e 100644 --- a/markdown/src/main/java/it/niedermann/android/markdown/markwon/MarkwonMarkdownEditor.java +++ b/markdown/src/main/java/it/niedermann/android/markdown/markwon/MarkwonMarkdownEditor.java @@ -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); diff --git a/markdown/src/main/java/it/niedermann/android/markdown/markwon/MarkwonMarkdownUtil.java b/markdown/src/main/java/it/niedermann/android/markdown/markwon/MarkwonMarkdownUtil.java index f2863ebc..a49b0691 100644 --- a/markdown/src/main/java/it/niedermann/android/markdown/markwon/MarkwonMarkdownUtil.java +++ b/markdown/src/main/java/it/niedermann/android/markdown/markwon/MarkwonMarkdownUtil.java @@ -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 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 void removeSpans(@NonNull Spannable spannable, @SuppressWarnings("SameParameterValue") Class 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; - } - } } diff --git a/markdown/src/main/java/it/niedermann/android/markdown/markwon/MarkwonMarkdownViewer.java b/markdown/src/main/java/it/niedermann/android/markdown/markwon/MarkwonMarkdownViewer.java index 019e5576..3340a7f4 100644 --- a/markdown/src/main/java/it/niedermann/android/markdown/markwon/MarkwonMarkdownViewer.java +++ b/markdown/src/main/java/it/niedermann/android/markdown/markwon/MarkwonMarkdownViewer.java @@ -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 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 mentions) { + return createMarkwonBuilder(context) + .usePlugin(NextcloudMentionsPlugin.create(context, mentions)); } @Override public void registerOnLinkClickCallback(@NonNull Function 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 mentions) { - this.markwon = initMarkwonViewer(getContext(), mentions).build(); + this.markwon = createMarkwonBuilder(getContext(), mentions).build(); setMarkdownString(text); } diff --git a/markdown/src/main/java/it/niedermann/android/markdown/markwon/format/ContextBasedFormattingCallback.java b/markdown/src/main/java/it/niedermann/android/markdown/markwon/format/ContextBasedFormattingCallback.java index c4469bc3..37d81554 100644 --- a/markdown/src/main/java/it/niedermann/android/markdown/markwon/format/ContextBasedFormattingCallback.java +++ b/markdown/src/main/java/it/niedermann/android/markdown/markwon/format/ContextBasedFormattingCallback.java @@ -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; diff --git a/markdown/src/main/java/it/niedermann/android/markdown/markwon/format/ContextBasedRangeFormattingCallback.java b/markdown/src/main/java/it/niedermann/android/markdown/markwon/format/ContextBasedRangeFormattingCallback.java index 86ce5cdb..f3f90be4 100644 --- a/markdown/src/main/java/it/niedermann/android/markdown/markwon/format/ContextBasedRangeFormattingCallback.java +++ b/markdown/src/main/java/it/niedermann/android/markdown/markwon/format/ContextBasedRangeFormattingCallback.java @@ -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; diff --git a/markdown/src/main/java/it/niedermann/android/markdown/markwon/plugins/LinkClickInterceptorPlugin.java b/markdown/src/main/java/it/niedermann/android/markdown/markwon/plugins/LinkClickInterceptorPlugin.java index 8896a2a3..e815b54b 100644 --- a/markdown/src/main/java/it/niedermann/android/markdown/markwon/plugins/LinkClickInterceptorPlugin.java +++ b/markdown/src/main/java/it/niedermann/android/markdown/markwon/plugins/LinkClickInterceptorPlugin.java @@ -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 { diff --git a/markdown/src/main/java/it/niedermann/android/markdown/markwon/plugins/SearchHighlightPlugin.java b/markdown/src/main/java/it/niedermann/android/markdown/markwon/plugins/SearchHighlightPlugin.java index 53d4562e..b48db3dd 100644 --- a/markdown/src/main/java/it/niedermann/android/markdown/markwon/plugins/SearchHighlightPlugin.java +++ b/markdown/src/main/java/it/niedermann/android/markdown/markwon/plugins/SearchHighlightPlugin.java @@ -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); } } } diff --git a/markdown/src/main/java/it/niedermann/android/markdown/markwon/textwatcher/AutoContinuationTextWatcher.java b/markdown/src/main/java/it/niedermann/android/markdown/markwon/textwatcher/AutoContinuationTextWatcher.java index 44bde738..150a5c0b 100644 --- a/markdown/src/main/java/it/niedermann/android/markdown/markwon/textwatcher/AutoContinuationTextWatcher.java +++ b/markdown/src/main/java/it/niedermann/android/markdown/markwon/textwatcher/AutoContinuationTextWatcher.java @@ -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 diff --git a/markdown/src/main/java/it/niedermann/android/markdown/markwon/textwatcher/SearchHighlightTextWatcher.java b/markdown/src/main/java/it/niedermann/android/markdown/markwon/textwatcher/SearchHighlightTextWatcher.java index 67b630d9..2e226ff5 100644 --- a/markdown/src/main/java/it/niedermann/android/markdown/markwon/textwatcher/SearchHighlightTextWatcher.java +++ b/markdown/src/main/java/it/niedermann/android/markdown/markwon/textwatcher/SearchHighlightTextWatcher.java @@ -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); } } } diff --git a/markdown/src/main/java/it/niedermann/android/markdown/markwon/model/EListType.java b/markdown/src/main/java/it/niedermann/android/markdown/model/EListType.java similarity index 92% rename from markdown/src/main/java/it/niedermann/android/markdown/markwon/model/EListType.java rename to markdown/src/main/java/it/niedermann/android/markdown/model/EListType.java index a03987fd..0bd7d7a9 100644 --- a/markdown/src/main/java/it/niedermann/android/markdown/markwon/model/EListType.java +++ b/markdown/src/main/java/it/niedermann/android/markdown/model/EListType.java @@ -1,4 +1,4 @@ -package it.niedermann.android.markdown.markwon.model; +package it.niedermann.android.markdown.model; public enum EListType { STAR('*'), diff --git a/markdown/src/main/java/it/niedermann/android/markdown/markwon/span/SearchSpan.java b/markdown/src/main/java/it/niedermann/android/markdown/model/SearchSpan.java similarity index 71% rename from markdown/src/main/java/it/niedermann/android/markdown/markwon/span/SearchSpan.java rename to markdown/src/main/java/it/niedermann/android/markdown/model/SearchSpan.java index fa740cf6..b6c049c3 100644 --- a/markdown/src/main/java/it/niedermann/android/markdown/markwon/span/SearchSpan.java +++ b/markdown/src/main/java/it/niedermann/android/markdown/model/SearchSpan.java @@ -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; - } } \ No newline at end of file diff --git a/markdown/src/main/java/it/niedermann/android/markdown/rxmarkdown/RxMarkdownEditor.java b/markdown/src/main/java/it/niedermann/android/markdown/rxmarkdown/RxMarkdownEditor.java deleted file mode 100644 index 1d02d989..00000000 --- a/markdown/src/main/java/it/niedermann/android/markdown/rxmarkdown/RxMarkdownEditor.java +++ /dev/null @@ -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 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 getMarkdownString() { - return distinctUntilChanged(unrenderedText$); - } -} diff --git a/markdown/src/main/java/it/niedermann/android/markdown/rxmarkdown/RxMarkdownUtil.java b/markdown/src/main/java/it/niedermann/android/markdown/rxmarkdown/RxMarkdownUtil.java deleted file mode 100644 index 16c1652a..00000000 --- a/markdown/src/main/java/it/niedermann/android/markdown/rxmarkdown/RxMarkdownUtil.java +++ /dev/null @@ -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); - } -} diff --git a/markdown/src/main/java/it/niedermann/android/markdown/rxmarkdown/RxMarkdownViewer.java b/markdown/src/main/java/it/niedermann/android/markdown/rxmarkdown/RxMarkdownViewer.java deleted file mode 100644 index 3c670d33..00000000 --- a/markdown/src/main/java/it/niedermann/android/markdown/rxmarkdown/RxMarkdownViewer.java +++ /dev/null @@ -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 getMarkdownString() { - return new MutableLiveData<>(); - } - - @Override - public void setMarkdownString(CharSequence text, @NonNull Map mentions) { - try { - setMarkdownString(text); - setupMentions(SingleAccountHelper.getCurrentSingleSignOnAccount(getContext()), mentions, this); - } catch (NextcloudFilesAppAccountNotFoundException | NoCurrentAccountSelectedException e) { - e.printStackTrace(); - } - } -}