From ca9e63b9fe6d2d99461f605052e6aadde3e8f58b Mon Sep 17 00:00:00 2001 From: Dimitry Ivanov Date: Thu, 25 Oct 2018 21:58:15 +0300 Subject: [PATCH 001/103] Version 2.0.1-SNAPSHOT --- gradle.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle.properties b/gradle.properties index 7f10785b..7ef25526 100644 --- a/gradle.properties +++ b/gradle.properties @@ -6,7 +6,7 @@ org.gradle.configureondemand=true android.enableBuildCache=true android.buildCacheDir=build/pre-dex-cache -VERSION_NAME=2.0.0 +VERSION_NAME=2.0.1-SNAPSHOT GROUP=ru.noties POM_DESCRIPTION=Markwon From f78f153f9c488d2abd7c6f47bc33f90b13520595 Mon Sep 17 00:00:00 2001 From: Cyrus Bakhtiari-Haftlang Date: Thu, 25 Oct 2018 20:59:32 +0200 Subject: [PATCH 002/103] Introduced a "copy" builder for SpannableTheme (#72) --- .../markwon/SpannableConfiguration.java | 23 ++++++++ .../renderer/SpannableConfigurationTest.java | 52 +++++++++++++++++++ 2 files changed, 75 insertions(+) create mode 100644 markwon/src/test/java/ru/noties/markwon/renderer/SpannableConfigurationTest.java diff --git a/markwon/src/main/java/ru/noties/markwon/SpannableConfiguration.java b/markwon/src/main/java/ru/noties/markwon/SpannableConfiguration.java index cb2a34bc..5df9d316 100644 --- a/markwon/src/main/java/ru/noties/markwon/SpannableConfiguration.java +++ b/markwon/src/main/java/ru/noties/markwon/SpannableConfiguration.java @@ -51,6 +51,14 @@ public class SpannableConfiguration { this.htmlAllowNonClosedTags = builder.htmlAllowNonClosedTags; } + /** + * Returns a new builder based on this configuration + */ + @NonNull + public Builder newBuilder(@NonNull Context context) { + return new Builder(context, this); + } + @NonNull public SpannableTheme theme() { return theme; @@ -138,6 +146,21 @@ public class SpannableConfiguration { this.context = context; } + Builder(@NonNull Context context, @NonNull SpannableConfiguration configuration) { + this(context); + this.theme = configuration.theme; + this.asyncDrawableLoader = configuration.asyncDrawableLoader; + this.syntaxHighlight = configuration.syntaxHighlight; + this.linkResolver = configuration.linkResolver; + this.urlProcessor = configuration.urlProcessor; + this.imageSizeResolver = configuration.imageSizeResolver; + this.factory = configuration.factory; + this.softBreakAddsNewLine = configuration.softBreakAddsNewLine; + this.htmlParser = configuration.htmlParser; + this.htmlRenderer = configuration.htmlRenderer; + this.htmlAllowNonClosedTags = configuration.htmlAllowNonClosedTags; + } + @NonNull public Builder theme(@NonNull SpannableTheme theme) { this.theme = theme; diff --git a/markwon/src/test/java/ru/noties/markwon/renderer/SpannableConfigurationTest.java b/markwon/src/test/java/ru/noties/markwon/renderer/SpannableConfigurationTest.java new file mode 100644 index 00000000..daa70332 --- /dev/null +++ b/markwon/src/test/java/ru/noties/markwon/renderer/SpannableConfigurationTest.java @@ -0,0 +1,52 @@ +package ru.noties.markwon.renderer; + +import org.junit.Test; + +import ru.noties.markwon.SpannableConfiguration; +import ru.noties.markwon.SpannableFactory; +import ru.noties.markwon.SyntaxHighlight; +import ru.noties.markwon.UrlProcessor; +import ru.noties.markwon.html.api.MarkwonHtmlParser; +import ru.noties.markwon.renderer.html2.MarkwonHtmlRenderer; +import ru.noties.markwon.spans.AsyncDrawable; +import ru.noties.markwon.spans.LinkSpan; +import ru.noties.markwon.spans.SpannableTheme; + +import static org.junit.Assert.assertEquals; +import static org.mockito.Mockito.mock; +public class SpannableConfigurationTest { + + @Test + public void testNewBuilder() { + final SpannableConfiguration configuration = SpannableConfiguration + .builder(null) + .theme(mock(SpannableTheme.class)) + .asyncDrawableLoader(mock(AsyncDrawable.Loader.class)) + .syntaxHighlight(mock(SyntaxHighlight.class)) + .linkResolver(mock(LinkSpan.Resolver.class)) + .urlProcessor(mock(UrlProcessor.class)) + .imageSizeResolver(mock(ImageSizeResolver.class)) + .factory(mock(SpannableFactory.class)) + .softBreakAddsNewLine(true) + .htmlParser(mock(MarkwonHtmlParser.class)) + .htmlRenderer(mock(MarkwonHtmlRenderer.class)) + .htmlAllowNonClosedTags(true) + .build(); + + final SpannableConfiguration newConfiguration = configuration + .newBuilder(null) + .build(); + + assertEquals(configuration.theme(), newConfiguration.theme()); + assertEquals(configuration.asyncDrawableLoader(), newConfiguration.asyncDrawableLoader()); + assertEquals(configuration.syntaxHighlight(), newConfiguration.syntaxHighlight()); + assertEquals(configuration.linkResolver(), newConfiguration.linkResolver()); + assertEquals(configuration.urlProcessor(), newConfiguration.urlProcessor()); + assertEquals(configuration.imageSizeResolver(), newConfiguration.imageSizeResolver()); + assertEquals(configuration.factory(), newConfiguration.factory()); + assertEquals(configuration.softBreakAddsNewLine(), newConfiguration.softBreakAddsNewLine()); + assertEquals(configuration.htmlParser(), newConfiguration.htmlParser()); + assertEquals(configuration.htmlRenderer(), newConfiguration.htmlRenderer()); + assertEquals(configuration.htmlAllowNonClosedTags(), newConfiguration.htmlAllowNonClosedTags()); + } +} From fed3d1fe334b9ab7ca1dbb9fb3a1c8b5c025f3a5 Mon Sep 17 00:00:00 2001 From: Dimitry Ivanov Date: Thu, 25 Oct 2018 22:08:15 +0300 Subject: [PATCH 003/103] Fix DataUri scheme handler in image-loader --- .../markwon/il/DataUriSchemeHandler.java | 10 ++++--- .../markwon/il/DataUriSchemeHandlerTest.java | 27 +++++++++++++++++++ 2 files changed, 34 insertions(+), 3 deletions(-) diff --git a/markwon-image-loader/src/main/java/ru/noties/markwon/il/DataUriSchemeHandler.java b/markwon-image-loader/src/main/java/ru/noties/markwon/il/DataUriSchemeHandler.java index 73f415af..260cc5b6 100644 --- a/markwon-image-loader/src/main/java/ru/noties/markwon/il/DataUriSchemeHandler.java +++ b/markwon-image-loader/src/main/java/ru/noties/markwon/il/DataUriSchemeHandler.java @@ -3,7 +3,6 @@ package ru.noties.markwon.il; import android.net.Uri; import android.support.annotation.NonNull; import android.support.annotation.Nullable; -import android.text.TextUtils; import java.io.ByteArrayInputStream; import java.util.Collection; @@ -19,7 +18,7 @@ public class DataUriSchemeHandler extends SchemeHandler { return new DataUriSchemeHandler(DataUriParser.create(), DataUriDecoder.create()); } - private static final String START = "data://"; + private static final String START = "data:"; private final DataUriParser uriParser; private final DataUriDecoder uriDecoder; @@ -38,7 +37,12 @@ public class DataUriSchemeHandler extends SchemeHandler { return null; } - final String part = raw.substring(START.length()); + String part = raw.substring(START.length()); + + // this part is added to support `data://` with which this functionality was released + if (part.startsWith("//")) { + part = part.substring(2); + } final DataUri dataUri = uriParser.parse(part); if (dataUri == null) { diff --git a/markwon-image-loader/src/test/java/ru/noties/markwon/il/DataUriSchemeHandlerTest.java b/markwon-image-loader/src/test/java/ru/noties/markwon/il/DataUriSchemeHandlerTest.java index 5274c5fb..1473744a 100644 --- a/markwon-image-loader/src/test/java/ru/noties/markwon/il/DataUriSchemeHandlerTest.java +++ b/markwon-image-loader/src/test/java/ru/noties/markwon/il/DataUriSchemeHandlerTest.java @@ -71,6 +71,33 @@ public class DataUriSchemeHandlerTest { } } + @Test + public void correct_real() { + + final class Item { + + final String contentType; + final String data; + + Item(String contentType, String data) { + this.contentType = contentType; + this.data = data; + } + } + + final Map expected = new HashMap() {{ + put("data:text/plain;,123", new Item("text/plain", "123")); + put("data:image/svg+xml;base64,MTIz", new Item("image/svg+xml", "123")); + }}; + + for (Map.Entry entry : expected.entrySet()) { + final ImageItem item = handler.handle(entry.getKey(), Uri.parse(entry.getKey())); + assertNotNull(entry.getKey(), item); + assertEquals(entry.getKey(), entry.getValue().contentType, item.contentType()); + assertEquals(entry.getKey(), entry.getValue().data, readStream(item.inputStream())); + } + } + @NonNull private static String readStream(@NonNull InputStream stream) { try { From 499b8b47fc8f7428c8aba3675c00466d1f4aeb27 Mon Sep 17 00:00:00 2001 From: Dimitry Ivanov Date: Thu, 25 Oct 2018 22:16:36 +0300 Subject: [PATCH 004/103] Update android gradle plugin to 3.2.1 --- build.gradle | 8 ++++---- .../java/ru/noties/markwon/il/DataUriSchemeHandler.java | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/build.gradle b/build.gradle index a99710f4..d70cf8b5 100644 --- a/build.gradle +++ b/build.gradle @@ -1,10 +1,10 @@ buildscript { repositories { - jcenter() google() + jcenter() } dependencies { - classpath 'com.android.tools.build:gradle:3.0.1' + classpath 'com.android.tools.build:gradle:3.2.1' classpath 'com.github.ben-manes:gradle-versions-plugin:0.20.0' } } @@ -14,8 +14,8 @@ allprojects { if (project.hasProperty('LOCAL_MAVEN_URL')) { maven { url LOCAL_MAVEN_URL } } - jcenter() google() + jcenter() } version = VERSION_NAME group = GROUP @@ -41,7 +41,7 @@ if (hasProperty('local')) { ext { config = [ - 'build-tools' : '27.0.3', + 'build-tools' : '28.0.3', 'compile-sdk' : 27, 'target-sdk' : 27, 'min-sdk' : 16, diff --git a/markwon-image-loader/src/main/java/ru/noties/markwon/il/DataUriSchemeHandler.java b/markwon-image-loader/src/main/java/ru/noties/markwon/il/DataUriSchemeHandler.java index 260cc5b6..c70ea863 100644 --- a/markwon-image-loader/src/main/java/ru/noties/markwon/il/DataUriSchemeHandler.java +++ b/markwon-image-loader/src/main/java/ru/noties/markwon/il/DataUriSchemeHandler.java @@ -38,7 +38,7 @@ public class DataUriSchemeHandler extends SchemeHandler { } String part = raw.substring(START.length()); - + // this part is added to support `data://` with which this functionality was released if (part.startsWith("//")) { part = part.substring(2); From 6dade2bf980a8d262632bddd3de41e8ee205f301 Mon Sep 17 00:00:00 2001 From: Dimitry Ivanov Date: Thu, 25 Oct 2018 22:22:55 +0300 Subject: [PATCH 005/103] Update travis config to use 28.0.3 --- .travis.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index 93df6e80..bd4791e7 100644 --- a/.travis.yml +++ b/.travis.yml @@ -9,7 +9,7 @@ android: - platform-tools - tools - - build-tools-27.0.3 + - build-tools-28.0.3 - android-27 branches: From b9c1107f56d234bb653c8055159505aa6940717c Mon Sep 17 00:00:00 2001 From: Dimitry Ivanov Date: Sun, 4 Nov 2018 13:08:55 +0300 Subject: [PATCH 006/103] Add syntaxHighlight small test --- .../markwon/renderer/SyntaxHighlightTest.java | 72 +++++++++++++++++++ 1 file changed, 72 insertions(+) create mode 100644 markwon/src/test/java/ru/noties/markwon/renderer/SyntaxHighlightTest.java diff --git a/markwon/src/test/java/ru/noties/markwon/renderer/SyntaxHighlightTest.java b/markwon/src/test/java/ru/noties/markwon/renderer/SyntaxHighlightTest.java new file mode 100644 index 00000000..6eefe748 --- /dev/null +++ b/markwon/src/test/java/ru/noties/markwon/renderer/SyntaxHighlightTest.java @@ -0,0 +1,72 @@ +package ru.noties.markwon.renderer; + +import android.content.Context; +import android.support.annotation.NonNull; +import android.support.annotation.Nullable; +import android.text.SpannableStringBuilder; +import android.text.Spanned; + +import org.commonmark.node.FencedCodeBlock; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.robolectric.RobolectricTestRunner; +import org.robolectric.annotation.Config; + +import ru.noties.markwon.SpannableBuilder; +import ru.noties.markwon.SpannableConfiguration; +import ru.noties.markwon.SpannableFactory; +import ru.noties.markwon.SyntaxHighlight; +import ru.noties.markwon.spans.SpannableTheme; + +import static org.junit.Assert.assertEquals; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyBoolean; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +@RunWith(RobolectricTestRunner.class) +@Config(manifest = Config.NONE) +public class SyntaxHighlightTest { + + // codeSpan must be before actual highlight spans (true reverse of builder) + + @Test + public void test() { + + final Object highlightSpan = new Object(); + final Object codeSpan = new Object(); + + final SyntaxHighlight highlight = new SyntaxHighlight() { + @NonNull + @Override + public CharSequence highlight(@Nullable String info, @NonNull String code) { + final SpannableStringBuilder builder = new SpannableStringBuilder(code); + builder.setSpan(highlightSpan, 0, builder.length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); + return builder; + } + }; + + final SpannableFactory factory = mock(SpannableFactory.class); + when(factory.code(any(SpannableTheme.class), anyBoolean())).thenReturn(codeSpan); + + final SpannableConfiguration configuration = SpannableConfiguration.builder(mock(Context.class)) + .syntaxHighlight(highlight) + .factory(factory) + .theme(mock(SpannableTheme.class)) + .build(); + + final SpannableBuilder builder = new SpannableBuilder(); + + final SpannableMarkdownVisitor visitor = new SpannableMarkdownVisitor(configuration, builder); + final FencedCodeBlock fencedCodeBlock = new FencedCodeBlock(); + fencedCodeBlock.setLiteral("{code}"); + + visitor.visit(fencedCodeBlock); + + final Object[] spans = builder.spannableStringBuilder().getSpans(0, builder.length(), Object.class); + + assertEquals(2, spans.length); + assertEquals(codeSpan, spans[0]); + assertEquals(highlightSpan, spans[1]); + } +} From 3eea327671afa0c8f1bd78abb30926df6e5b440b Mon Sep 17 00:00:00 2001 From: Dimitry Ivanov Date: Sun, 4 Nov 2018 13:44:09 +0300 Subject: [PATCH 007/103] Add travis CI build status badge --- README.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/README.md b/README.md index 7545d2b4..cc471fdd 100644 --- a/README.md +++ b/README.md @@ -7,6 +7,9 @@ [![markwon-syntax-highlight](https://img.shields.io/maven-central/v/ru.noties/markwon-syntax-highlight.svg?label=markwon-syntax-highlight)](http://search.maven.org/#search|ga|1|g%3A%22ru.noties%22%20AND%20a%3A%22markwon-syntax-highlight%22) [![markwon-view](https://img.shields.io/maven-central/v/ru.noties/markwon-view.svg?label=markwon-view)](http://search.maven.org/#search|ga|1|g%3A%22ru.noties%22%20AND%20a%3A%22markwon-view%22) +[![Build Status](https://travis-ci.org/noties/Markwon.svg?branch=master)](https://travis-ci.org/noties/Markwon) +[![Build Status](https://travis-ci.org/noties/Markwon.svg?branch=master)](https://travis-ci.org/noties/Markwon) + **Markwon** is a markdown library for Android. It parses markdown following [commonmark-spec] with the help of amazing [commonmark-java] library and renders result as _Android-native_ Spannables. **No HTML** From eb232037a12f5a280e0e4b9c429c6727590b0df5 Mon Sep 17 00:00:00 2001 From: Dimitry Ivanov Date: Sun, 4 Nov 2018 14:15:10 +0300 Subject: [PATCH 008/103] Update syntaxHighlight test --- .../markwon/renderer/SyntaxHighlightTest.java | 53 ++++++++++++++++--- 1 file changed, 46 insertions(+), 7 deletions(-) diff --git a/markwon/src/test/java/ru/noties/markwon/renderer/SyntaxHighlightTest.java b/markwon/src/test/java/ru/noties/markwon/renderer/SyntaxHighlightTest.java index 6eefe748..1f5d88d4 100644 --- a/markwon/src/test/java/ru/noties/markwon/renderer/SyntaxHighlightTest.java +++ b/markwon/src/test/java/ru/noties/markwon/renderer/SyntaxHighlightTest.java @@ -1,6 +1,7 @@ package ru.noties.markwon.renderer; import android.content.Context; +import android.os.Build; import android.support.annotation.NonNull; import android.support.annotation.Nullable; import android.text.SpannableStringBuilder; @@ -19,21 +20,34 @@ import ru.noties.markwon.SyntaxHighlight; import ru.noties.markwon.spans.SpannableTheme; import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyBoolean; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; @RunWith(RobolectricTestRunner.class) -@Config(manifest = Config.NONE) +@Config(manifest = Config.NONE, sdk = { + Build.VERSION_CODES.JELLY_BEAN, + Build.VERSION_CODES.M, + Build.VERSION_CODES.O +}) public class SyntaxHighlightTest { // codeSpan must be before actual highlight spans (true reverse of builder) + // if we go with path of reversing spans inside SpannableBuilder (which + // might extend SpannableStringBuilder like https://github.com/noties/Markwon/pull/71) + // then on M (23) codeSpan will always be _before_ actual highlight and thus + // no highlight will be present + // note that bad behaviour is present on M (emulator/device/robolectric) + // other SDKs are added to validate that they do not fail @Test public void test() { - final Object highlightSpan = new Object(); + class Highlight { + } + final Object codeSpan = new Object(); final SyntaxHighlight highlight = new SyntaxHighlight() { @@ -41,7 +55,9 @@ public class SyntaxHighlightTest { @Override public CharSequence highlight(@Nullable String info, @NonNull String code) { final SpannableStringBuilder builder = new SpannableStringBuilder(code); - builder.setSpan(highlightSpan, 0, builder.length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); + for (int i = 0, length = code.length(); i < length; i++) { + builder.setSpan(new Highlight(), i, i + 1, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); + } return builder; } }; @@ -57,16 +73,39 @@ public class SyntaxHighlightTest { final SpannableBuilder builder = new SpannableBuilder(); + append(builder, "# Header 1\n", new Object()); + append(builder, "## Header 2\n", new Object()); + append(builder, "### Header 3\n", new Object()); + + final int start = builder.length(); + final SpannableMarkdownVisitor visitor = new SpannableMarkdownVisitor(configuration, builder); final FencedCodeBlock fencedCodeBlock = new FencedCodeBlock(); fencedCodeBlock.setLiteral("{code}"); visitor.visit(fencedCodeBlock); - final Object[] spans = builder.spannableStringBuilder().getSpans(0, builder.length(), Object.class); + final int end = builder.length(); - assertEquals(2, spans.length); + append(builder, "### Footer 3\n", new Object()); + append(builder, "## Footer 2\n", new Object()); + append(builder, "# Footer 1\n", new Object()); + + final Object[] spans = builder.spannableStringBuilder().getSpans(start, end, Object.class); + + // each character + code span + final int length = fencedCodeBlock.getLiteral().length() + 1; + assertEquals(length, spans.length); assertEquals(codeSpan, spans[0]); - assertEquals(highlightSpan, spans[1]); + + for (int i = 1; i < length; i++) { + assertTrue(spans[i] instanceof Highlight); + } } -} + + private static void append(@NonNull SpannableBuilder builder, @NonNull String text, @NonNull Object span) { + final int start = builder.length(); + builder.append(text); + builder.setSpan(span, start, builder.length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); + } +} \ No newline at end of file From cbf9a7b4a626b4a1d9229822267b688fc7d58fd0 Mon Sep 17 00:00:00 2001 From: Dimitry Ivanov Date: Thu, 25 Oct 2018 22:29:57 +0300 Subject: [PATCH 009/103] Small NB to build.gradle --- build.gradle | 1 + 1 file changed, 1 insertion(+) diff --git a/build.gradle b/build.gradle index d70cf8b5..f177c38a 100644 --- a/build.gradle +++ b/build.gradle @@ -40,6 +40,7 @@ if (hasProperty('local')) { ext { + // NB, updating build-tools or compile-sdk will require updating Travis config (.travis.yml) config = [ 'build-tools' : '28.0.3', 'compile-sdk' : 27, From 7a1b76af6679bcad87c91ff725e0048c66c46d72 Mon Sep 17 00:00:00 2001 From: Dimitry Ivanov Date: Sun, 4 Nov 2018 15:37:17 +0300 Subject: [PATCH 010/103] Add SpannableBuilder#getSpans method --- README.md | 1 - .../ru/noties/markwon/SpannableBuilder.java | 88 +++++++- .../markwon/SpannableStringBuilderImpl.java | 13 -- .../ru/noties/markwon/SpannedReversed.java | 9 - .../noties/markwon/SpannableBuilderTest.java | 194 ++++++++++++++++++ 5 files changed, 274 insertions(+), 31 deletions(-) delete mode 100644 markwon/src/main/java/ru/noties/markwon/SpannableStringBuilderImpl.java delete mode 100644 markwon/src/main/java/ru/noties/markwon/SpannedReversed.java create mode 100644 markwon/src/test/java/ru/noties/markwon/SpannableBuilderTest.java diff --git a/README.md b/README.md index cc471fdd..dee13c57 100644 --- a/README.md +++ b/README.md @@ -7,7 +7,6 @@ [![markwon-syntax-highlight](https://img.shields.io/maven-central/v/ru.noties/markwon-syntax-highlight.svg?label=markwon-syntax-highlight)](http://search.maven.org/#search|ga|1|g%3A%22ru.noties%22%20AND%20a%3A%22markwon-syntax-highlight%22) [![markwon-view](https://img.shields.io/maven-central/v/ru.noties/markwon-view.svg?label=markwon-view)](http://search.maven.org/#search|ga|1|g%3A%22ru.noties%22%20AND%20a%3A%22markwon-view%22) -[![Build Status](https://travis-ci.org/noties/Markwon.svg?branch=master)](https://travis-ci.org/noties/Markwon) [![Build Status](https://travis-ci.org/noties/Markwon.svg?branch=master)](https://travis-ci.org/noties/Markwon) **Markwon** is a markdown library for Android. It parses markdown diff --git a/markwon/src/main/java/ru/noties/markwon/SpannableBuilder.java b/markwon/src/main/java/ru/noties/markwon/SpannableBuilder.java index 9e3ec713..715c3c92 100644 --- a/markwon/src/main/java/ru/noties/markwon/SpannableBuilder.java +++ b/markwon/src/main/java/ru/noties/markwon/SpannableBuilder.java @@ -2,12 +2,16 @@ package ru.noties.markwon; import android.support.annotation.NonNull; import android.support.annotation.Nullable; +import android.support.annotation.VisibleForTesting; import android.text.SpannableStringBuilder; import android.text.Spanned; import java.util.ArrayDeque; +import java.util.ArrayList; +import java.util.Collections; import java.util.Deque; import java.util.Iterator; +import java.util.List; /** * This class is used to _revert_ order of applied spans. Original SpannableStringBuilder @@ -44,7 +48,9 @@ public class SpannableBuilder implements Appendable, CharSequence { } } - private static boolean isPositionValid(int length, int start, int end) { + // @since 2.0.1 package-private visibility for testing + @VisibleForTesting + static boolean isPositionValid(int length, int start, int end) { return end > start && start >= 0 && end <= length; @@ -157,9 +163,60 @@ public class SpannableBuilder implements Appendable, CharSequence { */ @Override public CharSequence subSequence(int start, int end) { + // todo: NB, we do not copy spans here... we should I think + // the thing to deal with: implement own `getSpans` method to mimic _native_ SpannableStringBuilder + // behaviour. For example originally it will return all spans that at least _overlap_ with specified + // range... which can be confusing return builder.subSequence(start, end); } + /** + * This method will return all {@link Span} spans that overlap specified range, + * so if for example a 1..9 range is specified some spans might have 0..6 or 0..10 start/end ranges. + * NB spans are returned in reversed order (no in order that we store them internally) + * + * @since 2.0.1 + */ + @NonNull + public List getSpans(int start, int end) { + + final int length = length(); + + if (!isPositionValid(length, start, end)) { + // we might as well throw here + return Collections.emptyList(); + } + + // all requested + if (start == 0 + && length == end) { + // but also copy (do not allow external modification) + final List list = new ArrayList<>(spans); + Collections.reverse(list); + return Collections.unmodifiableList(list); + } + + final List list = new ArrayList<>(0); + + final Iterator iterator = spans.descendingIterator(); + Span span; + + while (iterator.hasNext()) { + span = iterator.next(); + // we must execute 2 checks: if overlap with specified range or fully include it + // if span.start is >= range.start -> check if it's before range.end + // if span.end is <= end -> check if it's after range.start + if ( + (span.start >= start && span.start < end) + || (span.end <= end && span.end > start) + || (span.start < start && span.end > end)) { + list.add(span); + } + } + + return Collections.unmodifiableList(list); + } + public char lastChar() { return builder.charAt(length() - 1); } @@ -173,7 +230,7 @@ public class SpannableBuilder implements Appendable, CharSequence { final int end = length(); // as we do not expose builder and do no apply spans to it, we are safe to NOT to convert to String - final SpannableStringBuilderImpl impl = new SpannableStringBuilderImpl(builder.subSequence(start, end)); + final SpannableStringBuilderReversed impl = new SpannableStringBuilderReversed(builder.subSequence(start, end)); final Iterator iterator = spans.iterator(); @@ -222,13 +279,15 @@ public class SpannableBuilder implements Appendable, CharSequence { // as we do not expose builder and do no apply spans to it, we are safe to NOT to convert to String - final SpannableStringBuilderImpl impl = new SpannableStringBuilderImpl(builder); + final SpannableStringBuilderReversed reversed = new SpannableStringBuilderReversed(builder); + // NB, as e are using Deque -> iteration will be started with last element + // so, spans will be appearing in the for loop in reverse order for (Span span : spans) { - impl.setSpan(span.what, span.start, span.end, span.flags); + reversed.setSpan(span.what, span.start, span.end, span.flags); } - return impl; + return reversed; } private void copySpans(final int index, @Nullable CharSequence cs) { @@ -239,7 +298,7 @@ public class SpannableBuilder implements Appendable, CharSequence { if (cs instanceof Spanned) { final Spanned spanned = (Spanned) cs; - final boolean reverse = spanned instanceof SpannedReversed; + final boolean reversed = spanned instanceof SpannableStringBuilderReversed; final Object[] spans = spanned.getSpans(0, spanned.length(), Object.class); final int length = spans != null @@ -247,7 +306,7 @@ public class SpannableBuilder implements Appendable, CharSequence { : 0; if (length > 0) { - if (reverse) { + if (reversed) { Object o; for (int i = length - 1; i >= 0; i--) { o = spans[i]; @@ -274,7 +333,10 @@ public class SpannableBuilder implements Appendable, CharSequence { } } - static class Span { + /** + * @since 2.0.1 made public in order to be returned from `getSpans` method, initially added in 1.0.1 + */ + public static class Span { final Object what; int start; @@ -288,4 +350,14 @@ public class SpannableBuilder implements Appendable, CharSequence { this.flags = flags; } } + + /** + * @since 2.0.1 made inner class of {@link SpannableBuilder}, initially added in 1.0.1 + */ + static class SpannableStringBuilderReversed extends SpannableStringBuilder { + + SpannableStringBuilderReversed(CharSequence text) { + super(text); + } + } } diff --git a/markwon/src/main/java/ru/noties/markwon/SpannableStringBuilderImpl.java b/markwon/src/main/java/ru/noties/markwon/SpannableStringBuilderImpl.java deleted file mode 100644 index 7b29440b..00000000 --- a/markwon/src/main/java/ru/noties/markwon/SpannableStringBuilderImpl.java +++ /dev/null @@ -1,13 +0,0 @@ -package ru.noties.markwon; - -import android.text.SpannableStringBuilder; - -/** - * @since 1.0.1 - */ -class SpannableStringBuilderImpl extends SpannableStringBuilder implements SpannedReversed { - - SpannableStringBuilderImpl(CharSequence text) { - super(text); - } -} diff --git a/markwon/src/main/java/ru/noties/markwon/SpannedReversed.java b/markwon/src/main/java/ru/noties/markwon/SpannedReversed.java deleted file mode 100644 index 3fd7f566..00000000 --- a/markwon/src/main/java/ru/noties/markwon/SpannedReversed.java +++ /dev/null @@ -1,9 +0,0 @@ -package ru.noties.markwon; - -import android.text.Spanned; - -/** - * @since 1.0.1 - */ -interface SpannedReversed extends Spanned { -} diff --git a/markwon/src/test/java/ru/noties/markwon/SpannableBuilderTest.java b/markwon/src/test/java/ru/noties/markwon/SpannableBuilderTest.java new file mode 100644 index 00000000..d94e17dc --- /dev/null +++ b/markwon/src/test/java/ru/noties/markwon/SpannableBuilderTest.java @@ -0,0 +1,194 @@ +package ru.noties.markwon; + +import android.support.annotation.NonNull; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.robolectric.RobolectricTestRunner; +import org.robolectric.annotation.Config; + +import java.util.Arrays; +import java.util.List; + +import ix.Ix; +import ix.IxFunction; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; +import static ru.noties.markwon.SpannableBuilder.isPositionValid; + +@RunWith(RobolectricTestRunner.class) +@Config(manifest = Config.NONE) +public class SpannableBuilderTest { + + private SpannableBuilder builder; + + @Before + public void before() { + builder = new SpannableBuilder(); + } + + @Test + public void position_invalid() { + + final Position[] positions = { + Position.of(0, 0, 0), + Position.of(-1, -1, -1), + Position.of(0, -1, 1), + Position.of(1, 1, 1), + Position.of(0, 0, 10), + Position.of(10, 10, 0), + Position.of(10, 5, 2), + Position.of(5, 1, 1) + }; + + for (Position position : positions) { + assertFalse(position.toString(), isPositionValid(position.length, position.start, position.end)); + } + } + + @Test + public void position_valid() { + + final Position[] positions = { + Position.of(1, 0, 1), + Position.of(2, 0, 1), + Position.of(2, 1, 2), + Position.of(10, 0, 10), + Position.of(7, 6, 7) + }; + + for (Position position : positions) { + assertTrue(position.toString(), isPositionValid(position.length, position.start, position.end)); + } + } + +// @Test +// public void set_spans_position_invalid() { +// // will be silently ignored +// } + + @Test + public void get_spans() { + + // all spans that overlap with specified range or spans that include it fully -> should be returned + + final int length = 10; + + for (int i = 0; i < length; i++) { + builder.append(String.valueOf(i)); + } + + for (int start = 0, end = length - 1; start < end; start++, end--) { + builder.setSpan("" + start + "-" + end, start, end); + } + + // all (simple check that spans that take range greater that supplied range are also returned) + final List all = Arrays.asList("0-9", "1-8", "2-7", "3-6", "4-5"); + for (int start = 0, end = length - 1; start < end; start++, end--) { + assertEquals( + "" + start + "-" + end, + all, + getSpans(start, end) + ); + } + + assertEquals( + "1-3", + Arrays.asList("0-9", "1-8", "2-7"), + getSpans(1, 3) + ); + + assertEquals( + "1-10", + all, + getSpans(1, 10) + ); + + assertEquals( + "5-10", + Arrays.asList("0-9", "1-8", "2-7", "3-6"), + getSpans(5, 10) + ); + + assertEquals( + "7-10", + Arrays.asList("0-9", "1-8"), + getSpans(7, 10) + ); + } + + @Test + public void get_spans_out_of_range() { + + // let's test that if span.start >= range.start -> it will be less than range.end + // if span.end <= end -> it will be greater than range.start + + for (int i = 0; i < 10; i++) { + builder.append(String.valueOf(i)); + builder.setSpan("" + i + "-" + (i + 1), i, i + 1); + } + + assertEquals(10, getSpans(0, 10).size()); + + // so + // 0-1 + // 1-2 + // 2-3 + // etc + + //noinspection ArraysAsListWithZeroOrOneArgument + assertEquals( + "0-1", + Arrays.asList("0-1"), + getSpans(0, 1) + ); + + assertEquals( + "1-5", + Arrays.asList("1-2", "2-3", "3-4", "4-5"), + getSpans(1, 5) + ); + } + + @NonNull + private List getSpans(int start, int end) { + return Ix.from(builder.getSpans(start, end)) + .map(new IxFunction() { + @Override + public String apply(SpannableBuilder.Span span) { + return (String) span.what; + } + }) + .toList(); + } + + private static class Position { + + @NonNull + static Position of(int length, int start, int end) { + return new Position(length, start, end); + } + + final int length; + final int start; + final int end; + + private Position(int length, int start, int end) { + this.length = length; + this.start = start; + this.end = end; + } + + @Override + public String toString() { + return "Position{" + + "length=" + length + + ", start=" + start + + ", end=" + end + + '}'; + } + } +} \ No newline at end of file From cb917e7391ec61add1c81faff713f7125e6f53d1 Mon Sep 17 00:00:00 2001 From: Dimitry Ivanov Date: Mon, 5 Nov 2018 12:14:15 +0300 Subject: [PATCH 011/103] Adding tests for SpannableBuilder --- .../ru/noties/markwon/SpannableBuilder.java | 56 ++++-- .../noties/markwon/SpannableBuilderTest.java | 175 +++++++++++++++++- 2 files changed, 215 insertions(+), 16 deletions(-) diff --git a/markwon/src/main/java/ru/noties/markwon/SpannableBuilder.java b/markwon/src/main/java/ru/noties/markwon/SpannableBuilder.java index 715c3c92..07e2bb85 100644 --- a/markwon/src/main/java/ru/noties/markwon/SpannableBuilder.java +++ b/markwon/src/main/java/ru/noties/markwon/SpannableBuilder.java @@ -163,11 +163,46 @@ public class SpannableBuilder implements Appendable, CharSequence { */ @Override public CharSequence subSequence(int start, int end) { - // todo: NB, we do not copy spans here... we should I think - // the thing to deal with: implement own `getSpans` method to mimic _native_ SpannableStringBuilder - // behaviour. For example originally it will return all spans that at least _overlap_ with specified - // range... which can be confusing - return builder.subSequence(start, end); + + final CharSequence out; + + // @since 2.0.1 we copy spans to resulting subSequence + final List spans = getSpans(start, end); + if (spans.isEmpty()) { + out = builder.subSequence(start, end); + } else { + + // we should not be SpannableStringBuilderReversed here + final SpannableStringBuilder builder = new SpannableStringBuilder(this.builder.subSequence(start, end)); + + final int length = builder.length(); + + int s; + int e; + + for (Span span : spans) { + + // we should limit start/end to resulting subSequence length + // + // for example, originally it was 5-7 and range 5-7 requested + // span should have 0-2 + // + // if a span was fully including resulting subSequence it's start and + // end must be within 0..length bounds + s = Math.max(0, span.start - start); + e = Math.max(length, s + (span.end - span.start)); + + builder.setSpan( + span.what, + s, + e, + span.flags + ); + } + out = builder; + } + + return out; } /** @@ -263,7 +298,7 @@ public class SpannableBuilder implements Appendable, CharSequence { /** * Simple method to create a SpannableStringBuilder, which is created anyway. Unlike {@link #text()} * method which returns the same SpannableStringBuilder there is no need to cast the resulting - * CharSequence + * CharSequence and makes the thing more explicit * * @since 2.0.0 */ @@ -338,10 +373,10 @@ public class SpannableBuilder implements Appendable, CharSequence { */ public static class Span { - final Object what; - int start; - int end; - final int flags; + public final Object what; + public int start; + public int end; + public final int flags; Span(@NonNull Object what, int start, int end, int flags) { this.what = what; @@ -355,7 +390,6 @@ public class SpannableBuilder implements Appendable, CharSequence { * @since 2.0.1 made inner class of {@link SpannableBuilder}, initially added in 1.0.1 */ static class SpannableStringBuilderReversed extends SpannableStringBuilder { - SpannableStringBuilderReversed(CharSequence text) { super(text); } diff --git a/markwon/src/test/java/ru/noties/markwon/SpannableBuilderTest.java b/markwon/src/test/java/ru/noties/markwon/SpannableBuilderTest.java index d94e17dc..a1daf218 100644 --- a/markwon/src/test/java/ru/noties/markwon/SpannableBuilderTest.java +++ b/markwon/src/test/java/ru/noties/markwon/SpannableBuilderTest.java @@ -1,6 +1,8 @@ package ru.noties.markwon; import android.support.annotation.NonNull; +import android.text.SpannableStringBuilder; +import android.text.Spanned; import org.junit.Before; import org.junit.Test; @@ -18,6 +20,7 @@ import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertTrue; import static ru.noties.markwon.SpannableBuilder.isPositionValid; +import static ru.noties.markwon.SpannableBuilder.setSpans; @RunWith(RobolectricTestRunner.class) @Config(manifest = Config.NONE) @@ -65,11 +68,6 @@ public class SpannableBuilderTest { } } -// @Test -// public void set_spans_position_invalid() { -// // will be silently ignored -// } - @Test public void get_spans() { @@ -165,6 +163,173 @@ public class SpannableBuilderTest { .toList(); } + @Test + public void set_spans_position_invalid() { + // if supplied position is invalid, no spans should be added + + builder.append('0'); + + assertTrue(builder.getSpans(0, builder.length()).isEmpty()); + + setSpans(builder, new Object(), -1, -1); + + assertTrue(builder.getSpans(0, builder.length()).isEmpty()); + } + + @Test + public void set_spans_single() { + // single span as `spans` argument correctly added + + builder.append('0'); + + assertTrue(builder.getSpans(0, builder.length()).isEmpty()); + + final Object span = new Object(); + setSpans(builder, span, 0, 1); + + final List spans = builder.getSpans(0, builder.length()); + assertEquals(1, spans.size()); + assertEquals(span, spans.get(0).what); + } + + @Test + public void set_spans_array_detected() { + // if supplied `spans` argument is an array -> it should be expanded + + builder.append('0'); + + assertTrue(builder.getSpans(0, builder.length()).isEmpty()); + + final Object[] spans = { + new Object(), + new Object(), + new Object() + }; + + setSpans(builder, spans, 0, 1); + + final List actual = builder.getSpans(0, builder.length()); + assertEquals(spans.length, actual.size()); + + for (int i = 0, length = spans.length; i < length; i++) { + assertEquals(spans[i], actual.get(i).what); + } + } + + @Test + public void set_spans_array_of_arrays() { + // if array of arrays is supplied -> it won't be expanded to single elements + + builder.append('0'); + + assertTrue(builder.getSpans(0, builder.length()).isEmpty()); + + final Object[] spans = { + new Object[]{ + new Object(), new Object() + }, + new Object[]{ + new Object(), new Object(), new Object() + } + }; + + setSpans(builder, spans, 0, 1); + + final List actual = builder.getSpans(0, builder.length()); + assertEquals(2, actual.size()); + + for (int i = 0, length = spans.length; i < length; i++) { + assertEquals(spans[i], actual.get(i).what); + } + } + + @Test + public void set_spans_null() { + // if `spans` argument is null, then nothing will be added + + builder.append('0'); + + assertTrue(builder.getSpans(0, builder.length()).isEmpty()); + + setSpans(builder, null, 0, builder.length()); + + assertTrue(builder.getSpans(0, builder.length()).isEmpty()); + } + + @Test + public void spans_reversed() { + // resulting SpannableStringBuilder should have spans reversed + + final Object[] spans = { + 0, + 1, + 2 + }; + + for (Object span : spans) { + builder.append(span.toString(), span); + } + + final SpannableStringBuilder spannableStringBuilder = builder.spannableStringBuilder(); + final Object[] actual = spannableStringBuilder.getSpans(0, builder.length(), Object.class); + + for (int start = 0, length = spans.length, end = length - 1; start < length; start++, end--) { + assertEquals(spans[start], actual[end]); + } + } + + @Test + public void append_spanned_normal() { + // #append is called with regular Spanned content -> spans should be added in reverse + + final SpannableStringBuilder ssb = new SpannableStringBuilder(); + for (int i = 0; i < 3; i++) { + ssb.append(String.valueOf(i)); + ssb.setSpan(i, i, i + 1, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); + } + + assertTrue(builder.getSpans(0, builder.length()).isEmpty()); + + builder.append(ssb); + + assertEquals("012", builder.toString()); + + // this one would return normal order as spans are reversed here +// final List spans = builder.getSpans(0, builder.length()); + + final SpannableStringBuilder spannableStringBuilder = builder.spannableStringBuilder(); + final Object[] spans = spannableStringBuilder.getSpans(0, builder.length(), Object.class); + assertEquals(3, spans.length); + + for (int i = 0, length = spans.length; i < length; i++) { + assertEquals(length - 1 - i, spans[i]); + } + } + + @Test + public void append_spanned_reversed() { + // #append is called with reversed spanned content -> spans should be added as-are + + final SpannableBuilder spannableBuilder = new SpannableBuilder(); + for (int i = 0; i < 3; i++) { + spannableBuilder.append(String.valueOf(i), i); + } + + assertTrue(builder.getSpans(0, builder.length()).isEmpty()); + + builder.append(spannableBuilder.spannableStringBuilder()); + + final SpannableStringBuilder spannableStringBuilder = builder.spannableStringBuilder(); + final Object[] spans = spannableStringBuilder.getSpans(0, spannableStringBuilder.length(), Object.class); + assertEquals(3, spans.length); + + for (int i = 0, length = spans.length; i < length; i++) { + // in the end order should be as we expect in order to properly render it + // (no matter if reversed is used or not) + assertEquals(length - 1 - i, spans[i]); + } + } + private static class Position { @NonNull From b2a467ffe9fb94c71a590dcb3102334f496a03a4 Mon Sep 17 00:00:00 2001 From: Dimitry Ivanov Date: Tue, 20 Nov 2018 15:17:30 +0300 Subject: [PATCH 012/103] Add OrderedListItemSpan measure utility method --- app/build.gradle | 1 - .../java/ru/noties/markwon/MainActivity.java | 3 +- build.gradle | 3 +- gradle/wrapper/gradle-wrapper.jar | Bin 54329 -> 54417 bytes gradle/wrapper/gradle-wrapper.properties | 2 +- .../main/java/ru/noties/markwon/Markwon.java | 7 +++ .../markwon/spans/OrderedListItemSpan.java | 45 ++++++++++++++++-- 7 files changed, 52 insertions(+), 9 deletions(-) diff --git a/app/build.gradle b/app/build.gradle index a8154b4a..0869e5e5 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -36,7 +36,6 @@ dependencies { implementation it['okhttp'] implementation it['prism4j'] implementation it['debug'] - implementation it['better-link-movement'] implementation it['dagger'] } diff --git a/app/src/main/java/ru/noties/markwon/MainActivity.java b/app/src/main/java/ru/noties/markwon/MainActivity.java index 19882a74..3bf49109 100644 --- a/app/src/main/java/ru/noties/markwon/MainActivity.java +++ b/app/src/main/java/ru/noties/markwon/MainActivity.java @@ -11,7 +11,6 @@ import android.widget.TextView; import javax.inject.Inject; -import me.saket.bettermovementmethod.BetterLinkMovementMethod; import ru.noties.debug.Debug; public class MainActivity extends Activity { @@ -71,7 +70,7 @@ public class MainActivity extends Activity { @Override public void onMarkdownReady(CharSequence markdown) { - Markwon.setText(textView, markdown, BetterLinkMovementMethod.getInstance()); + Markwon.setText(textView, markdown); gifProcessor.process(textView); diff --git a/build.gradle b/build.gradle index f177c38a..4e69d605 100644 --- a/build.gradle +++ b/build.gradle @@ -26,7 +26,7 @@ task clean(type: Delete) { } task wrapper(type: Wrapper) { - gradleVersion '4.8.1' + gradleVersion '4.10.2' distributionType 'all' } @@ -64,7 +64,6 @@ ext { 'okhttp' : 'com.squareup.okhttp3:okhttp:3.9.0', 'prism4j' : 'ru.noties:prism4j:1.1.0', 'debug' : 'ru.noties:debug:3.0.0@jar', - 'better-link-movement' : 'me.saket:better-link-movement-method:2.2.0', 'dagger' : "com.google.dagger:dagger:$daggerVersion" ] diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar index a5fe1cb94b9ee5ce57e6113458225bcba12d83e3..758de960ec7947253b058ff79c88ce51f3abe08a 100644 GIT binary patch delta 7592 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 delta 7527 zcmZ9R1x#F9_qSnicXulc?yf}(6dk0vyZc~+d$GaY-3n72TD-Wng<{36#a;jXnwxvy zd?z`{^Q_;>$vQhHImzBB)d(F`2+2Zdw?a0bq~PG-Kq=DNDB0&8XCCly$>bke%{kEW zYe{bxkm2A2p|qUrkN`qGvS9UV-f^1{Tmv^lyIF-rb}Woy4YW{nG-ugNX^Pi~mfp=` zPROtLj()Lc)?7ukwK~-5mOJ!-;(e=AnFyVa>VMqFzl40c*SoDc5o*a@b;>~91z+ch ztOsV^1g?v%i+~^28+(z>D4ts}4Nu!KY0@@ic}aOyN0Zg*A@O0ze6fgX4lJ)y8u7V^f&}%mO4uz`^vA6Lw-Qo5uGH1}vU$a+^|3w-WK=eF_MX>O$GLV)wcAcp zh*T6(L7XBKYUMaiM49U?hWO;vi-q?5hn!~l&|9-5j>yVW53HH{!By|ludBatTh1%I zB<5SDlSLkLXE_TG{URmGqsQ8OhUpwH)igC2r{Pupf>5+__=jg$-USeGmSt)gWkvYZ z-kZ4kYsYKeP;19vcOcmWSx| z+-U`!P#CBplekzlnu)n_x@Iuiyc^K#h`e_RucIjtwccf#Yv!rlCk-Ad{T>ugl(U)K zcNe#pomfa^@rF;Nj;3tB|G|i){vJ0%uGh6cD z2FMo4ZF!>UMst*&*CY9-S$Cus(VF%ebF~qH?wXCx#Pzts_0RRM+ zYv5Sj6g+ByZ#7_5Adc?4B8=U)D=~kH5C>uV<*=;qLoh_%)P#|P$_zYvnM4X;V@w{t zs^YP1ookyt9BdnCtmj&-sAR|gdMh&!aS}xz88G+T-@wuH5H>b&HHYMO2Og>Si1B1{ zKh9cyP>h=J)WnrLcY9Tl!|2?1o*i|FZvjWf|hn zJA~1%+!E@OTjXpyEc&CBS#OGiI2^{wB3mELt`oI@mCrNMI)1pa*3dLUp8SAx{&tqRVFF1ZJ{z$z>2gi} zTXE63p5N1T$3t2kHa}RBa`yJ=4-p~SRJ%cdmJ?kwkEC3eW{imSw-;xNUMxcTHA0OP zGd`#=MOT0UpQp>NN+Zf55Z>oyU9Z$e?`GYDyqqkm$9qAXjo#^Jkihuzlu08}ES|vm^XWsf4)4S3EMyPmjw~ano-X zU9?8uhiY_7Z9;jciUT!{C0Ku zM)M%VYP7tV-{aX%1+kEQPPE;irmt&z0_?tCgmy-iiVx!3o&u(fL2|eB zT%0gk7fXpV#$KfR8bs+Ra?S8o?Kgd)ht(V-7(^RgF

>O|5`!Y@WVeHnjeM=H)Z) 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 + * NB, this method must be called before setting text to a TextView (`TextView#setText` + * internally can trigger new Layout creation which will ask for leading margins right away) + * + * @param textView to which markdown will be applied + * @param text parsed markdown to process + * @since 2.0.1 + */ + public static void measure(@NonNull TextView textView, @NonNull CharSequence text) { + + if (!(text instanceof Spanned)) { + // nothing to do here + return; + } + + final OrderedListItemSpan[] spans = ((Spanned) text).getSpans( + 0, + text.length(), + OrderedListItemSpan.class); + + if (spans != null) { + final TextPaint paint = textView.getPaint(); + for (OrderedListItemSpan span : spans) { + span.margin = (int) (paint.measureText(span.number) + .5F); + } + } + } + private final SpannableTheme theme; private final String number; private final Paint paint = ObjectsPool.paint(); @@ -27,8 +61,8 @@ public class OrderedListItemSpan implements LeadingMarginSpan { @Override public int getLeadingMargin(boolean first) { - // @since 1.0.3 - return margin > 0 ? margin : theme.getBlockMargin(); + // @since 2.0.1 we return maximum value of both (now we should measure number before) + return Math.max(margin, theme.getBlockMargin()); } @Override @@ -44,11 +78,16 @@ public class OrderedListItemSpan implements LeadingMarginSpan { theme.applyListItemStyle(paint); - final int numberWidth = (int) (p.measureText(number) + .5F); + // if we could force usage of #measure method then we might want skip this measuring here + // but this won't hold against new values that a TextView can receive (new text size for + // example...) + final int numberWidth = (int) (paint.measureText(number) + .5F); // @since 1.0.3 int width = theme.getBlockMargin(); if (numberWidth > width) { + // let's keep this logic here in case a user decided not to call #measure and is fine + // with current implementation width = numberWidth; margin = numberWidth; } else { From 65ece1621cb956966a08dcc4af57c206c6073829 Mon Sep 17 00:00:00 2001 From: Dimitry Ivanov Date: Thu, 22 Nov 2018 13:02:48 +0300 Subject: [PATCH 013/103] Updated commonmark-java to 0.12.1 --- build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index 4e69d605..6ba06545 100644 --- a/build.gradle +++ b/build.gradle @@ -50,7 +50,7 @@ ext { ] final def supportVersion = '27.1.1' - final def commonMarkVersion = '0.11.0' + final def commonMarkVersion = '0.12.1' final def daggerVersion = '2.10' deps = [ From b531684872c5a2ee3bb74f73e275408a71080e18 Mon Sep 17 00:00:00 2001 From: Dimitry Ivanov Date: Thu, 22 Nov 2018 13:05:14 +0300 Subject: [PATCH 014/103] Allow TaskListSpan isDone mutation --- .../ru/noties/markwon/spans/TaskListSpan.java | 21 ++++++++++++++++++- 1 file changed, 20 insertions(+), 1 deletion(-) diff --git a/markwon/src/main/java/ru/noties/markwon/spans/TaskListSpan.java b/markwon/src/main/java/ru/noties/markwon/spans/TaskListSpan.java index 172b952c..25bc6a41 100644 --- a/markwon/src/main/java/ru/noties/markwon/spans/TaskListSpan.java +++ b/markwon/src/main/java/ru/noties/markwon/spans/TaskListSpan.java @@ -18,7 +18,9 @@ public class TaskListSpan implements LeadingMarginSpan { private final SpannableTheme theme; private final int blockIndent; - private final boolean isDone; + + // @since 2.0.1 field is NOT final (to allow mutation) + private boolean isDone; public TaskListSpan(@NonNull SpannableTheme theme, int blockIndent, boolean isDone) { this.theme = theme; @@ -26,6 +28,23 @@ public class TaskListSpan implements LeadingMarginSpan { this.isDone = isDone; } + /** + * @since 2.0.1 + */ + public boolean isDone() { + return isDone; + } + + /** + * Update {@link #isDone} property of this span. Please note that this is merely a visual change + * which is not changing underlying text in any means. + * + * @since 2.0.1 + */ + public void setDone(boolean isDone) { + this.isDone = isDone; + } + @Override public int getLeadingMargin(boolean first) { return theme.getBlockMargin() * blockIndent; From 82b8f2fdfdb2d3877c8fa044b82d7ece2abaa1e6 Mon Sep 17 00:00:00 2001 From: Dimitry Ivanov Date: Thu, 22 Nov 2018 13:08:01 +0300 Subject: [PATCH 015/103] Update SpannableTheme to use Px instead of Dimension annotation --- .../noties/markwon/spans/SpannableTheme.java | 23 ++++++++++--------- 1 file changed, 12 insertions(+), 11 deletions(-) diff --git a/markwon/src/main/java/ru/noties/markwon/spans/SpannableTheme.java b/markwon/src/main/java/ru/noties/markwon/spans/SpannableTheme.java index e82d0d8b..a3ba8c55 100644 --- a/markwon/src/main/java/ru/noties/markwon/spans/SpannableTheme.java +++ b/markwon/src/main/java/ru/noties/markwon/spans/SpannableTheme.java @@ -12,6 +12,7 @@ import android.support.annotation.FloatRange; import android.support.annotation.IntRange; import android.support.annotation.NonNull; import android.support.annotation.Nullable; +import android.support.annotation.Px; import android.support.annotation.Size; import android.text.TextPaint; import android.util.TypedValue; @@ -600,13 +601,13 @@ public class SpannableTheme { } @NonNull - public Builder blockMargin(@Dimension int blockMargin) { + public Builder blockMargin(@Px int blockMargin) { this.blockMargin = blockMargin; return this; } @NonNull - public Builder blockQuoteWidth(@Dimension int blockQuoteWidth) { + public Builder blockQuoteWidth(@Px int blockQuoteWidth) { this.blockQuoteWidth = blockQuoteWidth; return this; } @@ -625,13 +626,13 @@ public class SpannableTheme { } @NonNull - public Builder bulletListItemStrokeWidth(@Dimension int bulletListItemStrokeWidth) { + public Builder bulletListItemStrokeWidth(@Px int bulletListItemStrokeWidth) { this.bulletListItemStrokeWidth = bulletListItemStrokeWidth; return this; } @NonNull - public Builder bulletWidth(@Dimension int bulletWidth) { + public Builder bulletWidth(@Px int bulletWidth) { this.bulletWidth = bulletWidth; return this; } @@ -668,7 +669,7 @@ public class SpannableTheme { } @NonNull - public Builder codeMultilineMargin(@Dimension int codeMultilineMargin) { + public Builder codeMultilineMargin(@Px int codeMultilineMargin) { this.codeMultilineMargin = codeMultilineMargin; return this; } @@ -680,13 +681,13 @@ public class SpannableTheme { } @NonNull - public Builder codeTextSize(@Dimension int codeTextSize) { + public Builder codeTextSize(@Px int codeTextSize) { this.codeTextSize = codeTextSize; return this; } @NonNull - public Builder headingBreakHeight(@Dimension int headingBreakHeight) { + public Builder headingBreakHeight(@Px int headingBreakHeight) { this.headingBreakHeight = headingBreakHeight; return this; } @@ -733,13 +734,13 @@ public class SpannableTheme { } @NonNull - public Builder thematicBreakHeight(@Dimension int thematicBreakHeight) { + public Builder thematicBreakHeight(@Px int thematicBreakHeight) { this.thematicBreakHeight = thematicBreakHeight; return this; } @NonNull - public Builder tableCellPadding(@Dimension int tableCellPadding) { + public Builder tableCellPadding(@Px int tableCellPadding) { this.tableCellPadding = tableCellPadding; return this; } @@ -751,7 +752,7 @@ public class SpannableTheme { } @NonNull - public Builder tableBorderWidth(@Dimension int tableBorderWidth) { + public Builder tableBorderWidth(@Px int tableBorderWidth) { this.tableBorderWidth = tableBorderWidth; return this; } @@ -775,7 +776,7 @@ public class SpannableTheme { * @since 1.1.1 */ @NonNull - public Builder tableHeaderRowBackgroundColor(int tableHeaderRowBackgroundColor) { + public Builder tableHeaderRowBackgroundColor(@ColorInt int tableHeaderRowBackgroundColor) { this.tableHeaderRowBackgroundColor = tableHeaderRowBackgroundColor; return this; } From 340ce5753c3d5a3d47888b6606ef39aea8cbb7ac Mon Sep 17 00:00:00 2001 From: Dimitry Ivanov Date: Thu, 22 Nov 2018 13:35:45 +0300 Subject: [PATCH 016/103] Update README (apps using...) --- README.md | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index dee13c57..be6976a9 100644 --- a/README.md +++ b/README.md @@ -93,6 +93,14 @@ Please visit [documentation] web-site for reference [documentation]: https://noties.github.io/Markwon +--- + +## Applications using Markwon + +* [Partiko](https://partiko.app) +* [FairNote Notepad](https://play.google.com/store/apps/details?id=com.rgiskard.fairnote) + + --- # Demo @@ -279,12 +287,6 @@ ___ Underscores (`_`) ---- - -## Applications using Markwon - -* [FairNote Notepad](https://play.google.com/store/apps/details?id=com.rgiskard.fairnote) - ## License From 498c811987b44c8626ab1dd487d0d8b0de105d79 Mon Sep 17 00:00:00 2001 From: Dimitry Ivanov Date: Sat, 24 Nov 2018 16:03:08 +0300 Subject: [PATCH 017/103] POC for plugin system --- .../debug/DebugCheckboxDrawableView.java | 2 +- .../markwon/GifAwareSpannableFactory.java | 4 +- .../java/ru/noties/markwon/MainActivity.java | 15 +- .../ru/noties/markwon/MarkdownRenderer.java | 6 +- docs/docs/theme.md | 2 +- .../debug/DebugConfigurationProvider.java | 14 +- .../ru/noties/markwon/view/IMarkwonView.java | 6 +- .../ru/noties/markwon/view/MarkwonView.java | 4 +- .../markwon/view/MarkwonViewCompat.java | 4 +- .../markwon/view/MarkwonViewHelper.java | 8 +- .../noties/markwon/AbstractMarkwonPlugin.java | 46 +++ .../main/java/ru/noties/markwon/Markwon.java | 24 +- .../main/java/ru/noties/markwon/Markwon2.java | 32 ++ .../ru/noties/markwon/MarkwonBuilderImpl.java | 53 ++++ ...uration.java => MarkwonConfiguration.java} | 33 +- .../java/ru/noties/markwon/MarkwonImpl.java | 48 +++ .../java/ru/noties/markwon/MarkwonPlugin.java | 29 ++ .../ru/noties/markwon/MarkwonVisitor.java | 60 ++++ .../ru/noties/markwon/MarkwonVisitorImpl.java | 290 ++++++++++++++++++ .../ru/noties/markwon/SpannableBuilder.java | 2 +- .../ru/noties/markwon/SpannableFactory.java | 26 +- .../noties/markwon/SpannableFactoryDef.java | 28 +- .../ru/noties/markwon/core/CorePlugin.java | 268 ++++++++++++++++ .../renderer/SpannableMarkdownVisitor.java | 10 +- .../markwon/renderer/SpannableRenderer.java | 4 +- .../renderer/html2/MarkwonHtmlRenderer.java | 4 +- .../html2/MarkwonHtmlRendererImpl.java | 4 +- .../renderer/html2/tag/BlockquoteHandler.java | 4 +- .../renderer/html2/tag/EmphasisHandler.java | 4 +- .../renderer/html2/tag/HeadingHandler.java | 4 +- .../renderer/html2/tag/ImageHandler.java | 4 +- .../renderer/html2/tag/LinkHandler.java | 4 +- .../renderer/html2/tag/ListHandler.java | 4 +- .../renderer/html2/tag/SimpleTagHandler.java | 6 +- .../renderer/html2/tag/StrikeHandler.java | 4 +- .../html2/tag/StrongEmphasisHandler.java | 4 +- .../renderer/html2/tag/SubScriptHandler.java | 4 +- .../html2/tag/SuperScriptHandler.java | 4 +- .../renderer/html2/tag/TagHandler.java | 6 +- .../renderer/html2/tag/UnderlineHandler.java | 4 +- .../markwon/spans/AsyncDrawableSpan.java | 8 +- .../noties/markwon/spans/BlockQuoteSpan.java | 4 +- .../markwon/spans/BulletListItemSpan.java | 6 +- .../ru/noties/markwon/spans/CodeSpan.java | 4 +- .../ru/noties/markwon/spans/HeadingSpan.java | 6 +- .../ru/noties/markwon/spans/LinkSpan.java | 5 +- ...{SpannableTheme.java => MarkwonTheme.java} | 30 +- .../markwon/spans/OrderedListItemSpan.java | 6 +- .../noties/markwon/spans/SubScriptSpan.java | 4 +- .../noties/markwon/spans/SuperScriptSpan.java | 4 +- .../ru/noties/markwon/spans/TableRowSpan.java | 4 +- .../markwon/spans/ThematicBreakSpan.java | 4 +- .../{spans => tasklist}/TaskListDrawable.java | 2 +- .../markwon/tasklist/TaskListExtension.java | 1 + .../markwon/tasklist/TaskListPlugin.java | 93 ++++++ .../{spans => tasklist}/TaskListSpan.java | 27 +- .../{spans => utils}/LeadingMarginUtils.java | 8 +- ...est.java => MarkwonConfigurationTest.java} | 12 +- .../markwon/renderer/SyntaxHighlightTest.java | 10 +- .../visitor/SpannableMarkdownVisitorTest.java | 12 +- .../markwon/renderer/visitor/TestFactory.java | 26 +- .../markwon/sample/extension/IconVisitor.java | 5 +- .../sample/extension/MainActivity.java | 8 +- .../sample/jlatexmath/MainActivity.java | 6 +- 64 files changed, 1170 insertions(+), 207 deletions(-) create mode 100644 markwon/src/main/java/ru/noties/markwon/AbstractMarkwonPlugin.java create mode 100644 markwon/src/main/java/ru/noties/markwon/Markwon2.java create mode 100644 markwon/src/main/java/ru/noties/markwon/MarkwonBuilderImpl.java rename markwon/src/main/java/ru/noties/markwon/{SpannableConfiguration.java => MarkwonConfiguration.java} (92%) create mode 100644 markwon/src/main/java/ru/noties/markwon/MarkwonImpl.java create mode 100644 markwon/src/main/java/ru/noties/markwon/MarkwonPlugin.java create mode 100644 markwon/src/main/java/ru/noties/markwon/MarkwonVisitor.java create mode 100644 markwon/src/main/java/ru/noties/markwon/MarkwonVisitorImpl.java create mode 100644 markwon/src/main/java/ru/noties/markwon/core/CorePlugin.java rename markwon/src/main/java/ru/noties/markwon/spans/{SpannableTheme.java => MarkwonTheme.java} (97%) rename markwon/src/main/java/ru/noties/markwon/{spans => tasklist}/TaskListDrawable.java (99%) create mode 100644 markwon/src/main/java/ru/noties/markwon/tasklist/TaskListPlugin.java rename markwon/src/main/java/ru/noties/markwon/{spans => tasklist}/TaskListSpan.java (77%) rename markwon/src/main/java/ru/noties/markwon/{spans => utils}/LeadingMarginUtils.java (52%) rename markwon/src/test/java/ru/noties/markwon/renderer/{SpannableConfigurationTest.java => MarkwonConfigurationTest.java} (86%) diff --git a/app/src/debug/java/ru/noties/markwon/debug/DebugCheckboxDrawableView.java b/app/src/debug/java/ru/noties/markwon/debug/DebugCheckboxDrawableView.java index 34bef820..e99e35fc 100644 --- a/app/src/debug/java/ru/noties/markwon/debug/DebugCheckboxDrawableView.java +++ b/app/src/debug/java/ru/noties/markwon/debug/DebugCheckboxDrawableView.java @@ -9,7 +9,7 @@ import android.util.AttributeSet; import android.view.View; import ru.noties.markwon.R; -import ru.noties.markwon.spans.TaskListDrawable; +import ru.noties.markwon.tasklist.TaskListDrawable; public class DebugCheckboxDrawableView extends View { diff --git a/app/src/main/java/ru/noties/markwon/GifAwareSpannableFactory.java b/app/src/main/java/ru/noties/markwon/GifAwareSpannableFactory.java index f070e9fc..7c387a1c 100644 --- a/app/src/main/java/ru/noties/markwon/GifAwareSpannableFactory.java +++ b/app/src/main/java/ru/noties/markwon/GifAwareSpannableFactory.java @@ -7,7 +7,7 @@ import ru.noties.markwon.renderer.ImageSize; import ru.noties.markwon.renderer.ImageSizeResolver; import ru.noties.markwon.spans.AsyncDrawable; import ru.noties.markwon.spans.AsyncDrawableSpan; -import ru.noties.markwon.spans.SpannableTheme; +import ru.noties.markwon.spans.MarkwonTheme; public class GifAwareSpannableFactory extends SpannableFactoryDef { @@ -19,7 +19,7 @@ public class GifAwareSpannableFactory extends SpannableFactoryDef { @Nullable @Override - public Object image(@NonNull SpannableTheme theme, @NonNull String destination, @NonNull AsyncDrawable.Loader loader, @NonNull ImageSizeResolver imageSizeResolver, @Nullable ImageSize imageSize, boolean replacementTextIsLink) { + public Object image(@NonNull MarkwonTheme theme, @NonNull String destination, @NonNull AsyncDrawable.Loader loader, @NonNull ImageSizeResolver imageSizeResolver, @Nullable ImageSize imageSize, boolean replacementTextIsLink) { return new AsyncDrawableSpan( theme, new GifAwareAsyncDrawable( diff --git a/app/src/main/java/ru/noties/markwon/MainActivity.java b/app/src/main/java/ru/noties/markwon/MainActivity.java index 3bf49109..a14caf2e 100644 --- a/app/src/main/java/ru/noties/markwon/MainActivity.java +++ b/app/src/main/java/ru/noties/markwon/MainActivity.java @@ -12,6 +12,9 @@ import android.widget.TextView; import javax.inject.Inject; import ru.noties.debug.Debug; +import ru.noties.markwon.core.CorePlugin; +import ru.noties.markwon.tasklist.TaskListDrawable; +import ru.noties.markwon.tasklist.TaskListPlugin; public class MainActivity extends Activity { @@ -40,7 +43,7 @@ public class MainActivity extends Activity { themes.apply(this); - // how can we obtain SpannableConfiguration after theme was applied? + // how can we obtain MarkwonConfiguration after theme was applied? // as we inject `themes` we won't be able to inject configuration, as it requires theme set setContentView(R.layout.activity_main); @@ -63,6 +66,16 @@ public class MainActivity extends Activity { appBarRenderer.render(appBarState()); + if (true) { + final Markwon2 markwon2 = Markwon2.builder(this) + .use(new CorePlugin()) + .use(TaskListPlugin.create(new TaskListDrawable(0xffff0000, 0xffff0000, -1))) + .build(); + final CharSequence markdown = markwon2.markdown("**hello _dear_** `code`\n\n- [ ] first\n- [x] second"); + textView.setText(markdown); + return; + } + markdownLoader.load(uri(), new MarkdownLoader.OnMarkdownTextLoaded() { @Override public void apply(final String text) { diff --git a/app/src/main/java/ru/noties/markwon/MarkdownRenderer.java b/app/src/main/java/ru/noties/markwon/MarkdownRenderer.java index 23ff268b..9151b115 100644 --- a/app/src/main/java/ru/noties/markwon/MarkdownRenderer.java +++ b/app/src/main/java/ru/noties/markwon/MarkdownRenderer.java @@ -14,7 +14,7 @@ import javax.inject.Inject; import ru.noties.debug.Debug; import ru.noties.markwon.spans.AsyncDrawable; -import ru.noties.markwon.spans.SpannableTheme; +import ru.noties.markwon.spans.MarkwonTheme; import ru.noties.markwon.syntax.Prism4jSyntaxHighlight; import ru.noties.markwon.syntax.Prism4jTheme; import ru.noties.markwon.syntax.Prism4jThemeDarkula; @@ -87,11 +87,11 @@ public class MarkdownRenderer { 0x20000000 ); - final SpannableConfiguration configuration = SpannableConfiguration.builder(context) + final MarkwonConfiguration configuration = MarkwonConfiguration.builder(context) .asyncDrawableLoader(loader) .urlProcessor(urlProcessor) .syntaxHighlight(Prism4jSyntaxHighlight.create(prism4j, prism4jTheme)) - .theme(SpannableTheme.builderWithDefaults(context) + .theme(MarkwonTheme.builderWithDefaults(context) .codeBackgroundColor(background) .codeTextColor(prism4jTheme.textColor()) .build()) diff --git a/docs/docs/theme.md b/docs/docs/theme.md index b01f7f31..4bc6cfb0 100644 --- a/docs/docs/theme.md +++ b/docs/docs/theme.md @@ -209,4 +209,4 @@ Background of header table row Drawable of task list item - + diff --git a/markwon-view/src/debug/java/ru/noties/markwon/view/debug/DebugConfigurationProvider.java b/markwon-view/src/debug/java/ru/noties/markwon/view/debug/DebugConfigurationProvider.java index e530bab5..50ad7fec 100644 --- a/markwon-view/src/debug/java/ru/noties/markwon/view/debug/DebugConfigurationProvider.java +++ b/markwon-view/src/debug/java/ru/noties/markwon/view/debug/DebugConfigurationProvider.java @@ -3,27 +3,27 @@ package ru.noties.markwon.view.debug; import android.content.Context; import android.support.annotation.NonNull; -import ru.noties.markwon.SpannableConfiguration; -import ru.noties.markwon.spans.SpannableTheme; +import ru.noties.markwon.MarkwonConfiguration; +import ru.noties.markwon.spans.MarkwonTheme; import ru.noties.markwon.view.IMarkwonView; public class DebugConfigurationProvider implements IMarkwonView.ConfigurationProvider { - private SpannableConfiguration cached; + private MarkwonConfiguration cached; @NonNull @Override - public SpannableConfiguration provide(@NonNull Context context) { + public MarkwonConfiguration provide(@NonNull Context context) { if (cached == null) { - cached = SpannableConfiguration.builder(context) + cached = MarkwonConfiguration.builder(context) .theme(debugTheme(context)) .build(); } return cached; } - private static SpannableTheme debugTheme(@NonNull Context context) { - return SpannableTheme.builderWithDefaults(context) + private static MarkwonTheme debugTheme(@NonNull Context context) { + return MarkwonTheme.builderWithDefaults(context) .blockQuoteColor(0xFFff0000) .codeBackgroundColor(0x40FF0000) .build(); diff --git a/markwon-view/src/main/java/ru/noties/markwon/view/IMarkwonView.java b/markwon-view/src/main/java/ru/noties/markwon/view/IMarkwonView.java index 4db6d2c7..bc66e8b8 100644 --- a/markwon-view/src/main/java/ru/noties/markwon/view/IMarkwonView.java +++ b/markwon-view/src/main/java/ru/noties/markwon/view/IMarkwonView.java @@ -4,19 +4,19 @@ import android.content.Context; import android.support.annotation.NonNull; import android.support.annotation.Nullable; -import ru.noties.markwon.SpannableConfiguration; +import ru.noties.markwon.MarkwonConfiguration; public interface IMarkwonView { interface ConfigurationProvider { @NonNull - SpannableConfiguration provide(@NonNull Context context); + MarkwonConfiguration provide(@NonNull Context context); } void setConfigurationProvider(@NonNull ConfigurationProvider provider); void setMarkdown(@Nullable String markdown); - void setMarkdown(@Nullable SpannableConfiguration configuration, @Nullable String markdown); + void setMarkdown(@Nullable MarkwonConfiguration configuration, @Nullable String markdown); @Nullable String getMarkdown(); diff --git a/markwon-view/src/main/java/ru/noties/markwon/view/MarkwonView.java b/markwon-view/src/main/java/ru/noties/markwon/view/MarkwonView.java index 19ab09b0..f42d8727 100644 --- a/markwon-view/src/main/java/ru/noties/markwon/view/MarkwonView.java +++ b/markwon-view/src/main/java/ru/noties/markwon/view/MarkwonView.java @@ -7,7 +7,7 @@ import android.support.annotation.Nullable; import android.util.AttributeSet; import android.widget.TextView; -import ru.noties.markwon.SpannableConfiguration; +import ru.noties.markwon.MarkwonConfiguration; @SuppressLint("AppCompatCustomView") public class MarkwonView extends TextView implements IMarkwonView { @@ -38,7 +38,7 @@ public class MarkwonView extends TextView implements IMarkwonView { helper.setMarkdown(markdown); } - public void setMarkdown(@Nullable SpannableConfiguration configuration, @Nullable String markdown) { + public void setMarkdown(@Nullable MarkwonConfiguration configuration, @Nullable String markdown) { helper.setMarkdown(configuration, markdown); } diff --git a/markwon-view/src/main/java/ru/noties/markwon/view/MarkwonViewCompat.java b/markwon-view/src/main/java/ru/noties/markwon/view/MarkwonViewCompat.java index da5c1934..b30ecaa2 100644 --- a/markwon-view/src/main/java/ru/noties/markwon/view/MarkwonViewCompat.java +++ b/markwon-view/src/main/java/ru/noties/markwon/view/MarkwonViewCompat.java @@ -6,7 +6,7 @@ import android.support.annotation.Nullable; import android.support.v7.widget.AppCompatTextView; import android.util.AttributeSet; -import ru.noties.markwon.SpannableConfiguration; +import ru.noties.markwon.MarkwonConfiguration; public class MarkwonViewCompat extends AppCompatTextView implements IMarkwonView { @@ -38,7 +38,7 @@ public class MarkwonViewCompat extends AppCompatTextView implements IMarkwonView } @Override - public void setMarkdown(@Nullable SpannableConfiguration configuration, @Nullable String markdown) { + public void setMarkdown(@Nullable MarkwonConfiguration configuration, @Nullable String markdown) { helper.setMarkdown(configuration, markdown); } diff --git a/markwon-view/src/main/java/ru/noties/markwon/view/MarkwonViewHelper.java b/markwon-view/src/main/java/ru/noties/markwon/view/MarkwonViewHelper.java index c8c813f6..f3669b77 100644 --- a/markwon-view/src/main/java/ru/noties/markwon/view/MarkwonViewHelper.java +++ b/markwon-view/src/main/java/ru/noties/markwon/view/MarkwonViewHelper.java @@ -9,7 +9,7 @@ import android.util.AttributeSet; import android.widget.TextView; import ru.noties.markwon.Markwon; -import ru.noties.markwon.SpannableConfiguration; +import ru.noties.markwon.MarkwonConfiguration; public class MarkwonViewHelper implements IMarkwonView { @@ -21,7 +21,7 @@ public class MarkwonViewHelper implements IMarkwonView { private ConfigurationProvider provider; - private SpannableConfiguration configuration; + private MarkwonConfiguration configuration; private String markdown; private MarkwonViewHelper(@NonNull TextView textView) { @@ -71,14 +71,14 @@ public class MarkwonViewHelper implements IMarkwonView { } @Override - public void setMarkdown(@Nullable SpannableConfiguration configuration, @Nullable String markdown) { + public void setMarkdown(@Nullable MarkwonConfiguration configuration, @Nullable String markdown) { this.markdown = markdown; if (configuration == null) { if (this.configuration == null) { if (provider != null) { this.configuration = provider.provide(textView.getContext()); } else { - this.configuration = SpannableConfiguration.create(textView.getContext()); + this.configuration = MarkwonConfiguration.create(textView.getContext()); } } configuration = this.configuration; diff --git a/markwon/src/main/java/ru/noties/markwon/AbstractMarkwonPlugin.java b/markwon/src/main/java/ru/noties/markwon/AbstractMarkwonPlugin.java new file mode 100644 index 00000000..d96e2a6d --- /dev/null +++ b/markwon/src/main/java/ru/noties/markwon/AbstractMarkwonPlugin.java @@ -0,0 +1,46 @@ +package ru.noties.markwon; + +import android.support.annotation.NonNull; +import android.widget.TextView; + +import org.commonmark.parser.Parser; + +import ru.noties.markwon.spans.MarkwonTheme; + +public abstract class AbstractMarkwonPlugin implements MarkwonPlugin { + @Override + public void configureParser(@NonNull Parser.Builder builder) { + + } + + @Override + public void configureTheme(@NonNull MarkwonTheme.Builder builder) { + + } + + @Override + public void configureConfiguration(@NonNull MarkwonConfiguration.Builder builder) { + + } + + @Override + public void configureVisitor(@NonNull MarkwonVisitor.Builder builder) { + + } + + @NonNull + @Override + public String processMarkdown(@NonNull String markdown) { + return markdown; + } + + @Override + public void beforeSetText(@NonNull TextView textView, @NonNull SpannableBuilder builder) { + + } + + @Override + public void afterSetText(@NonNull TextView textView, @NonNull SpannableBuilder builder) { + + } +} diff --git a/markwon/src/main/java/ru/noties/markwon/Markwon.java b/markwon/src/main/java/ru/noties/markwon/Markwon.java index 6ecb9ca8..c7fdf550 100644 --- a/markwon/src/main/java/ru/noties/markwon/Markwon.java +++ b/markwon/src/main/java/ru/noties/markwon/Markwon.java @@ -40,11 +40,11 @@ public abstract class Markwon { } /** - * @see #setMarkdown(TextView, SpannableConfiguration, String) + * @see #setMarkdown(TextView, MarkwonConfiguration, String) * @since 1.0.0 */ public static void setMarkdown(@NonNull TextView view, @NonNull String markdown) { - setMarkdown(view, SpannableConfiguration.create(view.getContext()), markdown); + setMarkdown(view, MarkwonConfiguration.create(view.getContext()), markdown); } /** @@ -52,16 +52,16 @@ public abstract class Markwon { * and applies it to view * * @param view {@link TextView} to set markdown into - * @param configuration a {@link SpannableConfiguration} instance + * @param configuration a {@link MarkwonConfiguration} instance * @param markdown raw markdown String (for example: {@code `**Hello**`}) - * @see #markdown(SpannableConfiguration, String) + * @see #markdown(MarkwonConfiguration, String) * @see #setText(TextView, CharSequence) - * @see SpannableConfiguration + * @see MarkwonConfiguration * @since 1.0.0 */ public static void setMarkdown( @NonNull TextView view, - @NonNull SpannableConfiguration configuration, + @NonNull MarkwonConfiguration configuration, @NonNull String markdown ) { @@ -117,7 +117,7 @@ public abstract class Markwon { } /** - * Returns parsed markdown with default {@link SpannableConfiguration} obtained from {@link Context} + * Returns parsed markdown with default {@link MarkwonConfiguration} obtained from {@link Context} * * @param context {@link Context} * @param markdown raw markdown @@ -126,21 +126,21 @@ public abstract class Markwon { */ @NonNull public static CharSequence markdown(@NonNull Context context, @NonNull String markdown) { - final SpannableConfiguration configuration = SpannableConfiguration.create(context); + final MarkwonConfiguration configuration = MarkwonConfiguration.create(context); return markdown(configuration, markdown); } /** - * Returns parsed markdown with provided {@link SpannableConfiguration} + * Returns parsed markdown with provided {@link MarkwonConfiguration} * - * @param configuration a {@link SpannableConfiguration} + * @param configuration a {@link MarkwonConfiguration} * @param markdown raw markdown * @return parsed markdown - * @see SpannableConfiguration + * @see MarkwonConfiguration * @since 1.0.0 */ @NonNull - public static CharSequence markdown(@NonNull SpannableConfiguration configuration, @NonNull String markdown) { + public static CharSequence markdown(@NonNull MarkwonConfiguration configuration, @NonNull String markdown) { final Parser parser = createParser(); final Node node = parser.parse(markdown); final SpannableRenderer renderer = new SpannableRenderer(); diff --git a/markwon/src/main/java/ru/noties/markwon/Markwon2.java b/markwon/src/main/java/ru/noties/markwon/Markwon2.java new file mode 100644 index 00000000..59feb9a5 --- /dev/null +++ b/markwon/src/main/java/ru/noties/markwon/Markwon2.java @@ -0,0 +1,32 @@ +package ru.noties.markwon; + +import android.content.Context; +import android.support.annotation.NonNull; + +import org.commonmark.node.Node; + +public abstract class Markwon2 { + + @NonNull + public static Builder builder(@NonNull Context context) { + return new MarkwonBuilderImpl(context); + } + + @NonNull + public abstract Node parse(@NonNull String input); + + @NonNull + public abstract CharSequence render(@NonNull Node node); + + @NonNull + public abstract CharSequence markdown(@NonNull String input); + + public interface Builder { + + @NonNull + Builder use(@NonNull MarkwonPlugin plugin); + + @NonNull + Markwon2 build(); + } +} diff --git a/markwon/src/main/java/ru/noties/markwon/MarkwonBuilderImpl.java b/markwon/src/main/java/ru/noties/markwon/MarkwonBuilderImpl.java new file mode 100644 index 00000000..635e3138 --- /dev/null +++ b/markwon/src/main/java/ru/noties/markwon/MarkwonBuilderImpl.java @@ -0,0 +1,53 @@ +package ru.noties.markwon; + +import android.content.Context; +import android.support.annotation.NonNull; + +import org.commonmark.parser.Parser; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +import ru.noties.markwon.spans.MarkwonTheme; + +class MarkwonBuilderImpl implements Markwon2.Builder { + + private final Context context; + + private final List plugins = new ArrayList<>(3); + + MarkwonBuilderImpl(@NonNull Context context) { + this.context = context; + } + + @NonNull + @Override + public Markwon2.Builder use(@NonNull MarkwonPlugin plugin) { + plugins.add(plugin); + return this; + } + + @NonNull + @Override + public Markwon2 build() { + + final Parser.Builder parserBuilder = new Parser.Builder(); + final MarkwonTheme.Builder themeBuilder = MarkwonTheme.builderWithDefaults(context); + final MarkwonConfiguration.Builder configurationBuilder = new MarkwonConfiguration.Builder(context); + final MarkwonVisitor.Builder visitorBuilder = new MarkwonVisitorImpl.BuilderImpl(); + + for (MarkwonPlugin plugin : plugins) { + plugin.configureParser(parserBuilder); + plugin.configureTheme(themeBuilder); + plugin.configureConfiguration(configurationBuilder); + plugin.configureVisitor(visitorBuilder); + } + + return new MarkwonImpl( + parserBuilder.build(), + visitorBuilder.build(themeBuilder.build(), configurationBuilder.build()), + Collections.unmodifiableList(plugins) + ); + } +} diff --git a/markwon/src/main/java/ru/noties/markwon/SpannableConfiguration.java b/markwon/src/main/java/ru/noties/markwon/MarkwonConfiguration.java similarity index 92% rename from markwon/src/main/java/ru/noties/markwon/SpannableConfiguration.java rename to markwon/src/main/java/ru/noties/markwon/MarkwonConfiguration.java index 5df9d316..510f10b1 100644 --- a/markwon/src/main/java/ru/noties/markwon/SpannableConfiguration.java +++ b/markwon/src/main/java/ru/noties/markwon/MarkwonConfiguration.java @@ -9,14 +9,17 @@ import ru.noties.markwon.renderer.ImageSizeResolverDef; import ru.noties.markwon.renderer.html2.MarkwonHtmlRenderer; import ru.noties.markwon.spans.AsyncDrawable; import ru.noties.markwon.spans.LinkSpan; -import ru.noties.markwon.spans.SpannableTheme; +import ru.noties.markwon.spans.MarkwonTheme; +/** + * since 3.0.0 renamed `SpannableConfiguration` -> `MarkwonConfiguration` + */ @SuppressWarnings("WeakerAccess") -public class SpannableConfiguration { +public class MarkwonConfiguration { // creates default configuration @NonNull - public static SpannableConfiguration create(@NonNull Context context) { + public static MarkwonConfiguration create(@NonNull Context context) { return new Builder(context).build(); } @@ -25,7 +28,9 @@ public class SpannableConfiguration { return new Builder(context); } - private final SpannableTheme theme; + @Deprecated + private final MarkwonTheme theme; + private final AsyncDrawable.Loader asyncDrawableLoader; private final SyntaxHighlight syntaxHighlight; private final LinkSpan.Resolver linkResolver; @@ -37,7 +42,7 @@ public class SpannableConfiguration { private final MarkwonHtmlRenderer htmlRenderer; // @since 2.0.0 private final boolean htmlAllowNonClosedTags; // @since 2.0.0 - private SpannableConfiguration(@NonNull Builder builder) { + private MarkwonConfiguration(@NonNull Builder builder) { this.theme = builder.theme; this.asyncDrawableLoader = builder.asyncDrawableLoader; this.syntaxHighlight = builder.syntaxHighlight; @@ -60,7 +65,8 @@ public class SpannableConfiguration { } @NonNull - public SpannableTheme theme() { + @Deprecated + public MarkwonTheme theme() { return theme; } @@ -130,7 +136,9 @@ public class SpannableConfiguration { public static class Builder { private final Context context; - private SpannableTheme theme; + + @Deprecated + private MarkwonTheme theme; private AsyncDrawable.Loader asyncDrawableLoader; private SyntaxHighlight syntaxHighlight; private LinkSpan.Resolver linkResolver; @@ -146,7 +154,7 @@ public class SpannableConfiguration { this.context = context; } - Builder(@NonNull Context context, @NonNull SpannableConfiguration configuration) { + Builder(@NonNull Context context, @NonNull MarkwonConfiguration configuration) { this(context); this.theme = configuration.theme; this.asyncDrawableLoader = configuration.asyncDrawableLoader; @@ -162,7 +170,8 @@ public class SpannableConfiguration { } @NonNull - public Builder theme(@NonNull SpannableTheme theme) { + @Deprecated + public Builder theme(@NonNull MarkwonTheme theme) { this.theme = theme; return this; } @@ -254,10 +263,10 @@ public class SpannableConfiguration { } @NonNull - public SpannableConfiguration build() { + public MarkwonConfiguration build() { if (theme == null) { - theme = SpannableTheme.create(context); + theme = MarkwonTheme.create(context); } if (asyncDrawableLoader == null) { @@ -300,7 +309,7 @@ public class SpannableConfiguration { htmlRenderer = MarkwonHtmlRenderer.create(); } - return new SpannableConfiguration(this); + return new MarkwonConfiguration(this); } } diff --git a/markwon/src/main/java/ru/noties/markwon/MarkwonImpl.java b/markwon/src/main/java/ru/noties/markwon/MarkwonImpl.java new file mode 100644 index 00000000..0252568e --- /dev/null +++ b/markwon/src/main/java/ru/noties/markwon/MarkwonImpl.java @@ -0,0 +1,48 @@ +package ru.noties.markwon; + +import android.support.annotation.NonNull; + +import org.commonmark.node.Node; +import org.commonmark.parser.Parser; + +import java.util.List; + +class MarkwonImpl extends Markwon2 { + + private final Parser parser; + private final MarkwonVisitor visitor; + private final List plugins; + + MarkwonImpl( + @NonNull Parser parser, + @NonNull MarkwonVisitor visitor, + @NonNull List plugins) { + this.parser = parser; + this.visitor = visitor; + this.plugins = plugins; + } + + @NonNull + @Override + public Node parse(@NonNull String input) { + + for (MarkwonPlugin plugin : plugins) { + input = plugin.processMarkdown(input); + } + + return parser.parse(input); + } + + @NonNull + @Override + public CharSequence render(@NonNull Node node) { + node.accept(visitor); + return visitor.builder().text(); + } + + @NonNull + @Override + public CharSequence markdown(@NonNull String input) { + return render(parse(input)); + } +} diff --git a/markwon/src/main/java/ru/noties/markwon/MarkwonPlugin.java b/markwon/src/main/java/ru/noties/markwon/MarkwonPlugin.java new file mode 100644 index 00000000..48e3674e --- /dev/null +++ b/markwon/src/main/java/ru/noties/markwon/MarkwonPlugin.java @@ -0,0 +1,29 @@ +package ru.noties.markwon; + +import android.support.annotation.NonNull; +import android.widget.TextView; + +import org.commonmark.parser.Parser; + +import ru.noties.markwon.spans.MarkwonTheme; + +public interface MarkwonPlugin { + + void configureParser(@NonNull Parser.Builder builder); + + void configureTheme(@NonNull MarkwonTheme.Builder builder); + + void configureConfiguration(@NonNull MarkwonConfiguration.Builder builder); + + void configureVisitor(@NonNull MarkwonVisitor.Builder builder); + + // images + // html + + @NonNull + String processMarkdown(@NonNull String markdown); + + void beforeSetText(@NonNull TextView textView, @NonNull SpannableBuilder builder); + + void afterSetText(@NonNull TextView textView, @NonNull SpannableBuilder builder); +} diff --git a/markwon/src/main/java/ru/noties/markwon/MarkwonVisitor.java b/markwon/src/main/java/ru/noties/markwon/MarkwonVisitor.java new file mode 100644 index 00000000..58c28111 --- /dev/null +++ b/markwon/src/main/java/ru/noties/markwon/MarkwonVisitor.java @@ -0,0 +1,60 @@ +package ru.noties.markwon; + +import android.support.annotation.NonNull; +import android.support.annotation.Nullable; + +import org.commonmark.node.Node; +import org.commonmark.node.Visitor; + +import ru.noties.markwon.spans.MarkwonTheme; + +public interface MarkwonVisitor extends Visitor { + + interface NodeVisitor { + void visit(@NonNull MarkwonVisitor visitor, @NonNull N n); + } + + interface Builder { + + @NonNull + Builder on(@NonNull Class node, @NonNull NodeVisitor nodeVisitor); + + @NonNull + MarkwonVisitor build(@NonNull MarkwonTheme theme, @NonNull MarkwonConfiguration configuration); + } + + @NonNull + MarkwonTheme theme(); + + @NonNull + MarkwonConfiguration configuration(); + + @NonNull + SpannableBuilder builder(); + + void visitChildren(@NonNull Node node); + + boolean hasNext(@NonNull Node node); + + void incrementBlockQuoteIndent(); + + void decrementBlockQuoteIndent(); + + void blockQuoteIntent(int blockQuoteIndent); + + int blockQuoteIndent(); + + void incrementListLevel(); + + void decrementListLevel(); + + int listLevel(); + + void ensureNewLine(); + + void forceNewLine(); + + int length(); + + void setSpans(int start, @Nullable Object spans); +} diff --git a/markwon/src/main/java/ru/noties/markwon/MarkwonVisitorImpl.java b/markwon/src/main/java/ru/noties/markwon/MarkwonVisitorImpl.java new file mode 100644 index 00000000..55971a38 --- /dev/null +++ b/markwon/src/main/java/ru/noties/markwon/MarkwonVisitorImpl.java @@ -0,0 +1,290 @@ +package ru.noties.markwon; + +import android.support.annotation.NonNull; +import android.support.annotation.Nullable; + +import org.commonmark.node.BlockQuote; +import org.commonmark.node.BulletList; +import org.commonmark.node.Code; +import org.commonmark.node.CustomBlock; +import org.commonmark.node.CustomNode; +import org.commonmark.node.Document; +import org.commonmark.node.Emphasis; +import org.commonmark.node.FencedCodeBlock; +import org.commonmark.node.HardLineBreak; +import org.commonmark.node.Heading; +import org.commonmark.node.HtmlBlock; +import org.commonmark.node.HtmlInline; +import org.commonmark.node.Image; +import org.commonmark.node.IndentedCodeBlock; +import org.commonmark.node.Link; +import org.commonmark.node.ListItem; +import org.commonmark.node.Node; +import org.commonmark.node.OrderedList; +import org.commonmark.node.Paragraph; +import org.commonmark.node.SoftLineBreak; +import org.commonmark.node.StrongEmphasis; +import org.commonmark.node.Text; +import org.commonmark.node.ThematicBreak; + +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; + +import ru.noties.markwon.spans.MarkwonTheme; + +class MarkwonVisitorImpl implements MarkwonVisitor { + + private final Map, NodeVisitor> nodes; + + private final MarkwonTheme theme; + private final MarkwonConfiguration configuration; + private final SpannableBuilder builder = new SpannableBuilder(); + + private int blockQuoteIndent; + private int listLevel; + + private MarkwonVisitorImpl( + @NonNull MarkwonTheme theme, + @NonNull MarkwonConfiguration configuration, + @NonNull Map, NodeVisitor> nodes) { + this.theme = theme; + this.configuration = configuration; + this.nodes = nodes; + } + + @Override + public void visit(BlockQuote blockQuote) { + visit((Node) blockQuote); + } + + @Override + public void visit(BulletList bulletList) { + visit((Node) bulletList); + } + + @Override + public void visit(Code code) { + visit((Node) code); + } + + @Override + public void visit(Document document) { + visit((Node) document); + } + + @Override + public void visit(Emphasis emphasis) { + visit((Node) emphasis); + } + + @Override + public void visit(FencedCodeBlock fencedCodeBlock) { + visit((Node) fencedCodeBlock); + } + + @Override + public void visit(HardLineBreak hardLineBreak) { + visit((Node) hardLineBreak); + } + + @Override + public void visit(Heading heading) { + visit((Node) heading); + } + + @Override + public void visit(ThematicBreak thematicBreak) { + visit((Node) thematicBreak); + } + + @Override + public void visit(HtmlInline htmlInline) { + visit((Node) htmlInline); + } + + @Override + public void visit(HtmlBlock htmlBlock) { + visit((Node) htmlBlock); + } + + @Override + public void visit(Image image) { + visit((Node) image); + } + + @Override + public void visit(IndentedCodeBlock indentedCodeBlock) { + visit((Node) indentedCodeBlock); + } + + @Override + public void visit(Link link) { + visit((Node) link); + } + + @Override + public void visit(ListItem listItem) { + visit((Node) listItem); + } + + @Override + public void visit(OrderedList orderedList) { + visit((Node) orderedList); + } + + @Override + public void visit(Paragraph paragraph) { + visit((Node) paragraph); + } + + @Override + public void visit(SoftLineBreak softLineBreak) { + visit((Node) softLineBreak); + } + + @Override + public void visit(StrongEmphasis strongEmphasis) { + visit((Node) strongEmphasis); + } + + @Override + public void visit(Text text) { + visit((Node) text); + } + + @Override + public void visit(CustomBlock customBlock) { + visit((Node) customBlock); + } + + @Override + public void visit(CustomNode customNode) { + visit((Node) customNode); + } + + private void visit(@NonNull Node node) { + //noinspection unchecked + final NodeVisitor nodeVisitor = (NodeVisitor) nodes.get(node.getClass()); + if (nodeVisitor != null) { + nodeVisitor.visit(this, node); + } else { + visitChildren(node); + } + } + + @NonNull + @Override + public MarkwonTheme theme() { + return theme; + } + + @NonNull + @Override + public MarkwonConfiguration configuration() { + return configuration; + } + + @NonNull + @Override + public SpannableBuilder builder() { + return builder; + } + + @Override + public void visitChildren(@NonNull Node parent) { + Node node = parent.getFirstChild(); + while (node != null) { + // A subclass of this visitor might modify the node, resulting in getNext returning a different node or no + // node after visiting it. So get the next node before visiting. + Node next = node.getNext(); + node.accept(this); + node = next; + } + } + + @Override + public boolean hasNext(@NonNull Node node) { + return node.getNext() != null; + } + + @Override + public void incrementBlockQuoteIndent() { + blockQuoteIndent += 1; + } + + @Override + public void decrementBlockQuoteIndent() { + blockQuoteIndent -= 1; + } + + @Override + public void blockQuoteIntent(int blockQuoteIndent) { + this.blockQuoteIndent = blockQuoteIndent; + } + + @Override + public int blockQuoteIndent() { + return blockQuoteIndent; + } + + @Override + public void incrementListLevel() { + listLevel += 1; + } + + @Override + public void decrementListLevel() { + listLevel -= 1; + } + + @Override + public int listLevel() { + return listLevel; + } + + @Override + public void ensureNewLine() { + if (builder.length() > 0 + && '\n' != builder.lastChar()) { + builder.append('\n'); + } + } + + @Override + public void forceNewLine() { + builder.append('\n'); + } + + @Override + public int length() { + return builder.length(); + } + + @Override + public void setSpans(int start, @Nullable Object spans) { + SpannableBuilder.setSpans(builder, spans, start, builder.length()); + } + + static class BuilderImpl implements Builder { + + private final Map, NodeVisitor> nodes = + new HashMap<>(3); + + @NonNull + @Override + public Builder on(@NonNull Class node, @NonNull NodeVisitor nodeVisitor) { + nodes.put(node, nodeVisitor); + return this; + } + + @NonNull + @Override + public MarkwonVisitor build(@NonNull MarkwonTheme theme, @NonNull MarkwonConfiguration configuration) { + return new MarkwonVisitorImpl( + theme, + configuration, + Collections.unmodifiableMap(nodes)); + } + } +} diff --git a/markwon/src/main/java/ru/noties/markwon/SpannableBuilder.java b/markwon/src/main/java/ru/noties/markwon/SpannableBuilder.java index 07e2bb85..556d7cc7 100644 --- a/markwon/src/main/java/ru/noties/markwon/SpannableBuilder.java +++ b/markwon/src/main/java/ru/noties/markwon/SpannableBuilder.java @@ -208,7 +208,7 @@ public class SpannableBuilder implements Appendable, CharSequence { /** * This method will return all {@link Span} spans that overlap specified range, * so if for example a 1..9 range is specified some spans might have 0..6 or 0..10 start/end ranges. - * NB spans are returned in reversed order (no in order that we store them internally) + * NB spans are returned in reversed order (not in order that we store them internally) * * @since 2.0.1 */ diff --git a/markwon/src/main/java/ru/noties/markwon/SpannableFactory.java b/markwon/src/main/java/ru/noties/markwon/SpannableFactory.java index 472261a4..6297b777 100644 --- a/markwon/src/main/java/ru/noties/markwon/SpannableFactory.java +++ b/markwon/src/main/java/ru/noties/markwon/SpannableFactory.java @@ -9,7 +9,7 @@ import ru.noties.markwon.renderer.ImageSize; import ru.noties.markwon.renderer.ImageSizeResolver; import ru.noties.markwon.spans.AsyncDrawable; import ru.noties.markwon.spans.LinkSpan; -import ru.noties.markwon.spans.SpannableTheme; +import ru.noties.markwon.spans.MarkwonTheme; import ru.noties.markwon.spans.TableRowSpan; /** @@ -26,32 +26,32 @@ public interface SpannableFactory { Object emphasis(); @Nullable - Object blockQuote(@NonNull SpannableTheme theme); + Object blockQuote(@NonNull MarkwonTheme theme); @Nullable - Object code(@NonNull SpannableTheme theme, boolean multiline); + Object code(@NonNull MarkwonTheme theme, boolean multiline); @Nullable - Object orderedListItem(@NonNull SpannableTheme theme, int startNumber); + Object orderedListItem(@NonNull MarkwonTheme theme, int startNumber); @Nullable - Object bulletListItem(@NonNull SpannableTheme theme, int level); + Object bulletListItem(@NonNull MarkwonTheme theme, int level); @Nullable - Object thematicBreak(@NonNull SpannableTheme theme); + Object thematicBreak(@NonNull MarkwonTheme theme); @Nullable - Object heading(@NonNull SpannableTheme theme, int level); + Object heading(@NonNull MarkwonTheme theme, int level); @Nullable Object strikethrough(); @Nullable - Object taskListItem(@NonNull SpannableTheme theme, int blockIndent, boolean isDone); + Object taskListItem(@NonNull MarkwonTheme theme, int blockIndent, boolean isDone); @Nullable Object tableRow( - @NonNull SpannableTheme theme, + @NonNull MarkwonTheme theme, @NonNull List cells, boolean isHeader, boolean isOdd); @@ -64,7 +64,7 @@ public interface SpannableFactory { @Nullable Object image( - @NonNull SpannableTheme theme, + @NonNull MarkwonTheme theme, @NonNull String destination, @NonNull AsyncDrawable.Loader loader, @NonNull ImageSizeResolver imageSizeResolver, @@ -73,17 +73,17 @@ public interface SpannableFactory { @Nullable Object link( - @NonNull SpannableTheme theme, + @NonNull MarkwonTheme theme, @NonNull String destination, @NonNull LinkSpan.Resolver resolver); // Currently used by HTML parser @Nullable - Object superScript(@NonNull SpannableTheme theme); + Object superScript(@NonNull MarkwonTheme theme); // Currently used by HTML parser @Nullable - Object subScript(@NonNull SpannableTheme theme); + Object subScript(@NonNull MarkwonTheme theme); // Currently used by HTML parser @Nullable diff --git a/markwon/src/main/java/ru/noties/markwon/SpannableFactoryDef.java b/markwon/src/main/java/ru/noties/markwon/SpannableFactoryDef.java index ee553329..6fc8d48f 100644 --- a/markwon/src/main/java/ru/noties/markwon/SpannableFactoryDef.java +++ b/markwon/src/main/java/ru/noties/markwon/SpannableFactoryDef.java @@ -18,12 +18,12 @@ import ru.noties.markwon.spans.EmphasisSpan; import ru.noties.markwon.spans.HeadingSpan; import ru.noties.markwon.spans.LinkSpan; import ru.noties.markwon.spans.OrderedListItemSpan; -import ru.noties.markwon.spans.SpannableTheme; +import ru.noties.markwon.spans.MarkwonTheme; import ru.noties.markwon.spans.StrongEmphasisSpan; import ru.noties.markwon.spans.SubScriptSpan; import ru.noties.markwon.spans.SuperScriptSpan; import ru.noties.markwon.spans.TableRowSpan; -import ru.noties.markwon.spans.TaskListSpan; +import ru.noties.markwon.tasklist.TaskListSpan; import ru.noties.markwon.spans.ThematicBreakSpan; /** @@ -50,38 +50,38 @@ public class SpannableFactoryDef implements SpannableFactory { @Nullable @Override - public Object blockQuote(@NonNull SpannableTheme theme) { + public Object blockQuote(@NonNull MarkwonTheme theme) { return new BlockQuoteSpan(theme); } @Nullable @Override - public Object code(@NonNull SpannableTheme theme, boolean multiline) { + public Object code(@NonNull MarkwonTheme theme, boolean multiline) { return new CodeSpan(theme, multiline); } @Nullable @Override - public Object orderedListItem(@NonNull SpannableTheme theme, int startNumber) { + public Object orderedListItem(@NonNull MarkwonTheme theme, int startNumber) { // todo| in order to provide real RTL experience there must be a way to provide this string return new OrderedListItemSpan(theme, String.valueOf(startNumber) + "." + '\u00a0'); } @Nullable @Override - public Object bulletListItem(@NonNull SpannableTheme theme, int level) { + public Object bulletListItem(@NonNull MarkwonTheme theme, int level) { return new BulletListItemSpan(theme, level); } @Nullable @Override - public Object thematicBreak(@NonNull SpannableTheme theme) { + public Object thematicBreak(@NonNull MarkwonTheme theme) { return new ThematicBreakSpan(theme); } @Nullable @Override - public Object heading(@NonNull SpannableTheme theme, int level) { + public Object heading(@NonNull MarkwonTheme theme, int level) { return new HeadingSpan(theme, level); } @@ -93,13 +93,13 @@ public class SpannableFactoryDef implements SpannableFactory { @Nullable @Override - public Object taskListItem(@NonNull SpannableTheme theme, int blockIndent, boolean isDone) { + public Object taskListItem(@NonNull MarkwonTheme theme, int blockIndent, boolean isDone) { return new TaskListSpan(theme, blockIndent, isDone); } @Nullable @Override - public Object tableRow(@NonNull SpannableTheme theme, @NonNull List cells, boolean isHeader, boolean isOdd) { + public Object tableRow(@NonNull MarkwonTheme theme, @NonNull List cells, boolean isHeader, boolean isOdd) { return new TableRowSpan(theme, cells, isHeader, isOdd); } @@ -114,7 +114,7 @@ public class SpannableFactoryDef implements SpannableFactory { @Nullable @Override - public Object image(@NonNull SpannableTheme theme, @NonNull String destination, @NonNull AsyncDrawable.Loader loader, @NonNull ImageSizeResolver imageSizeResolver, @Nullable ImageSize imageSize, boolean replacementTextIsLink) { + public Object image(@NonNull MarkwonTheme theme, @NonNull String destination, @NonNull AsyncDrawable.Loader loader, @NonNull ImageSizeResolver imageSizeResolver, @Nullable ImageSize imageSize, boolean replacementTextIsLink) { return new AsyncDrawableSpan( theme, new AsyncDrawable( @@ -130,18 +130,18 @@ public class SpannableFactoryDef implements SpannableFactory { @Nullable @Override - public Object link(@NonNull SpannableTheme theme, @NonNull String destination, @NonNull LinkSpan.Resolver resolver) { + public Object link(@NonNull MarkwonTheme theme, @NonNull String destination, @NonNull LinkSpan.Resolver resolver) { return new LinkSpan(theme, destination, resolver); } @Nullable @Override - public Object superScript(@NonNull SpannableTheme theme) { + public Object superScript(@NonNull MarkwonTheme theme) { return new SuperScriptSpan(theme); } @Override - public Object subScript(@NonNull SpannableTheme theme) { + public Object subScript(@NonNull MarkwonTheme theme) { return new SubScriptSpan(theme); } diff --git a/markwon/src/main/java/ru/noties/markwon/core/CorePlugin.java b/markwon/src/main/java/ru/noties/markwon/core/CorePlugin.java new file mode 100644 index 00000000..a1495c67 --- /dev/null +++ b/markwon/src/main/java/ru/noties/markwon/core/CorePlugin.java @@ -0,0 +1,268 @@ +package ru.noties.markwon.core; + +import android.support.annotation.NonNull; +import android.support.annotation.Nullable; +import android.widget.TextView; + +import org.commonmark.node.BlockQuote; +import org.commonmark.node.BulletList; +import org.commonmark.node.Code; +import org.commonmark.node.Emphasis; +import org.commonmark.node.FencedCodeBlock; +import org.commonmark.node.IndentedCodeBlock; +import org.commonmark.node.ListItem; +import org.commonmark.node.Node; +import org.commonmark.node.OrderedList; +import org.commonmark.node.StrongEmphasis; +import org.commonmark.node.Text; +import org.commonmark.node.ThematicBreak; + +import ru.noties.markwon.AbstractMarkwonPlugin; +import ru.noties.markwon.MarkwonVisitor; +import ru.noties.markwon.SpannableBuilder; +import ru.noties.markwon.spans.OrderedListItemSpan; + +public class CorePlugin extends AbstractMarkwonPlugin { + + // todo: factory. Logically it must be here only, but in order to make spans + // uniform in HTML (for example) we should expose it... Anyway, this factory _must_ + // include only _core_ spans + + // todo: softBreak adds new line should be here (or maybe removed even?) + + @Override + public void configureVisitor(@NonNull MarkwonVisitor.Builder builder) { + text(builder); + strongEmphasis(builder); + emphasis(builder); + blockQuote(builder); + code(builder); + fencedCodeBlock(builder); + indentedCodeBlock(builder); + bulletList(builder); + orderedList(builder); + listItem(builder); + thematicBreak(builder); + } + + @Override + public void beforeSetText(@NonNull TextView textView, @NonNull SpannableBuilder builder) { + OrderedListItemSpan.measure(textView, builder); + } + + protected void text(@NonNull MarkwonVisitor.Builder builder) { + builder.on(Text.class, new MarkwonVisitor.NodeVisitor() { + @Override + public void visit(@NonNull MarkwonVisitor visitor, @NonNull Text text) { + visitor.builder().append(text.getLiteral()); + } + }); + } + + protected void strongEmphasis(@NonNull MarkwonVisitor.Builder builder) { + builder.on(StrongEmphasis.class, new MarkwonVisitor.NodeVisitor() { + @Override + public void visit(@NonNull MarkwonVisitor visitor, @NonNull StrongEmphasis strongEmphasis) { + final int length = visitor.length(); + visitor.visitChildren(strongEmphasis); + visitor.setSpans(length, visitor.configuration().factory().strongEmphasis()); + } + }); + } + + protected void emphasis(@NonNull MarkwonVisitor.Builder builder) { + builder.on(Emphasis.class, new MarkwonVisitor.NodeVisitor() { + @Override + public void visit(@NonNull MarkwonVisitor visitor, @NonNull Emphasis emphasis) { + final int length = visitor.length(); + visitor.visitChildren(emphasis); + visitor.setSpans(length, visitor.configuration().factory().emphasis()); + } + }); + } + + protected void blockQuote(@NonNull MarkwonVisitor.Builder builder) { + builder.on(BlockQuote.class, new MarkwonVisitor.NodeVisitor

() { + @Override + public void visit(@NonNull MarkwonVisitor visitor, @NonNull BlockQuote blockQuote) { + + visitor.ensureNewLine(); + + if (visitor.blockQuoteIndent() > 0) { + visitor.forceNewLine(); + } + + final int length = visitor.length(); + visitor.incrementBlockQuoteIndent(); + visitor.visitChildren(blockQuote); + visitor.setSpans(length, visitor.configuration().factory().blockQuote(visitor.theme())); + visitor.decrementBlockQuoteIndent(); + + if (visitor.hasNext(blockQuote)) { + visitor.ensureNewLine(); + if (visitor.blockQuoteIndent() > 0) { + visitor.forceNewLine(); + } + } + } + }); + } + + protected void code(@NonNull MarkwonVisitor.Builder builder) { + builder.on(Code.class, new MarkwonVisitor.NodeVisitor() { + @Override + public void visit(@NonNull MarkwonVisitor visitor, @NonNull Code code) { + + final int length = visitor.length(); + + // NB, in order to provide a _padding_ feeling code is wrapped inside two unbreakable spaces + // unfortunately we cannot use this for multiline code as we cannot control where a new line break will be inserted + visitor.builder() + .append('\u00a0') + .append(code.getLiteral()) + .append('\u00a0'); + + visitor.setSpans(length, visitor.configuration().factory().code(visitor.theme(), false)); + } + }); + } + + protected void fencedCodeBlock(@NonNull MarkwonVisitor.Builder builder) { + builder.on(FencedCodeBlock.class, new MarkwonVisitor.NodeVisitor() { + @Override + public void visit(@NonNull MarkwonVisitor visitor, @NonNull FencedCodeBlock fencedCodeBlock) { + visitCodeBlock(visitor, fencedCodeBlock.getInfo(), fencedCodeBlock.getLiteral(), fencedCodeBlock); + } + }); + } + + protected void indentedCodeBlock(@NonNull MarkwonVisitor.Builder builder) { + builder.on(IndentedCodeBlock.class, new MarkwonVisitor.NodeVisitor() { + @Override + public void visit(@NonNull MarkwonVisitor visitor, @NonNull IndentedCodeBlock indentedCodeBlock) { + visitCodeBlock(visitor, null, indentedCodeBlock.getLiteral(), indentedCodeBlock); + } + }); + } + + protected void visitCodeBlock( + @NonNull MarkwonVisitor visitor, + @Nullable String info, + @NonNull String code, + @NonNull Node node) { + + visitor.ensureNewLine(); + + final int length = visitor.length(); + + visitor.builder() + .append('\u00a0').append('\n') + .append(visitor.configuration().syntaxHighlight().highlight(info, code)); + + visitor.ensureNewLine(); + + visitor.builder().append('\u00a0'); + + visitor.setSpans(length, visitor.configuration().factory().code(visitor.theme(), true)); + + if (visitor.hasNext(node)) { + visitor.ensureNewLine(); + visitor.forceNewLine(); + } + } + + protected void bulletList(@NonNull MarkwonVisitor.Builder builder) { + builder.on(BulletList.class, new MarkwonVisitor.NodeVisitor() { + @Override + public void visit(@NonNull MarkwonVisitor visitor, @NonNull BulletList bulletList) { + visitList(visitor, bulletList); + } + }); + } + + protected void orderedList(@NonNull MarkwonVisitor.Builder builder) { + builder.on(OrderedList.class, new MarkwonVisitor.NodeVisitor() { + @Override + public void visit(@NonNull MarkwonVisitor visitor, @NonNull OrderedList orderedList) { + visitList(visitor, orderedList); + } + }); + } + + protected void visitList(@NonNull MarkwonVisitor visitor, @NonNull Node node) { + + visitor.ensureNewLine(); + + visitor.visitChildren(node); + + if (visitor.hasNext(node)) { + visitor.ensureNewLine(); + if (visitor.listLevel() == 0 + && visitor.blockQuoteIndent() == 0) { + visitor.forceNewLine(); + } + } + } + + protected void listItem(@NonNull MarkwonVisitor.Builder builder) { + builder.on(ListItem.class, new MarkwonVisitor.NodeVisitor() { + @Override + public void visit(@NonNull MarkwonVisitor visitor, @NonNull ListItem listItem) { + + final int length = visitor.length(); + + visitor.incrementBlockQuoteIndent(); + visitor.incrementListLevel(); + + final Node parent = listItem.getParent(); + if (parent instanceof OrderedList) { + + final int start = ((OrderedList) parent).getStartNumber(); + + visitor.visitChildren(listItem); + visitor.setSpans(length, visitor.configuration().factory().orderedListItem(visitor.theme(), start)); + + + // after we have visited the children increment start number + final OrderedList orderedList = (OrderedList) parent; + orderedList.setStartNumber(orderedList.getStartNumber() + 1); + + } else { + + visitor.visitChildren(listItem); + visitor.setSpans(length, visitor.configuration().factory().bulletListItem(visitor.theme(), visitor.listLevel() - 1)); + + } + + visitor.decrementBlockQuoteIndent(); + visitor.decrementListLevel(); + + if (visitor.hasNext(listItem)) { + visitor.ensureNewLine(); + } + } + }); + } + + protected void thematicBreak(@NonNull MarkwonVisitor.Builder builder) { + builder.on(ThematicBreak.class, new MarkwonVisitor.NodeVisitor() { + @Override + public void visit(@NonNull MarkwonVisitor visitor, @NonNull ThematicBreak thematicBreak) { + + visitor.ensureNewLine(); + + final int length = visitor.length(); + + // without space it won't render + visitor.builder().append('\u00a0'); + + visitor.setSpans(length, visitor.configuration().factory().thematicBreak(visitor.theme())); + + if (visitor.hasNext(thematicBreak)) { + visitor.ensureNewLine(); + visitor.forceNewLine(); + } + } + }); + } +} diff --git a/markwon/src/main/java/ru/noties/markwon/renderer/SpannableMarkdownVisitor.java b/markwon/src/main/java/ru/noties/markwon/renderer/SpannableMarkdownVisitor.java index 967784e5..3ea3cc83 100644 --- a/markwon/src/main/java/ru/noties/markwon/renderer/SpannableMarkdownVisitor.java +++ b/markwon/src/main/java/ru/noties/markwon/renderer/SpannableMarkdownVisitor.java @@ -38,10 +38,10 @@ import java.util.ArrayList; import java.util.List; import ru.noties.markwon.SpannableBuilder; -import ru.noties.markwon.SpannableConfiguration; +import ru.noties.markwon.MarkwonConfiguration; import ru.noties.markwon.SpannableFactory; import ru.noties.markwon.html.api.MarkwonHtmlParser; -import ru.noties.markwon.spans.SpannableTheme; +import ru.noties.markwon.spans.MarkwonTheme; import ru.noties.markwon.spans.TableRowSpan; import ru.noties.markwon.tasklist.TaskListBlock; import ru.noties.markwon.tasklist.TaskListItem; @@ -49,11 +49,11 @@ import ru.noties.markwon.tasklist.TaskListItem; @SuppressWarnings("WeakerAccess") public class SpannableMarkdownVisitor extends AbstractVisitor { - private final SpannableConfiguration configuration; + private final MarkwonConfiguration configuration; private final SpannableBuilder builder; private final MarkwonHtmlParser htmlParser; - private final SpannableTheme theme; + private final MarkwonTheme theme; private final SpannableFactory factory; private int blockQuoteIndent; @@ -64,7 +64,7 @@ public class SpannableMarkdownVisitor extends AbstractVisitor { private int tableRows; public SpannableMarkdownVisitor( - @NonNull SpannableConfiguration configuration, + @NonNull MarkwonConfiguration configuration, @NonNull SpannableBuilder builder ) { this.configuration = configuration; diff --git a/markwon/src/main/java/ru/noties/markwon/renderer/SpannableRenderer.java b/markwon/src/main/java/ru/noties/markwon/renderer/SpannableRenderer.java index 32d620dd..b78f9c72 100644 --- a/markwon/src/main/java/ru/noties/markwon/renderer/SpannableRenderer.java +++ b/markwon/src/main/java/ru/noties/markwon/renderer/SpannableRenderer.java @@ -4,13 +4,13 @@ import android.support.annotation.NonNull; import org.commonmark.node.Node; +import ru.noties.markwon.MarkwonConfiguration; import ru.noties.markwon.SpannableBuilder; -import ru.noties.markwon.SpannableConfiguration; public class SpannableRenderer { @NonNull - public CharSequence render(@NonNull SpannableConfiguration configuration, @NonNull Node node) { + public CharSequence render(@NonNull MarkwonConfiguration configuration, @NonNull Node node) { final SpannableBuilder builder = new SpannableBuilder(); node.accept(new SpannableMarkdownVisitor(configuration, builder)); return builder.text(); diff --git a/markwon/src/main/java/ru/noties/markwon/renderer/html2/MarkwonHtmlRenderer.java b/markwon/src/main/java/ru/noties/markwon/renderer/html2/MarkwonHtmlRenderer.java index bd69445a..b66045e6 100644 --- a/markwon/src/main/java/ru/noties/markwon/renderer/html2/MarkwonHtmlRenderer.java +++ b/markwon/src/main/java/ru/noties/markwon/renderer/html2/MarkwonHtmlRenderer.java @@ -9,7 +9,7 @@ import java.util.Locale; import java.util.Map; import ru.noties.markwon.SpannableBuilder; -import ru.noties.markwon.SpannableConfiguration; +import ru.noties.markwon.MarkwonConfiguration; import ru.noties.markwon.html.api.MarkwonHtmlParser; import ru.noties.markwon.renderer.html2.tag.BlockquoteHandler; import ru.noties.markwon.renderer.html2.tag.EmphasisHandler; @@ -30,7 +30,7 @@ import ru.noties.markwon.renderer.html2.tag.UnderlineHandler; public abstract class MarkwonHtmlRenderer { public abstract void render( - @NonNull SpannableConfiguration configuration, + @NonNull MarkwonConfiguration configuration, @NonNull SpannableBuilder builder, @NonNull MarkwonHtmlParser parser ); diff --git a/markwon/src/main/java/ru/noties/markwon/renderer/html2/MarkwonHtmlRendererImpl.java b/markwon/src/main/java/ru/noties/markwon/renderer/html2/MarkwonHtmlRendererImpl.java index 6de698f5..cc34f55b 100644 --- a/markwon/src/main/java/ru/noties/markwon/renderer/html2/MarkwonHtmlRendererImpl.java +++ b/markwon/src/main/java/ru/noties/markwon/renderer/html2/MarkwonHtmlRendererImpl.java @@ -7,7 +7,7 @@ import java.util.List; import java.util.Map; import ru.noties.markwon.SpannableBuilder; -import ru.noties.markwon.SpannableConfiguration; +import ru.noties.markwon.MarkwonConfiguration; import ru.noties.markwon.html.api.HtmlTag; import ru.noties.markwon.html.api.MarkwonHtmlParser; import ru.noties.markwon.renderer.html2.tag.TagHandler; @@ -22,7 +22,7 @@ class MarkwonHtmlRendererImpl extends MarkwonHtmlRenderer { @Override public void render( - @NonNull final SpannableConfiguration configuration, + @NonNull final MarkwonConfiguration configuration, @NonNull final SpannableBuilder builder, @NonNull MarkwonHtmlParser parser) { diff --git a/markwon/src/main/java/ru/noties/markwon/renderer/html2/tag/BlockquoteHandler.java b/markwon/src/main/java/ru/noties/markwon/renderer/html2/tag/BlockquoteHandler.java index 99ddf153..9f90dc93 100644 --- a/markwon/src/main/java/ru/noties/markwon/renderer/html2/tag/BlockquoteHandler.java +++ b/markwon/src/main/java/ru/noties/markwon/renderer/html2/tag/BlockquoteHandler.java @@ -3,14 +3,14 @@ package ru.noties.markwon.renderer.html2.tag; import android.support.annotation.NonNull; import ru.noties.markwon.SpannableBuilder; -import ru.noties.markwon.SpannableConfiguration; +import ru.noties.markwon.MarkwonConfiguration; import ru.noties.markwon.html.api.HtmlTag; public class BlockquoteHandler extends TagHandler { @Override public void handle( - @NonNull SpannableConfiguration configuration, + @NonNull MarkwonConfiguration configuration, @NonNull SpannableBuilder builder, @NonNull HtmlTag tag) { diff --git a/markwon/src/main/java/ru/noties/markwon/renderer/html2/tag/EmphasisHandler.java b/markwon/src/main/java/ru/noties/markwon/renderer/html2/tag/EmphasisHandler.java index d34218de..aa452ebf 100644 --- a/markwon/src/main/java/ru/noties/markwon/renderer/html2/tag/EmphasisHandler.java +++ b/markwon/src/main/java/ru/noties/markwon/renderer/html2/tag/EmphasisHandler.java @@ -3,13 +3,13 @@ package ru.noties.markwon.renderer.html2.tag; import android.support.annotation.NonNull; import android.support.annotation.Nullable; -import ru.noties.markwon.SpannableConfiguration; +import ru.noties.markwon.MarkwonConfiguration; import ru.noties.markwon.html.api.HtmlTag; public class EmphasisHandler extends SimpleTagHandler { @Nullable @Override - public Object getSpans(@NonNull SpannableConfiguration configuration, @NonNull HtmlTag tag) { + public Object getSpans(@NonNull MarkwonConfiguration configuration, @NonNull HtmlTag tag) { return configuration.factory().emphasis(); } } diff --git a/markwon/src/main/java/ru/noties/markwon/renderer/html2/tag/HeadingHandler.java b/markwon/src/main/java/ru/noties/markwon/renderer/html2/tag/HeadingHandler.java index e2138b05..99626259 100644 --- a/markwon/src/main/java/ru/noties/markwon/renderer/html2/tag/HeadingHandler.java +++ b/markwon/src/main/java/ru/noties/markwon/renderer/html2/tag/HeadingHandler.java @@ -3,7 +3,7 @@ package ru.noties.markwon.renderer.html2.tag; import android.support.annotation.NonNull; import android.support.annotation.Nullable; -import ru.noties.markwon.SpannableConfiguration; +import ru.noties.markwon.MarkwonConfiguration; import ru.noties.markwon.html.api.HtmlTag; public class HeadingHandler extends SimpleTagHandler { @@ -16,7 +16,7 @@ public class HeadingHandler extends SimpleTagHandler { @Nullable @Override - public Object getSpans(@NonNull SpannableConfiguration configuration, @NonNull HtmlTag tag) { + public Object getSpans(@NonNull MarkwonConfiguration configuration, @NonNull HtmlTag tag) { return configuration.factory().heading(configuration.theme(), level); } } diff --git a/markwon/src/main/java/ru/noties/markwon/renderer/html2/tag/ImageHandler.java b/markwon/src/main/java/ru/noties/markwon/renderer/html2/tag/ImageHandler.java index ed5f7f3f..fe41f9d1 100644 --- a/markwon/src/main/java/ru/noties/markwon/renderer/html2/tag/ImageHandler.java +++ b/markwon/src/main/java/ru/noties/markwon/renderer/html2/tag/ImageHandler.java @@ -6,7 +6,7 @@ import android.text.TextUtils; import java.util.Map; -import ru.noties.markwon.SpannableConfiguration; +import ru.noties.markwon.MarkwonConfiguration; import ru.noties.markwon.html.api.HtmlTag; import ru.noties.markwon.renderer.ImageSize; import ru.noties.markwon.renderer.html2.CssInlineStyleParser; @@ -31,7 +31,7 @@ public class ImageHandler extends SimpleTagHandler { @Nullable @Override - public Object getSpans(@NonNull SpannableConfiguration configuration, @NonNull HtmlTag tag) { + public Object getSpans(@NonNull MarkwonConfiguration configuration, @NonNull HtmlTag tag) { final Map attributes = tag.attributes(); final String src = attributes.get("src"); diff --git a/markwon/src/main/java/ru/noties/markwon/renderer/html2/tag/LinkHandler.java b/markwon/src/main/java/ru/noties/markwon/renderer/html2/tag/LinkHandler.java index 134874b9..74ac3c00 100644 --- a/markwon/src/main/java/ru/noties/markwon/renderer/html2/tag/LinkHandler.java +++ b/markwon/src/main/java/ru/noties/markwon/renderer/html2/tag/LinkHandler.java @@ -4,13 +4,13 @@ import android.support.annotation.NonNull; import android.support.annotation.Nullable; import android.text.TextUtils; -import ru.noties.markwon.SpannableConfiguration; +import ru.noties.markwon.MarkwonConfiguration; import ru.noties.markwon.html.api.HtmlTag; public class LinkHandler extends SimpleTagHandler { @Nullable @Override - public Object getSpans(@NonNull SpannableConfiguration configuration, @NonNull HtmlTag tag) { + public Object getSpans(@NonNull MarkwonConfiguration configuration, @NonNull HtmlTag tag) { final String destination = tag.attributes().get("href"); if (!TextUtils.isEmpty(destination)) { return configuration.factory().link( diff --git a/markwon/src/main/java/ru/noties/markwon/renderer/html2/tag/ListHandler.java b/markwon/src/main/java/ru/noties/markwon/renderer/html2/tag/ListHandler.java index fca098e7..671e2297 100644 --- a/markwon/src/main/java/ru/noties/markwon/renderer/html2/tag/ListHandler.java +++ b/markwon/src/main/java/ru/noties/markwon/renderer/html2/tag/ListHandler.java @@ -2,15 +2,15 @@ package ru.noties.markwon.renderer.html2.tag; import android.support.annotation.NonNull; +import ru.noties.markwon.MarkwonConfiguration; import ru.noties.markwon.SpannableBuilder; -import ru.noties.markwon.SpannableConfiguration; import ru.noties.markwon.html.api.HtmlTag; public class ListHandler extends TagHandler { @Override public void handle( - @NonNull SpannableConfiguration configuration, + @NonNull MarkwonConfiguration configuration, @NonNull SpannableBuilder builder, @NonNull HtmlTag tag) { diff --git a/markwon/src/main/java/ru/noties/markwon/renderer/html2/tag/SimpleTagHandler.java b/markwon/src/main/java/ru/noties/markwon/renderer/html2/tag/SimpleTagHandler.java index e5940cc7..d6fd93dc 100644 --- a/markwon/src/main/java/ru/noties/markwon/renderer/html2/tag/SimpleTagHandler.java +++ b/markwon/src/main/java/ru/noties/markwon/renderer/html2/tag/SimpleTagHandler.java @@ -4,16 +4,16 @@ import android.support.annotation.NonNull; import android.support.annotation.Nullable; import ru.noties.markwon.SpannableBuilder; -import ru.noties.markwon.SpannableConfiguration; +import ru.noties.markwon.MarkwonConfiguration; import ru.noties.markwon.html.api.HtmlTag; public abstract class SimpleTagHandler extends TagHandler { @Nullable - public abstract Object getSpans(@NonNull SpannableConfiguration configuration, @NonNull HtmlTag tag); + public abstract Object getSpans(@NonNull MarkwonConfiguration configuration, @NonNull HtmlTag tag); @Override - public void handle(@NonNull SpannableConfiguration configuration, @NonNull SpannableBuilder builder, @NonNull HtmlTag tag) { + public void handle(@NonNull MarkwonConfiguration configuration, @NonNull SpannableBuilder builder, @NonNull HtmlTag tag) { final Object spans = getSpans(configuration, tag); if (spans != null) { SpannableBuilder.setSpans(builder, spans, tag.start(), tag.end()); diff --git a/markwon/src/main/java/ru/noties/markwon/renderer/html2/tag/StrikeHandler.java b/markwon/src/main/java/ru/noties/markwon/renderer/html2/tag/StrikeHandler.java index 965ddfea..9b8cad2d 100644 --- a/markwon/src/main/java/ru/noties/markwon/renderer/html2/tag/StrikeHandler.java +++ b/markwon/src/main/java/ru/noties/markwon/renderer/html2/tag/StrikeHandler.java @@ -2,15 +2,15 @@ package ru.noties.markwon.renderer.html2.tag; import android.support.annotation.NonNull; +import ru.noties.markwon.MarkwonConfiguration; import ru.noties.markwon.SpannableBuilder; -import ru.noties.markwon.SpannableConfiguration; import ru.noties.markwon.html.api.HtmlTag; public class StrikeHandler extends TagHandler { @Override public void handle( - @NonNull SpannableConfiguration configuration, + @NonNull MarkwonConfiguration configuration, @NonNull SpannableBuilder builder, @NonNull HtmlTag tag) { diff --git a/markwon/src/main/java/ru/noties/markwon/renderer/html2/tag/StrongEmphasisHandler.java b/markwon/src/main/java/ru/noties/markwon/renderer/html2/tag/StrongEmphasisHandler.java index 04d18a25..7e50bc72 100644 --- a/markwon/src/main/java/ru/noties/markwon/renderer/html2/tag/StrongEmphasisHandler.java +++ b/markwon/src/main/java/ru/noties/markwon/renderer/html2/tag/StrongEmphasisHandler.java @@ -3,13 +3,13 @@ package ru.noties.markwon.renderer.html2.tag; import android.support.annotation.NonNull; import android.support.annotation.Nullable; -import ru.noties.markwon.SpannableConfiguration; +import ru.noties.markwon.MarkwonConfiguration; import ru.noties.markwon.html.api.HtmlTag; public class StrongEmphasisHandler extends SimpleTagHandler { @Nullable @Override - public Object getSpans(@NonNull SpannableConfiguration configuration, @NonNull HtmlTag tag) { + public Object getSpans(@NonNull MarkwonConfiguration configuration, @NonNull HtmlTag tag) { return configuration.factory().strongEmphasis(); } } diff --git a/markwon/src/main/java/ru/noties/markwon/renderer/html2/tag/SubScriptHandler.java b/markwon/src/main/java/ru/noties/markwon/renderer/html2/tag/SubScriptHandler.java index a96f34bc..145cf261 100644 --- a/markwon/src/main/java/ru/noties/markwon/renderer/html2/tag/SubScriptHandler.java +++ b/markwon/src/main/java/ru/noties/markwon/renderer/html2/tag/SubScriptHandler.java @@ -3,13 +3,13 @@ package ru.noties.markwon.renderer.html2.tag; import android.support.annotation.NonNull; import android.support.annotation.Nullable; -import ru.noties.markwon.SpannableConfiguration; +import ru.noties.markwon.MarkwonConfiguration; import ru.noties.markwon.html.api.HtmlTag; public class SubScriptHandler extends SimpleTagHandler { @Nullable @Override - public Object getSpans(@NonNull SpannableConfiguration configuration, @NonNull HtmlTag tag) { + public Object getSpans(@NonNull MarkwonConfiguration configuration, @NonNull HtmlTag tag) { return configuration.factory().subScript(configuration.theme()); } } diff --git a/markwon/src/main/java/ru/noties/markwon/renderer/html2/tag/SuperScriptHandler.java b/markwon/src/main/java/ru/noties/markwon/renderer/html2/tag/SuperScriptHandler.java index c5eee815..60da420d 100644 --- a/markwon/src/main/java/ru/noties/markwon/renderer/html2/tag/SuperScriptHandler.java +++ b/markwon/src/main/java/ru/noties/markwon/renderer/html2/tag/SuperScriptHandler.java @@ -3,13 +3,13 @@ package ru.noties.markwon.renderer.html2.tag; import android.support.annotation.NonNull; import android.support.annotation.Nullable; -import ru.noties.markwon.SpannableConfiguration; +import ru.noties.markwon.MarkwonConfiguration; import ru.noties.markwon.html.api.HtmlTag; public class SuperScriptHandler extends SimpleTagHandler { @Nullable @Override - public Object getSpans(@NonNull SpannableConfiguration configuration, @NonNull HtmlTag tag) { + public Object getSpans(@NonNull MarkwonConfiguration configuration, @NonNull HtmlTag tag) { return configuration.factory().superScript(configuration.theme()); } } diff --git a/markwon/src/main/java/ru/noties/markwon/renderer/html2/tag/TagHandler.java b/markwon/src/main/java/ru/noties/markwon/renderer/html2/tag/TagHandler.java index 818c4a98..bdf9b134 100644 --- a/markwon/src/main/java/ru/noties/markwon/renderer/html2/tag/TagHandler.java +++ b/markwon/src/main/java/ru/noties/markwon/renderer/html2/tag/TagHandler.java @@ -2,20 +2,20 @@ package ru.noties.markwon.renderer.html2.tag; import android.support.annotation.NonNull; +import ru.noties.markwon.MarkwonConfiguration; import ru.noties.markwon.SpannableBuilder; -import ru.noties.markwon.SpannableConfiguration; import ru.noties.markwon.html.api.HtmlTag; public abstract class TagHandler { public abstract void handle( - @NonNull SpannableConfiguration configuration, + @NonNull MarkwonConfiguration configuration, @NonNull SpannableBuilder builder, @NonNull HtmlTag tag ); protected static void visitChildren( - @NonNull SpannableConfiguration configuration, + @NonNull MarkwonConfiguration configuration, @NonNull SpannableBuilder builder, @NonNull HtmlTag.Block block) { diff --git a/markwon/src/main/java/ru/noties/markwon/renderer/html2/tag/UnderlineHandler.java b/markwon/src/main/java/ru/noties/markwon/renderer/html2/tag/UnderlineHandler.java index ff870ef6..d9e03e2a 100644 --- a/markwon/src/main/java/ru/noties/markwon/renderer/html2/tag/UnderlineHandler.java +++ b/markwon/src/main/java/ru/noties/markwon/renderer/html2/tag/UnderlineHandler.java @@ -3,14 +3,14 @@ package ru.noties.markwon.renderer.html2.tag; import android.support.annotation.NonNull; import ru.noties.markwon.SpannableBuilder; -import ru.noties.markwon.SpannableConfiguration; +import ru.noties.markwon.MarkwonConfiguration; import ru.noties.markwon.html.api.HtmlTag; public class UnderlineHandler extends TagHandler { @Override public void handle( - @NonNull SpannableConfiguration configuration, + @NonNull MarkwonConfiguration configuration, @NonNull SpannableBuilder builder, @NonNull HtmlTag tag) { diff --git a/markwon/src/main/java/ru/noties/markwon/spans/AsyncDrawableSpan.java b/markwon/src/main/java/ru/noties/markwon/spans/AsyncDrawableSpan.java index f9bd8f33..b1ac85d4 100644 --- a/markwon/src/main/java/ru/noties/markwon/spans/AsyncDrawableSpan.java +++ b/markwon/src/main/java/ru/noties/markwon/spans/AsyncDrawableSpan.java @@ -24,24 +24,24 @@ public class AsyncDrawableSpan extends ReplacementSpan { public static final int ALIGN_BASELINE = 1; public static final int ALIGN_CENTER = 2; // will only center if drawable height is less than text line height - private final SpannableTheme theme; + private final MarkwonTheme theme; private final AsyncDrawable drawable; private final int alignment; private final boolean replacementTextIsLink; - public AsyncDrawableSpan(@NonNull SpannableTheme theme, @NonNull AsyncDrawable drawable) { + public AsyncDrawableSpan(@NonNull MarkwonTheme theme, @NonNull AsyncDrawable drawable) { this(theme, drawable, ALIGN_BOTTOM); } public AsyncDrawableSpan( - @NonNull SpannableTheme theme, + @NonNull MarkwonTheme theme, @NonNull AsyncDrawable drawable, @Alignment int alignment) { this(theme, drawable, alignment, false); } public AsyncDrawableSpan( - @NonNull SpannableTheme theme, + @NonNull MarkwonTheme theme, @NonNull AsyncDrawable drawable, @Alignment int alignment, boolean replacementTextIsLink) { diff --git a/markwon/src/main/java/ru/noties/markwon/spans/BlockQuoteSpan.java b/markwon/src/main/java/ru/noties/markwon/spans/BlockQuoteSpan.java index aa85c33c..0101199f 100644 --- a/markwon/src/main/java/ru/noties/markwon/spans/BlockQuoteSpan.java +++ b/markwon/src/main/java/ru/noties/markwon/spans/BlockQuoteSpan.java @@ -9,11 +9,11 @@ import android.text.style.LeadingMarginSpan; public class BlockQuoteSpan implements LeadingMarginSpan { - private final SpannableTheme theme; + private final MarkwonTheme theme; private final Rect rect = ObjectsPool.rect(); private final Paint paint = ObjectsPool.paint(); - public BlockQuoteSpan(@NonNull SpannableTheme theme) { + public BlockQuoteSpan(@NonNull MarkwonTheme theme) { this.theme = theme; } diff --git a/markwon/src/main/java/ru/noties/markwon/spans/BulletListItemSpan.java b/markwon/src/main/java/ru/noties/markwon/spans/BulletListItemSpan.java index 368f13a9..ee9aefc7 100644 --- a/markwon/src/main/java/ru/noties/markwon/spans/BulletListItemSpan.java +++ b/markwon/src/main/java/ru/noties/markwon/spans/BulletListItemSpan.java @@ -9,9 +9,11 @@ import android.support.annotation.NonNull; import android.text.Layout; import android.text.style.LeadingMarginSpan; +import ru.noties.markwon.utils.LeadingMarginUtils; + public class BulletListItemSpan implements LeadingMarginSpan { - private SpannableTheme theme; + private MarkwonTheme theme; private final Paint paint = ObjectsPool.paint(); private final RectF circle = ObjectsPool.rectF(); @@ -20,7 +22,7 @@ public class BulletListItemSpan implements LeadingMarginSpan { private final int level; public BulletListItemSpan( - @NonNull SpannableTheme theme, + @NonNull MarkwonTheme theme, @IntRange(from = 0) int level) { this.theme = theme; this.level = level; diff --git a/markwon/src/main/java/ru/noties/markwon/spans/CodeSpan.java b/markwon/src/main/java/ru/noties/markwon/spans/CodeSpan.java index b1098654..7488885d 100644 --- a/markwon/src/main/java/ru/noties/markwon/spans/CodeSpan.java +++ b/markwon/src/main/java/ru/noties/markwon/spans/CodeSpan.java @@ -11,13 +11,13 @@ import android.text.style.MetricAffectingSpan; public class CodeSpan extends MetricAffectingSpan implements LeadingMarginSpan { - private final SpannableTheme theme; + private final MarkwonTheme theme; private final Rect rect = ObjectsPool.rect(); private final Paint paint = ObjectsPool.paint(); private final boolean multiline; - public CodeSpan(@NonNull SpannableTheme theme, boolean multiline) { + public CodeSpan(@NonNull MarkwonTheme theme, boolean multiline) { this.theme = theme; this.multiline = multiline; } diff --git a/markwon/src/main/java/ru/noties/markwon/spans/HeadingSpan.java b/markwon/src/main/java/ru/noties/markwon/spans/HeadingSpan.java index 6293d554..c7d8d595 100644 --- a/markwon/src/main/java/ru/noties/markwon/spans/HeadingSpan.java +++ b/markwon/src/main/java/ru/noties/markwon/spans/HeadingSpan.java @@ -10,14 +10,16 @@ import android.text.TextPaint; import android.text.style.LeadingMarginSpan; import android.text.style.MetricAffectingSpan; +import ru.noties.markwon.utils.LeadingMarginUtils; + public class HeadingSpan extends MetricAffectingSpan implements LeadingMarginSpan { - private final SpannableTheme theme; + private final MarkwonTheme theme; private final Rect rect = ObjectsPool.rect(); private final Paint paint = ObjectsPool.paint(); private final int level; - public HeadingSpan(@NonNull SpannableTheme theme, @IntRange(from = 1, to = 6) int level) { + public HeadingSpan(@NonNull MarkwonTheme theme, @IntRange(from = 1, to = 6) int level) { this.theme = theme; this.level = level; } diff --git a/markwon/src/main/java/ru/noties/markwon/spans/LinkSpan.java b/markwon/src/main/java/ru/noties/markwon/spans/LinkSpan.java index 2359f9a4..f879c5ec 100644 --- a/markwon/src/main/java/ru/noties/markwon/spans/LinkSpan.java +++ b/markwon/src/main/java/ru/noties/markwon/spans/LinkSpan.java @@ -2,7 +2,6 @@ package ru.noties.markwon.spans; import android.support.annotation.NonNull; import android.text.TextPaint; -import android.text.style.ClickableSpan; import android.text.style.URLSpan; import android.view.View; @@ -12,11 +11,11 @@ public class LinkSpan extends URLSpan { void resolve(View view, @NonNull String link); } - private final SpannableTheme theme; + private final MarkwonTheme theme; private final String link; private final Resolver resolver; - public LinkSpan(@NonNull SpannableTheme theme, @NonNull String link, @NonNull Resolver resolver) { + public LinkSpan(@NonNull MarkwonTheme theme, @NonNull String link, @NonNull Resolver resolver) { super(link); this.theme = theme; this.link = link; diff --git a/markwon/src/main/java/ru/noties/markwon/spans/SpannableTheme.java b/markwon/src/main/java/ru/noties/markwon/spans/MarkwonTheme.java similarity index 97% rename from markwon/src/main/java/ru/noties/markwon/spans/SpannableTheme.java rename to markwon/src/main/java/ru/noties/markwon/spans/MarkwonTheme.java index a3ba8c55..b5c9f34a 100644 --- a/markwon/src/main/java/ru/noties/markwon/spans/SpannableTheme.java +++ b/markwon/src/main/java/ru/noties/markwon/spans/MarkwonTheme.java @@ -7,7 +7,6 @@ import android.graphics.Typeface; import android.graphics.drawable.Drawable; import android.support.annotation.AttrRes; import android.support.annotation.ColorInt; -import android.support.annotation.Dimension; import android.support.annotation.FloatRange; import android.support.annotation.IntRange; import android.support.annotation.NonNull; @@ -20,19 +19,21 @@ import android.util.TypedValue; import java.util.Arrays; import java.util.Locale; +import ru.noties.markwon.tasklist.TaskListDrawable; + @SuppressWarnings("WeakerAccess") -public class SpannableTheme { +public class MarkwonTheme { /** - * Factory method to obtain an instance of {@link SpannableTheme} with all values as defaults + * Factory method to obtain an instance of {@link MarkwonTheme} with all values as defaults * * @param context Context in order to resolve defaults - * @return {@link SpannableTheme} instance + * @return {@link MarkwonTheme} instance * @see #builderWithDefaults(Context) * @since 1.0.0 */ @NonNull - public static SpannableTheme create(@NonNull Context context) { + public static MarkwonTheme create(@NonNull Context context) { return builderWithDefaults(context).build(); } @@ -43,7 +44,7 @@ public class SpannableTheme { * * @return {@link Builder instance} * @see #builderWithDefaults(Context) - * @see #builder(SpannableTheme) + * @see #builder(MarkwonTheme) * @since 1.0.0 */ @NonNull @@ -53,15 +54,15 @@ public class SpannableTheme { /** * Factory method to create a {@link Builder} instance and initialize it with values - * from supplied {@link SpannableTheme} + * from supplied {@link MarkwonTheme} * - * @param copyFrom {@link SpannableTheme} to copy values from + * @param copyFrom {@link MarkwonTheme} to copy values from * @return {@link Builder} instance * @see #builderWithDefaults(Context) * @since 1.0.0 */ @NonNull - public static Builder builder(@NonNull SpannableTheme copyFrom) { + public static Builder builder(@NonNull MarkwonTheme copyFrom) { return new Builder(copyFrom); } @@ -217,9 +218,10 @@ public class SpannableTheme { // drawable that will be used to render checkbox (should be stateful) // TaskListDrawable can be used + @Deprecated protected final Drawable taskListDrawable; - protected SpannableTheme(@NonNull Builder builder) { + protected MarkwonTheme(@NonNull Builder builder) { this.linkColor = builder.linkColor; this.blockMargin = builder.blockMargin; this.blockQuoteWidth = builder.blockQuoteWidth; @@ -526,6 +528,7 @@ public class SpannableTheme { * @since 1.0.1 */ @Nullable + @Deprecated public Drawable getTaskListDrawable() { return taskListDrawable; } @@ -565,7 +568,7 @@ public class SpannableTheme { Builder() { } - Builder(@NonNull SpannableTheme theme) { + Builder(@NonNull MarkwonTheme theme) { this.linkColor = theme.linkColor; this.blockMargin = theme.blockMargin; this.blockQuoteWidth = theme.blockQuoteWidth; @@ -792,14 +795,15 @@ public class SpannableTheme { * @since 1.0.1 */ @NonNull + @Deprecated public Builder taskListDrawable(@NonNull Drawable taskListDrawable) { this.taskListDrawable = taskListDrawable; return this; } @NonNull - public SpannableTheme build() { - return new SpannableTheme(this); + public MarkwonTheme build() { + return new MarkwonTheme(this); } } diff --git a/markwon/src/main/java/ru/noties/markwon/spans/OrderedListItemSpan.java b/markwon/src/main/java/ru/noties/markwon/spans/OrderedListItemSpan.java index 1db29e1a..e2bc1fb0 100644 --- a/markwon/src/main/java/ru/noties/markwon/spans/OrderedListItemSpan.java +++ b/markwon/src/main/java/ru/noties/markwon/spans/OrderedListItemSpan.java @@ -9,6 +9,8 @@ import android.text.TextPaint; import android.text.style.LeadingMarginSpan; import android.widget.TextView; +import ru.noties.markwon.utils.LeadingMarginUtils; + public class OrderedListItemSpan implements LeadingMarginSpan { /** @@ -42,7 +44,7 @@ public class OrderedListItemSpan implements LeadingMarginSpan { } } - private final SpannableTheme theme; + private final MarkwonTheme theme; private final String number; private final Paint paint = ObjectsPool.paint(); @@ -52,7 +54,7 @@ public class OrderedListItemSpan implements LeadingMarginSpan { private int margin; public OrderedListItemSpan( - @NonNull SpannableTheme theme, + @NonNull MarkwonTheme theme, @NonNull String number ) { this.theme = theme; diff --git a/markwon/src/main/java/ru/noties/markwon/spans/SubScriptSpan.java b/markwon/src/main/java/ru/noties/markwon/spans/SubScriptSpan.java index 4386613d..318697b1 100644 --- a/markwon/src/main/java/ru/noties/markwon/spans/SubScriptSpan.java +++ b/markwon/src/main/java/ru/noties/markwon/spans/SubScriptSpan.java @@ -6,9 +6,9 @@ import android.text.style.MetricAffectingSpan; public class SubScriptSpan extends MetricAffectingSpan { - private final SpannableTheme theme; + private final MarkwonTheme theme; - public SubScriptSpan(@NonNull SpannableTheme theme) { + public SubScriptSpan(@NonNull MarkwonTheme theme) { this.theme = theme; } diff --git a/markwon/src/main/java/ru/noties/markwon/spans/SuperScriptSpan.java b/markwon/src/main/java/ru/noties/markwon/spans/SuperScriptSpan.java index 4b8151ec..d1aa172b 100644 --- a/markwon/src/main/java/ru/noties/markwon/spans/SuperScriptSpan.java +++ b/markwon/src/main/java/ru/noties/markwon/spans/SuperScriptSpan.java @@ -6,9 +6,9 @@ import android.text.style.MetricAffectingSpan; public class SuperScriptSpan extends MetricAffectingSpan { - private final SpannableTheme theme; + private final MarkwonTheme theme; - public SuperScriptSpan(@NonNull SpannableTheme theme) { + public SuperScriptSpan(@NonNull MarkwonTheme theme) { this.theme = theme; } diff --git a/markwon/src/main/java/ru/noties/markwon/spans/TableRowSpan.java b/markwon/src/main/java/ru/noties/markwon/spans/TableRowSpan.java index 4d3a35fb..a54b3206 100644 --- a/markwon/src/main/java/ru/noties/markwon/spans/TableRowSpan.java +++ b/markwon/src/main/java/ru/noties/markwon/spans/TableRowSpan.java @@ -61,7 +61,7 @@ public class TableRowSpan extends ReplacementSpan { } } - private final SpannableTheme theme; + private final MarkwonTheme theme; private final List cells; private final List layouts; private final TextPaint textPaint; @@ -76,7 +76,7 @@ public class TableRowSpan extends ReplacementSpan { private Invalidator invalidator; public TableRowSpan( - @NonNull SpannableTheme theme, + @NonNull MarkwonTheme theme, @NonNull List cells, boolean header, boolean odd) { diff --git a/markwon/src/main/java/ru/noties/markwon/spans/ThematicBreakSpan.java b/markwon/src/main/java/ru/noties/markwon/spans/ThematicBreakSpan.java index 316e4312..a19f7528 100644 --- a/markwon/src/main/java/ru/noties/markwon/spans/ThematicBreakSpan.java +++ b/markwon/src/main/java/ru/noties/markwon/spans/ThematicBreakSpan.java @@ -9,11 +9,11 @@ import android.text.style.LeadingMarginSpan; public class ThematicBreakSpan implements LeadingMarginSpan { - private final SpannableTheme theme; + private final MarkwonTheme theme; private final Rect rect = ObjectsPool.rect(); private final Paint paint = ObjectsPool.paint(); - public ThematicBreakSpan(@NonNull SpannableTheme theme) { + public ThematicBreakSpan(@NonNull MarkwonTheme theme) { this.theme = theme; } diff --git a/markwon/src/main/java/ru/noties/markwon/spans/TaskListDrawable.java b/markwon/src/main/java/ru/noties/markwon/tasklist/TaskListDrawable.java similarity index 99% rename from markwon/src/main/java/ru/noties/markwon/spans/TaskListDrawable.java rename to markwon/src/main/java/ru/noties/markwon/tasklist/TaskListDrawable.java index 5119deb4..15924e8f 100644 --- a/markwon/src/main/java/ru/noties/markwon/spans/TaskListDrawable.java +++ b/markwon/src/main/java/ru/noties/markwon/tasklist/TaskListDrawable.java @@ -1,4 +1,4 @@ -package ru.noties.markwon.spans; +package ru.noties.markwon.tasklist; import android.graphics.Canvas; import android.graphics.ColorFilter; diff --git a/markwon/src/main/java/ru/noties/markwon/tasklist/TaskListExtension.java b/markwon/src/main/java/ru/noties/markwon/tasklist/TaskListExtension.java index 3bb49355..a721c297 100644 --- a/markwon/src/main/java/ru/noties/markwon/tasklist/TaskListExtension.java +++ b/markwon/src/main/java/ru/noties/markwon/tasklist/TaskListExtension.java @@ -7,6 +7,7 @@ import org.commonmark.parser.Parser; /** * @since 1.0.1 */ +@Deprecated public class TaskListExtension implements Parser.ParserExtension { @NonNull diff --git a/markwon/src/main/java/ru/noties/markwon/tasklist/TaskListPlugin.java b/markwon/src/main/java/ru/noties/markwon/tasklist/TaskListPlugin.java new file mode 100644 index 00000000..bf75d5c7 --- /dev/null +++ b/markwon/src/main/java/ru/noties/markwon/tasklist/TaskListPlugin.java @@ -0,0 +1,93 @@ +package ru.noties.markwon.tasklist; + +import android.content.Context; +import android.graphics.drawable.Drawable; +import android.support.annotation.ColorInt; +import android.support.annotation.NonNull; + +import org.commonmark.parser.Parser; + +import ru.noties.markwon.AbstractMarkwonPlugin; +import ru.noties.markwon.MarkwonVisitor; + +public class TaskListPlugin extends AbstractMarkwonPlugin { + + /** + * @see TaskListDrawable + */ + @NonNull + public static TaskListPlugin create(@NonNull Drawable drawable) { + return new TaskListPlugin(drawable); + } + + @NonNull + public static TaskListPlugin create(@NonNull Context context) { + // resolve link color and background color + return null; + } + + @NonNull + public static TaskListPlugin create( + @ColorInt int checkedFillColor, + @ColorInt int normalOutlineColor, + @ColorInt int checkMarkColor) { + return new TaskListPlugin(new TaskListDrawable( + checkedFillColor, + normalOutlineColor, + checkMarkColor)); + } + + private final Drawable drawable; + + private TaskListPlugin(@NonNull Drawable drawable) { + this.drawable = drawable; + } + + @Override + public void configureParser(@NonNull Parser.Builder builder) { + builder.customBlockParserFactory(new TaskListBlockParser.Factory()); + } + + @Override + public void configureVisitor(@NonNull MarkwonVisitor.Builder builder) { + builder + .on(TaskListBlock.class, new MarkwonVisitor.NodeVisitor() { + @Override + public void visit(@NonNull MarkwonVisitor visitor, @NonNull TaskListBlock taskListBlock) { + + visitor.ensureNewLine(); + + visitor.incrementBlockQuoteIndent(); + visitor.visitChildren(taskListBlock); + visitor.decrementBlockQuoteIndent(); + + if (visitor.hasNext(taskListBlock)) { + visitor.ensureNewLine(); + visitor.forceNewLine(); + } + } + }) + .on(TaskListItem.class, new MarkwonVisitor.NodeVisitor() { + @Override + public void visit(@NonNull MarkwonVisitor visitor, @NonNull TaskListItem taskListItem) { + + final int length = visitor.length(); + + final int indent = visitor.blockQuoteIndent(); + visitor.blockQuoteIntent(indent + taskListItem.indent()); + visitor.visitChildren(taskListItem); + visitor.setSpans(length, new TaskListSpan( + visitor.theme(), + drawable, + visitor.blockQuoteIndent(), + taskListItem.done())); + + if (visitor.hasNext(taskListItem)) { + visitor.ensureNewLine(); + } + + visitor.blockQuoteIntent(indent); + } + }); + } +} diff --git a/markwon/src/main/java/ru/noties/markwon/spans/TaskListSpan.java b/markwon/src/main/java/ru/noties/markwon/tasklist/TaskListSpan.java similarity index 77% rename from markwon/src/main/java/ru/noties/markwon/spans/TaskListSpan.java rename to markwon/src/main/java/ru/noties/markwon/tasklist/TaskListSpan.java index 25bc6a41..5c59b167 100644 --- a/markwon/src/main/java/ru/noties/markwon/spans/TaskListSpan.java +++ b/markwon/src/main/java/ru/noties/markwon/tasklist/TaskListSpan.java @@ -1,4 +1,4 @@ -package ru.noties.markwon.spans; +package ru.noties.markwon.tasklist; import android.graphics.Canvas; import android.graphics.Paint; @@ -7,6 +7,9 @@ import android.support.annotation.NonNull; import android.text.Layout; import android.text.style.LeadingMarginSpan; +import ru.noties.markwon.spans.MarkwonTheme; +import ru.noties.markwon.utils.LeadingMarginUtils; + /** * @since 1.0.1 */ @@ -16,14 +19,24 @@ public class TaskListSpan implements LeadingMarginSpan { private static final int[] STATE_NONE = new int[0]; - private final SpannableTheme theme; + private final MarkwonTheme theme; + private final Drawable drawable; private final int blockIndent; // @since 2.0.1 field is NOT final (to allow mutation) private boolean isDone; - public TaskListSpan(@NonNull SpannableTheme theme, int blockIndent, boolean isDone) { + @Deprecated + public TaskListSpan(@NonNull MarkwonTheme theme, int blockIndent, boolean isDone) { this.theme = theme; + this.drawable = null; + this.blockIndent = blockIndent; + this.isDone = isDone; + } + + public TaskListSpan(@NonNull MarkwonTheme theme, @NonNull Drawable drawable, int blockIndent, boolean isDone) { + this.theme = theme; + this.drawable = drawable; this.blockIndent = blockIndent; this.isDone = isDone; } @@ -58,10 +71,10 @@ public class TaskListSpan implements LeadingMarginSpan { return; } - final Drawable drawable = theme.getTaskListDrawable(); - if (drawable == null) { - return; - } +// final Drawable drawable = theme.getTaskListDrawable(); +// if (drawable == null) { +// return; +// } final int save = c.save(); try { diff --git a/markwon/src/main/java/ru/noties/markwon/spans/LeadingMarginUtils.java b/markwon/src/main/java/ru/noties/markwon/utils/LeadingMarginUtils.java similarity index 52% rename from markwon/src/main/java/ru/noties/markwon/spans/LeadingMarginUtils.java rename to markwon/src/main/java/ru/noties/markwon/utils/LeadingMarginUtils.java index ab1a7de3..f783fcae 100644 --- a/markwon/src/main/java/ru/noties/markwon/spans/LeadingMarginUtils.java +++ b/markwon/src/main/java/ru/noties/markwon/utils/LeadingMarginUtils.java @@ -1,14 +1,14 @@ -package ru.noties.markwon.spans; +package ru.noties.markwon.utils; import android.text.Spanned; -abstract class LeadingMarginUtils { +public abstract class LeadingMarginUtils { - static boolean selfStart(int start, CharSequence text, Object span) { + public static boolean selfStart(int start, CharSequence text, Object span) { return text instanceof Spanned && ((Spanned) text).getSpanStart(span) == start; } - static boolean selfEnd(int end, CharSequence text, Object span) { + public static boolean selfEnd(int end, CharSequence text, Object span) { return text instanceof Spanned && ((Spanned) text).getSpanEnd(span) == end; } diff --git a/markwon/src/test/java/ru/noties/markwon/renderer/SpannableConfigurationTest.java b/markwon/src/test/java/ru/noties/markwon/renderer/MarkwonConfigurationTest.java similarity index 86% rename from markwon/src/test/java/ru/noties/markwon/renderer/SpannableConfigurationTest.java rename to markwon/src/test/java/ru/noties/markwon/renderer/MarkwonConfigurationTest.java index daa70332..e094164b 100644 --- a/markwon/src/test/java/ru/noties/markwon/renderer/SpannableConfigurationTest.java +++ b/markwon/src/test/java/ru/noties/markwon/renderer/MarkwonConfigurationTest.java @@ -2,7 +2,7 @@ package ru.noties.markwon.renderer; import org.junit.Test; -import ru.noties.markwon.SpannableConfiguration; +import ru.noties.markwon.MarkwonConfiguration; import ru.noties.markwon.SpannableFactory; import ru.noties.markwon.SyntaxHighlight; import ru.noties.markwon.UrlProcessor; @@ -10,17 +10,17 @@ import ru.noties.markwon.html.api.MarkwonHtmlParser; import ru.noties.markwon.renderer.html2.MarkwonHtmlRenderer; import ru.noties.markwon.spans.AsyncDrawable; import ru.noties.markwon.spans.LinkSpan; -import ru.noties.markwon.spans.SpannableTheme; +import ru.noties.markwon.spans.MarkwonTheme; import static org.junit.Assert.assertEquals; import static org.mockito.Mockito.mock; -public class SpannableConfigurationTest { +public class MarkwonConfigurationTest { @Test public void testNewBuilder() { - final SpannableConfiguration configuration = SpannableConfiguration + final MarkwonConfiguration configuration = MarkwonConfiguration .builder(null) - .theme(mock(SpannableTheme.class)) + .theme(mock(MarkwonTheme.class)) .asyncDrawableLoader(mock(AsyncDrawable.Loader.class)) .syntaxHighlight(mock(SyntaxHighlight.class)) .linkResolver(mock(LinkSpan.Resolver.class)) @@ -33,7 +33,7 @@ public class SpannableConfigurationTest { .htmlAllowNonClosedTags(true) .build(); - final SpannableConfiguration newConfiguration = configuration + final MarkwonConfiguration newConfiguration = configuration .newBuilder(null) .build(); diff --git a/markwon/src/test/java/ru/noties/markwon/renderer/SyntaxHighlightTest.java b/markwon/src/test/java/ru/noties/markwon/renderer/SyntaxHighlightTest.java index 1f5d88d4..ecca6e91 100644 --- a/markwon/src/test/java/ru/noties/markwon/renderer/SyntaxHighlightTest.java +++ b/markwon/src/test/java/ru/noties/markwon/renderer/SyntaxHighlightTest.java @@ -13,11 +13,11 @@ import org.junit.runner.RunWith; import org.robolectric.RobolectricTestRunner; import org.robolectric.annotation.Config; +import ru.noties.markwon.MarkwonConfiguration; import ru.noties.markwon.SpannableBuilder; -import ru.noties.markwon.SpannableConfiguration; import ru.noties.markwon.SpannableFactory; import ru.noties.markwon.SyntaxHighlight; -import ru.noties.markwon.spans.SpannableTheme; +import ru.noties.markwon.spans.MarkwonTheme; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertTrue; @@ -63,12 +63,12 @@ public class SyntaxHighlightTest { }; final SpannableFactory factory = mock(SpannableFactory.class); - when(factory.code(any(SpannableTheme.class), anyBoolean())).thenReturn(codeSpan); + when(factory.code(any(MarkwonTheme.class), anyBoolean())).thenReturn(codeSpan); - final SpannableConfiguration configuration = SpannableConfiguration.builder(mock(Context.class)) + final MarkwonConfiguration configuration = MarkwonConfiguration.builder(mock(Context.class)) .syntaxHighlight(highlight) .factory(factory) - .theme(mock(SpannableTheme.class)) + .theme(mock(MarkwonTheme.class)) .build(); final SpannableBuilder builder = new SpannableBuilder(); diff --git a/markwon/src/test/java/ru/noties/markwon/renderer/visitor/SpannableMarkdownVisitorTest.java b/markwon/src/test/java/ru/noties/markwon/renderer/visitor/SpannableMarkdownVisitorTest.java index 047a0584..6d77b130 100644 --- a/markwon/src/test/java/ru/noties/markwon/renderer/visitor/SpannableMarkdownVisitorTest.java +++ b/markwon/src/test/java/ru/noties/markwon/renderer/visitor/SpannableMarkdownVisitorTest.java @@ -14,12 +14,12 @@ import java.util.Collection; import ru.noties.markwon.LinkResolverDef; import ru.noties.markwon.Markwon; +import ru.noties.markwon.MarkwonConfiguration; import ru.noties.markwon.SpannableBuilder; -import ru.noties.markwon.SpannableConfiguration; import ru.noties.markwon.SpannableFactory; import ru.noties.markwon.html.api.MarkwonHtmlParser; import ru.noties.markwon.renderer.SpannableMarkdownVisitor; -import ru.noties.markwon.spans.SpannableTheme; +import ru.noties.markwon.spans.MarkwonTheme; import static org.junit.Assert.assertEquals; import static org.mockito.Mockito.mock; @@ -44,7 +44,7 @@ public class SpannableMarkdownVisitorTest { final TestData data = TestDataReader.readTest(file); - final SpannableConfiguration configuration = configuration(data.config()); + final MarkwonConfiguration configuration = configuration(data.config()); final SpannableBuilder builder = new SpannableBuilder(); final SpannableMarkdownVisitor visitor = new SpannableMarkdownVisitor(configuration, builder); final Node node = Markwon.createParser().parse(data.input()); @@ -73,7 +73,7 @@ public class SpannableMarkdownVisitorTest { @SuppressWarnings("ConstantConditions") @NonNull - private SpannableConfiguration configuration(@NonNull TestConfig config) { + private MarkwonConfiguration configuration(@NonNull TestConfig config) { final SpannableFactory factory = new TestFactory(config.hasOption(TestConfig.USE_PARAGRAPHS)); final MarkwonHtmlParser htmlParser = config.hasOption(TestConfig.USE_HTML) @@ -83,8 +83,8 @@ public class SpannableMarkdownVisitorTest { final boolean softBreakAddsNewLine = config.hasOption(TestConfig.SOFT_BREAK_ADDS_NEW_LINE); final boolean htmlAllowNonClosedTags = config.hasOption(TestConfig.HTML_ALLOW_NON_CLOSED_TAGS); - return SpannableConfiguration.builder(null) - .theme(mock(SpannableTheme.class)) + return MarkwonConfiguration.builder(null) + .theme(mock(MarkwonTheme.class)) .linkResolver(mock(LinkResolverDef.class)) .htmlParser(htmlParser) .factory(factory) diff --git a/markwon/src/test/java/ru/noties/markwon/renderer/visitor/TestFactory.java b/markwon/src/test/java/ru/noties/markwon/renderer/visitor/TestFactory.java index 89a0f646..42d46f99 100644 --- a/markwon/src/test/java/ru/noties/markwon/renderer/visitor/TestFactory.java +++ b/markwon/src/test/java/ru/noties/markwon/renderer/visitor/TestFactory.java @@ -13,7 +13,7 @@ import ru.noties.markwon.renderer.ImageSize; import ru.noties.markwon.renderer.ImageSizeResolver; import ru.noties.markwon.spans.AsyncDrawable; import ru.noties.markwon.spans.LinkSpan; -import ru.noties.markwon.spans.SpannableTheme; +import ru.noties.markwon.spans.MarkwonTheme; import ru.noties.markwon.spans.TableRowSpan; import static ru.noties.markwon.renderer.visitor.TestSpan.BLOCK_QUOTE; @@ -57,13 +57,13 @@ class TestFactory implements SpannableFactory { @Nullable @Override - public Object blockQuote(@NonNull SpannableTheme theme) { + public Object blockQuote(@NonNull MarkwonTheme theme) { return new TestSpan(BLOCK_QUOTE); } @Nullable @Override - public Object code(@NonNull SpannableTheme theme, boolean multiline) { + public Object code(@NonNull MarkwonTheme theme, boolean multiline) { final String name = multiline ? CODE_BLOCK : CODE; @@ -72,25 +72,25 @@ class TestFactory implements SpannableFactory { @Nullable @Override - public Object orderedListItem(@NonNull SpannableTheme theme, int startNumber) { + public Object orderedListItem(@NonNull MarkwonTheme theme, int startNumber) { return new TestSpan(ORDERED_LIST, map("start", startNumber)); } @Nullable @Override - public Object bulletListItem(@NonNull SpannableTheme theme, int level) { + public Object bulletListItem(@NonNull MarkwonTheme theme, int level) { return new TestSpan(BULLET_LIST, map("level", level)); } @Nullable @Override - public Object thematicBreak(@NonNull SpannableTheme theme) { + public Object thematicBreak(@NonNull MarkwonTheme theme) { return new TestSpan(THEMATIC_BREAK); } @Nullable @Override - public Object heading(@NonNull SpannableTheme theme, int level) { + public Object heading(@NonNull MarkwonTheme theme, int level) { return new TestSpan(HEADING + level); } @@ -102,7 +102,7 @@ class TestFactory implements SpannableFactory { @Nullable @Override - public Object taskListItem(@NonNull SpannableTheme theme, int blockIndent, boolean isDone) { + public Object taskListItem(@NonNull MarkwonTheme theme, int blockIndent, boolean isDone) { return new TestSpan(TASK_LIST, map( Pair.of("blockIdent", blockIndent), Pair.of("done", isDone) @@ -111,7 +111,7 @@ class TestFactory implements SpannableFactory { @Nullable @Override - public Object tableRow(@NonNull SpannableTheme theme, @NonNull List cells, boolean isHeader, boolean isOdd) { + public Object tableRow(@NonNull MarkwonTheme theme, @NonNull List cells, boolean isHeader, boolean isOdd) { return new TestSpan(TABLE_ROW, map( Pair.of("cells", cells), Pair.of("header", isHeader), @@ -129,7 +129,7 @@ class TestFactory implements SpannableFactory { @Nullable @Override - public Object image(@NonNull SpannableTheme theme, @NonNull String destination, @NonNull AsyncDrawable.Loader loader, @NonNull ImageSizeResolver imageSizeResolver, @Nullable ImageSize imageSize, boolean replacementTextIsLink) { + public Object image(@NonNull MarkwonTheme theme, @NonNull String destination, @NonNull AsyncDrawable.Loader loader, @NonNull ImageSizeResolver imageSizeResolver, @Nullable ImageSize imageSize, boolean replacementTextIsLink) { return new TestSpan(IMAGE, map( Pair.of("src", destination), Pair.of("imageSize", imageSize), @@ -139,19 +139,19 @@ class TestFactory implements SpannableFactory { @Nullable @Override - public Object link(@NonNull SpannableTheme theme, @NonNull String destination, @NonNull LinkSpan.Resolver resolver) { + public Object link(@NonNull MarkwonTheme theme, @NonNull String destination, @NonNull LinkSpan.Resolver resolver) { return new TestSpan(LINK, map("href", destination)); } @Nullable @Override - public Object superScript(@NonNull SpannableTheme theme) { + public Object superScript(@NonNull MarkwonTheme theme) { return new TestSpan(SUPER_SCRIPT); } @Nullable @Override - public Object subScript(@NonNull SpannableTheme theme) { + public Object subScript(@NonNull MarkwonTheme theme) { return new TestSpan(SUB_SCRIPT); } diff --git a/sample-custom-extension/src/main/java/ru/noties/markwon/sample/extension/IconVisitor.java b/sample-custom-extension/src/main/java/ru/noties/markwon/sample/extension/IconVisitor.java index d373ff75..77c5da2e 100644 --- a/sample-custom-extension/src/main/java/ru/noties/markwon/sample/extension/IconVisitor.java +++ b/sample-custom-extension/src/main/java/ru/noties/markwon/sample/extension/IconVisitor.java @@ -2,12 +2,11 @@ package ru.noties.markwon.sample.extension; import android.support.annotation.NonNull; import android.text.TextUtils; -import android.widget.TextView; import org.commonmark.node.CustomNode; import ru.noties.markwon.SpannableBuilder; -import ru.noties.markwon.SpannableConfiguration; +import ru.noties.markwon.MarkwonConfiguration; import ru.noties.markwon.renderer.SpannableMarkdownVisitor; @SuppressWarnings("WeakerAccess") @@ -18,7 +17,7 @@ public class IconVisitor extends SpannableMarkdownVisitor { private final IconSpanProvider iconSpanProvider; public IconVisitor( - @NonNull SpannableConfiguration configuration, + @NonNull MarkwonConfiguration configuration, @NonNull SpannableBuilder builder, @NonNull IconSpanProvider iconSpanProvider ) { diff --git a/sample-custom-extension/src/main/java/ru/noties/markwon/sample/extension/MainActivity.java b/sample-custom-extension/src/main/java/ru/noties/markwon/sample/extension/MainActivity.java index 19a69704..b104e88e 100644 --- a/sample-custom-extension/src/main/java/ru/noties/markwon/sample/extension/MainActivity.java +++ b/sample-custom-extension/src/main/java/ru/noties/markwon/sample/extension/MainActivity.java @@ -12,9 +12,9 @@ import org.commonmark.parser.Parser; import java.util.Arrays; +import ru.noties.markwon.MarkwonConfiguration; import ru.noties.markwon.SpannableBuilder; -import ru.noties.markwon.SpannableConfiguration; -import ru.noties.markwon.spans.SpannableTheme; +import ru.noties.markwon.spans.MarkwonTheme; import ru.noties.markwon.tasklist.TaskListExtension; public class MainActivity extends Activity { @@ -53,8 +53,8 @@ public class MainActivity extends Activity { final IconSpanProvider spanProvider = IconSpanProvider.create(this, 0); final float[] textSizeMultipliers = new float[]{3f, 2f, 1.5f, 1f, .5f, .25f}; - SpannableConfiguration configuration = SpannableConfiguration.builder(this) - .theme(SpannableTheme.builder() + MarkwonConfiguration configuration = MarkwonConfiguration.builder(this) + .theme(MarkwonTheme.builder() .headingTypeface(Typeface.MONOSPACE) .headingTextSizeMultipliers(textSizeMultipliers) .build()) diff --git a/sample-latex-math/src/main/java/ru/noties/markwon/sample/jlatexmath/MainActivity.java b/sample-latex-math/src/main/java/ru/noties/markwon/sample/jlatexmath/MainActivity.java index dee84ee4..b613eac5 100644 --- a/sample-latex-math/src/main/java/ru/noties/markwon/sample/jlatexmath/MainActivity.java +++ b/sample-latex-math/src/main/java/ru/noties/markwon/sample/jlatexmath/MainActivity.java @@ -10,8 +10,8 @@ import org.commonmark.parser.Parser; import ru.noties.jlatexmath.JLatexMathAndroid; import ru.noties.markwon.Markwon; +import ru.noties.markwon.MarkwonConfiguration; import ru.noties.markwon.SpannableBuilder; -import ru.noties.markwon.SpannableConfiguration; import ru.noties.markwon.il.AsyncDrawableLoader; import ru.noties.markwon.renderer.ImageSize; import ru.noties.markwon.renderer.SpannableMarkdownVisitor; @@ -55,7 +55,7 @@ public class MainActivity extends Activity { .mediaDecoders(jLatexMathMedia.mediaDecoder()) .build(); - final SpannableConfiguration configuration = SpannableConfiguration.builder(this) + final MarkwonConfiguration configuration = MarkwonConfiguration.builder(this) .asyncDrawableLoader(asyncDrawableLoader) .build(); @@ -68,7 +68,7 @@ public class MainActivity extends Activity { final Node node = parser.parse(markdown); final SpannableBuilder builder = new SpannableBuilder(); - final SpannableMarkdownVisitor visitor = new SpannableMarkdownVisitor(SpannableConfiguration.create(this), builder) { + final SpannableMarkdownVisitor visitor = new SpannableMarkdownVisitor(MarkwonConfiguration.create(this), builder) { @Override public void visit(CustomBlock customBlock) { From 3526e165653dd8d393886dca737524dac19bc5cb Mon Sep 17 00:00:00 2001 From: Dimitry Ivanov Date: Sat, 24 Nov 2018 16:59:20 +0300 Subject: [PATCH 018/103] Core functionality plugin --- app/src/main/AndroidManifest.xml | 6 +- .../java/ru/noties/markwon/MainActivity.java | 2 +- .../ru/noties/markwon/MarkdownRenderer.java | 2 +- .../markwon/view/MarkwonViewHelper.java | 2 +- .../noties/markwon/AbstractMarkwonPlugin.java | 4 +- .../main/java/ru/noties/markwon/Markwon.java | 32 +-- .../main/java/ru/noties/markwon/Markwon2.java | 8 +- .../ru/noties/markwon/MarkwonBuilderImpl.java | 2 +- .../noties/markwon/MarkwonConfiguration.java | 25 +-- .../java/ru/noties/markwon/MarkwonImpl.java | 22 ++- .../java/ru/noties/markwon/MarkwonPlugin.java | 4 +- .../ru/noties/markwon/MarkwonVisitor.java | 7 +- .../ru/noties/markwon/MarkwonVisitorImpl.java | 21 +- .../ru/noties/markwon/core/CorePlugin.java | 182 ++++++++++++++++-- .../markwon/spans/OrderedListItemSpan.java | 4 +- .../markwon/utils/LeadingMarginUtils.java | 1 + .../sample/extension/MainActivity.java | 4 +- 17 files changed, 260 insertions(+), 68 deletions(-) diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 1cf83a05..96db5266 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -41,17 +41,17 @@ - + - + - + diff --git a/app/src/main/java/ru/noties/markwon/MainActivity.java b/app/src/main/java/ru/noties/markwon/MainActivity.java index a14caf2e..bd477bcd 100644 --- a/app/src/main/java/ru/noties/markwon/MainActivity.java +++ b/app/src/main/java/ru/noties/markwon/MainActivity.java @@ -71,7 +71,7 @@ public class MainActivity extends Activity { .use(new CorePlugin()) .use(TaskListPlugin.create(new TaskListDrawable(0xffff0000, 0xffff0000, -1))) .build(); - final CharSequence markdown = markwon2.markdown("**hello _dear_** `code`\n\n- [ ] first\n- [x] second"); + final CharSequence markdown = markwon2.toMarkdown("**hello _dear_** `code`\n\n- [ ] first\n- [x] second"); textView.setText(markdown); return; } diff --git a/app/src/main/java/ru/noties/markwon/MarkdownRenderer.java b/app/src/main/java/ru/noties/markwon/MarkdownRenderer.java index 9151b115..f0eb38e5 100644 --- a/app/src/main/java/ru/noties/markwon/MarkdownRenderer.java +++ b/app/src/main/java/ru/noties/markwon/MarkdownRenderer.java @@ -104,7 +104,7 @@ public class MarkdownRenderer { final long end = SystemClock.uptimeMillis(); - Debug.i("markdown rendered: %d ms", end - start); + Debug.i("toMarkdown rendered: %d ms", end - start); if (!isCancelled()) { handler.post(new Runnable() { diff --git a/markwon-view/src/main/java/ru/noties/markwon/view/MarkwonViewHelper.java b/markwon-view/src/main/java/ru/noties/markwon/view/MarkwonViewHelper.java index f3669b77..95518023 100644 --- a/markwon-view/src/main/java/ru/noties/markwon/view/MarkwonViewHelper.java +++ b/markwon-view/src/main/java/ru/noties/markwon/view/MarkwonViewHelper.java @@ -60,7 +60,7 @@ public class MarkwonViewHelper implements IMarkwonView { this.provider = provider; this.configuration = provider.provide(textView.getContext()); if (!TextUtils.isEmpty(markdown)) { - // invalidate rendered markdown + // invalidate rendered toMarkdown setMarkdown(markdown); } } diff --git a/markwon/src/main/java/ru/noties/markwon/AbstractMarkwonPlugin.java b/markwon/src/main/java/ru/noties/markwon/AbstractMarkwonPlugin.java index d96e2a6d..d0187fac 100644 --- a/markwon/src/main/java/ru/noties/markwon/AbstractMarkwonPlugin.java +++ b/markwon/src/main/java/ru/noties/markwon/AbstractMarkwonPlugin.java @@ -35,12 +35,12 @@ public abstract class AbstractMarkwonPlugin implements MarkwonPlugin { } @Override - public void beforeSetText(@NonNull TextView textView, @NonNull SpannableBuilder builder) { + public void beforeSetText(@NonNull TextView textView, @NonNull CharSequence markdown) { } @Override - public void afterSetText(@NonNull TextView textView, @NonNull SpannableBuilder builder) { + public void afterSetText(@NonNull TextView textView, @NonNull CharSequence markdown) { } } diff --git a/markwon/src/main/java/ru/noties/markwon/Markwon.java b/markwon/src/main/java/ru/noties/markwon/Markwon.java index c7fdf550..5728f21a 100644 --- a/markwon/src/main/java/ru/noties/markwon/Markwon.java +++ b/markwon/src/main/java/ru/noties/markwon/Markwon.java @@ -48,12 +48,12 @@ public abstract class Markwon { } /** - * Parses submitted raw markdown, converts it to CharSequence (with Spannables) + * Parses submitted raw toMarkdown, converts it to CharSequence (with Spannables) * and applies it to view * - * @param view {@link TextView} to set markdown into + * @param view {@link TextView} to set toMarkdown into * @param configuration a {@link MarkwonConfiguration} instance - * @param markdown raw markdown String (for example: {@code `**Hello**`}) + * @param markdown raw toMarkdown String (for example: {@code `**Hello**`}) * @see #markdown(MarkwonConfiguration, String) * @see #setText(TextView, CharSequence) * @see MarkwonConfiguration @@ -69,13 +69,13 @@ public abstract class Markwon { } /** - * Helper method to apply parsed markdown. + * Helper method to apply parsed toMarkdown. *

* Since 1.0.6 redirects it\'s call to {@link #setText(TextView, CharSequence, MovementMethod)} * with LinkMovementMethod as an argument to preserve current API. * - * @param view {@link TextView} to set markdown into - * @param text parsed markdown + * @param view {@link TextView} to set toMarkdown into + * @param text parsed toMarkdown * @see #setText(TextView, CharSequence, MovementMethod) * @since 1.0.0 */ @@ -84,13 +84,13 @@ public abstract class Markwon { } /** - * Helper method to apply parsed markdown with additional argument of a MovementMethod. Used + * Helper method to apply parsed toMarkdown with additional argument of a MovementMethod. Used * to workaround problems that occur when using system LinkMovementMethod (for example: * https://issuetracker.google.com/issues/37068143). As a better alternative to it consider * using: https://github.com/saket/Better-Link-Movement-Method * - * @param view TextView to set markdown into - * @param text parsed markdown + * @param view TextView to set toMarkdown into + * @param text parsed toMarkdown * @param movementMethod an implementation if MovementMethod or null * @see #scheduleDrawables(TextView) * @see #scheduleTableRows(TextView) @@ -102,7 +102,7 @@ public abstract class Markwon { unscheduleTableRows(view); // @since 2.0.1 we must measure ordered-list-item-spans before applying text to a TextView. - // if markdown has a lot of ordered list items (or text size is relatively big, or block-margin + // if toMarkdown has a lot of ordered list items (or text size is relatively big, or block-margin // is relatively small) then this list won't be rendered properly: it will take correct // layout (width and margin) but will be clipped if margin is not _consistent_ between calls. OrderedListItemSpan.measure(view, text); @@ -117,11 +117,11 @@ public abstract class Markwon { } /** - * Returns parsed markdown with default {@link MarkwonConfiguration} obtained from {@link Context} + * Returns parsed toMarkdown with default {@link MarkwonConfiguration} obtained from {@link Context} * * @param context {@link Context} - * @param markdown raw markdown - * @return parsed markdown + * @param markdown raw toMarkdown + * @return parsed toMarkdown * @since 1.0.0 */ @NonNull @@ -131,11 +131,11 @@ public abstract class Markwon { } /** - * Returns parsed markdown with provided {@link MarkwonConfiguration} + * Returns parsed toMarkdown with provided {@link MarkwonConfiguration} * * @param configuration a {@link MarkwonConfiguration} - * @param markdown raw markdown - * @return parsed markdown + * @param markdown raw toMarkdown + * @return parsed toMarkdown * @see MarkwonConfiguration * @since 1.0.0 */ diff --git a/markwon/src/main/java/ru/noties/markwon/Markwon2.java b/markwon/src/main/java/ru/noties/markwon/Markwon2.java index 59feb9a5..734a9ee3 100644 --- a/markwon/src/main/java/ru/noties/markwon/Markwon2.java +++ b/markwon/src/main/java/ru/noties/markwon/Markwon2.java @@ -2,6 +2,7 @@ package ru.noties.markwon; import android.content.Context; import android.support.annotation.NonNull; +import android.widget.TextView; import org.commonmark.node.Node; @@ -18,8 +19,13 @@ public abstract class Markwon2 { @NonNull public abstract CharSequence render(@NonNull Node node); + // parse + render @NonNull - public abstract CharSequence markdown(@NonNull String input); + public abstract CharSequence toMarkdown(@NonNull String input); + + public abstract void setMarkdown(@NonNull TextView textView, @NonNull String markdown); + + public abstract void setParsedMarkdown(@NonNull TextView textView, @NonNull CharSequence markdown); public interface Builder { diff --git a/markwon/src/main/java/ru/noties/markwon/MarkwonBuilderImpl.java b/markwon/src/main/java/ru/noties/markwon/MarkwonBuilderImpl.java index 635e3138..841c70d4 100644 --- a/markwon/src/main/java/ru/noties/markwon/MarkwonBuilderImpl.java +++ b/markwon/src/main/java/ru/noties/markwon/MarkwonBuilderImpl.java @@ -46,7 +46,7 @@ class MarkwonBuilderImpl implements Markwon2.Builder { return new MarkwonImpl( parserBuilder.build(), - visitorBuilder.build(themeBuilder.build(), configurationBuilder.build()), + visitorBuilder.build(configurationBuilder.build(themeBuilder.build())), Collections.unmodifiableList(plugins) ); } diff --git a/markwon/src/main/java/ru/noties/markwon/MarkwonConfiguration.java b/markwon/src/main/java/ru/noties/markwon/MarkwonConfiguration.java index 510f10b1..82d48f4f 100644 --- a/markwon/src/main/java/ru/noties/markwon/MarkwonConfiguration.java +++ b/markwon/src/main/java/ru/noties/markwon/MarkwonConfiguration.java @@ -20,7 +20,7 @@ public class MarkwonConfiguration { // creates default configuration @NonNull public static MarkwonConfiguration create(@NonNull Context context) { - return new Builder(context).build(); + return new Builder(context).build(MarkwonTheme.create(context)); } @NonNull @@ -28,9 +28,8 @@ public class MarkwonConfiguration { return new Builder(context); } - @Deprecated - private final MarkwonTheme theme; + private final MarkwonTheme theme; private final AsyncDrawable.Loader asyncDrawableLoader; private final SyntaxHighlight syntaxHighlight; private final LinkSpan.Resolver linkResolver; @@ -65,7 +64,6 @@ public class MarkwonConfiguration { } @NonNull - @Deprecated public MarkwonTheme theme() { return theme; } @@ -137,7 +135,6 @@ public class MarkwonConfiguration { private final Context context; - @Deprecated private MarkwonTheme theme; private AsyncDrawable.Loader asyncDrawableLoader; private SyntaxHighlight syntaxHighlight; @@ -169,12 +166,12 @@ public class MarkwonConfiguration { this.htmlAllowNonClosedTags = configuration.htmlAllowNonClosedTags; } - @NonNull - @Deprecated - public Builder theme(@NonNull MarkwonTheme theme) { - this.theme = theme; - return this; - } +// @NonNull +// @Deprecated +// public Builder theme(@NonNull MarkwonTheme theme) { +// this.theme = theme; +// return this; +// } @NonNull public Builder asyncDrawableLoader(@NonNull AsyncDrawable.Loader asyncDrawableLoader) { @@ -263,11 +260,9 @@ public class MarkwonConfiguration { } @NonNull - public MarkwonConfiguration build() { + public MarkwonConfiguration build(@NonNull MarkwonTheme theme) { - if (theme == null) { - theme = MarkwonTheme.create(context); - } + this.theme = theme; if (asyncDrawableLoader == null) { asyncDrawableLoader = new AsyncDrawableLoaderNoOp(); diff --git a/markwon/src/main/java/ru/noties/markwon/MarkwonImpl.java b/markwon/src/main/java/ru/noties/markwon/MarkwonImpl.java index 0252568e..db38e183 100644 --- a/markwon/src/main/java/ru/noties/markwon/MarkwonImpl.java +++ b/markwon/src/main/java/ru/noties/markwon/MarkwonImpl.java @@ -1,6 +1,7 @@ package ru.noties.markwon; import android.support.annotation.NonNull; +import android.widget.TextView; import org.commonmark.node.Node; import org.commonmark.parser.Parser; @@ -42,7 +43,26 @@ class MarkwonImpl extends Markwon2 { @NonNull @Override - public CharSequence markdown(@NonNull String input) { + public CharSequence toMarkdown(@NonNull String input) { return render(parse(input)); } + + @Override + public void setMarkdown(@NonNull TextView textView, @NonNull String markdown) { + setParsedMarkdown(textView, toMarkdown(markdown)); + } + + @Override + public void setParsedMarkdown(@NonNull TextView textView, @NonNull CharSequence markdown) { + + for (MarkwonPlugin plugin : plugins) { + plugin.beforeSetText(textView, markdown); + } + + textView.setText(markdown); + + for (MarkwonPlugin plugin : plugins) { + plugin.afterSetText(textView, markdown); + } + } } diff --git a/markwon/src/main/java/ru/noties/markwon/MarkwonPlugin.java b/markwon/src/main/java/ru/noties/markwon/MarkwonPlugin.java index 48e3674e..bd54d1d0 100644 --- a/markwon/src/main/java/ru/noties/markwon/MarkwonPlugin.java +++ b/markwon/src/main/java/ru/noties/markwon/MarkwonPlugin.java @@ -23,7 +23,7 @@ public interface MarkwonPlugin { @NonNull String processMarkdown(@NonNull String markdown); - void beforeSetText(@NonNull TextView textView, @NonNull SpannableBuilder builder); + void beforeSetText(@NonNull TextView textView, @NonNull CharSequence markdown); - void afterSetText(@NonNull TextView textView, @NonNull SpannableBuilder builder); + void afterSetText(@NonNull TextView textView, @NonNull CharSequence markdown); } diff --git a/markwon/src/main/java/ru/noties/markwon/MarkwonVisitor.java b/markwon/src/main/java/ru/noties/markwon/MarkwonVisitor.java index 58c28111..12a912fc 100644 --- a/markwon/src/main/java/ru/noties/markwon/MarkwonVisitor.java +++ b/markwon/src/main/java/ru/noties/markwon/MarkwonVisitor.java @@ -20,14 +20,17 @@ public interface MarkwonVisitor extends Visitor { Builder on(@NonNull Class node, @NonNull NodeVisitor nodeVisitor); @NonNull - MarkwonVisitor build(@NonNull MarkwonTheme theme, @NonNull MarkwonConfiguration configuration); + MarkwonVisitor build(@NonNull MarkwonConfiguration configuration); } + @NonNull + MarkwonConfiguration configuration(); + @NonNull MarkwonTheme theme(); @NonNull - MarkwonConfiguration configuration(); + SpannableFactory factory(); @NonNull SpannableBuilder builder(); diff --git a/markwon/src/main/java/ru/noties/markwon/MarkwonVisitorImpl.java b/markwon/src/main/java/ru/noties/markwon/MarkwonVisitorImpl.java index 55971a38..c4f98d04 100644 --- a/markwon/src/main/java/ru/noties/markwon/MarkwonVisitorImpl.java +++ b/markwon/src/main/java/ru/noties/markwon/MarkwonVisitorImpl.java @@ -37,19 +37,21 @@ class MarkwonVisitorImpl implements MarkwonVisitor { private final Map, NodeVisitor> nodes; - private final MarkwonTheme theme; private final MarkwonConfiguration configuration; + private final MarkwonTheme theme; + private final SpannableFactory factory; + private final SpannableBuilder builder = new SpannableBuilder(); private int blockQuoteIndent; private int listLevel; private MarkwonVisitorImpl( - @NonNull MarkwonTheme theme, @NonNull MarkwonConfiguration configuration, @NonNull Map, NodeVisitor> nodes) { - this.theme = theme; this.configuration = configuration; + this.theme = configuration.theme(); + this.factory = configuration.factory(); this.nodes = nodes; } @@ -173,6 +175,12 @@ class MarkwonVisitorImpl implements MarkwonVisitor { } } + @NonNull + @Override + public MarkwonConfiguration configuration() { + return configuration; + } + @NonNull @Override public MarkwonTheme theme() { @@ -181,8 +189,8 @@ class MarkwonVisitorImpl implements MarkwonVisitor { @NonNull @Override - public MarkwonConfiguration configuration() { - return configuration; + public SpannableFactory factory() { + return factory; } @NonNull @@ -280,9 +288,8 @@ class MarkwonVisitorImpl implements MarkwonVisitor { @NonNull @Override - public MarkwonVisitor build(@NonNull MarkwonTheme theme, @NonNull MarkwonConfiguration configuration) { + public MarkwonVisitor build(@NonNull MarkwonConfiguration configuration) { return new MarkwonVisitorImpl( - theme, configuration, Collections.unmodifiableMap(nodes)); } diff --git a/markwon/src/main/java/ru/noties/markwon/core/CorePlugin.java b/markwon/src/main/java/ru/noties/markwon/core/CorePlugin.java index a1495c67..3d4ef0c4 100644 --- a/markwon/src/main/java/ru/noties/markwon/core/CorePlugin.java +++ b/markwon/src/main/java/ru/noties/markwon/core/CorePlugin.java @@ -9,17 +9,24 @@ import org.commonmark.node.BulletList; import org.commonmark.node.Code; import org.commonmark.node.Emphasis; import org.commonmark.node.FencedCodeBlock; +import org.commonmark.node.HardLineBreak; +import org.commonmark.node.Heading; +import org.commonmark.node.Image; import org.commonmark.node.IndentedCodeBlock; +import org.commonmark.node.Link; +import org.commonmark.node.ListBlock; import org.commonmark.node.ListItem; import org.commonmark.node.Node; import org.commonmark.node.OrderedList; +import org.commonmark.node.Paragraph; +import org.commonmark.node.SoftLineBreak; import org.commonmark.node.StrongEmphasis; import org.commonmark.node.Text; import org.commonmark.node.ThematicBreak; import ru.noties.markwon.AbstractMarkwonPlugin; +import ru.noties.markwon.MarkwonConfiguration; import ru.noties.markwon.MarkwonVisitor; -import ru.noties.markwon.SpannableBuilder; import ru.noties.markwon.spans.OrderedListItemSpan; public class CorePlugin extends AbstractMarkwonPlugin { @@ -30,6 +37,25 @@ public class CorePlugin extends AbstractMarkwonPlugin { // todo: softBreak adds new line should be here (or maybe removed even?) + // todo: add a simple HTML handler + // todo: configure primitive images (without okhttp -> just HttpUrlConnection and simple types (static, data) + + @NonNull + public static CorePlugin create() { + return create(false); + } + + @NonNull + public static CorePlugin create(boolean softBreakAddsNewLine) { + return new CorePlugin(softBreakAddsNewLine); + } + + private final boolean softBreakAddsNewLine; + + protected CorePlugin(boolean softBreakAddsNewLine) { + this.softBreakAddsNewLine = softBreakAddsNewLine; + } + @Override public void configureVisitor(@NonNull MarkwonVisitor.Builder builder) { text(builder); @@ -43,11 +69,17 @@ public class CorePlugin extends AbstractMarkwonPlugin { orderedList(builder); listItem(builder); thematicBreak(builder); + heading(builder); + softLineBreak(builder); + hardLineBreak(builder); + paragraph(builder); + image(builder); + link(builder); } @Override - public void beforeSetText(@NonNull TextView textView, @NonNull SpannableBuilder builder) { - OrderedListItemSpan.measure(textView, builder); + public void beforeSetText(@NonNull TextView textView, @NonNull CharSequence markdown) { + OrderedListItemSpan.measure(textView, markdown); } protected void text(@NonNull MarkwonVisitor.Builder builder) { @@ -65,7 +97,7 @@ public class CorePlugin extends AbstractMarkwonPlugin { public void visit(@NonNull MarkwonVisitor visitor, @NonNull StrongEmphasis strongEmphasis) { final int length = visitor.length(); visitor.visitChildren(strongEmphasis); - visitor.setSpans(length, visitor.configuration().factory().strongEmphasis()); + visitor.setSpans(length, visitor.factory().strongEmphasis()); } }); } @@ -76,7 +108,7 @@ public class CorePlugin extends AbstractMarkwonPlugin { public void visit(@NonNull MarkwonVisitor visitor, @NonNull Emphasis emphasis) { final int length = visitor.length(); visitor.visitChildren(emphasis); - visitor.setSpans(length, visitor.configuration().factory().emphasis()); + visitor.setSpans(length, visitor.factory().emphasis()); } }); } @@ -95,7 +127,7 @@ public class CorePlugin extends AbstractMarkwonPlugin { final int length = visitor.length(); visitor.incrementBlockQuoteIndent(); visitor.visitChildren(blockQuote); - visitor.setSpans(length, visitor.configuration().factory().blockQuote(visitor.theme())); + visitor.setSpans(length, visitor.factory().blockQuote(visitor.theme())); visitor.decrementBlockQuoteIndent(); if (visitor.hasNext(blockQuote)) { @@ -122,7 +154,7 @@ public class CorePlugin extends AbstractMarkwonPlugin { .append(code.getLiteral()) .append('\u00a0'); - visitor.setSpans(length, visitor.configuration().factory().code(visitor.theme(), false)); + visitor.setSpans(length, visitor.factory().code(visitor.theme(), false)); } }); } @@ -163,7 +195,7 @@ public class CorePlugin extends AbstractMarkwonPlugin { visitor.builder().append('\u00a0'); - visitor.setSpans(length, visitor.configuration().factory().code(visitor.theme(), true)); + visitor.setSpans(length, visitor.factory().code(visitor.theme(), true)); if (visitor.hasNext(node)) { visitor.ensureNewLine(); @@ -220,7 +252,7 @@ public class CorePlugin extends AbstractMarkwonPlugin { final int start = ((OrderedList) parent).getStartNumber(); visitor.visitChildren(listItem); - visitor.setSpans(length, visitor.configuration().factory().orderedListItem(visitor.theme(), start)); + visitor.setSpans(length, visitor.factory().orderedListItem(visitor.theme(), start)); // after we have visited the children increment start number @@ -230,7 +262,7 @@ public class CorePlugin extends AbstractMarkwonPlugin { } else { visitor.visitChildren(listItem); - visitor.setSpans(length, visitor.configuration().factory().bulletListItem(visitor.theme(), visitor.listLevel() - 1)); + visitor.setSpans(length, visitor.factory().bulletListItem(visitor.theme(), visitor.listLevel() - 1)); } @@ -256,7 +288,7 @@ public class CorePlugin extends AbstractMarkwonPlugin { // without space it won't render visitor.builder().append('\u00a0'); - visitor.setSpans(length, visitor.configuration().factory().thematicBreak(visitor.theme())); + visitor.setSpans(length, visitor.factory().thematicBreak(visitor.theme())); if (visitor.hasNext(thematicBreak)) { visitor.ensureNewLine(); @@ -265,4 +297,132 @@ public class CorePlugin extends AbstractMarkwonPlugin { } }); } + + protected void heading(@NonNull MarkwonVisitor.Builder builder) { + builder.on(Heading.class, new MarkwonVisitor.NodeVisitor() { + @Override + public void visit(@NonNull MarkwonVisitor visitor, @NonNull Heading heading) { + + visitor.ensureNewLine(); + + final int length = visitor.length(); + visitor.visitChildren(heading); + visitor.setSpans(length, visitor.factory().heading(visitor.theme(), heading.getLevel())); + + if (visitor.hasNext(heading)) { + visitor.ensureNewLine(); + visitor.forceNewLine(); + } + } + }); + } + + protected void softLineBreak(@NonNull MarkwonVisitor.Builder builder) { + builder.on(SoftLineBreak.class, new MarkwonVisitor.NodeVisitor() { + @Override + public void visit(@NonNull MarkwonVisitor visitor, @NonNull SoftLineBreak softLineBreak) { + if (softBreakAddsNewLine) { + visitor.ensureNewLine(); + } else { + visitor.builder().append(' '); + } + } + }); + } + + protected void hardLineBreak(@NonNull MarkwonVisitor.Builder builder) { + builder.on(HardLineBreak.class, new MarkwonVisitor.NodeVisitor() { + @Override + public void visit(@NonNull MarkwonVisitor visitor, @NonNull HardLineBreak hardLineBreak) { + visitor.ensureNewLine(); + } + }); + } + + protected void paragraph(@NonNull MarkwonVisitor.Builder builder) { + builder.on(Paragraph.class, new MarkwonVisitor.NodeVisitor() { + @Override + public void visit(@NonNull MarkwonVisitor visitor, @NonNull Paragraph paragraph) { + + final boolean inTightList = isInTightList(paragraph); + + if (!inTightList) { + visitor.ensureNewLine(); + } + + final int length = visitor.length(); + visitor.visitChildren(paragraph); + + // @since 1.1.1 apply paragraph span + visitor.setSpans(length, visitor.factory().paragraph(inTightList)); + + if (!inTightList && visitor.hasNext(paragraph)) { + visitor.ensureNewLine(); + if (visitor.blockQuoteIndent() == 0) { + visitor.forceNewLine(); + } + } + } + }); + } + + protected void image(@NonNull MarkwonVisitor.Builder builder) { + builder.on(Image.class, new MarkwonVisitor.NodeVisitor() { + @Override + public void visit(@NonNull MarkwonVisitor visitor, @NonNull Image image) { + + final int length = visitor.length(); + + visitor.visitChildren(image); + + // we must check if anything _was_ added, as we need at least one char to render + if (length == visitor.length()) { + visitor.builder().append('\uFFFC'); + } + + final MarkwonConfiguration configuration = visitor.configuration(); + + final Node parent = image.getParent(); + final boolean link = parent instanceof Link; + final String destination = configuration + .urlProcessor() + .process(image.getDestination()); + + final Object spans = visitor.factory().image( + visitor.theme(), + destination, + configuration.asyncDrawableLoader(), + configuration.imageSizeResolver(), + null, + link); + + visitor.setSpans(length, spans); + } + }); + } + + protected void link(@NonNull MarkwonVisitor.Builder builder) { + builder.on(Link.class, new MarkwonVisitor.NodeVisitor() { + @Override + public void visit(@NonNull MarkwonVisitor visitor, @NonNull Link link) { + final int length = visitor.length(); + visitor.visitChildren(link); + final MarkwonConfiguration configuration = visitor.configuration(); + final String destination = configuration.urlProcessor().process(link.getDestination()); + visitor.setSpans(length, visitor.factory().link(visitor.theme(), destination, configuration.linkResolver())); + } + }); + } + + private static boolean isInTightList(@NonNull Paragraph paragraph) { + final Node parent = paragraph.getParent(); + if (parent != null) { + final Node gramps = parent.getParent(); + if (gramps instanceof ListBlock) { + ListBlock list = (ListBlock) gramps; + return list.isTight(); + } + } + return false; + } } diff --git a/markwon/src/main/java/ru/noties/markwon/spans/OrderedListItemSpan.java b/markwon/src/main/java/ru/noties/markwon/spans/OrderedListItemSpan.java index e2bc1fb0..f7bacfeb 100644 --- a/markwon/src/main/java/ru/noties/markwon/spans/OrderedListItemSpan.java +++ b/markwon/src/main/java/ru/noties/markwon/spans/OrderedListItemSpan.java @@ -20,8 +20,8 @@ public class OrderedListItemSpan implements LeadingMarginSpan { * NB, this method must be called before setting text to a TextView (`TextView#setText` * internally can trigger new Layout creation which will ask for leading margins right away) * - * @param textView to which markdown will be applied - * @param text parsed markdown to process + * @param textView to which toMarkdown will be applied + * @param text parsed toMarkdown to process * @since 2.0.1 */ public static void measure(@NonNull TextView textView, @NonNull CharSequence text) { diff --git a/markwon/src/main/java/ru/noties/markwon/utils/LeadingMarginUtils.java b/markwon/src/main/java/ru/noties/markwon/utils/LeadingMarginUtils.java index f783fcae..bc18a140 100644 --- a/markwon/src/main/java/ru/noties/markwon/utils/LeadingMarginUtils.java +++ b/markwon/src/main/java/ru/noties/markwon/utils/LeadingMarginUtils.java @@ -4,6 +4,7 @@ import android.text.Spanned; public abstract class LeadingMarginUtils { + @SuppressWarnings("BooleanMethodIsAlwaysInverted") public static boolean selfStart(int start, CharSequence text, Object span) { return text instanceof Spanned && ((Spanned) text).getSpanStart(span) == start; } diff --git a/sample-custom-extension/src/main/java/ru/noties/markwon/sample/extension/MainActivity.java b/sample-custom-extension/src/main/java/ru/noties/markwon/sample/extension/MainActivity.java index b104e88e..9cb46349 100644 --- a/sample-custom-extension/src/main/java/ru/noties/markwon/sample/extension/MainActivity.java +++ b/sample-custom-extension/src/main/java/ru/noties/markwon/sample/extension/MainActivity.java @@ -47,7 +47,7 @@ public class MainActivity extends Activity { final SpannableBuilder builder = new SpannableBuilder(); - // please note that here I am passing `0` as fallback it means that if markdown references + // please note that here I am passing `0` as fallback it means that if toMarkdown references // unknown icon, it will try to load fallback one and will fail with ResourceNotFound. It's // better to provide a valid fallback option final IconSpanProvider spanProvider = IconSpanProvider.create(this, 0); @@ -59,7 +59,7 @@ public class MainActivity extends Activity { .headingTextSizeMultipliers(textSizeMultipliers) .build()) .build(); - // create an instance of visitor to process parsed markdown + // create an instance of visitor to process parsed toMarkdown final IconVisitor visitor = new IconVisitor( configuration, builder, From 2efd12f020707c353f650fd8d3a09f10ea7df960 Mon Sep 17 00:00:00 2001 From: Dimitry Ivanov Date: Sun, 25 Nov 2018 15:48:58 +0300 Subject: [PATCH 019/103] Syntax highlight plugin, image modules (svg, gif), working sample application --- app/build.gradle | 3 +- .../java/ru/noties/markwon/AppModule.java | 36 +-- .../noties/markwon/GifAwareAsyncDrawable.java | 5 +- .../ru/noties/markwon/GifAwarePlugin.java | 35 +++ .../markwon/GifAwareSpannableFactory.java | 5 +- .../java/ru/noties/markwon/MainActivity.java | 22 +- .../ru/noties/markwon/MarkdownRenderer.java | 52 ++-- markwon-image-gif/build.gradle | 25 ++ .../src/main/AndroidManifest.xml | 1 + .../markwon/image/gif/GifMediaDecoder.java | 82 +++++++ .../noties/markwon/image/gif/GifPlugin.java | 30 +++ .../markwon/il/AsyncDrawableLoader.java | 2 +- markwon-image-svg/build.gradle | 25 ++ .../src/main/AndroidManifest.xml | 1 + .../markwon/image/svg/SvgMediaDecoder.java | 73 ++++++ .../noties/markwon/image/svg/SvgPlugin.java | 26 ++ .../markwon/syntax/Prism4jThemeDarkula.java | 16 +- .../markwon/syntax/SyntaxHighlightPlugin.java | 52 ++++ .../noties/markwon/AbstractMarkwonPlugin.java | 8 +- .../main/java/ru/noties/markwon/Markwon.java | 21 +- .../ru/noties/markwon/MarkwonBuilderImpl.java | 9 +- .../noties/markwon/MarkwonConfiguration.java | 31 +-- .../java/ru/noties/markwon/MarkwonImpl.java | 2 +- .../java/ru/noties/markwon/MarkwonPlugin.java | 9 +- .../ru/noties/markwon/MarkwonVisitor.java | 2 +- .../ru/noties/markwon/MarkwonVisitorImpl.java | 10 +- .../ru/noties/markwon/SpannableFactory.java | 17 +- .../noties/markwon/SpannableFactoryDef.java | 23 +- .../AsyncDrawableScheduler.java} | 8 +- .../ru/noties/markwon/core/CorePlugin.java | 44 +--- .../{spans => image}/AsyncDrawable.java | 13 +- .../markwon/image/AsyncDrawableLoader.java | 104 ++++++++ .../image/AsyncDrawableLoaderImpl.java | 135 +++++++++++ .../{ => image}/AsyncDrawableLoaderNoOp.java | 6 +- .../ru/noties/markwon/image/ImageItem.java | 31 +++ .../markwon/image/ImageMediaDecoder.java | 52 ++++ .../ru/noties/markwon/image/ImagesPlugin.java | 92 +++++++ .../ru/noties/markwon/image/MediaDecoder.java | 16 ++ .../noties/markwon/image/SchemeHandler.java | 14 ++ .../ru/noties/markwon/image/data/DataUri.java | 60 +++++ .../markwon/image/data/DataUriDecoder.java | 41 ++++ .../markwon/image/data/DataUriParser.java | 79 ++++++ .../image/data/DataUriSchemeHandler.java | 65 +++++ .../markwon/image/file/FileSchemeHandler.java | 105 ++++++++ .../image/network/NetworkSchemeHandler.java | 64 +++++ .../renderer/SpannableMarkdownVisitor.java | 226 ++++++++---------- .../markwon/spans/AsyncDrawableSpan.java | 27 ++- .../ru/noties/markwon/spans/CanvasUtils.java | 15 -- .../ru/noties/markwon/spans/ColorUtils.java | 11 - .../ru/noties/markwon/spans/MarkwonTheme.java | 216 +---------------- .../ru/noties/markwon/table/TablePlugin.java | 189 +++++++++++++++ .../{spans => table}/TableRowSpan.java | 13 +- .../{ => table}/TableRowsScheduler.java | 5 +- .../ru/noties/markwon/table/TableTheme.java | 164 +++++++++++++ .../markwon/tasklist/TaskListPlugin.java | 28 ++- .../ru/noties/markwon/utils/ColorUtils.java | 11 + .../java/ru/noties/markwon/utils/Dip.java | 29 +++ .../noties/markwon/utils/DrawableUtils.java | 13 + .../markwon/image/data/DataUriParserTest.java | 119 +++++++++ .../image/data/DataUriSchemeHandlerTest.java | 114 +++++++++ .../renderer/MarkwonConfigurationTest.java | 2 +- .../renderer/visitor/TestDataReader.java | 2 +- .../markwon/renderer/visitor/TestFactory.java | 4 +- sample-latex-math/build.gradle | 2 +- settings.gradle | 2 +- 65 files changed, 2146 insertions(+), 598 deletions(-) create mode 100644 app/src/main/java/ru/noties/markwon/GifAwarePlugin.java create mode 100644 markwon-image-gif/build.gradle create mode 100644 markwon-image-gif/src/main/AndroidManifest.xml create mode 100644 markwon-image-gif/src/main/java/ru/noties/markwon/image/gif/GifMediaDecoder.java create mode 100644 markwon-image-gif/src/main/java/ru/noties/markwon/image/gif/GifPlugin.java create mode 100644 markwon-image-svg/build.gradle create mode 100644 markwon-image-svg/src/main/AndroidManifest.xml create mode 100644 markwon-image-svg/src/main/java/ru/noties/markwon/image/svg/SvgMediaDecoder.java create mode 100644 markwon-image-svg/src/main/java/ru/noties/markwon/image/svg/SvgPlugin.java create mode 100644 markwon-syntax-highlight/src/main/java/ru/noties/markwon/syntax/SyntaxHighlightPlugin.java rename markwon/src/main/java/ru/noties/markwon/{DrawablesScheduler.java => core/AsyncDrawableScheduler.java} (97%) rename markwon/src/main/java/ru/noties/markwon/{spans => image}/AsyncDrawable.java (93%) create mode 100644 markwon/src/main/java/ru/noties/markwon/image/AsyncDrawableLoader.java create mode 100644 markwon/src/main/java/ru/noties/markwon/image/AsyncDrawableLoaderImpl.java rename markwon/src/main/java/ru/noties/markwon/{ => image}/AsyncDrawableLoaderNoOp.java (62%) create mode 100644 markwon/src/main/java/ru/noties/markwon/image/ImageItem.java create mode 100644 markwon/src/main/java/ru/noties/markwon/image/ImageMediaDecoder.java create mode 100644 markwon/src/main/java/ru/noties/markwon/image/ImagesPlugin.java create mode 100644 markwon/src/main/java/ru/noties/markwon/image/MediaDecoder.java create mode 100644 markwon/src/main/java/ru/noties/markwon/image/SchemeHandler.java create mode 100644 markwon/src/main/java/ru/noties/markwon/image/data/DataUri.java create mode 100644 markwon/src/main/java/ru/noties/markwon/image/data/DataUriDecoder.java create mode 100644 markwon/src/main/java/ru/noties/markwon/image/data/DataUriParser.java create mode 100644 markwon/src/main/java/ru/noties/markwon/image/data/DataUriSchemeHandler.java create mode 100644 markwon/src/main/java/ru/noties/markwon/image/file/FileSchemeHandler.java create mode 100644 markwon/src/main/java/ru/noties/markwon/image/network/NetworkSchemeHandler.java delete mode 100644 markwon/src/main/java/ru/noties/markwon/spans/CanvasUtils.java delete mode 100644 markwon/src/main/java/ru/noties/markwon/spans/ColorUtils.java create mode 100644 markwon/src/main/java/ru/noties/markwon/table/TablePlugin.java rename markwon/src/main/java/ru/noties/markwon/{spans => table}/TableRowSpan.java (96%) rename markwon/src/main/java/ru/noties/markwon/{ => table}/TableRowsScheduler.java (96%) create mode 100644 markwon/src/main/java/ru/noties/markwon/table/TableTheme.java create mode 100644 markwon/src/main/java/ru/noties/markwon/utils/ColorUtils.java create mode 100644 markwon/src/main/java/ru/noties/markwon/utils/Dip.java create mode 100644 markwon/src/main/java/ru/noties/markwon/utils/DrawableUtils.java create mode 100644 markwon/src/test/java/ru/noties/markwon/image/data/DataUriParserTest.java create mode 100644 markwon/src/test/java/ru/noties/markwon/image/data/DataUriSchemeHandlerTest.java diff --git a/app/build.gradle b/app/build.gradle index 0869e5e5..9512e8d2 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -29,7 +29,8 @@ android { dependencies { implementation project(':markwon') - implementation project(':markwon-image-loader') + implementation project(':markwon-image-gif') + implementation project(':markwon-image-svg') implementation project(':markwon-syntax-highlight') deps.with { diff --git a/app/src/main/java/ru/noties/markwon/AppModule.java b/app/src/main/java/ru/noties/markwon/AppModule.java index 3e2f3967..32d3e931 100644 --- a/app/src/main/java/ru/noties/markwon/AppModule.java +++ b/app/src/main/java/ru/noties/markwon/AppModule.java @@ -14,11 +14,6 @@ import dagger.Module; import dagger.Provides; import okhttp3.Cache; import okhttp3.OkHttpClient; -import ru.noties.markwon.il.AsyncDrawableLoader; -import ru.noties.markwon.il.GifMediaDecoder; -import ru.noties.markwon.il.ImageMediaDecoder; -import ru.noties.markwon.il.SvgMediaDecoder; -import ru.noties.markwon.spans.AsyncDrawable; import ru.noties.markwon.syntax.Prism4jThemeDarkula; import ru.noties.markwon.syntax.Prism4jThemeDefault; import ru.noties.prism4j.Prism4j; @@ -72,23 +67,6 @@ class AppModule { return new UriProcessorImpl(); } - @Provides - AsyncDrawable.Loader asyncDrawableLoader( - OkHttpClient client, - ExecutorService executorService, - Resources resources) { - return AsyncDrawableLoader.builder() - .client(client) - .executorService(executorService) - .resources(resources) - .mediaDecoders( - SvgMediaDecoder.create(resources), - GifMediaDecoder.create(false), - ImageMediaDecoder.create(resources) - ) - .build(); - } - @Provides @Singleton Prism4j prism4j() { @@ -104,12 +82,12 @@ class AppModule { @Singleton @Provides Prism4jThemeDarkula prism4jThemeDarkula() { - return Prism4jThemeDarkula.create(); - } - - @Singleton - @Provides - GifProcessor gifProcessor() { - return GifProcessor.create(); + return Prism4jThemeDarkula.create(0x0Fffffff); } +// +// @Singleton +// @Provides +// GifProcessor gifProcessor() { +// return GifProcessor.create(); +// } } diff --git a/app/src/main/java/ru/noties/markwon/GifAwareAsyncDrawable.java b/app/src/main/java/ru/noties/markwon/GifAwareAsyncDrawable.java index 78d286af..ed16554f 100644 --- a/app/src/main/java/ru/noties/markwon/GifAwareAsyncDrawable.java +++ b/app/src/main/java/ru/noties/markwon/GifAwareAsyncDrawable.java @@ -6,9 +6,10 @@ import android.support.annotation.NonNull; import android.support.annotation.Nullable; import pl.droidsonroids.gif.GifDrawable; +import ru.noties.markwon.image.AsyncDrawableLoader; import ru.noties.markwon.renderer.ImageSize; import ru.noties.markwon.renderer.ImageSizeResolver; -import ru.noties.markwon.spans.AsyncDrawable; +import ru.noties.markwon.image.AsyncDrawable; public class GifAwareAsyncDrawable extends AsyncDrawable { @@ -23,7 +24,7 @@ public class GifAwareAsyncDrawable extends AsyncDrawable { public GifAwareAsyncDrawable( @NonNull Drawable gifPlaceholder, @NonNull String destination, - @NonNull Loader loader, + @NonNull AsyncDrawableLoader loader, @Nullable ImageSizeResolver imageSizeResolver, @Nullable ImageSize imageSize) { super(destination, loader, imageSizeResolver, imageSize); diff --git a/app/src/main/java/ru/noties/markwon/GifAwarePlugin.java b/app/src/main/java/ru/noties/markwon/GifAwarePlugin.java new file mode 100644 index 00000000..bb2fafd4 --- /dev/null +++ b/app/src/main/java/ru/noties/markwon/GifAwarePlugin.java @@ -0,0 +1,35 @@ +package ru.noties.markwon; + +import android.content.Context; +import android.support.annotation.NonNull; +import android.widget.TextView; + +public class GifAwarePlugin extends AbstractMarkwonPlugin { + + @NonNull + public static GifAwarePlugin create(@NonNull Context context) { + return new GifAwarePlugin(context); + } + + private final Context context; + private final GifProcessor processor; + + public GifAwarePlugin(@NonNull Context context) { + this.context = context; + this.processor = GifProcessor.create(); + } + + @Override + public void configureConfiguration(@NonNull MarkwonConfiguration.Builder builder) { + final GifPlaceholder gifPlaceholder = new GifPlaceholder( + context.getResources().getDrawable(R.drawable.ic_play_circle_filled_18dp_white), + 0x20000000 + ); + builder.factory(new GifAwareSpannableFactory(gifPlaceholder)); + } + + @Override + public void afterSetText(@NonNull TextView textView) { + processor.process(textView); + } +} diff --git a/app/src/main/java/ru/noties/markwon/GifAwareSpannableFactory.java b/app/src/main/java/ru/noties/markwon/GifAwareSpannableFactory.java index 7c387a1c..432a379c 100644 --- a/app/src/main/java/ru/noties/markwon/GifAwareSpannableFactory.java +++ b/app/src/main/java/ru/noties/markwon/GifAwareSpannableFactory.java @@ -3,9 +3,10 @@ package ru.noties.markwon; import android.support.annotation.NonNull; import android.support.annotation.Nullable; +import ru.noties.markwon.image.AsyncDrawableLoader; import ru.noties.markwon.renderer.ImageSize; import ru.noties.markwon.renderer.ImageSizeResolver; -import ru.noties.markwon.spans.AsyncDrawable; +import ru.noties.markwon.image.AsyncDrawable; import ru.noties.markwon.spans.AsyncDrawableSpan; import ru.noties.markwon.spans.MarkwonTheme; @@ -19,7 +20,7 @@ public class GifAwareSpannableFactory extends SpannableFactoryDef { @Nullable @Override - public Object image(@NonNull MarkwonTheme theme, @NonNull String destination, @NonNull AsyncDrawable.Loader loader, @NonNull ImageSizeResolver imageSizeResolver, @Nullable ImageSize imageSize, boolean replacementTextIsLink) { + public Object image(@NonNull MarkwonTheme theme, @NonNull String destination, @NonNull AsyncDrawableLoader loader, @NonNull ImageSizeResolver imageSizeResolver, @Nullable ImageSize imageSize, boolean replacementTextIsLink) { return new AsyncDrawableSpan( theme, new GifAwareAsyncDrawable( diff --git a/app/src/main/java/ru/noties/markwon/MainActivity.java b/app/src/main/java/ru/noties/markwon/MainActivity.java index bd477bcd..84904934 100644 --- a/app/src/main/java/ru/noties/markwon/MainActivity.java +++ b/app/src/main/java/ru/noties/markwon/MainActivity.java @@ -29,9 +29,9 @@ public class MainActivity extends Activity { @Inject UriProcessor uriProcessor; - - @Inject - GifProcessor gifProcessor; +// +// @Inject +// GifProcessor gifProcessor; @Override protected void onCreate(final Bundle savedInstanceState) { @@ -66,26 +66,14 @@ public class MainActivity extends Activity { appBarRenderer.render(appBarState()); - if (true) { - final Markwon2 markwon2 = Markwon2.builder(this) - .use(new CorePlugin()) - .use(TaskListPlugin.create(new TaskListDrawable(0xffff0000, 0xffff0000, -1))) - .build(); - final CharSequence markdown = markwon2.toMarkdown("**hello _dear_** `code`\n\n- [ ] first\n- [x] second"); - textView.setText(markdown); - return; - } - markdownLoader.load(uri(), new MarkdownLoader.OnMarkdownTextLoaded() { @Override public void apply(final String text) { markdownRenderer.render(MainActivity.this, themes.isLight(), uri(), text, new MarkdownRenderer.MarkdownReadyListener() { @Override - public void onMarkdownReady(CharSequence markdown) { + public void onMarkdownReady(@NonNull Markwon2 markwon2, CharSequence markdown) { - Markwon.setText(textView, markdown); - - gifProcessor.process(textView); + markwon2.setParsedMarkdown(textView, markdown); Views.setVisible(progress, false); } diff --git a/app/src/main/java/ru/noties/markwon/MarkdownRenderer.java b/app/src/main/java/ru/noties/markwon/MarkdownRenderer.java index f0eb38e5..053fde93 100644 --- a/app/src/main/java/ru/noties/markwon/MarkdownRenderer.java +++ b/app/src/main/java/ru/noties/markwon/MarkdownRenderer.java @@ -13,24 +13,23 @@ import java.util.concurrent.Future; import javax.inject.Inject; import ru.noties.debug.Debug; -import ru.noties.markwon.spans.AsyncDrawable; -import ru.noties.markwon.spans.MarkwonTheme; -import ru.noties.markwon.syntax.Prism4jSyntaxHighlight; +import ru.noties.markwon.core.CorePlugin; +import ru.noties.markwon.image.ImagesPlugin; +import ru.noties.markwon.image.gif.GifPlugin; +import ru.noties.markwon.image.svg.SvgPlugin; import ru.noties.markwon.syntax.Prism4jTheme; import ru.noties.markwon.syntax.Prism4jThemeDarkula; import ru.noties.markwon.syntax.Prism4jThemeDefault; +import ru.noties.markwon.syntax.SyntaxHighlightPlugin; import ru.noties.prism4j.Prism4j; @ActivityScope public class MarkdownRenderer { interface MarkdownReadyListener { - void onMarkdownReady(CharSequence markdown); + void onMarkdownReady(@NonNull Markwon2 markwon2, CharSequence markdown); } - @Inject - AsyncDrawable.Loader loader; - @Inject ExecutorService service; @@ -78,40 +77,39 @@ public class MarkdownRenderer { ? prism4jThemeDefault : prism4JThemeDarkula; - final int background = isLightTheme - ? prism4jTheme.background() - : 0x0Fffffff; +// final int background = isLightTheme +// ? prism4jTheme.background() +// : 0x0Fffffff; - final GifPlaceholder gifPlaceholder = new GifPlaceholder( - context.getResources().getDrawable(R.drawable.ic_play_circle_filled_18dp_white), - 0x20000000 - ); - - final MarkwonConfiguration configuration = MarkwonConfiguration.builder(context) - .asyncDrawableLoader(loader) - .urlProcessor(urlProcessor) - .syntaxHighlight(Prism4jSyntaxHighlight.create(prism4j, prism4jTheme)) - .theme(MarkwonTheme.builderWithDefaults(context) - .codeBackgroundColor(background) - .codeTextColor(prism4jTheme.textColor()) - .build()) - .factory(new GifAwareSpannableFactory(gifPlaceholder)) + final Markwon2 markwon2 = Markwon2.builder(context) + .use(CorePlugin.create()) + .use(ImagesPlugin.createWithAssets(context)) + .use(SvgPlugin.create(context.getResources())) + .use(GifPlugin.create(false)) + .use(SyntaxHighlightPlugin.create(prism4j, prism4jTheme)) + .use(GifAwarePlugin.create(context)) + .use(new AbstractMarkwonPlugin() { + @Override + public void configureConfiguration(@NonNull MarkwonConfiguration.Builder builder) { + builder.urlProcessor(urlProcessor); + } + }) .build(); final long start = SystemClock.uptimeMillis(); - final CharSequence text = Markwon.markdown(configuration, markdown); + final CharSequence text = markwon2.toMarkdown(markdown); final long end = SystemClock.uptimeMillis(); - Debug.i("toMarkdown rendered: %d ms", end - start); + Debug.i("markdown rendered: %d ms", end - start); if (!isCancelled()) { handler.post(new Runnable() { @Override public void run() { if (!isCancelled()) { - listener.onMarkdownReady(text); + listener.onMarkdownReady(markwon2, text); task = null; } } diff --git a/markwon-image-gif/build.gradle b/markwon-image-gif/build.gradle new file mode 100644 index 00000000..2eeb680e --- /dev/null +++ b/markwon-image-gif/build.gradle @@ -0,0 +1,25 @@ +apply plugin: 'com.android.library' + +android { + + compileSdkVersion config['compile-sdk'] + buildToolsVersion config['build-tools'] + + defaultConfig { + minSdkVersion config['min-sdk'] + targetSdkVersion config['target-sdk'] + versionCode 1 + versionName version + } +} + +dependencies { + + api project(':markwon') + + deps.with { + api it['android-gif'] + } +} + +registerArtifact(this) \ No newline at end of file diff --git a/markwon-image-gif/src/main/AndroidManifest.xml b/markwon-image-gif/src/main/AndroidManifest.xml new file mode 100644 index 00000000..649a9a70 --- /dev/null +++ b/markwon-image-gif/src/main/AndroidManifest.xml @@ -0,0 +1 @@ + diff --git a/markwon-image-gif/src/main/java/ru/noties/markwon/image/gif/GifMediaDecoder.java b/markwon-image-gif/src/main/java/ru/noties/markwon/image/gif/GifMediaDecoder.java new file mode 100644 index 00000000..7a6600b8 --- /dev/null +++ b/markwon-image-gif/src/main/java/ru/noties/markwon/image/gif/GifMediaDecoder.java @@ -0,0 +1,82 @@ +package ru.noties.markwon.image.gif; + +import android.graphics.drawable.Drawable; +import android.support.annotation.NonNull; +import android.support.annotation.Nullable; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.InputStream; + +import pl.droidsonroids.gif.GifDrawable; +import ru.noties.markwon.image.MediaDecoder; +import ru.noties.markwon.utils.DrawableUtils; + +/** + * @since 1.1.0 + */ +@SuppressWarnings("WeakerAccess") +public class GifMediaDecoder extends MediaDecoder { + + public static final String CONTENT_TYPE = "image/gif"; + + @NonNull + public static GifMediaDecoder create(boolean autoPlayGif) { + return new GifMediaDecoder(autoPlayGif); + } + + private final boolean autoPlayGif; + + protected GifMediaDecoder(boolean autoPlayGif) { + this.autoPlayGif = autoPlayGif; + } + + @Nullable + @Override + public Drawable decode(@NonNull InputStream inputStream) { + + Drawable out = null; + + final byte[] bytes = readBytes(inputStream); + if (bytes != null) { + try { + out = newGifDrawable(bytes); + DrawableUtils.intrinsicBounds(out); + + if (!autoPlayGif) { + ((GifDrawable) out).pause(); + } + } catch (IOException e) { + e.printStackTrace(); + } + } + + return out; + } + + @NonNull + protected Drawable newGifDrawable(@NonNull byte[] bytes) throws IOException { + return new GifDrawable(bytes); + } + + @Nullable + protected static byte[] readBytes(@NonNull InputStream stream) { + + byte[] out = null; + + try { + final ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); + final int length = 1024 * 8; + final byte[] buffer = new byte[length]; + int read; + while ((read = stream.read(buffer, 0, length)) != -1) { + outputStream.write(buffer, 0, read); + } + out = outputStream.toByteArray(); + } catch (IOException e) { + e.printStackTrace(); + } + + return out; + } +} diff --git a/markwon-image-gif/src/main/java/ru/noties/markwon/image/gif/GifPlugin.java b/markwon-image-gif/src/main/java/ru/noties/markwon/image/gif/GifPlugin.java new file mode 100644 index 00000000..3ee0c7aa --- /dev/null +++ b/markwon-image-gif/src/main/java/ru/noties/markwon/image/gif/GifPlugin.java @@ -0,0 +1,30 @@ +package ru.noties.markwon.image.gif; + +import android.support.annotation.NonNull; + +import ru.noties.markwon.AbstractMarkwonPlugin; +import ru.noties.markwon.image.AsyncDrawableLoader; + +public class GifPlugin extends AbstractMarkwonPlugin { + + @NonNull + public static GifPlugin create() { + return create(true); + } + + @NonNull + public static GifPlugin create(boolean autoPlay) { + return new GifPlugin(autoPlay); + } + + private final boolean autoPlay; + + public GifPlugin(boolean autoPlay) { + this.autoPlay = autoPlay; + } + + @Override + public void configureImages(@NonNull AsyncDrawableLoader.Builder builder) { + builder.addMediaDecoder(GifMediaDecoder.CONTENT_TYPE, GifMediaDecoder.create(autoPlay)); + } +} diff --git a/markwon-image-loader/src/main/java/ru/noties/markwon/il/AsyncDrawableLoader.java b/markwon-image-loader/src/main/java/ru/noties/markwon/il/AsyncDrawableLoader.java index 432e8797..5f7a5f01 100644 --- a/markwon-image-loader/src/main/java/ru/noties/markwon/il/AsyncDrawableLoader.java +++ b/markwon-image-loader/src/main/java/ru/noties/markwon/il/AsyncDrawableLoader.java @@ -20,7 +20,7 @@ import java.util.concurrent.Executors; import java.util.concurrent.Future; import okhttp3.OkHttpClient; -import ru.noties.markwon.spans.AsyncDrawable; +import ru.noties.markwon.image.AsyncDrawable; public class AsyncDrawableLoader implements AsyncDrawable.Loader { diff --git a/markwon-image-svg/build.gradle b/markwon-image-svg/build.gradle new file mode 100644 index 00000000..06243a28 --- /dev/null +++ b/markwon-image-svg/build.gradle @@ -0,0 +1,25 @@ +apply plugin: 'com.android.library' + +android { + + compileSdkVersion config['compile-sdk'] + buildToolsVersion config['build-tools'] + + defaultConfig { + minSdkVersion config['min-sdk'] + targetSdkVersion config['target-sdk'] + versionCode 1 + versionName version + } +} + +dependencies { + + api project(':markwon') + + deps.with { + api it['android-svg'] + } +} + +registerArtifact(this) \ No newline at end of file diff --git a/markwon-image-svg/src/main/AndroidManifest.xml b/markwon-image-svg/src/main/AndroidManifest.xml new file mode 100644 index 00000000..10432a1d --- /dev/null +++ b/markwon-image-svg/src/main/AndroidManifest.xml @@ -0,0 +1 @@ + diff --git a/markwon-image-svg/src/main/java/ru/noties/markwon/image/svg/SvgMediaDecoder.java b/markwon-image-svg/src/main/java/ru/noties/markwon/image/svg/SvgMediaDecoder.java new file mode 100644 index 00000000..5c8661d8 --- /dev/null +++ b/markwon-image-svg/src/main/java/ru/noties/markwon/image/svg/SvgMediaDecoder.java @@ -0,0 +1,73 @@ +package ru.noties.markwon.image.svg; + +import android.content.res.Resources; +import android.graphics.Bitmap; +import android.graphics.Canvas; +import android.graphics.drawable.BitmapDrawable; +import android.graphics.drawable.Drawable; +import android.support.annotation.NonNull; +import android.support.annotation.Nullable; + +import com.caverock.androidsvg.SVG; +import com.caverock.androidsvg.SVGParseException; + +import java.io.InputStream; + +import ru.noties.markwon.image.MediaDecoder; +import ru.noties.markwon.utils.DrawableUtils; + +/** + * @since 1.1.0 + */ +public class SvgMediaDecoder extends MediaDecoder { + + public static final String CONTENT_TYPE = "image/svg+xml"; + + @NonNull + public static SvgMediaDecoder create(@NonNull Resources resources) { + return new SvgMediaDecoder(resources); + } + + private final Resources resources; + + @SuppressWarnings("WeakerAccess") + SvgMediaDecoder(Resources resources) { + this.resources = resources; + } + + @Nullable + @Override + public Drawable decode(@NonNull InputStream inputStream) { + + final Drawable out; + + SVG svg = null; + try { + svg = SVG.getFromInputStream(inputStream); + } catch (SVGParseException e) { + e.printStackTrace(); + } + + if (svg == null) { + out = null; + } else { + + final float w = svg.getDocumentWidth(); + final float h = svg.getDocumentHeight(); + final float density = resources.getDisplayMetrics().density; + + final int width = (int) (w * density + .5F); + final int height = (int) (h * density + .5F); + + final Bitmap bitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_4444); + final Canvas canvas = new Canvas(bitmap); + canvas.scale(density, density); + svg.renderToCanvas(canvas); + + out = new BitmapDrawable(resources, bitmap); + DrawableUtils.intrinsicBounds(out); + } + + return out; + } +} diff --git a/markwon-image-svg/src/main/java/ru/noties/markwon/image/svg/SvgPlugin.java b/markwon-image-svg/src/main/java/ru/noties/markwon/image/svg/SvgPlugin.java new file mode 100644 index 00000000..d2396741 --- /dev/null +++ b/markwon-image-svg/src/main/java/ru/noties/markwon/image/svg/SvgPlugin.java @@ -0,0 +1,26 @@ +package ru.noties.markwon.image.svg; + +import android.content.res.Resources; +import android.support.annotation.NonNull; + +import ru.noties.markwon.AbstractMarkwonPlugin; +import ru.noties.markwon.image.AsyncDrawableLoader; + +public class SvgPlugin extends AbstractMarkwonPlugin { + + @NonNull + public static SvgPlugin create(@NonNull Resources resources) { + return new SvgPlugin(resources); + } + + private final Resources resources; + + public SvgPlugin(@NonNull Resources resources) { + this.resources = resources; + } + + @Override + public void configureImages(@NonNull AsyncDrawableLoader.Builder builder) { + builder.addMediaDecoder(SvgMediaDecoder.CONTENT_TYPE, SvgMediaDecoder.create(resources)); + } +} diff --git a/markwon-syntax-highlight/src/main/java/ru/noties/markwon/syntax/Prism4jThemeDarkula.java b/markwon-syntax-highlight/src/main/java/ru/noties/markwon/syntax/Prism4jThemeDarkula.java index 7d5c90b4..9a941951 100644 --- a/markwon-syntax-highlight/src/main/java/ru/noties/markwon/syntax/Prism4jThemeDarkula.java +++ b/markwon-syntax-highlight/src/main/java/ru/noties/markwon/syntax/Prism4jThemeDarkula.java @@ -1,5 +1,6 @@ package ru.noties.markwon.syntax; +import android.support.annotation.ColorInt; import android.support.annotation.NonNull; import android.support.annotation.Nullable; import android.text.SpannableStringBuilder; @@ -12,12 +13,23 @@ public class Prism4jThemeDarkula extends Prism4jThemeBase { @NonNull public static Prism4jThemeDarkula create() { - return new Prism4jThemeDarkula(); + return new Prism4jThemeDarkula(0xFF2d2d2d); + } + + @NonNull + public static Prism4jThemeDarkula create(@ColorInt int background) { + return new Prism4jThemeDarkula(background); + } + + private final int background; + + public Prism4jThemeDarkula(@ColorInt int background) { + this.background = background; } @Override public int background() { - return 0xFF2d2d2d; + return background; } @Override diff --git a/markwon-syntax-highlight/src/main/java/ru/noties/markwon/syntax/SyntaxHighlightPlugin.java b/markwon-syntax-highlight/src/main/java/ru/noties/markwon/syntax/SyntaxHighlightPlugin.java new file mode 100644 index 00000000..08bf2a73 --- /dev/null +++ b/markwon-syntax-highlight/src/main/java/ru/noties/markwon/syntax/SyntaxHighlightPlugin.java @@ -0,0 +1,52 @@ +package ru.noties.markwon.syntax; + +import android.support.annotation.NonNull; +import android.support.annotation.Nullable; + +import ru.noties.markwon.AbstractMarkwonPlugin; +import ru.noties.markwon.MarkwonConfiguration; +import ru.noties.markwon.spans.MarkwonTheme; +import ru.noties.prism4j.Prism4j; + +public class SyntaxHighlightPlugin extends AbstractMarkwonPlugin { + + @NonNull + public static SyntaxHighlightPlugin create( + @NonNull Prism4j prism4j, + @NonNull Prism4jTheme theme) { + return create(prism4j, theme, null); + } + + @NonNull + public static SyntaxHighlightPlugin create( + @NonNull Prism4j prism4j, + @NonNull Prism4jTheme theme, + @Nullable String fallbackLanguage) { + return new SyntaxHighlightPlugin(prism4j, theme, fallbackLanguage); + } + + private final Prism4j prism4j; + private final Prism4jTheme theme; + private final String fallbackLanguage; + + public SyntaxHighlightPlugin( + @NonNull Prism4j prism4j, + @NonNull Prism4jTheme theme, + @Nullable String fallbackLanguage) { + this.prism4j = prism4j; + this.theme = theme; + this.fallbackLanguage = fallbackLanguage; + } + + @Override + public void configureTheme(@NonNull MarkwonTheme.Builder builder) { + builder + .codeTextColor(theme.textColor()) + .codeBackgroundColor(theme.background()); + } + + @Override + public void configureConfiguration(@NonNull MarkwonConfiguration.Builder builder) { + builder.syntaxHighlight(Prism4jSyntaxHighlight.create(prism4j, theme, fallbackLanguage)); + } +} diff --git a/markwon/src/main/java/ru/noties/markwon/AbstractMarkwonPlugin.java b/markwon/src/main/java/ru/noties/markwon/AbstractMarkwonPlugin.java index d0187fac..b1671899 100644 --- a/markwon/src/main/java/ru/noties/markwon/AbstractMarkwonPlugin.java +++ b/markwon/src/main/java/ru/noties/markwon/AbstractMarkwonPlugin.java @@ -5,6 +5,7 @@ import android.widget.TextView; import org.commonmark.parser.Parser; +import ru.noties.markwon.image.AsyncDrawableLoader; import ru.noties.markwon.spans.MarkwonTheme; public abstract class AbstractMarkwonPlugin implements MarkwonPlugin { @@ -18,6 +19,11 @@ public abstract class AbstractMarkwonPlugin implements MarkwonPlugin { } + @Override + public void configureImages(@NonNull AsyncDrawableLoader.Builder builder) { + + } + @Override public void configureConfiguration(@NonNull MarkwonConfiguration.Builder builder) { @@ -40,7 +46,7 @@ public abstract class AbstractMarkwonPlugin implements MarkwonPlugin { } @Override - public void afterSetText(@NonNull TextView textView, @NonNull CharSequence markdown) { + public void afterSetText(@NonNull TextView textView) { } } diff --git a/markwon/src/main/java/ru/noties/markwon/Markwon.java b/markwon/src/main/java/ru/noties/markwon/Markwon.java index 5728f21a..022c3ca8 100644 --- a/markwon/src/main/java/ru/noties/markwon/Markwon.java +++ b/markwon/src/main/java/ru/noties/markwon/Markwon.java @@ -14,8 +14,11 @@ import org.commonmark.parser.Parser; import java.util.Arrays; +import ru.noties.markwon.image.AsyncDrawable; +//import ru.noties.markwon.image.DrawablesScheduler; import ru.noties.markwon.renderer.SpannableRenderer; import ru.noties.markwon.spans.OrderedListItemSpan; +import ru.noties.markwon.table.TableRowSpan; import ru.noties.markwon.tasklist.TaskListExtension; @SuppressWarnings({"WeakerAccess", "unused"}) @@ -148,7 +151,7 @@ public abstract class Markwon { } /** - * This method adds support for {@link ru.noties.markwon.spans.AsyncDrawable} to be used. As + * This method adds support for {@link AsyncDrawable} to be used. As * textView seems not to support drawables that change bounds (and gives no means * to update the layout), we create own {@link android.graphics.drawable.Drawable.Callback} * and apply it. So, textView can display drawables, that are: async (loading from disk, network); @@ -157,14 +160,14 @@ public abstract class Markwon { * in order to avoid keeping drawables in memory after they have been removed from layout * * @param view a {@link TextView} - * @see ru.noties.markwon.spans.AsyncDrawable + * @see AsyncDrawable * @see ru.noties.markwon.spans.AsyncDrawableSpan * @see DrawablesScheduler#schedule(TextView) * @see DrawablesScheduler#unschedule(TextView) * @since 1.0.0 */ public static void scheduleDrawables(@NonNull TextView view) { - DrawablesScheduler.schedule(view); +// DrawablesScheduler.schedule(view); } /** @@ -175,7 +178,7 @@ public abstract class Markwon { * @since 1.0.0 */ public static void unscheduleDrawables(@NonNull TextView view) { - DrawablesScheduler.unschedule(view); +// DrawablesScheduler.unschedule(view); } /** @@ -185,28 +188,28 @@ public abstract class Markwon { * to return `size` (width) of our replacement, but we are not provided * with the total one (canvas width). In order to correctly calculate height of our * table cell text, we must have available width first. This method gives - * ability for {@link ru.noties.markwon.spans.TableRowSpan} to invalidate + * ability for {@link TableRowSpan} to invalidate * `view` when it encounters such a situation (when available width is not known or have changed). * Precede this call with {@link #unscheduleTableRows(TextView)} in order to - * de-reference previously scheduled {@link ru.noties.markwon.spans.TableRowSpan}'s + * de-reference previously scheduled {@link TableRowSpan}'s * * @param view a {@link TextView} * @see #unscheduleTableRows(TextView) * @since 1.0.0 */ public static void scheduleTableRows(@NonNull TextView view) { - TableRowsScheduler.schedule(view); +// TableRowsScheduler.schedule(view); } /** - * De-references previously scheduled {@link ru.noties.markwon.spans.TableRowSpan}'s + * De-references previously scheduled {@link TableRowSpan}'s * * @param view a {@link TextView} * @see #scheduleTableRows(TextView) * @since 1.0.0 */ public static void unscheduleTableRows(@NonNull TextView view) { - TableRowsScheduler.unschedule(view); +// TableRowsScheduler.unschedule(view); } private Markwon() { diff --git a/markwon/src/main/java/ru/noties/markwon/MarkwonBuilderImpl.java b/markwon/src/main/java/ru/noties/markwon/MarkwonBuilderImpl.java index 841c70d4..5bbe9fa5 100644 --- a/markwon/src/main/java/ru/noties/markwon/MarkwonBuilderImpl.java +++ b/markwon/src/main/java/ru/noties/markwon/MarkwonBuilderImpl.java @@ -9,6 +9,7 @@ import java.util.ArrayList; import java.util.Collections; import java.util.List; +import ru.noties.markwon.image.AsyncDrawableLoader; import ru.noties.markwon.spans.MarkwonTheme; class MarkwonBuilderImpl implements Markwon2.Builder { @@ -34,19 +35,25 @@ class MarkwonBuilderImpl implements Markwon2.Builder { final Parser.Builder parserBuilder = new Parser.Builder(); final MarkwonTheme.Builder themeBuilder = MarkwonTheme.builderWithDefaults(context); + final AsyncDrawableLoader.Builder asyncDrawableLoaderBuilder = new AsyncDrawableLoader.Builder(); final MarkwonConfiguration.Builder configurationBuilder = new MarkwonConfiguration.Builder(context); final MarkwonVisitor.Builder visitorBuilder = new MarkwonVisitorImpl.BuilderImpl(); for (MarkwonPlugin plugin : plugins) { plugin.configureParser(parserBuilder); plugin.configureTheme(themeBuilder); + plugin.configureImages(asyncDrawableLoaderBuilder); plugin.configureConfiguration(configurationBuilder); plugin.configureVisitor(visitorBuilder); } + final MarkwonConfiguration configuration = configurationBuilder.build( + themeBuilder.build(), + asyncDrawableLoaderBuilder.build()); + return new MarkwonImpl( parserBuilder.build(), - visitorBuilder.build(configurationBuilder.build(themeBuilder.build())), + visitorBuilder.build(configuration), Collections.unmodifiableList(plugins) ); } diff --git a/markwon/src/main/java/ru/noties/markwon/MarkwonConfiguration.java b/markwon/src/main/java/ru/noties/markwon/MarkwonConfiguration.java index 82d48f4f..1f6e2216 100644 --- a/markwon/src/main/java/ru/noties/markwon/MarkwonConfiguration.java +++ b/markwon/src/main/java/ru/noties/markwon/MarkwonConfiguration.java @@ -4,10 +4,11 @@ import android.content.Context; import android.support.annotation.NonNull; import ru.noties.markwon.html.api.MarkwonHtmlParser; +import ru.noties.markwon.image.AsyncDrawableLoader; +import ru.noties.markwon.image.AsyncDrawableLoaderNoOp; import ru.noties.markwon.renderer.ImageSizeResolver; import ru.noties.markwon.renderer.ImageSizeResolverDef; import ru.noties.markwon.renderer.html2.MarkwonHtmlRenderer; -import ru.noties.markwon.spans.AsyncDrawable; import ru.noties.markwon.spans.LinkSpan; import ru.noties.markwon.spans.MarkwonTheme; @@ -20,7 +21,7 @@ public class MarkwonConfiguration { // creates default configuration @NonNull public static MarkwonConfiguration create(@NonNull Context context) { - return new Builder(context).build(MarkwonTheme.create(context)); + return new Builder(context).build(MarkwonTheme.create(context), new AsyncDrawableLoaderNoOp()); } @NonNull @@ -30,7 +31,7 @@ public class MarkwonConfiguration { private final MarkwonTheme theme; - private final AsyncDrawable.Loader asyncDrawableLoader; + private final AsyncDrawableLoader asyncDrawableLoader; private final SyntaxHighlight syntaxHighlight; private final LinkSpan.Resolver linkResolver; private final UrlProcessor urlProcessor; @@ -69,7 +70,7 @@ public class MarkwonConfiguration { } @NonNull - public AsyncDrawable.Loader asyncDrawableLoader() { + public AsyncDrawableLoader asyncDrawableLoader() { return asyncDrawableLoader; } @@ -136,7 +137,7 @@ public class MarkwonConfiguration { private final Context context; private MarkwonTheme theme; - private AsyncDrawable.Loader asyncDrawableLoader; + private AsyncDrawableLoader asyncDrawableLoader; private SyntaxHighlight syntaxHighlight; private LinkSpan.Resolver linkResolver; private UrlProcessor urlProcessor; @@ -166,19 +167,6 @@ public class MarkwonConfiguration { this.htmlAllowNonClosedTags = configuration.htmlAllowNonClosedTags; } -// @NonNull -// @Deprecated -// public Builder theme(@NonNull MarkwonTheme theme) { -// this.theme = theme; -// return this; -// } - - @NonNull - public Builder asyncDrawableLoader(@NonNull AsyncDrawable.Loader asyncDrawableLoader) { - this.asyncDrawableLoader = asyncDrawableLoader; - return this; - } - @NonNull public Builder syntaxHighlight(@NonNull SyntaxHighlight syntaxHighlight) { this.syntaxHighlight = syntaxHighlight; @@ -260,13 +248,10 @@ public class MarkwonConfiguration { } @NonNull - public MarkwonConfiguration build(@NonNull MarkwonTheme theme) { + public MarkwonConfiguration build(@NonNull MarkwonTheme theme, @NonNull AsyncDrawableLoader asyncDrawableLoader) { this.theme = theme; - - if (asyncDrawableLoader == null) { - asyncDrawableLoader = new AsyncDrawableLoaderNoOp(); - } + this.asyncDrawableLoader = asyncDrawableLoader; if (syntaxHighlight == null) { syntaxHighlight = new SyntaxHighlightNoOp(); diff --git a/markwon/src/main/java/ru/noties/markwon/MarkwonImpl.java b/markwon/src/main/java/ru/noties/markwon/MarkwonImpl.java index db38e183..f92881e2 100644 --- a/markwon/src/main/java/ru/noties/markwon/MarkwonImpl.java +++ b/markwon/src/main/java/ru/noties/markwon/MarkwonImpl.java @@ -62,7 +62,7 @@ class MarkwonImpl extends Markwon2 { textView.setText(markdown); for (MarkwonPlugin plugin : plugins) { - plugin.afterSetText(textView, markdown); + plugin.afterSetText(textView); } } } diff --git a/markwon/src/main/java/ru/noties/markwon/MarkwonPlugin.java b/markwon/src/main/java/ru/noties/markwon/MarkwonPlugin.java index bd54d1d0..690c8f97 100644 --- a/markwon/src/main/java/ru/noties/markwon/MarkwonPlugin.java +++ b/markwon/src/main/java/ru/noties/markwon/MarkwonPlugin.java @@ -5,6 +5,7 @@ import android.widget.TextView; import org.commonmark.parser.Parser; +import ru.noties.markwon.image.AsyncDrawableLoader; import ru.noties.markwon.spans.MarkwonTheme; public interface MarkwonPlugin { @@ -13,11 +14,12 @@ public interface MarkwonPlugin { void configureTheme(@NonNull MarkwonTheme.Builder builder); + void configureImages(@NonNull AsyncDrawableLoader.Builder builder); + void configureConfiguration(@NonNull MarkwonConfiguration.Builder builder); void configureVisitor(@NonNull MarkwonVisitor.Builder builder); - // images // html @NonNull @@ -25,5 +27,8 @@ public interface MarkwonPlugin { void beforeSetText(@NonNull TextView textView, @NonNull CharSequence markdown); - void afterSetText(@NonNull TextView textView, @NonNull CharSequence markdown); + // this method do not receive markdown like `beforeSetText` does because at this + // point TextView already has markdown set and to manipulate spans one must + // request them from TextView (getText()) + void afterSetText(@NonNull TextView textView); } diff --git a/markwon/src/main/java/ru/noties/markwon/MarkwonVisitor.java b/markwon/src/main/java/ru/noties/markwon/MarkwonVisitor.java index 12a912fc..5bb22998 100644 --- a/markwon/src/main/java/ru/noties/markwon/MarkwonVisitor.java +++ b/markwon/src/main/java/ru/noties/markwon/MarkwonVisitor.java @@ -17,7 +17,7 @@ public interface MarkwonVisitor extends Visitor { interface Builder { @NonNull - Builder on(@NonNull Class node, @NonNull NodeVisitor nodeVisitor); + Builder on(@NonNull Class node, @Nullable NodeVisitor nodeVisitor); @NonNull MarkwonVisitor build(@NonNull MarkwonConfiguration configuration); diff --git a/markwon/src/main/java/ru/noties/markwon/MarkwonVisitorImpl.java b/markwon/src/main/java/ru/noties/markwon/MarkwonVisitorImpl.java index c4f98d04..07684737 100644 --- a/markwon/src/main/java/ru/noties/markwon/MarkwonVisitorImpl.java +++ b/markwon/src/main/java/ru/noties/markwon/MarkwonVisitorImpl.java @@ -281,8 +281,14 @@ class MarkwonVisitorImpl implements MarkwonVisitor { @NonNull @Override - public Builder on(@NonNull Class node, @NonNull NodeVisitor nodeVisitor) { - nodes.put(node, nodeVisitor); + public Builder on(@NonNull Class node, @Nullable NodeVisitor nodeVisitor) { + // we should allow `null` to exclude node from being visited (for example to disable + // some functionality) + if (nodeVisitor == null) { + nodes.remove(node); + } else { + nodes.put(node, nodeVisitor); + } return this; } diff --git a/markwon/src/main/java/ru/noties/markwon/SpannableFactory.java b/markwon/src/main/java/ru/noties/markwon/SpannableFactory.java index 6297b777..13a64cf4 100644 --- a/markwon/src/main/java/ru/noties/markwon/SpannableFactory.java +++ b/markwon/src/main/java/ru/noties/markwon/SpannableFactory.java @@ -3,14 +3,11 @@ package ru.noties.markwon; import android.support.annotation.NonNull; import android.support.annotation.Nullable; -import java.util.List; - +import ru.noties.markwon.image.AsyncDrawableLoader; import ru.noties.markwon.renderer.ImageSize; import ru.noties.markwon.renderer.ImageSizeResolver; -import ru.noties.markwon.spans.AsyncDrawable; import ru.noties.markwon.spans.LinkSpan; import ru.noties.markwon.spans.MarkwonTheme; -import ru.noties.markwon.spans.TableRowSpan; /** * Each method can return null or a Span object or an array of spans @@ -46,16 +43,6 @@ public interface SpannableFactory { @Nullable Object strikethrough(); - @Nullable - Object taskListItem(@NonNull MarkwonTheme theme, int blockIndent, boolean isDone); - - @Nullable - Object tableRow( - @NonNull MarkwonTheme theme, - @NonNull List cells, - boolean isHeader, - boolean isOdd); - /** * @since 1.1.1 */ @@ -66,7 +53,7 @@ public interface SpannableFactory { Object image( @NonNull MarkwonTheme theme, @NonNull String destination, - @NonNull AsyncDrawable.Loader loader, + @NonNull AsyncDrawableLoader loader, @NonNull ImageSizeResolver imageSizeResolver, @Nullable ImageSize imageSize, boolean replacementTextIsLink); diff --git a/markwon/src/main/java/ru/noties/markwon/SpannableFactoryDef.java b/markwon/src/main/java/ru/noties/markwon/SpannableFactoryDef.java index 6fc8d48f..21074983 100644 --- a/markwon/src/main/java/ru/noties/markwon/SpannableFactoryDef.java +++ b/markwon/src/main/java/ru/noties/markwon/SpannableFactoryDef.java @@ -5,11 +5,10 @@ import android.support.annotation.Nullable; import android.text.style.StrikethroughSpan; import android.text.style.UnderlineSpan; -import java.util.List; - +import ru.noties.markwon.image.AsyncDrawable; +import ru.noties.markwon.image.AsyncDrawableLoader; import ru.noties.markwon.renderer.ImageSize; import ru.noties.markwon.renderer.ImageSizeResolver; -import ru.noties.markwon.spans.AsyncDrawable; import ru.noties.markwon.spans.AsyncDrawableSpan; import ru.noties.markwon.spans.BlockQuoteSpan; import ru.noties.markwon.spans.BulletListItemSpan; @@ -17,13 +16,11 @@ import ru.noties.markwon.spans.CodeSpan; import ru.noties.markwon.spans.EmphasisSpan; import ru.noties.markwon.spans.HeadingSpan; import ru.noties.markwon.spans.LinkSpan; -import ru.noties.markwon.spans.OrderedListItemSpan; import ru.noties.markwon.spans.MarkwonTheme; +import ru.noties.markwon.spans.OrderedListItemSpan; import ru.noties.markwon.spans.StrongEmphasisSpan; import ru.noties.markwon.spans.SubScriptSpan; import ru.noties.markwon.spans.SuperScriptSpan; -import ru.noties.markwon.spans.TableRowSpan; -import ru.noties.markwon.tasklist.TaskListSpan; import ru.noties.markwon.spans.ThematicBreakSpan; /** @@ -91,18 +88,6 @@ public class SpannableFactoryDef implements SpannableFactory { return new StrikethroughSpan(); } - @Nullable - @Override - public Object taskListItem(@NonNull MarkwonTheme theme, int blockIndent, boolean isDone) { - return new TaskListSpan(theme, blockIndent, isDone); - } - - @Nullable - @Override - public Object tableRow(@NonNull MarkwonTheme theme, @NonNull List cells, boolean isHeader, boolean isOdd) { - return new TableRowSpan(theme, cells, isHeader, isOdd); - } - /** * @since 1.1.1 */ @@ -114,7 +99,7 @@ public class SpannableFactoryDef implements SpannableFactory { @Nullable @Override - public Object image(@NonNull MarkwonTheme theme, @NonNull String destination, @NonNull AsyncDrawable.Loader loader, @NonNull ImageSizeResolver imageSizeResolver, @Nullable ImageSize imageSize, boolean replacementTextIsLink) { + public Object image(@NonNull MarkwonTheme theme, @NonNull String destination, @NonNull AsyncDrawableLoader loader, @NonNull ImageSizeResolver imageSizeResolver, @Nullable ImageSize imageSize, boolean replacementTextIsLink) { return new AsyncDrawableSpan( theme, new AsyncDrawable( diff --git a/markwon/src/main/java/ru/noties/markwon/DrawablesScheduler.java b/markwon/src/main/java/ru/noties/markwon/core/AsyncDrawableScheduler.java similarity index 97% rename from markwon/src/main/java/ru/noties/markwon/DrawablesScheduler.java rename to markwon/src/main/java/ru/noties/markwon/core/AsyncDrawableScheduler.java index fd64d9d5..3d7fae23 100644 --- a/markwon/src/main/java/ru/noties/markwon/DrawablesScheduler.java +++ b/markwon/src/main/java/ru/noties/markwon/core/AsyncDrawableScheduler.java @@ -1,4 +1,4 @@ -package ru.noties.markwon; +package ru.noties.markwon.core; import android.graphics.Rect; import android.graphics.drawable.Drawable; @@ -15,10 +15,10 @@ import java.util.Collections; import java.util.List; import ru.noties.markwon.renderer.R; -import ru.noties.markwon.spans.AsyncDrawable; +import ru.noties.markwon.image.AsyncDrawable; import ru.noties.markwon.spans.AsyncDrawableSpan; -abstract class DrawablesScheduler { +abstract class AsyncDrawableScheduler { static void schedule(@NonNull final TextView textView) { @@ -104,7 +104,7 @@ abstract class DrawablesScheduler { return list; } - private DrawablesScheduler() { + private AsyncDrawableScheduler() { } private static class DrawableCallbackImpl implements Drawable.Callback { diff --git a/markwon/src/main/java/ru/noties/markwon/core/CorePlugin.java b/markwon/src/main/java/ru/noties/markwon/core/CorePlugin.java index 3d4ef0c4..e2893c53 100644 --- a/markwon/src/main/java/ru/noties/markwon/core/CorePlugin.java +++ b/markwon/src/main/java/ru/noties/markwon/core/CorePlugin.java @@ -11,7 +11,6 @@ import org.commonmark.node.Emphasis; import org.commonmark.node.FencedCodeBlock; import org.commonmark.node.HardLineBreak; import org.commonmark.node.Heading; -import org.commonmark.node.Image; import org.commonmark.node.IndentedCodeBlock; import org.commonmark.node.Link; import org.commonmark.node.ListBlock; @@ -73,13 +72,19 @@ public class CorePlugin extends AbstractMarkwonPlugin { softLineBreak(builder); hardLineBreak(builder); paragraph(builder); - image(builder); +// image(builder); link(builder); } @Override public void beforeSetText(@NonNull TextView textView, @NonNull CharSequence markdown) { OrderedListItemSpan.measure(textView, markdown); + AsyncDrawableScheduler.unschedule(textView); + } + + @Override + public void afterSetText(@NonNull TextView textView) { + AsyncDrawableScheduler.schedule(textView); } protected void text(@NonNull MarkwonVisitor.Builder builder) { @@ -366,41 +371,6 @@ public class CorePlugin extends AbstractMarkwonPlugin { }); } - protected void image(@NonNull MarkwonVisitor.Builder builder) { - builder.on(Image.class, new MarkwonVisitor.NodeVisitor() { - @Override - public void visit(@NonNull MarkwonVisitor visitor, @NonNull Image image) { - - final int length = visitor.length(); - - visitor.visitChildren(image); - - // we must check if anything _was_ added, as we need at least one char to render - if (length == visitor.length()) { - visitor.builder().append('\uFFFC'); - } - - final MarkwonConfiguration configuration = visitor.configuration(); - - final Node parent = image.getParent(); - final boolean link = parent instanceof Link; - final String destination = configuration - .urlProcessor() - .process(image.getDestination()); - - final Object spans = visitor.factory().image( - visitor.theme(), - destination, - configuration.asyncDrawableLoader(), - configuration.imageSizeResolver(), - null, - link); - - visitor.setSpans(length, spans); - } - }); - } - protected void link(@NonNull MarkwonVisitor.Builder builder) { builder.on(Link.class, new MarkwonVisitor.NodeVisitor() { @Override diff --git a/markwon/src/main/java/ru/noties/markwon/spans/AsyncDrawable.java b/markwon/src/main/java/ru/noties/markwon/image/AsyncDrawable.java similarity index 93% rename from markwon/src/main/java/ru/noties/markwon/spans/AsyncDrawable.java rename to markwon/src/main/java/ru/noties/markwon/image/AsyncDrawable.java index 588b8a04..3184f599 100644 --- a/markwon/src/main/java/ru/noties/markwon/spans/AsyncDrawable.java +++ b/markwon/src/main/java/ru/noties/markwon/image/AsyncDrawable.java @@ -1,4 +1,4 @@ -package ru.noties.markwon.spans; +package ru.noties.markwon.image; import android.graphics.Canvas; import android.graphics.ColorFilter; @@ -15,15 +15,8 @@ import ru.noties.markwon.renderer.ImageSizeResolver; public class AsyncDrawable extends Drawable { - public interface Loader { - - void load(@NonNull String destination, @NonNull AsyncDrawable drawable); - - void cancel(@NonNull String destination); - } - private final String destination; - private final Loader loader; + private final AsyncDrawableLoader loader; private final ImageSize imageSize; private final ImageSizeResolver imageSizeResolver; @@ -38,7 +31,7 @@ public class AsyncDrawable extends Drawable { */ public AsyncDrawable( @NonNull String destination, - @NonNull Loader loader, + @NonNull AsyncDrawableLoader loader, @Nullable ImageSizeResolver imageSizeResolver, @Nullable ImageSize imageSize ) { diff --git a/markwon/src/main/java/ru/noties/markwon/image/AsyncDrawableLoader.java b/markwon/src/main/java/ru/noties/markwon/image/AsyncDrawableLoader.java new file mode 100644 index 00000000..1e841f49 --- /dev/null +++ b/markwon/src/main/java/ru/noties/markwon/image/AsyncDrawableLoader.java @@ -0,0 +1,104 @@ +package ru.noties.markwon.image; + +import android.graphics.drawable.Drawable; +import android.support.annotation.NonNull; +import android.support.annotation.Nullable; + +import java.util.Collection; +import java.util.HashMap; +import java.util.Map; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; + +public abstract class AsyncDrawableLoader { + + + public abstract void load(@NonNull String destination, @NonNull AsyncDrawable drawable); + + public abstract void cancel(@NonNull String destination); + + + public static class Builder { + + ExecutorService executorService; + final Map schemeHandlers = new HashMap<>(3); + final Map mediaDecoders = new HashMap<>(3); + MediaDecoder defaultMediaDecoder; + Drawable errorDrawable; + + @NonNull + public Builder executorService(@NonNull ExecutorService executorService) { + this.executorService = executorService; + return this; + } + + @NonNull + public Builder addSchemeHandler(@NonNull String scheme, @NonNull SchemeHandler schemeHandler) { + schemeHandlers.put(scheme, schemeHandler); + return this; + } + + @NonNull + public Builder addSchemeHandler(@NonNull Collection schemes, @NonNull SchemeHandler schemeHandler) { + for (String scheme : schemes) { + schemeHandlers.put(scheme, schemeHandler); + } + return this; + } + + @NonNull + public Builder addMediaDecoder(@NonNull String contentType, @NonNull MediaDecoder mediaDecoder) { + mediaDecoders.put(contentType, mediaDecoder); + return this; + } + + @NonNull + public Builder addMediaDecoder(@NonNull Collection contentTypes, @NonNull MediaDecoder mediaDecoder) { + for (String contentType : contentTypes) { + mediaDecoders.put(contentType, mediaDecoder); + } + return this; + } + + @NonNull + public Builder removeSchemeHandler(@NonNull String scheme) { + schemeHandlers.remove(scheme); + return this; + } + + @NonNull + public Builder removeMediaDecoder(@NonNull String contentType) { + mediaDecoders.remove(contentType); + return this; + } + + @NonNull + public Builder defaultMediaDecoder(@Nullable MediaDecoder mediaDecoder) { + this.defaultMediaDecoder = mediaDecoder; + return this; + } + + @NonNull + public Builder errorDrawable(Drawable errorDrawable) { + this.errorDrawable = errorDrawable; + return this; + } + + @NonNull + public AsyncDrawableLoader build() { + + // if we have no schemeHandlers -> we cannot show anything + // OR if we have no media decoders + if (schemeHandlers.size() == 0 + || (mediaDecoders.size() == 0 && defaultMediaDecoder == null)) { + return new AsyncDrawableLoaderNoOp(); + } + + if (executorService == null) { + executorService = Executors.newCachedThreadPool(); + } + + return new AsyncDrawableLoaderImpl(this); + } + } +} diff --git a/markwon/src/main/java/ru/noties/markwon/image/AsyncDrawableLoaderImpl.java b/markwon/src/main/java/ru/noties/markwon/image/AsyncDrawableLoaderImpl.java new file mode 100644 index 00000000..281d61fc --- /dev/null +++ b/markwon/src/main/java/ru/noties/markwon/image/AsyncDrawableLoaderImpl.java @@ -0,0 +1,135 @@ +package ru.noties.markwon.image; + +import android.graphics.drawable.Drawable; +import android.net.Uri; +import android.os.Handler; +import android.os.Looper; +import android.support.annotation.NonNull; + +import java.io.IOException; +import java.io.InputStream; +import java.lang.ref.WeakReference; +import java.util.HashMap; +import java.util.Map; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Future; + +class AsyncDrawableLoaderImpl extends AsyncDrawableLoader { + + private final ExecutorService executorService; + private final Map schemeHandlers; + private final Map mediaDecoders; + private final MediaDecoder defaultMediaDecoder; + private final Drawable errorDrawable; + + private final Handler mainThread; + + private final Map> requests = new HashMap<>(2); + + AsyncDrawableLoaderImpl(@NonNull Builder builder) { + this.executorService = builder.executorService; + this.schemeHandlers = builder.schemeHandlers; + this.mediaDecoders = builder.mediaDecoders; + this.defaultMediaDecoder = builder.defaultMediaDecoder; + this.errorDrawable = builder.errorDrawable; + this.mainThread = new Handler(Looper.getMainLooper()); + } + + @Override + public void load(@NonNull String destination, @NonNull AsyncDrawable drawable) { + // if drawable is not a link -> show loading placeholder... + requests.put(destination, execute(destination, drawable)); + } + + @Override + public void cancel(@NonNull String destination) { + final Future request = requests.remove(destination); + if (request != null) { + request.cancel(true); + } + } + + private Future execute(@NonNull final String destination, @NonNull AsyncDrawable drawable) { + + final WeakReference reference = new WeakReference(drawable); + + // todo: should we cancel pending request for the same destination? + // we _could_ but there is possibility that one resource is request in multiple places + + // todo: error handing (simply applying errorDrawable is not a good solution + // as reason for an error is unclear (no scheme handler, no input data, error decoding, etc) + + // todo: more efficient ImageMediaDecoder... BitmapFactory.decodeStream is a bit not optimal + // for big images for sure. We _could_ introduce internal Drawable that will check for + // image bounds (but we will need to cache inputStream in order to inspect and optimize + // input image...) + + return executorService.submit(new Runnable() { + @Override + public void run() { + + final ImageItem item; + + final Uri uri = Uri.parse(destination); + + final SchemeHandler schemeHandler = schemeHandlers.get(uri.getScheme()); + + if (schemeHandler != null) { + item = schemeHandler.handle(destination, uri); + } else { + item = null; + } + + final InputStream inputStream = item != null + ? item.inputStream() + : null; + + Drawable result = null; + + if (inputStream != null) { + try { + + MediaDecoder mediaDecoder = mediaDecoders.get(item.contentType()); + if (mediaDecoder == null) { + mediaDecoder = defaultMediaDecoder; + } + + if (mediaDecoder != null) { + result = mediaDecoder.decode(inputStream); + } + + } finally { + try { + inputStream.close(); + } catch (IOException e) { + // ignored + } + } + } + + // if result is null, we assume it's an error + if (result == null) { + result = errorDrawable; + } + + if (result != null) { + final Drawable out = result; + mainThread.post(new Runnable() { + @Override + public void run() { + final boolean canDeliver = requests.remove(destination) != null; + if (canDeliver) { + final AsyncDrawable asyncDrawable = reference.get(); + if (asyncDrawable != null && asyncDrawable.isAttached()) { + asyncDrawable.setResult(out); + } + } + } + }); + } else { + requests.remove(destination); + } + } + }); + } +} diff --git a/markwon/src/main/java/ru/noties/markwon/AsyncDrawableLoaderNoOp.java b/markwon/src/main/java/ru/noties/markwon/image/AsyncDrawableLoaderNoOp.java similarity index 62% rename from markwon/src/main/java/ru/noties/markwon/AsyncDrawableLoaderNoOp.java rename to markwon/src/main/java/ru/noties/markwon/image/AsyncDrawableLoaderNoOp.java index 6a568e3a..08e1283f 100644 --- a/markwon/src/main/java/ru/noties/markwon/AsyncDrawableLoaderNoOp.java +++ b/markwon/src/main/java/ru/noties/markwon/image/AsyncDrawableLoaderNoOp.java @@ -1,10 +1,8 @@ -package ru.noties.markwon; +package ru.noties.markwon.image; import android.support.annotation.NonNull; -import ru.noties.markwon.spans.AsyncDrawable; - -class AsyncDrawableLoaderNoOp implements AsyncDrawable.Loader { +public class AsyncDrawableLoaderNoOp extends AsyncDrawableLoader { @Override public void load(@NonNull String destination, @NonNull AsyncDrawable drawable) { diff --git a/markwon/src/main/java/ru/noties/markwon/image/ImageItem.java b/markwon/src/main/java/ru/noties/markwon/image/ImageItem.java new file mode 100644 index 00000000..2dc4b729 --- /dev/null +++ b/markwon/src/main/java/ru/noties/markwon/image/ImageItem.java @@ -0,0 +1,31 @@ +package ru.noties.markwon.image; + +import android.support.annotation.Nullable; + +import java.io.InputStream; + +/** + * @since 2.0.0 + */ +public class ImageItem { + + private final String contentType; + private final InputStream inputStream; + + public ImageItem( + @Nullable String contentType, + @Nullable InputStream inputStream) { + this.contentType = contentType; + this.inputStream = inputStream; + } + + @Nullable + public String contentType() { + return contentType; + } + + @Nullable + public InputStream inputStream() { + return inputStream; + } +} diff --git a/markwon/src/main/java/ru/noties/markwon/image/ImageMediaDecoder.java b/markwon/src/main/java/ru/noties/markwon/image/ImageMediaDecoder.java new file mode 100644 index 00000000..796d016e --- /dev/null +++ b/markwon/src/main/java/ru/noties/markwon/image/ImageMediaDecoder.java @@ -0,0 +1,52 @@ +package ru.noties.markwon.image; + +import android.content.res.Resources; +import android.graphics.Bitmap; +import android.graphics.BitmapFactory; +import android.graphics.drawable.BitmapDrawable; +import android.graphics.drawable.Drawable; +import android.support.annotation.NonNull; +import android.support.annotation.Nullable; + +import java.io.InputStream; + +import ru.noties.markwon.utils.DrawableUtils; + +/** + * This class can be used as the last {@link MediaDecoder} to _try_ to handle all rest cases. + * Here we just assume that supplied InputStream is of image type and try to decode it. + * + * @since 1.1.0 + */ +public class ImageMediaDecoder extends MediaDecoder { + + @NonNull + public static ImageMediaDecoder create(@NonNull Resources resources) { + return new ImageMediaDecoder(resources); + } + + private final Resources resources; + + @SuppressWarnings("WeakerAccess") + ImageMediaDecoder(Resources resources) { + this.resources = resources; + } + + @Nullable + @Override + public Drawable decode(@NonNull InputStream inputStream) { + + final Drawable out; + + // absolutely not optimal... thing + final Bitmap bitmap = BitmapFactory.decodeStream(inputStream); + if (bitmap != null) { + out = new BitmapDrawable(resources, bitmap); + DrawableUtils.intrinsicBounds(out); + } else { + out = null; + } + + return out; + } +} diff --git a/markwon/src/main/java/ru/noties/markwon/image/ImagesPlugin.java b/markwon/src/main/java/ru/noties/markwon/image/ImagesPlugin.java new file mode 100644 index 00000000..0154eb8f --- /dev/null +++ b/markwon/src/main/java/ru/noties/markwon/image/ImagesPlugin.java @@ -0,0 +1,92 @@ +package ru.noties.markwon.image; + +import android.content.Context; +import android.support.annotation.NonNull; + +import org.commonmark.node.Image; +import org.commonmark.node.Link; +import org.commonmark.node.Node; + +import java.util.Arrays; + +import ru.noties.markwon.AbstractMarkwonPlugin; +import ru.noties.markwon.MarkwonConfiguration; +import ru.noties.markwon.MarkwonVisitor; +import ru.noties.markwon.image.data.DataUriSchemeHandler; +import ru.noties.markwon.image.file.FileSchemeHandler; +import ru.noties.markwon.image.network.NetworkSchemeHandler; + +public class ImagesPlugin extends AbstractMarkwonPlugin { + + @NonNull + public static ImagesPlugin create(@NonNull Context context) { + return new ImagesPlugin(context, false); + } + + @NonNull + public static ImagesPlugin createWithAssets(@NonNull Context context) { + return new ImagesPlugin(context, true); + } + + private final Context context; + private final boolean useAssets; + + private ImagesPlugin(Context context, boolean useAssets) { + this.context = context; + this.useAssets = useAssets; + } + + @Override + public void configureImages(@NonNull AsyncDrawableLoader.Builder builder) { + + final FileSchemeHandler fileSchemeHandler = useAssets + ? FileSchemeHandler.createWithAssets(context.getAssets()) + : FileSchemeHandler.create(); + + builder + .addSchemeHandler(DataUriSchemeHandler.SCHEME, DataUriSchemeHandler.create()) + .addSchemeHandler(FileSchemeHandler.SCHEME, fileSchemeHandler) + .addSchemeHandler( + Arrays.asList( + NetworkSchemeHandler.SCHEME_HTTP, + NetworkSchemeHandler.SCHEME_HTTPS), + NetworkSchemeHandler.create()) + .defaultMediaDecoder(ImageMediaDecoder.create(context.getResources())); + } + + @Override + public void configureVisitor(@NonNull MarkwonVisitor.Builder builder) { + builder.on(Image.class, new MarkwonVisitor.NodeVisitor() { + @Override + public void visit(@NonNull MarkwonVisitor visitor, @NonNull Image image) { + + final int length = visitor.length(); + + visitor.visitChildren(image); + + // we must check if anything _was_ added, as we need at least one char to render + if (length == visitor.length()) { + visitor.builder().append('\uFFFC'); + } + + final MarkwonConfiguration configuration = visitor.configuration(); + + final Node parent = image.getParent(); + final boolean link = parent instanceof Link; + final String destination = configuration + .urlProcessor() + .process(image.getDestination()); + + final Object spans = visitor.factory().image( + visitor.theme(), + destination, + configuration.asyncDrawableLoader(), + configuration.imageSizeResolver(), + null, + link); + + visitor.setSpans(length, spans); + } + }); + } +} diff --git a/markwon/src/main/java/ru/noties/markwon/image/MediaDecoder.java b/markwon/src/main/java/ru/noties/markwon/image/MediaDecoder.java new file mode 100644 index 00000000..68d0ff33 --- /dev/null +++ b/markwon/src/main/java/ru/noties/markwon/image/MediaDecoder.java @@ -0,0 +1,16 @@ +package ru.noties.markwon.image; + +import android.graphics.drawable.Drawable; +import android.support.annotation.NonNull; +import android.support.annotation.Nullable; + +import java.io.InputStream; + +/** + * @since 3.0.0 + */ +public abstract class MediaDecoder { + + @Nullable + public abstract Drawable decode(@NonNull InputStream inputStream); +} diff --git a/markwon/src/main/java/ru/noties/markwon/image/SchemeHandler.java b/markwon/src/main/java/ru/noties/markwon/image/SchemeHandler.java new file mode 100644 index 00000000..cac1c801 --- /dev/null +++ b/markwon/src/main/java/ru/noties/markwon/image/SchemeHandler.java @@ -0,0 +1,14 @@ +package ru.noties.markwon.image; + +import android.net.Uri; +import android.support.annotation.NonNull; +import android.support.annotation.Nullable; + +/** + * @since 3.0.0 + */ +public abstract class SchemeHandler { + + @Nullable + public abstract ImageItem handle(@NonNull String raw, @NonNull Uri uri); +} diff --git a/markwon/src/main/java/ru/noties/markwon/image/data/DataUri.java b/markwon/src/main/java/ru/noties/markwon/image/data/DataUri.java new file mode 100644 index 00000000..6e812c92 --- /dev/null +++ b/markwon/src/main/java/ru/noties/markwon/image/data/DataUri.java @@ -0,0 +1,60 @@ +package ru.noties.markwon.image.data; + +import android.support.annotation.Nullable; + +public class DataUri { + + private final String contentType; + private final boolean base64; + private final String data; + + public DataUri(@Nullable String contentType, boolean base64, @Nullable String data) { + this.contentType = contentType; + this.base64 = base64; + this.data = data; + } + + @Nullable + public String contentType() { + return contentType; + } + + public boolean base64() { + return base64; + } + + @Nullable + public String data() { + return data; + } + + @Override + public String toString() { + return "DataUri{" + + "contentType='" + contentType + '\'' + + ", base64=" + base64 + + ", data='" + data + '\'' + + '}'; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + + DataUri dataUri = (DataUri) o; + + if (base64 != dataUri.base64) return false; + if (contentType != null ? !contentType.equals(dataUri.contentType) : dataUri.contentType != null) + return false; + return data != null ? data.equals(dataUri.data) : dataUri.data == null; + } + + @Override + public int hashCode() { + int result = contentType != null ? contentType.hashCode() : 0; + result = 31 * result + (base64 ? 1 : 0); + result = 31 * result + (data != null ? data.hashCode() : 0); + return result; + } +} diff --git a/markwon/src/main/java/ru/noties/markwon/image/data/DataUriDecoder.java b/markwon/src/main/java/ru/noties/markwon/image/data/DataUriDecoder.java new file mode 100644 index 00000000..7e3d4f73 --- /dev/null +++ b/markwon/src/main/java/ru/noties/markwon/image/data/DataUriDecoder.java @@ -0,0 +1,41 @@ +package ru.noties.markwon.image.data; + +import android.support.annotation.NonNull; +import android.support.annotation.Nullable; +import android.text.TextUtils; +import android.util.Base64; + +public abstract class DataUriDecoder { + + @Nullable + public abstract byte[] decode(@NonNull DataUri dataUri); + + @NonNull + public static DataUriDecoder create() { + return new Impl(); + } + + static class Impl extends DataUriDecoder { + + @Nullable + @Override + public byte[] decode(@NonNull DataUri dataUri) { + + final String data = dataUri.data(); + + if (!TextUtils.isEmpty(data)) { + try { + if (dataUri.base64()) { + return Base64.decode(data.getBytes("UTF-8"), Base64.DEFAULT); + } else { + return data.getBytes("UTF-8"); + } + } catch (Throwable t) { + return null; + } + } else { + return null; + } + } + } +} diff --git a/markwon/src/main/java/ru/noties/markwon/image/data/DataUriParser.java b/markwon/src/main/java/ru/noties/markwon/image/data/DataUriParser.java new file mode 100644 index 00000000..0768ee4a --- /dev/null +++ b/markwon/src/main/java/ru/noties/markwon/image/data/DataUriParser.java @@ -0,0 +1,79 @@ +package ru.noties.markwon.image.data; + +import android.support.annotation.NonNull; +import android.support.annotation.Nullable; + +public abstract class DataUriParser { + + @Nullable + public abstract DataUri parse(@NonNull String input); + + + @NonNull + public static DataUriParser create() { + return new Impl(); + } + + static class Impl extends DataUriParser { + + @Nullable + @Override + public DataUri parse(@NonNull String input) { + + final int index = input.indexOf(','); + // we expect exactly one comma + if (index < 0) { + return null; + } + + final String contentType; + final boolean base64; + + if (index > 0) { + final String part = input.substring(0, index); + final String[] parts = part.split(";"); + final int length = parts.length; + if (length > 0) { + // if one: either content-type or base64 + if (length == 1) { + final String value = parts[0]; + if ("base64".equals(value)) { + contentType = null; + base64 = true; + } else { + contentType = value.indexOf('/') > -1 + ? value + : null; + base64 = false; + } + } else { + contentType = parts[0].indexOf('/') > -1 + ? parts[0] + : null; + base64 = "base64".equals(parts[length - 1]); + } + } else { + contentType = null; + base64 = false; + } + } else { + contentType = null; + base64 = false; + } + + final String data; + if (index < input.length()) { + final String value = input.substring(index + 1, input.length()).replaceAll("\n", ""); + if (value.length() == 0) { + data = null; + } else { + data = value; + } + } else { + data = null; + } + + return new DataUri(contentType, base64, data); + } + } +} diff --git a/markwon/src/main/java/ru/noties/markwon/image/data/DataUriSchemeHandler.java b/markwon/src/main/java/ru/noties/markwon/image/data/DataUriSchemeHandler.java new file mode 100644 index 00000000..8f44bb02 --- /dev/null +++ b/markwon/src/main/java/ru/noties/markwon/image/data/DataUriSchemeHandler.java @@ -0,0 +1,65 @@ +package ru.noties.markwon.image.data; + +import android.net.Uri; +import android.support.annotation.NonNull; +import android.support.annotation.Nullable; + +import java.io.ByteArrayInputStream; + +import ru.noties.markwon.image.ImageItem; +import ru.noties.markwon.image.SchemeHandler; + +/** + * @since 3.0.0 + */ +public class DataUriSchemeHandler extends SchemeHandler { + + public static final String SCHEME = "data"; + + @NonNull + public static DataUriSchemeHandler create() { + return new DataUriSchemeHandler(DataUriParser.create(), DataUriDecoder.create()); + } + + private static final String START = "data:"; + + private final DataUriParser uriParser; + private final DataUriDecoder uriDecoder; + + @SuppressWarnings("WeakerAccess") + DataUriSchemeHandler(@NonNull DataUriParser uriParser, @NonNull DataUriDecoder uriDecoder) { + this.uriParser = uriParser; + this.uriDecoder = uriDecoder; + } + + @Nullable + @Override + public ImageItem handle(@NonNull String raw, @NonNull Uri uri) { + + if (!raw.startsWith(START)) { + return null; + } + + String part = raw.substring(START.length()); + + // this part is added to support `data://` with which this functionality was released + if (part.startsWith("//")) { + part = part.substring(2); + } + + final DataUri dataUri = uriParser.parse(part); + if (dataUri == null) { + return null; + } + + final byte[] bytes = uriDecoder.decode(dataUri); + if (bytes == null) { + return null; + } + + return new ImageItem( + dataUri.contentType(), + new ByteArrayInputStream(bytes) + ); + } +} diff --git a/markwon/src/main/java/ru/noties/markwon/image/file/FileSchemeHandler.java b/markwon/src/main/java/ru/noties/markwon/image/file/FileSchemeHandler.java new file mode 100644 index 00000000..712899aa --- /dev/null +++ b/markwon/src/main/java/ru/noties/markwon/image/file/FileSchemeHandler.java @@ -0,0 +1,105 @@ +package ru.noties.markwon.image.file; + +import android.content.res.AssetManager; +import android.net.Uri; +import android.support.annotation.NonNull; +import android.support.annotation.Nullable; +import android.webkit.MimeTypeMap; + +import java.io.BufferedInputStream; +import java.io.File; +import java.io.FileInputStream; +import java.io.FileNotFoundException; +import java.io.IOException; +import java.io.InputStream; +import java.util.List; + +import ru.noties.markwon.image.ImageItem; +import ru.noties.markwon.image.SchemeHandler; + +/** + * @since 3.0.0 + */ +public class FileSchemeHandler extends SchemeHandler { + + public static final String SCHEME = "file"; + + @NonNull + public static FileSchemeHandler createWithAssets(@NonNull AssetManager assetManager) { + return new FileSchemeHandler(assetManager); + } + + @NonNull + public static FileSchemeHandler create() { + return new FileSchemeHandler(null); + } + + private static final String FILE_ANDROID_ASSETS = "android_asset"; + + @Nullable + private final AssetManager assetManager; + + @SuppressWarnings("WeakerAccess") + FileSchemeHandler(@Nullable AssetManager assetManager) { + this.assetManager = assetManager; + } + + @Nullable + @Override + public ImageItem handle(@NonNull String raw, @NonNull Uri uri) { + + final List segments = uri.getPathSegments(); + if (segments == null + || segments.size() == 0) { + // pointing to file & having no path segments is no use + return null; + } + + final ImageItem out; + + InputStream inputStream = null; + + final boolean assets = FILE_ANDROID_ASSETS.equals(segments.get(0)); + final String fileName = uri.getLastPathSegment(); + + if (assets) { + + // no handling of assets here if we have no assetsManager + if (assetManager != null) { + + final StringBuilder path = new StringBuilder(); + for (int i = 1, size = segments.size(); i < size; i++) { + if (i != 1) { + path.append('/'); + } + path.append(segments.get(i)); + } + // load assets + + try { + inputStream = assetManager.open(path.toString()); + } catch (IOException e) { + e.printStackTrace(); + } + } + + } else { + try { + inputStream = new BufferedInputStream(new FileInputStream(new File(uri.getPath()))); + } catch (FileNotFoundException e) { + e.printStackTrace(); + } + } + + if (inputStream != null) { + final String contentType = MimeTypeMap + .getSingleton() + .getMimeTypeFromExtension(MimeTypeMap.getFileExtensionFromUrl(fileName)); + out = new ImageItem(contentType, inputStream); + } else { + out = null; + } + + return out; + } +} diff --git a/markwon/src/main/java/ru/noties/markwon/image/network/NetworkSchemeHandler.java b/markwon/src/main/java/ru/noties/markwon/image/network/NetworkSchemeHandler.java new file mode 100644 index 00000000..c5352d4b --- /dev/null +++ b/markwon/src/main/java/ru/noties/markwon/image/network/NetworkSchemeHandler.java @@ -0,0 +1,64 @@ +package ru.noties.markwon.image.network; + +import android.net.Uri; +import android.support.annotation.NonNull; +import android.support.annotation.Nullable; + +import java.io.BufferedInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.net.HttpURLConnection; +import java.net.URL; + +import ru.noties.markwon.image.ImageItem; +import ru.noties.markwon.image.SchemeHandler; + +public class NetworkSchemeHandler extends SchemeHandler { + + public static final String SCHEME_HTTP = "http"; + public static final String SCHEME_HTTPS = "https"; + + @NonNull + public static NetworkSchemeHandler create() { + return new NetworkSchemeHandler(); + } + + @Nullable + @Override + public ImageItem handle(@NonNull String raw, @NonNull Uri uri) { + + try { + + final URL url = new URL(raw); + final HttpURLConnection connection = (HttpURLConnection) url.openConnection(); + connection.connect(); + + final int responseCode = connection.getResponseCode(); + if (responseCode >= 200 && responseCode < 300) { + final String contentType = contentType(connection.getHeaderField("Content-Type")); + final InputStream inputStream = new BufferedInputStream(connection.getInputStream()); + return new ImageItem(contentType, inputStream); + } + + } catch (IOException e) { + e.printStackTrace(); + } + + return null; + } + + @Nullable + static String contentType(@Nullable String contentType) { + + if (contentType == null) { + return null; + } + + final int index = contentType.indexOf(';'); + if (index > -1) { + return contentType.substring(0, index); + } + + return contentType; + } +} diff --git a/markwon/src/main/java/ru/noties/markwon/renderer/SpannableMarkdownVisitor.java b/markwon/src/main/java/ru/noties/markwon/renderer/SpannableMarkdownVisitor.java index 3ea3cc83..6a3a94e3 100644 --- a/markwon/src/main/java/ru/noties/markwon/renderer/SpannableMarkdownVisitor.java +++ b/markwon/src/main/java/ru/noties/markwon/renderer/SpannableMarkdownVisitor.java @@ -42,7 +42,7 @@ import ru.noties.markwon.MarkwonConfiguration; import ru.noties.markwon.SpannableFactory; import ru.noties.markwon.html.api.MarkwonHtmlParser; import ru.noties.markwon.spans.MarkwonTheme; -import ru.noties.markwon.spans.TableRowSpan; +import ru.noties.markwon.table.TableRowSpan; import ru.noties.markwon.tasklist.TaskListBlock; import ru.noties.markwon.tasklist.TaskListItem; @@ -321,112 +321,92 @@ public class SpannableMarkdownVisitor extends AbstractVisitor { visitChildren(customNode); setSpan(length, factory.strikethrough()); - } else if (customNode instanceof TaskListItem) { - - // new in 1.0.1 - - final TaskListItem listItem = (TaskListItem) customNode; - - final int length = builder.length(); - - blockQuoteIndent += listItem.indent(); - - visitChildren(customNode); - - setSpan(length, factory.taskListItem(theme, blockQuoteIndent, listItem.done())); - - if (hasNext(customNode)) { - newLine(); - } - - blockQuoteIndent -= listItem.indent(); - - } else if (!handleTableNodes(customNode)) { + } else { super.visit(customNode); } } - private boolean handleTableNodes(CustomNode node) { - - final boolean handled; - - if (node instanceof TableBody) { - - visitChildren(node); - tableRows = 0; - handled = true; - - if (hasNext(node)) { - newLine(); - builder.append('\n'); - } - - } else if (node instanceof TableRow || node instanceof TableHead) { - - final int length = builder.length(); - - visitChildren(node); - - if (pendingTableRow != null) { - - // @since 2.0.0 - // we cannot rely on hasNext(TableHead) as it's not reliable - // we must apply new line manually and then exclude it from tableRow span - final boolean addNewLine; - { - final int builderLength = builder.length(); - addNewLine = builderLength > 0 - && '\n' != builder.charAt(builderLength - 1); - } - if (addNewLine) { - builder.append('\n'); - } - - // @since 1.0.4 Replace table char with non-breakable space - // we need this because if table is at the end of the text, then it will be - // trimmed from the final result - builder.append('\u00a0'); - - final Object span = factory.tableRow( - theme, - pendingTableRow, - tableRowIsHeader, - tableRows % 2 == 1); - - tableRows = tableRowIsHeader - ? 0 - : tableRows + 1; - - setSpan(addNewLine ? length + 1 : length, span); - - pendingTableRow = null; - } - - handled = true; - - } else if (node instanceof TableCell) { - - final TableCell cell = (TableCell) node; - final int length = builder.length(); - visitChildren(cell); - if (pendingTableRow == null) { - pendingTableRow = new ArrayList<>(2); - } - - pendingTableRow.add(new TableRowSpan.Cell( - tableCellAlignment(cell.getAlignment()), - builder.removeFromEnd(length) - )); - - tableRowIsHeader = cell.isHeader(); - - handled = true; - } else { - handled = false; - } - - return handled; - } +// private boolean handleTableNodes(CustomNode node) { +// +// final boolean handled; +// +// if (node instanceof TableBody) { +// +// visitChildren(node); +// tableRows = 0; +// handled = true; +// +// if (hasNext(node)) { +// newLine(); +// builder.append('\n'); +// } +// +// } else if (node instanceof TableRow || node instanceof TableHead) { +// +// final int length = builder.length(); +// +// visitChildren(node); +// +// if (pendingTableRow != null) { +// +// // @since 2.0.0 +// // we cannot rely on hasNext(TableHead) as it's not reliable +// // we must apply new line manually and then exclude it from tableRow span +// final boolean addNewLine; +// { +// final int builderLength = builder.length(); +// addNewLine = builderLength > 0 +// && '\n' != builder.charAt(builderLength - 1); +// } +// if (addNewLine) { +// builder.append('\n'); +// } +// +// // @since 1.0.4 Replace table char with non-breakable space +// // we need this because if table is at the end of the text, then it will be +// // trimmed from the final result +// builder.append('\u00a0'); +// +// final Object span = factory.tableRow( +// theme, +// pendingTableRow, +// tableRowIsHeader, +// tableRows % 2 == 1); +// +// tableRows = tableRowIsHeader +// ? 0 +// : tableRows + 1; +// +// setSpan(addNewLine ? length + 1 : length, span); +// +// pendingTableRow = null; +// } +// +// handled = true; +// +// } else if (node instanceof TableCell) { +// +// final TableCell cell = (TableCell) node; +// final int length = builder.length(); +// visitChildren(cell); +// if (pendingTableRow == null) { +// pendingTableRow = new ArrayList<>(2); +// } +// +// pendingTableRow.add(new TableRowSpan.Cell( +// tableCellAlignment(cell.getAlignment()), +// builder.removeFromEnd(length) +// )); +// +// tableRowIsHeader = cell.isHeader(); +// +// handled = true; +// } else { +// handled = false; +// } +// +// return handled; +// } @Override public void visit(Paragraph paragraph) { @@ -530,26 +510,26 @@ public class SpannableMarkdownVisitor extends AbstractVisitor { return false; } - @TableRowSpan.Alignment - private static int tableCellAlignment(TableCell.Alignment alignment) { - final int out; - if (alignment != null) { - switch (alignment) { - case CENTER: - out = TableRowSpan.ALIGN_CENTER; - break; - case RIGHT: - out = TableRowSpan.ALIGN_RIGHT; - break; - default: - out = TableRowSpan.ALIGN_LEFT; - break; - } - } else { - out = TableRowSpan.ALIGN_LEFT; - } - return out; - } +// @TableRowSpan.Alignment +// private static int tableCellAlignment(TableCell.Alignment alignment) { +// final int out; +// if (alignment != null) { +// switch (alignment) { +// case CENTER: +// out = TableRowSpan.ALIGN_CENTER; +// break; +// case RIGHT: +// out = TableRowSpan.ALIGN_RIGHT; +// break; +// default: +// out = TableRowSpan.ALIGN_LEFT; +// break; +// } +// } else { +// out = TableRowSpan.ALIGN_LEFT; +// } +// return out; +// } /** * @since 2.0.0 diff --git a/markwon/src/main/java/ru/noties/markwon/spans/AsyncDrawableSpan.java b/markwon/src/main/java/ru/noties/markwon/spans/AsyncDrawableSpan.java index b1ac85d4..20b5fe9d 100644 --- a/markwon/src/main/java/ru/noties/markwon/spans/AsyncDrawableSpan.java +++ b/markwon/src/main/java/ru/noties/markwon/spans/AsyncDrawableSpan.java @@ -12,6 +12,8 @@ import android.text.style.ReplacementSpan; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; +import ru.noties.markwon.image.AsyncDrawable; + @SuppressWarnings("WeakerAccess") public class AsyncDrawableSpan extends ReplacementSpan { @@ -29,16 +31,16 @@ public class AsyncDrawableSpan extends ReplacementSpan { private final int alignment; private final boolean replacementTextIsLink; - public AsyncDrawableSpan(@NonNull MarkwonTheme theme, @NonNull AsyncDrawable drawable) { - this(theme, drawable, ALIGN_BOTTOM); - } +// public AsyncDrawableSpan(@NonNull MarkwonTheme theme, @NonNull AsyncDrawable drawable) { +// this(theme, drawable, ALIGN_BOTTOM); +// } - public AsyncDrawableSpan( - @NonNull MarkwonTheme theme, - @NonNull AsyncDrawable drawable, - @Alignment int alignment) { - this(theme, drawable, alignment, false); - } +// public AsyncDrawableSpan( +// @NonNull MarkwonTheme theme, +// @NonNull AsyncDrawable drawable, +// @Alignment int alignment) { +// this(theme, drawable, alignment, false); +// } public AsyncDrawableSpan( @NonNull MarkwonTheme theme, @@ -137,7 +139,7 @@ public class AsyncDrawableSpan extends ReplacementSpan { // will it make sense to have additional background/borders for an image replacement? // let's focus on main functionality and then think of it - final float textY = CanvasUtils.textCenterY(top, bottom, paint); + final float textY = textCenterY(top, bottom, paint); if (replacementTextIsLink) { theme.applyLinkStyle(paint); } @@ -150,4 +152,9 @@ public class AsyncDrawableSpan extends ReplacementSpan { public AsyncDrawable getDrawable() { return drawable; } + + private static float textCenterY(int top, int bottom, @NonNull Paint paint) { + // @since 1.1.1 it's `top +` and not `bottom -` + return (int) (top + ((bottom - top) / 2) - ((paint.descent() + paint.ascent()) / 2.F + .5F)); + } } diff --git a/markwon/src/main/java/ru/noties/markwon/spans/CanvasUtils.java b/markwon/src/main/java/ru/noties/markwon/spans/CanvasUtils.java deleted file mode 100644 index 0851e855..00000000 --- a/markwon/src/main/java/ru/noties/markwon/spans/CanvasUtils.java +++ /dev/null @@ -1,15 +0,0 @@ -package ru.noties.markwon.spans; - -import android.graphics.Paint; -import android.support.annotation.NonNull; - -abstract class CanvasUtils { - - static float textCenterY(int top, int bottom, @NonNull Paint paint) { - // @since 1.1.1 it's `top +` and not `bottom -` - return (int) (top + ((bottom - top) / 2) - ((paint.descent() + paint.ascent()) / 2.F + .5F)); - } - - private CanvasUtils() { - } -} diff --git a/markwon/src/main/java/ru/noties/markwon/spans/ColorUtils.java b/markwon/src/main/java/ru/noties/markwon/spans/ColorUtils.java deleted file mode 100644 index 4739d531..00000000 --- a/markwon/src/main/java/ru/noties/markwon/spans/ColorUtils.java +++ /dev/null @@ -1,11 +0,0 @@ -package ru.noties.markwon.spans; - -abstract class ColorUtils { - - static int applyAlpha(int color, int alpha) { - return (color & 0x00FFFFFF) | (alpha << 24); - } - - private ColorUtils() { - } -} diff --git a/markwon/src/main/java/ru/noties/markwon/spans/MarkwonTheme.java b/markwon/src/main/java/ru/noties/markwon/spans/MarkwonTheme.java index b5c9f34a..8f803841 100644 --- a/markwon/src/main/java/ru/noties/markwon/spans/MarkwonTheme.java +++ b/markwon/src/main/java/ru/noties/markwon/spans/MarkwonTheme.java @@ -1,25 +1,22 @@ package ru.noties.markwon.spans; import android.content.Context; -import android.content.res.TypedArray; import android.graphics.Paint; import android.graphics.Typeface; import android.graphics.drawable.Drawable; -import android.support.annotation.AttrRes; import android.support.annotation.ColorInt; import android.support.annotation.FloatRange; import android.support.annotation.IntRange; import android.support.annotation.NonNull; -import android.support.annotation.Nullable; import android.support.annotation.Px; import android.support.annotation.Size; import android.text.TextPaint; -import android.util.TypedValue; import java.util.Arrays; import java.util.Locale; -import ru.noties.markwon.tasklist.TaskListDrawable; +import ru.noties.markwon.utils.ColorUtils; +import ru.noties.markwon.utils.Dip; @SuppressWarnings("WeakerAccess") public class MarkwonTheme { @@ -77,36 +74,14 @@ public class MarkwonTheme { @NonNull public static Builder builderWithDefaults(@NonNull Context context) { - // by default we will be using link color for the checkbox color - // & window background as a checkMark color - final int linkColor = resolve(context, android.R.attr.textColorLink); - final int backgroundColor = resolve(context, android.R.attr.colorBackground); - - // before 1.0.5 build had `linkColor` set, but in order for spans to use default link color - // set directly in widget (or any caller), we should not pass it here - - final Dip dip = new Dip(context); + final Dip dip = Dip.create(context); return new Builder() .codeMultilineMargin(dip.toPx(8)) .blockMargin(dip.toPx(24)) .blockQuoteWidth(dip.toPx(4)) .bulletListItemStrokeWidth(dip.toPx(1)) .headingBreakHeight(dip.toPx(1)) - .thematicBreakHeight(dip.toPx(4)) - .tableCellPadding(dip.toPx(4)) - .tableBorderWidth(dip.toPx(1)) - .taskListDrawable(new TaskListDrawable(linkColor, linkColor, backgroundColor)); - } - - private static int resolve(Context context, @AttrRes int attr) { - final TypedValue typedValue = new TypedValue(); - final int attrs[] = new int[]{attr}; - final TypedArray typedArray = context.obtainStyledAttributes(typedValue.data, attrs); - try { - return typedArray.getColor(0, 0); - } finally { - typedArray.recycle(); - } + .thematicBreakHeight(dip.toPx(4)); } protected static final int BLOCK_QUOTE_DEF_COLOR_ALPHA = 25; @@ -126,10 +101,6 @@ public class MarkwonTheme { protected static final int THEMATIC_BREAK_DEF_ALPHA = 25; - protected static final int TABLE_BORDER_DEF_ALPHA = 75; - - protected static final int TABLE_ODD_ROW_DEF_ALPHA = 22; - protected final int linkColor; // used in quote, lists @@ -197,30 +168,6 @@ public class MarkwonTheme { // by default paint.strokeWidth protected final int thematicBreakHeight; - // by default 0 - protected final int tableCellPadding; - - // by default paint.color * TABLE_BORDER_DEF_ALPHA - protected final int tableBorderColor; - - protected final int tableBorderWidth; - - // by default paint.color * TABLE_ODD_ROW_DEF_ALPHA - protected final int tableOddRowBackgroundColor; - - // @since 1.1.1 - // by default no background - protected final int tableEventRowBackgroundColor; - - // @since 1.1.1 - // by default no background - protected final int tableHeaderRowBackgroundColor; - - // drawable that will be used to render checkbox (should be stateful) - // TaskListDrawable can be used - @Deprecated - protected final Drawable taskListDrawable; - protected MarkwonTheme(@NonNull Builder builder) { this.linkColor = builder.linkColor; this.blockMargin = builder.blockMargin; @@ -243,13 +190,6 @@ public class MarkwonTheme { this.scriptTextSizeRatio = builder.scriptTextSizeRatio; this.thematicBreakColor = builder.thematicBreakColor; this.thematicBreakHeight = builder.thematicBreakHeight; - this.tableCellPadding = builder.tableCellPadding; - this.tableBorderColor = builder.tableBorderColor; - this.tableBorderWidth = builder.tableBorderWidth; - this.tableOddRowBackgroundColor = builder.tableOddRowBackgroundColor; - this.tableEventRowBackgroundColor = builder.tableEvenRowBackgroundColor; - this.tableHeaderRowBackgroundColor = builder.tableHeaderRowBackgroundColor; - this.taskListDrawable = builder.taskListDrawable; } /** @@ -468,71 +408,6 @@ public class MarkwonTheme { } } - public int tableCellPadding() { - return tableCellPadding; - } - - public int tableBorderWidth(@NonNull Paint paint) { - final int out; - if (tableBorderWidth == -1) { - out = (int) (paint.getStrokeWidth() + .5F); - } else { - out = tableBorderWidth; - } - return out; - } - - public void applyTableBorderStyle(@NonNull Paint paint) { - - final int color; - if (tableBorderColor == 0) { - color = ColorUtils.applyAlpha(paint.getColor(), TABLE_BORDER_DEF_ALPHA); - } else { - color = tableBorderColor; - } - - paint.setColor(color); - paint.setStyle(Paint.Style.STROKE); - } - - public void applyTableOddRowStyle(@NonNull Paint paint) { - final int color; - if (tableOddRowBackgroundColor == 0) { - color = ColorUtils.applyAlpha(paint.getColor(), TABLE_ODD_ROW_DEF_ALPHA); - } else { - color = tableOddRowBackgroundColor; - } - paint.setColor(color); - paint.setStyle(Paint.Style.FILL); - } - - /** - * @since 1.1.1 - */ - public void applyTableEvenRowStyle(@NonNull Paint paint) { - // by default to background to even row - paint.setColor(tableEventRowBackgroundColor); - paint.setStyle(Paint.Style.FILL); - } - - /** - * @since 1.1.1 - */ - public void applyTableHeaderRowStyle(@NonNull Paint paint) { - paint.setColor(tableHeaderRowBackgroundColor); - paint.setStyle(Paint.Style.FILL); - } - - /** - * @return a Drawable to be used as a checkbox indication in task lists - * @since 1.0.1 - */ - @Nullable - @Deprecated - public Drawable getTaskListDrawable() { - return taskListDrawable; - } - @SuppressWarnings("unused") public static class Builder { @@ -557,13 +432,6 @@ public class MarkwonTheme { private float scriptTextSizeRatio; private int thematicBreakColor; private int thematicBreakHeight = -1; - private int tableCellPadding; - private int tableBorderColor; - private int tableBorderWidth = -1; - private int tableOddRowBackgroundColor; - private int tableEvenRowBackgroundColor; // @since 1.1.1 - private int tableHeaderRowBackgroundColor; // @since 1.1.1 - private Drawable taskListDrawable; Builder() { } @@ -590,11 +458,6 @@ public class MarkwonTheme { this.scriptTextSizeRatio = theme.scriptTextSizeRatio; this.thematicBreakColor = theme.thematicBreakColor; this.thematicBreakHeight = theme.thematicBreakHeight; - this.tableCellPadding = theme.tableCellPadding; - this.tableBorderColor = theme.tableBorderColor; - this.tableBorderWidth = theme.tableBorderWidth; - this.tableOddRowBackgroundColor = theme.tableOddRowBackgroundColor; - this.taskListDrawable = theme.taskListDrawable; } @NonNull @@ -742,81 +605,10 @@ public class MarkwonTheme { return this; } - @NonNull - public Builder tableCellPadding(@Px int tableCellPadding) { - this.tableCellPadding = tableCellPadding; - return this; - } - - @NonNull - public Builder tableBorderColor(@ColorInt int tableBorderColor) { - this.tableBorderColor = tableBorderColor; - return this; - } - - @NonNull - public Builder tableBorderWidth(@Px int tableBorderWidth) { - this.tableBorderWidth = tableBorderWidth; - return this; - } - - @NonNull - public Builder tableOddRowBackgroundColor(@ColorInt int tableOddRowBackgroundColor) { - this.tableOddRowBackgroundColor = tableOddRowBackgroundColor; - return this; - } - - /** - * @since 1.1.1 - */ - @NonNull - public Builder tableEvenRowBackgroundColor(@ColorInt int tableEvenRowBackgroundColor) { - this.tableEvenRowBackgroundColor = tableEvenRowBackgroundColor; - return this; - } - - /** - * @since 1.1.1 - */ - @NonNull - public Builder tableHeaderRowBackgroundColor(@ColorInt int tableHeaderRowBackgroundColor) { - this.tableHeaderRowBackgroundColor = tableHeaderRowBackgroundColor; - return this; - } - - /** - * Supplied Drawable must be stateful ({@link Drawable#isStateful()} returns true). If a task - * is marked as done, then this drawable will be updated with an {@code int[] { android.R.attr.state_checked }} - * as the state, otherwise an empty array will be used. This library provides a ready to be - * used Drawable: {@link TaskListDrawable} - * - * @param taskListDrawable Drawable to be used as the task list indication (checkbox) - * @see TaskListDrawable - * @since 1.0.1 - */ - @NonNull - @Deprecated - public Builder taskListDrawable(@NonNull Drawable taskListDrawable) { - this.taskListDrawable = taskListDrawable; - return this; - } - @NonNull public MarkwonTheme build() { return new MarkwonTheme(this); } } - private static class Dip { - - private final float density; - - Dip(@NonNull Context context) { - this.density = context.getResources().getDisplayMetrics().density; - } - - int toPx(int dp) { - return (int) (dp * density + .5F); - } - } } diff --git a/markwon/src/main/java/ru/noties/markwon/table/TablePlugin.java b/markwon/src/main/java/ru/noties/markwon/table/TablePlugin.java new file mode 100644 index 00000000..506b8ff3 --- /dev/null +++ b/markwon/src/main/java/ru/noties/markwon/table/TablePlugin.java @@ -0,0 +1,189 @@ +package ru.noties.markwon.table; + +import android.content.Context; +import android.support.annotation.NonNull; +import android.widget.TextView; + +import org.commonmark.ext.gfm.tables.TableBody; +import org.commonmark.ext.gfm.tables.TableCell; +import org.commonmark.ext.gfm.tables.TableHead; +import org.commonmark.ext.gfm.tables.TableRow; +import org.commonmark.ext.gfm.tables.TablesExtension; +import org.commonmark.node.Node; +import org.commonmark.parser.Parser; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +import ru.noties.markwon.AbstractMarkwonPlugin; +import ru.noties.markwon.MarkwonVisitor; +import ru.noties.markwon.SpannableBuilder; + +public class TablePlugin extends AbstractMarkwonPlugin { + + @NonNull + public static TablePlugin create(@NonNull Context context) { + return new TablePlugin(TableTheme.create(context)); + } + + @NonNull + public static TablePlugin create(@NonNull TableTheme tableTheme) { + return new TablePlugin(tableTheme); + } + + private final TableTheme tableTheme; + + TablePlugin(@NonNull TableTheme tableTheme) { + this.tableTheme = tableTheme; + } + + @Override + public void configureParser(@NonNull Parser.Builder builder) { + builder.extensions(Collections.singleton(TablesExtension.create())); + } + + @Override + public void configureVisitor(@NonNull MarkwonVisitor.Builder builder) { + TableVisitor.configure(tableTheme, builder); + } + + @Override + public void beforeSetText(@NonNull TextView textView, @NonNull CharSequence markdown) { + TableRowsScheduler.unschedule(textView); + } + + @Override + public void afterSetText(@NonNull TextView textView) { + TableRowsScheduler.schedule(textView); + } + + private static class TableVisitor { + + static void configure(@NonNull TableTheme tableTheme, @NonNull MarkwonVisitor.Builder builder) { + new TableVisitor(tableTheme, builder); + } + + private final TableTheme tableTheme; + + private List pendingTableRow; + private boolean tableRowIsHeader; + private int tableRows; + + private TableVisitor(@NonNull TableTheme tableTheme, @NonNull MarkwonVisitor.Builder builder) { + this.tableTheme = tableTheme; + builder + .on(TableBody.class, new MarkwonVisitor.NodeVisitor() { + @Override + public void visit(@NonNull MarkwonVisitor visitor, @NonNull TableBody tableBody) { + + visitor.visitChildren(tableBody); + tableRows = 0; + + if (visitor.hasNext(tableBody)) { + visitor.ensureNewLine(); + visitor.forceNewLine(); + } + } + }) + .on(TableRow.class, new MarkwonVisitor.NodeVisitor() { + @Override + public void visit(@NonNull MarkwonVisitor visitor, @NonNull TableRow tableRow) { + visitRow(visitor, tableRow); + } + }) + .on(TableHead.class, new MarkwonVisitor.NodeVisitor() { + @Override + public void visit(@NonNull MarkwonVisitor visitor, @NonNull TableHead tableHead) { + visitRow(visitor, tableHead); + } + }) + .on(TableCell.class, new MarkwonVisitor.NodeVisitor() { + @Override + public void visit(@NonNull MarkwonVisitor visitor, @NonNull TableCell tableCell) { + + final int length = visitor.length(); + + visitor.visitChildren(tableCell); + + if (pendingTableRow == null) { + pendingTableRow = new ArrayList<>(2); + } + + pendingTableRow.add(new TableRowSpan.Cell( + tableCellAlignment(tableCell.getAlignment()), + visitor.builder().removeFromEnd(length) + )); + + tableRowIsHeader = tableCell.isHeader(); + } + }); + } + + private void visitRow(@NonNull MarkwonVisitor visitor, @NonNull Node node) { + + final int length = visitor.length(); + + visitor.visitChildren(node); + + if (pendingTableRow != null) { + + final SpannableBuilder builder = visitor.builder(); + + // @since 2.0.0 + // we cannot rely on hasNext(TableHead) as it's not reliable + // we must apply new line manually and then exclude it from tableRow span + final boolean addNewLine; + { + final int builderLength = builder.length(); + addNewLine = builderLength > 0 + && '\n' != builder.charAt(builderLength - 1); + } + + if (addNewLine) { + visitor.forceNewLine(); + } + + // @since 1.0.4 Replace table char with non-breakable space + // we need this because if table is at the end of the text, then it will be + // trimmed from the final result + builder.append('\u00a0'); + + final Object span = new TableRowSpan( + tableTheme, + pendingTableRow, + tableRowIsHeader, + tableRows % 2 == 1); + + tableRows = tableRowIsHeader + ? 0 + : tableRows + 1; + + visitor.setSpans(addNewLine ? length + 1 : length, span); + + pendingTableRow = null; + } + } + + @TableRowSpan.Alignment + private static int tableCellAlignment(TableCell.Alignment alignment) { + final int out; + if (alignment != null) { + switch (alignment) { + case CENTER: + out = TableRowSpan.ALIGN_CENTER; + break; + case RIGHT: + out = TableRowSpan.ALIGN_RIGHT; + break; + default: + out = TableRowSpan.ALIGN_LEFT; + break; + } + } else { + out = TableRowSpan.ALIGN_LEFT; + } + return out; + } + } +} diff --git a/markwon/src/main/java/ru/noties/markwon/spans/TableRowSpan.java b/markwon/src/main/java/ru/noties/markwon/table/TableRowSpan.java similarity index 96% rename from markwon/src/main/java/ru/noties/markwon/spans/TableRowSpan.java rename to markwon/src/main/java/ru/noties/markwon/table/TableRowSpan.java index a54b3206..903cf5f3 100644 --- a/markwon/src/main/java/ru/noties/markwon/spans/TableRowSpan.java +++ b/markwon/src/main/java/ru/noties/markwon/table/TableRowSpan.java @@ -1,4 +1,4 @@ -package ru.noties.markwon.spans; +package ru.noties.markwon.table; import android.annotation.SuppressLint; import android.graphics.Canvas; @@ -61,22 +61,22 @@ public class TableRowSpan extends ReplacementSpan { } } - private final MarkwonTheme theme; + private final TableTheme theme; private final List cells; private final List layouts; private final TextPaint textPaint; private final boolean header; private final boolean odd; - private final Rect rect = ObjectsPool.rect(); - private final Paint paint = ObjectsPool.paint(); + private final Rect rect = new Rect(); + private final Paint paint = new Paint(Paint.ANTI_ALIAS_FLAG); private int width; private int height; private Invalidator invalidator; public TableRowSpan( - @NonNull MarkwonTheme theme, + @NonNull TableTheme theme, @NonNull List cells, boolean header, boolean odd) { @@ -272,8 +272,7 @@ public class TableRowSpan extends ReplacementSpan { return out; } - public TableRowSpan invalidator(Invalidator invalidator) { + public void invalidator(@Nullable Invalidator invalidator) { this.invalidator = invalidator; - return this; } } diff --git a/markwon/src/main/java/ru/noties/markwon/TableRowsScheduler.java b/markwon/src/main/java/ru/noties/markwon/table/TableRowsScheduler.java similarity index 96% rename from markwon/src/main/java/ru/noties/markwon/TableRowsScheduler.java rename to markwon/src/main/java/ru/noties/markwon/table/TableRowsScheduler.java index 6fc9a584..1d1246b0 100644 --- a/markwon/src/main/java/ru/noties/markwon/TableRowsScheduler.java +++ b/markwon/src/main/java/ru/noties/markwon/table/TableRowsScheduler.java @@ -1,13 +1,13 @@ -package ru.noties.markwon; +package ru.noties.markwon.table; import android.support.annotation.NonNull; +import android.support.annotation.Nullable; import android.text.Spanned; import android.text.TextUtils; import android.view.View; import android.widget.TextView; import ru.noties.markwon.renderer.R; -import ru.noties.markwon.spans.TableRowSpan; abstract class TableRowsScheduler { @@ -57,6 +57,7 @@ abstract class TableRowsScheduler { } } + @Nullable private static Object[] extract(@NonNull TextView view) { final Object[] out; final CharSequence text = view.getText(); diff --git a/markwon/src/main/java/ru/noties/markwon/table/TableTheme.java b/markwon/src/main/java/ru/noties/markwon/table/TableTheme.java new file mode 100644 index 00000000..56a72321 --- /dev/null +++ b/markwon/src/main/java/ru/noties/markwon/table/TableTheme.java @@ -0,0 +1,164 @@ +package ru.noties.markwon.table; + +import android.content.Context; +import android.graphics.Paint; +import android.support.annotation.NonNull; + +import ru.noties.markwon.utils.ColorUtils; +import ru.noties.markwon.utils.Dip; + +public class TableTheme { + + @NonNull + public static TableTheme create(@NonNull Context context) { + final Dip dip = Dip.create(context); + return builder() + .tableCellPadding(dip.toPx(4)) + .tableBorderWidth(dip.toPx(1)) + .build(); + } + + @NonNull + public static Builder builder() { + return new Builder(); + } + + + protected static final int TABLE_BORDER_DEF_ALPHA = 75; + + protected static final int TABLE_ODD_ROW_DEF_ALPHA = 22; + + // by default 0 + protected final int tableCellPadding; + + // by default paint.color * TABLE_BORDER_DEF_ALPHA + protected final int tableBorderColor; + + protected final int tableBorderWidth; + + // by default paint.color * TABLE_ODD_ROW_DEF_ALPHA + protected final int tableOddRowBackgroundColor; + + // @since 1.1.1 + // by default no background + protected final int tableEvenRowBackgroundColor; + + // @since 1.1.1 + // by default no background + protected final int tableHeaderRowBackgroundColor; + + protected TableTheme(@NonNull Builder builder) { + this.tableCellPadding = builder.tableCellPadding; + this.tableBorderColor = builder.tableBorderColor; + this.tableBorderWidth = builder.tableBorderWidth; + this.tableOddRowBackgroundColor = builder.tableOddRowBackgroundColor; + this.tableEvenRowBackgroundColor = builder.tableEvenRowBackgroundColor; + this.tableHeaderRowBackgroundColor = builder.tableHeaderRowBackgroundColor; + } + + public int tableCellPadding() { + return tableCellPadding; + } + + public int tableBorderWidth(@NonNull Paint paint) { + final int out; + if (tableBorderWidth == -1) { + out = (int) (paint.getStrokeWidth() + .5F); + } else { + out = tableBorderWidth; + } + return out; + } + + public void applyTableBorderStyle(@NonNull Paint paint) { + + final int color; + if (tableBorderColor == 0) { + color = ColorUtils.applyAlpha(paint.getColor(), TABLE_BORDER_DEF_ALPHA); + } else { + color = tableBorderColor; + } + + paint.setColor(color); + paint.setStyle(Paint.Style.STROKE); + } + + public void applyTableOddRowStyle(@NonNull Paint paint) { + final int color; + if (tableOddRowBackgroundColor == 0) { + color = ColorUtils.applyAlpha(paint.getColor(), TABLE_ODD_ROW_DEF_ALPHA); + } else { + color = tableOddRowBackgroundColor; + } + paint.setColor(color); + paint.setStyle(Paint.Style.FILL); + } + + /** + * @since 1.1.1 + */ + public void applyTableEvenRowStyle(@NonNull Paint paint) { + // by default to background to even row + paint.setColor(tableEvenRowBackgroundColor); + paint.setStyle(Paint.Style.FILL); + } + + /** + * @since 1.1.1 + */ + public void applyTableHeaderRowStyle(@NonNull Paint paint) { + paint.setColor(tableHeaderRowBackgroundColor); + paint.setStyle(Paint.Style.FILL); + } + + public static class Builder { + + private int tableCellPadding; + private int tableBorderColor; + private int tableBorderWidth = -1; + private int tableOddRowBackgroundColor; + private int tableEvenRowBackgroundColor; // @since 1.1.1 + private int tableHeaderRowBackgroundColor; // @since 1.1.1 + + @NonNull + public Builder tableCellPadding(int tableCellPadding) { + this.tableCellPadding = tableCellPadding; + return this; + } + + @NonNull + public Builder tableBorderColor(int tableBorderColor) { + this.tableBorderColor = tableBorderColor; + return this; + } + + @NonNull + public Builder tableBorderWidth(int tableBorderWidth) { + this.tableBorderWidth = tableBorderWidth; + return this; + } + + @NonNull + public Builder tableOddRowBackgroundColor(int tableOddRowBackgroundColor) { + this.tableOddRowBackgroundColor = tableOddRowBackgroundColor; + return this; + } + + @NonNull + public Builder tableEvenRowBackgroundColor(int tableEvenRowBackgroundColor) { + this.tableEvenRowBackgroundColor = tableEvenRowBackgroundColor; + return this; + } + + @NonNull + public Builder tableHeaderRowBackgroundColor(int tableHeaderRowBackgroundColor) { + this.tableHeaderRowBackgroundColor = tableHeaderRowBackgroundColor; + return this; + } + + @NonNull + public TableTheme build() { + return new TableTheme(this); + } + } +} diff --git a/markwon/src/main/java/ru/noties/markwon/tasklist/TaskListPlugin.java b/markwon/src/main/java/ru/noties/markwon/tasklist/TaskListPlugin.java index bf75d5c7..77c865c4 100644 --- a/markwon/src/main/java/ru/noties/markwon/tasklist/TaskListPlugin.java +++ b/markwon/src/main/java/ru/noties/markwon/tasklist/TaskListPlugin.java @@ -1,9 +1,12 @@ package ru.noties.markwon.tasklist; import android.content.Context; +import android.content.res.TypedArray; import android.graphics.drawable.Drawable; +import android.support.annotation.AttrRes; import android.support.annotation.ColorInt; import android.support.annotation.NonNull; +import android.util.TypedValue; import org.commonmark.parser.Parser; @@ -13,6 +16,11 @@ import ru.noties.markwon.MarkwonVisitor; public class TaskListPlugin extends AbstractMarkwonPlugin { /** + * Supplied Drawable must be stateful ({@link Drawable#isStateful()} returns true). If a task + * is marked as done, then this drawable will be updated with an {@code int[] { android.R.attr.state_checked }} + * as the state, otherwise an empty array will be used. This library provides a ready to be + * used Drawable: {@link TaskListDrawable} + * * @see TaskListDrawable */ @NonNull @@ -22,8 +30,13 @@ public class TaskListPlugin extends AbstractMarkwonPlugin { @NonNull public static TaskListPlugin create(@NonNull Context context) { - // resolve link color and background color - return null; + + // by default we will be using link color for the checkbox color + // & window background as a checkMark color + final int linkColor = resolve(context, android.R.attr.textColorLink); + final int backgroundColor = resolve(context, android.R.attr.colorBackground); + + return new TaskListPlugin(new TaskListDrawable(linkColor, linkColor, backgroundColor)); } @NonNull @@ -90,4 +103,15 @@ public class TaskListPlugin extends AbstractMarkwonPlugin { } }); } + + private static int resolve(Context context, @AttrRes int attr) { + final TypedValue typedValue = new TypedValue(); + final int attrs[] = new int[]{attr}; + final TypedArray typedArray = context.obtainStyledAttributes(typedValue.data, attrs); + try { + return typedArray.getColor(0, 0); + } finally { + typedArray.recycle(); + } + } } diff --git a/markwon/src/main/java/ru/noties/markwon/utils/ColorUtils.java b/markwon/src/main/java/ru/noties/markwon/utils/ColorUtils.java new file mode 100644 index 00000000..d6305132 --- /dev/null +++ b/markwon/src/main/java/ru/noties/markwon/utils/ColorUtils.java @@ -0,0 +1,11 @@ +package ru.noties.markwon.utils; + +public abstract class ColorUtils { + + public static int applyAlpha(int color, int alpha) { + return (color & 0x00FFFFFF) | (alpha << 24); + } + + private ColorUtils() { + } +} diff --git a/markwon/src/main/java/ru/noties/markwon/utils/Dip.java b/markwon/src/main/java/ru/noties/markwon/utils/Dip.java new file mode 100644 index 00000000..6899df8f --- /dev/null +++ b/markwon/src/main/java/ru/noties/markwon/utils/Dip.java @@ -0,0 +1,29 @@ +package ru.noties.markwon.utils; + +import android.content.Context; +import android.support.annotation.NonNull; + +import ru.noties.markwon.spans.MarkwonTheme; + +public class Dip { + + @NonNull + public static Dip create(@NonNull Context context) { + return new Dip(context.getResources().getDisplayMetrics().density); + } + + @NonNull + public static Dip create(float density) { + return new Dip(density); + } + + private final float density; + + public Dip(float density) { + this.density = density; + } + + public int toPx(int dp) { + return (int) (dp * density + .5F); + } +} diff --git a/markwon/src/main/java/ru/noties/markwon/utils/DrawableUtils.java b/markwon/src/main/java/ru/noties/markwon/utils/DrawableUtils.java new file mode 100644 index 00000000..34342093 --- /dev/null +++ b/markwon/src/main/java/ru/noties/markwon/utils/DrawableUtils.java @@ -0,0 +1,13 @@ +package ru.noties.markwon.utils; + +import android.graphics.drawable.Drawable; +import android.support.annotation.NonNull; + +public abstract class DrawableUtils { + + public static void intrinsicBounds(@NonNull Drawable drawable) { + drawable.setBounds(0, 0, drawable.getIntrinsicWidth(), drawable.getIntrinsicHeight()); + } + + private DrawableUtils() {} +} diff --git a/markwon/src/test/java/ru/noties/markwon/image/data/DataUriParserTest.java b/markwon/src/test/java/ru/noties/markwon/image/data/DataUriParserTest.java new file mode 100644 index 00000000..b361a0d3 --- /dev/null +++ b/markwon/src/test/java/ru/noties/markwon/image/data/DataUriParserTest.java @@ -0,0 +1,119 @@ +package ru.noties.markwon.image.data; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.robolectric.RobolectricTestRunner; +import org.robolectric.annotation.Config; + +import java.util.LinkedHashMap; +import java.util.Map; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNull; + +@RunWith(RobolectricTestRunner.class) +@Config(manifest = Config.NONE) +public class DataUriParserTest { + + private DataUriParser.Impl impl; + + @Before + public void before() { + impl = new DataUriParser.Impl(); + } + + @Test + public void test() { + + final Map data = new LinkedHashMap() {{ + put(",", new DataUri(null, false, null)); + put("image/svg+xml;base64,!@#$%^&*(", new DataUri("image/svg+xml", true, "!@#$%^&*(")); + put("text/vnd-example+xyz;foo=bar;base64,R0lGODdh", new DataUri("text/vnd-example+xyz", true, "R0lGODdh")); + put("text/plain;charset=UTF-8;page=21,the%20data:1234,5678", new DataUri("text/plain", false, "the%20data:1234,5678")); + }}; + + for (Map.Entry entry : data.entrySet()) { + assertEquals(entry.getKey(), entry.getValue(), impl.parse(entry.getKey())); + } + } + + @Test + public void data_new_lines_are_ignored() { + + final String input = "image/png;base64,iVBORw0KGgoAAA\n" + + "ANSUhEUgAAAAUAAAAFCAYAAACNbyblAAAAHElEQVQI12P4\n" + + "//8/w38GIAXDIBKE0DHxgljNBAAO9TXL0Y4OHwAAAABJRU\n" + + "5ErkJggg=="; + + assertEquals( + new DataUri("image/png", true, "iVBORw0KGgoAAAANSUhEUgAAAAUAAAAFCAYAAACNbyblAAAAHElEQVQI12P4//8/w38GIAXDIBKE0DHxgljNBAAO9TXL0Y4OHwAAAABJRU5ErkJggg=="), + impl.parse(input) + ); + } + + @Test + public void no_comma_returns_null() { + + final String[] inputs = { + "", + "what-ever", + ";;;;;;;", + "some crazy data" + }; + + for (String input : inputs) { + assertNull(input, impl.parse(input)); + } + } + + @Test + public void two_commas() { + final String input = ",,"; // <- second one would be considered data... + assertEquals( + input, + new DataUri(null, false, ","), + impl.parse(input) + ); + } + + @Test + public void more_commas() { + final String input = "first,second,third"; // <- first is just a value (will be ignored) + assertEquals( + input, + new DataUri(null, false, "second,third"), + impl.parse(input) + ); + } + + @Test + public void base64_no_content_type() { + final String input = ";base64,12345"; + assertEquals( + input, + new DataUri(null, true, "12345"), + impl.parse(input) + ); + } + + @Test + public void not_base64_no_content_type() { + final String input = ",qweRTY"; + assertEquals( + input, + new DataUri(null, false, "qweRTY"), + impl.parse(input) + ); + } + + @Test + public void content_type_data_no_base64() { + final String input = "image/png,aSdFg"; + assertEquals( + input, + new DataUri("image/png", false, "aSdFg"), + impl.parse(input) + ); + } +} \ No newline at end of file diff --git a/markwon/src/test/java/ru/noties/markwon/image/data/DataUriSchemeHandlerTest.java b/markwon/src/test/java/ru/noties/markwon/image/data/DataUriSchemeHandlerTest.java new file mode 100644 index 00000000..16dc73b5 --- /dev/null +++ b/markwon/src/test/java/ru/noties/markwon/image/data/DataUriSchemeHandlerTest.java @@ -0,0 +1,114 @@ +package ru.noties.markwon.image.data; + +import android.net.Uri; +import android.support.annotation.NonNull; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.robolectric.RobolectricTestRunner; +import org.robolectric.annotation.Config; + +import java.io.InputStream; +import java.util.HashMap; +import java.util.Map; +import java.util.Scanner; + +import ru.noties.markwon.image.ImageItem; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; + +@RunWith(RobolectricTestRunner.class) +@Config(manifest = Config.NONE) +public class DataUriSchemeHandlerTest { + + private DataUriSchemeHandler handler; + + @Before + public void before() { + handler = DataUriSchemeHandler.create(); + } + + @Test + public void scheme_specific_part_is_empty() { + assertNull(handler.handle("data:", Uri.parse("data:"))); + } + + @Test + public void data_uri_is_empty() { + assertNull(handler.handle("data://whatever", Uri.parse("data://whatever"))); + } + + @Test + public void no_data() { + assertNull(handler.handle("data://,", Uri.parse("data://,"))); + } + + @Test + public void correct() { + + final class Item { + + final String contentType; + final String data; + + Item(String contentType, String data) { + this.contentType = contentType; + this.data = data; + } + } + + final Map expected = new HashMap() {{ + put("data://text/plain;,123", new Item("text/plain", "123")); + put("data://image/svg+xml;base64,MTIz", new Item("image/svg+xml", "123")); + }}; + + for (Map.Entry entry : expected.entrySet()) { + final ImageItem item = handler.handle(entry.getKey(), Uri.parse(entry.getKey())); + assertNotNull(entry.getKey(), item); + assertEquals(entry.getKey(), entry.getValue().contentType, item.contentType()); + assertEquals(entry.getKey(), entry.getValue().data, readStream(item.inputStream())); + } + } + + @Test + public void correct_real() { + + final class Item { + + final String contentType; + final String data; + + Item(String contentType, String data) { + this.contentType = contentType; + this.data = data; + } + } + + final Map expected = new HashMap() {{ + put("data:text/plain;,123", new Item("text/plain", "123")); + put("data:image/svg+xml;base64,MTIz", new Item("image/svg+xml", "123")); + }}; + + for (Map.Entry entry : expected.entrySet()) { + final ImageItem item = handler.handle(entry.getKey(), Uri.parse(entry.getKey())); + assertNotNull(entry.getKey(), item); + assertEquals(entry.getKey(), entry.getValue().contentType, item.contentType()); + assertEquals(entry.getKey(), entry.getValue().data, readStream(item.inputStream())); + } + } + + @NonNull + private static String readStream(@NonNull InputStream stream) { + try { + final Scanner scanner = new Scanner(stream, "UTF-8").useDelimiter("\\A"); + return scanner.hasNext() + ? scanner.next() + : ""; + } catch (Throwable t) { + throw new RuntimeException(t); + } + } +} \ No newline at end of file diff --git a/markwon/src/test/java/ru/noties/markwon/renderer/MarkwonConfigurationTest.java b/markwon/src/test/java/ru/noties/markwon/renderer/MarkwonConfigurationTest.java index e094164b..108df62d 100644 --- a/markwon/src/test/java/ru/noties/markwon/renderer/MarkwonConfigurationTest.java +++ b/markwon/src/test/java/ru/noties/markwon/renderer/MarkwonConfigurationTest.java @@ -8,7 +8,7 @@ import ru.noties.markwon.SyntaxHighlight; import ru.noties.markwon.UrlProcessor; import ru.noties.markwon.html.api.MarkwonHtmlParser; import ru.noties.markwon.renderer.html2.MarkwonHtmlRenderer; -import ru.noties.markwon.spans.AsyncDrawable; +import ru.noties.markwon.image.AsyncDrawable; import ru.noties.markwon.spans.LinkSpan; import ru.noties.markwon.spans.MarkwonTheme; diff --git a/markwon/src/test/java/ru/noties/markwon/renderer/visitor/TestDataReader.java b/markwon/src/test/java/ru/noties/markwon/renderer/visitor/TestDataReader.java index bfba93b1..7f04566b 100644 --- a/markwon/src/test/java/ru/noties/markwon/renderer/visitor/TestDataReader.java +++ b/markwon/src/test/java/ru/noties/markwon/renderer/visitor/TestDataReader.java @@ -30,7 +30,7 @@ import java.util.Set; import ix.Ix; import ix.IxFunction; import ix.IxPredicate; -import ru.noties.markwon.spans.TableRowSpan; +import ru.noties.markwon.table.TableRowSpan; import static ru.noties.markwon.renderer.visitor.TestSpan.BLOCK_QUOTE; import static ru.noties.markwon.renderer.visitor.TestSpan.BULLET_LIST; diff --git a/markwon/src/test/java/ru/noties/markwon/renderer/visitor/TestFactory.java b/markwon/src/test/java/ru/noties/markwon/renderer/visitor/TestFactory.java index 42d46f99..6e6feb30 100644 --- a/markwon/src/test/java/ru/noties/markwon/renderer/visitor/TestFactory.java +++ b/markwon/src/test/java/ru/noties/markwon/renderer/visitor/TestFactory.java @@ -11,10 +11,10 @@ import java.util.Map; import ru.noties.markwon.SpannableFactory; import ru.noties.markwon.renderer.ImageSize; import ru.noties.markwon.renderer.ImageSizeResolver; -import ru.noties.markwon.spans.AsyncDrawable; +import ru.noties.markwon.image.AsyncDrawable; import ru.noties.markwon.spans.LinkSpan; import ru.noties.markwon.spans.MarkwonTheme; -import ru.noties.markwon.spans.TableRowSpan; +import ru.noties.markwon.table.TableRowSpan; import static ru.noties.markwon.renderer.visitor.TestSpan.BLOCK_QUOTE; import static ru.noties.markwon.renderer.visitor.TestSpan.BULLET_LIST; diff --git a/sample-latex-math/build.gradle b/sample-latex-math/build.gradle index 81732d95..f9f7c1eb 100644 --- a/sample-latex-math/build.gradle +++ b/sample-latex-math/build.gradle @@ -18,6 +18,6 @@ android { dependencies { implementation project(':markwon') - implementation project(':markwon-image-loader') +// implementation project(':markwon-image-loader') implementation 'ru.noties:jlatexmath-android:0.1.0' } diff --git a/settings.gradle b/settings.gradle index 11192dbe..3a1678e4 100644 --- a/settings.gradle +++ b/settings.gradle @@ -1,3 +1,3 @@ rootProject.name = 'MarkwonProject' -include ':app', ':markwon', ':markwon-image-loader', ':markwon-view', ':sample-custom-extension', ':sample-latex-math', +include ':app', ':markwon', ':markwon-view', ':sample-custom-extension', ':sample-latex-math', ':markwon-image-svg', ':markwon-image-gif', ':markwon-syntax-highlight', ':markwon-html-parser-api', ':markwon-html-parser-impl' From 27ed17aaffc949f9a88574adb53d59c20db87a6c Mon Sep 17 00:00:00 2001 From: Dimitry Ivanov Date: Mon, 26 Nov 2018 15:16:11 +0300 Subject: [PATCH 020/103] Move all html entities to markwon-html module --- .../ru/noties/markwon/MarkdownRenderer.java | 4 - markwon-html-parser-api/build.gradle | 23 - markwon-html-parser-api/gradle.properties | 3 - .../src/main/AndroidManifest.xml | 1 - .../build.gradle | 3 +- .../gradle.properties | 0 .../src/main/AndroidManifest.xml | 0 .../markwon/html/impl/AppendableUtils.java | 0 .../html/impl}/CssInlineStyleParser.java | 2 +- .../markwon/html/impl}/CssProperty.java | 2 +- .../html/impl/HtmlEmptyTagReplacement.java | 2 +- .../noties/markwon/html/impl/HtmlTagImpl.java | 2 +- .../html/impl/MarkwonHtmlParserImpl.java | 8 +- .../html/impl/MarkwonHtmlRendererImpl.java | 167 ++++++++ .../markwon/html/impl/TrimmingAppender.java | 0 .../html/impl/jsoup/UncheckedIOException.java | 0 .../html/impl/jsoup/helper/Normalizer.java | 0 .../html/impl/jsoup/helper/Validate.java | 0 .../html/impl/jsoup/nodes/Attribute.java | 0 .../html/impl/jsoup/nodes/Attributes.java | 0 .../impl/jsoup/nodes/CommonMarkEntities.java | 0 .../html/impl/jsoup/nodes/DocumentType.java | 0 .../impl/jsoup/parser/CharacterReader.java | 0 .../html/impl/jsoup/parser/ParseError.java | 0 .../impl/jsoup/parser/ParseErrorList.java | 0 .../markwon/html/impl/jsoup/parser/Token.java | 0 .../html/impl/jsoup/parser/Tokeniser.java | 0 .../impl/jsoup/parser/TokeniserState.java | 0 .../markwon/html/impl/span/SubScriptSpan.java | 25 ++ .../html/impl/span/SuperScriptSpan.java | 25 ++ .../html/impl}/tag/BlockquoteHandler.java | 7 +- .../html/impl}/tag/EmphasisHandler.java | 4 +- .../html/impl}/tag/HeadingHandler.java | 4 +- .../markwon/html/impl}/tag/ImageHandler.java | 6 +- .../html/impl}/tag/ImageSizeParserImpl.java | 6 +- .../markwon/html/impl}/tag/LinkHandler.java | 4 +- .../markwon/html/impl}/tag/ListHandler.java | 5 +- .../html/impl}/tag/SimpleTagHandler.java | 7 +- .../markwon/html/impl}/tag/StrikeHandler.java | 5 +- .../html/impl}/tag/StrongEmphasisHandler.java | 4 +- .../html/impl}/tag/SubScriptHandler.java | 7 +- .../html/impl}/tag/SuperScriptHandler.java | 7 +- .../html/impl}/tag/UnderlineHandler.java | 10 +- .../html/impl/CssInlineStyleParserTest.java | 239 +++++++++++ .../impl/HtmlEmptyTagReplacementTest.java | 0 .../html/impl/MarkwonHtmlParserImplTest.java | 0 .../html/impl/TrimmingAppenderTest.java | 0 .../jsoup/nodes/CommonMarkEntitiesTest.java | 0 .../impl/tag/ImageSizeParserImplTest.java | 186 ++++++++ markwon-image-loader/build.gradle | 37 -- markwon-image-loader/gradle.properties | 3 - .../src/main/AndroidManifest.xml | 1 - .../markwon/il/AsyncDrawableLoader.java | 405 ------------------ .../java/ru/noties/markwon/il/DataUri.java | 60 --- .../ru/noties/markwon/il/DataUriDecoder.java | 41 -- .../ru/noties/markwon/il/DataUriParser.java | 79 ---- .../markwon/il/DataUriSchemeHandler.java | 74 ---- .../ru/noties/markwon/il/DrawableUtils.java | 13 - .../noties/markwon/il/FileSchemeHandler.java | 109 ----- .../ru/noties/markwon/il/GifMediaDecoder.java | 91 ---- .../java/ru/noties/markwon/il/ImageItem.java | 39 -- .../noties/markwon/il/ImageMediaDecoder.java | 60 --- .../ru/noties/markwon/il/MediaDecoder.java | 20 - .../markwon/il/NetworkSchemeHandler.java | 89 ---- .../ru/noties/markwon/il/SchemeHandler.java | 25 -- .../ru/noties/markwon/il/SvgMediaDecoder.java | 81 ---- .../noties/markwon/il/DataUriParserTest.java | 119 ----- .../markwon/il/DataUriSchemeHandlerTest.java | 112 ----- .../markwon/syntax/Prism4jThemeDarkula.java | 4 + markwon/build.gradle | 3 - .../noties/markwon/MarkwonConfiguration.java | 39 +- .../ru/noties/markwon/SpannableFactory.java | 12 - .../noties/markwon/SpannableFactoryDef.java | 17 - .../java/ru/noties/markwon/html}/HtmlTag.java | 2 +- .../markwon/html}/MarkwonHtmlParser.java | 6 +- .../markwon/html}/MarkwonHtmlParserNoOp.java | 2 +- .../markwon/html/MarkwonHtmlRenderer.java | 30 ++ .../markwon/html/MarkwonHtmlRendererNoOp.java | 21 + .../html2/tag => html}/TagHandler.java | 3 +- .../renderer/html2/MarkwonHtmlRenderer.java | 101 ----- .../html2/MarkwonHtmlRendererImpl.java | 88 ---- .../renderer/MarkwonConfigurationTest.java | 2 +- settings.gradle | 2 +- 83 files changed, 761 insertions(+), 1797 deletions(-) delete mode 100644 markwon-html-parser-api/build.gradle delete mode 100644 markwon-html-parser-api/gradle.properties delete mode 100644 markwon-html-parser-api/src/main/AndroidManifest.xml rename {markwon-html-parser-impl => markwon-html}/build.gradle (88%) rename {markwon-html-parser-impl => markwon-html}/gradle.properties (100%) rename {markwon-html-parser-impl => markwon-html}/src/main/AndroidManifest.xml (100%) rename {markwon-html-parser-impl => markwon-html}/src/main/java/ru/noties/markwon/html/impl/AppendableUtils.java (100%) rename {markwon/src/main/java/ru/noties/markwon/renderer/html2 => markwon-html/src/main/java/ru/noties/markwon/html/impl}/CssInlineStyleParser.java (99%) rename {markwon/src/main/java/ru/noties/markwon/renderer/html2 => markwon-html/src/main/java/ru/noties/markwon/html/impl}/CssProperty.java (94%) rename {markwon-html-parser-impl => markwon-html}/src/main/java/ru/noties/markwon/html/impl/HtmlEmptyTagReplacement.java (97%) rename {markwon-html-parser-impl => markwon-html}/src/main/java/ru/noties/markwon/html/impl/HtmlTagImpl.java (99%) rename {markwon-html-parser-impl => markwon-html}/src/main/java/ru/noties/markwon/html/impl/MarkwonHtmlParserImpl.java (98%) create mode 100644 markwon-html/src/main/java/ru/noties/markwon/html/impl/MarkwonHtmlRendererImpl.java rename {markwon-html-parser-impl => markwon-html}/src/main/java/ru/noties/markwon/html/impl/TrimmingAppender.java (100%) rename {markwon-html-parser-impl => markwon-html}/src/main/java/ru/noties/markwon/html/impl/jsoup/UncheckedIOException.java (100%) rename {markwon-html-parser-impl => markwon-html}/src/main/java/ru/noties/markwon/html/impl/jsoup/helper/Normalizer.java (100%) rename {markwon-html-parser-impl => markwon-html}/src/main/java/ru/noties/markwon/html/impl/jsoup/helper/Validate.java (100%) rename {markwon-html-parser-impl => markwon-html}/src/main/java/ru/noties/markwon/html/impl/jsoup/nodes/Attribute.java (100%) rename {markwon-html-parser-impl => markwon-html}/src/main/java/ru/noties/markwon/html/impl/jsoup/nodes/Attributes.java (100%) rename {markwon-html-parser-impl => markwon-html}/src/main/java/ru/noties/markwon/html/impl/jsoup/nodes/CommonMarkEntities.java (100%) rename {markwon-html-parser-impl => markwon-html}/src/main/java/ru/noties/markwon/html/impl/jsoup/nodes/DocumentType.java (100%) rename {markwon-html-parser-impl => markwon-html}/src/main/java/ru/noties/markwon/html/impl/jsoup/parser/CharacterReader.java (100%) rename {markwon-html-parser-impl => markwon-html}/src/main/java/ru/noties/markwon/html/impl/jsoup/parser/ParseError.java (100%) rename {markwon-html-parser-impl => markwon-html}/src/main/java/ru/noties/markwon/html/impl/jsoup/parser/ParseErrorList.java (100%) rename {markwon-html-parser-impl => markwon-html}/src/main/java/ru/noties/markwon/html/impl/jsoup/parser/Token.java (100%) rename {markwon-html-parser-impl => markwon-html}/src/main/java/ru/noties/markwon/html/impl/jsoup/parser/Tokeniser.java (100%) rename {markwon-html-parser-impl => markwon-html}/src/main/java/ru/noties/markwon/html/impl/jsoup/parser/TokeniserState.java (100%) create mode 100644 markwon-html/src/main/java/ru/noties/markwon/html/impl/span/SubScriptSpan.java create mode 100644 markwon-html/src/main/java/ru/noties/markwon/html/impl/span/SuperScriptSpan.java rename {markwon/src/main/java/ru/noties/markwon/renderer/html2 => markwon-html/src/main/java/ru/noties/markwon/html/impl}/tag/BlockquoteHandler.java (84%) rename {markwon/src/main/java/ru/noties/markwon/renderer/html2 => markwon-html/src/main/java/ru/noties/markwon/html/impl}/tag/EmphasisHandler.java (80%) rename {markwon/src/main/java/ru/noties/markwon/renderer/html2 => markwon-html/src/main/java/ru/noties/markwon/html/impl}/tag/HeadingHandler.java (85%) rename {markwon/src/main/java/ru/noties/markwon/renderer/html2 => markwon-html/src/main/java/ru/noties/markwon/html/impl}/tag/ImageHandler.java (92%) rename {markwon/src/main/java/ru/noties/markwon/renderer/html2 => markwon-html/src/main/java/ru/noties/markwon/html/impl}/tag/ImageSizeParserImpl.java (94%) rename {markwon/src/main/java/ru/noties/markwon/renderer/html2 => markwon-html/src/main/java/ru/noties/markwon/html/impl}/tag/LinkHandler.java (89%) rename {markwon/src/main/java/ru/noties/markwon/renderer/html2 => markwon-html/src/main/java/ru/noties/markwon/html/impl}/tag/ListHandler.java (93%) rename {markwon/src/main/java/ru/noties/markwon/renderer/html2 => markwon-html/src/main/java/ru/noties/markwon/html/impl}/tag/SimpleTagHandler.java (84%) rename {markwon/src/main/java/ru/noties/markwon/renderer/html2 => markwon-html/src/main/java/ru/noties/markwon/html/impl}/tag/StrikeHandler.java (84%) rename {markwon/src/main/java/ru/noties/markwon/renderer/html2 => markwon-html/src/main/java/ru/noties/markwon/html/impl}/tag/StrongEmphasisHandler.java (81%) rename {markwon/src/main/java/ru/noties/markwon/renderer/html2 => markwon-html/src/main/java/ru/noties/markwon/html/impl}/tag/SubScriptHandler.java (65%) rename {markwon/src/main/java/ru/noties/markwon/renderer/html2 => markwon-html/src/main/java/ru/noties/markwon/html/impl}/tag/SuperScriptHandler.java (65%) rename {markwon/src/main/java/ru/noties/markwon/renderer/html2 => markwon-html/src/main/java/ru/noties/markwon/html/impl}/tag/UnderlineHandler.java (78%) create mode 100644 markwon-html/src/test/java/ru/noties/markwon/html/impl/CssInlineStyleParserTest.java rename {markwon-html-parser-impl => markwon-html}/src/test/java/ru/noties/markwon/html/impl/HtmlEmptyTagReplacementTest.java (100%) rename {markwon-html-parser-impl => markwon-html}/src/test/java/ru/noties/markwon/html/impl/MarkwonHtmlParserImplTest.java (100%) rename {markwon-html-parser-impl => markwon-html}/src/test/java/ru/noties/markwon/html/impl/TrimmingAppenderTest.java (100%) rename {markwon-html-parser-impl => markwon-html}/src/test/java/ru/noties/markwon/html/impl/jsoup/nodes/CommonMarkEntitiesTest.java (100%) create mode 100644 markwon-html/src/test/java/ru/noties/markwon/html/impl/tag/ImageSizeParserImplTest.java delete mode 100644 markwon-image-loader/build.gradle delete mode 100644 markwon-image-loader/gradle.properties delete mode 100644 markwon-image-loader/src/main/AndroidManifest.xml delete mode 100644 markwon-image-loader/src/main/java/ru/noties/markwon/il/AsyncDrawableLoader.java delete mode 100644 markwon-image-loader/src/main/java/ru/noties/markwon/il/DataUri.java delete mode 100644 markwon-image-loader/src/main/java/ru/noties/markwon/il/DataUriDecoder.java delete mode 100644 markwon-image-loader/src/main/java/ru/noties/markwon/il/DataUriParser.java delete mode 100644 markwon-image-loader/src/main/java/ru/noties/markwon/il/DataUriSchemeHandler.java delete mode 100644 markwon-image-loader/src/main/java/ru/noties/markwon/il/DrawableUtils.java delete mode 100644 markwon-image-loader/src/main/java/ru/noties/markwon/il/FileSchemeHandler.java delete mode 100644 markwon-image-loader/src/main/java/ru/noties/markwon/il/GifMediaDecoder.java delete mode 100644 markwon-image-loader/src/main/java/ru/noties/markwon/il/ImageItem.java delete mode 100644 markwon-image-loader/src/main/java/ru/noties/markwon/il/ImageMediaDecoder.java delete mode 100644 markwon-image-loader/src/main/java/ru/noties/markwon/il/MediaDecoder.java delete mode 100644 markwon-image-loader/src/main/java/ru/noties/markwon/il/NetworkSchemeHandler.java delete mode 100644 markwon-image-loader/src/main/java/ru/noties/markwon/il/SchemeHandler.java delete mode 100644 markwon-image-loader/src/main/java/ru/noties/markwon/il/SvgMediaDecoder.java delete mode 100644 markwon-image-loader/src/test/java/ru/noties/markwon/il/DataUriParserTest.java delete mode 100644 markwon-image-loader/src/test/java/ru/noties/markwon/il/DataUriSchemeHandlerTest.java rename {markwon-html-parser-api/src/main/java/ru/noties/markwon/html/api => markwon/src/main/java/ru/noties/markwon/html}/HtmlTag.java (98%) rename {markwon-html-parser-api/src/main/java/ru/noties/markwon/html/api => markwon/src/main/java/ru/noties/markwon/html}/MarkwonHtmlParser.java (92%) rename {markwon-html-parser-api/src/main/java/ru/noties/markwon/html/api => markwon/src/main/java/ru/noties/markwon/html}/MarkwonHtmlParserNoOp.java (95%) create mode 100644 markwon/src/main/java/ru/noties/markwon/html/MarkwonHtmlRenderer.java create mode 100644 markwon/src/main/java/ru/noties/markwon/html/MarkwonHtmlRendererNoOp.java rename markwon/src/main/java/ru/noties/markwon/{renderer/html2/tag => html}/TagHandler.java (91%) delete mode 100644 markwon/src/main/java/ru/noties/markwon/renderer/html2/MarkwonHtmlRenderer.java delete mode 100644 markwon/src/main/java/ru/noties/markwon/renderer/html2/MarkwonHtmlRendererImpl.java diff --git a/app/src/main/java/ru/noties/markwon/MarkdownRenderer.java b/app/src/main/java/ru/noties/markwon/MarkdownRenderer.java index 053fde93..fc6af948 100644 --- a/app/src/main/java/ru/noties/markwon/MarkdownRenderer.java +++ b/app/src/main/java/ru/noties/markwon/MarkdownRenderer.java @@ -77,10 +77,6 @@ public class MarkdownRenderer { ? prism4jThemeDefault : prism4JThemeDarkula; -// final int background = isLightTheme -// ? prism4jTheme.background() -// : 0x0Fffffff; - final Markwon2 markwon2 = Markwon2.builder(context) .use(CorePlugin.create()) .use(ImagesPlugin.createWithAssets(context)) diff --git a/markwon-html-parser-api/build.gradle b/markwon-html-parser-api/build.gradle deleted file mode 100644 index 8e38acbb..00000000 --- a/markwon-html-parser-api/build.gradle +++ /dev/null @@ -1,23 +0,0 @@ -apply plugin: 'com.android.library' - -android { - - compileSdkVersion config['compile-sdk'] - buildToolsVersion config['build-tools'] - - defaultConfig { - minSdkVersion config['min-sdk'] - targetSdkVersion config['target-sdk'] - versionCode 1 - versionName version - } -} - -dependencies { - - deps.with { - api it['support-annotations'] - } -} - -registerArtifact(this) diff --git a/markwon-html-parser-api/gradle.properties b/markwon-html-parser-api/gradle.properties deleted file mode 100644 index 5be4658d..00000000 --- a/markwon-html-parser-api/gradle.properties +++ /dev/null @@ -1,3 +0,0 @@ -POM_NAME=Markwon -POM_ARTIFACT_ID=markwon-html-parser-api -POM_PACKAGING=aar \ No newline at end of file diff --git a/markwon-html-parser-api/src/main/AndroidManifest.xml b/markwon-html-parser-api/src/main/AndroidManifest.xml deleted file mode 100644 index 872543b3..00000000 --- a/markwon-html-parser-api/src/main/AndroidManifest.xml +++ /dev/null @@ -1 +0,0 @@ - diff --git a/markwon-html-parser-impl/build.gradle b/markwon-html/build.gradle similarity index 88% rename from markwon-html-parser-impl/build.gradle rename to markwon-html/build.gradle index 2af46373..b61b12de 100644 --- a/markwon-html-parser-impl/build.gradle +++ b/markwon-html/build.gradle @@ -15,7 +15,7 @@ android { dependencies { - api project(':markwon-html-parser-api') + api project(':markwon') deps.with { api it['support-annotations'] @@ -25,6 +25,7 @@ dependencies { deps.test.with { testImplementation it['junit'] testImplementation it['robolectric'] + testImplementation it['ix-java'] } } diff --git a/markwon-html-parser-impl/gradle.properties b/markwon-html/gradle.properties similarity index 100% rename from markwon-html-parser-impl/gradle.properties rename to markwon-html/gradle.properties diff --git a/markwon-html-parser-impl/src/main/AndroidManifest.xml b/markwon-html/src/main/AndroidManifest.xml similarity index 100% rename from markwon-html-parser-impl/src/main/AndroidManifest.xml rename to markwon-html/src/main/AndroidManifest.xml diff --git a/markwon-html-parser-impl/src/main/java/ru/noties/markwon/html/impl/AppendableUtils.java b/markwon-html/src/main/java/ru/noties/markwon/html/impl/AppendableUtils.java similarity index 100% rename from markwon-html-parser-impl/src/main/java/ru/noties/markwon/html/impl/AppendableUtils.java rename to markwon-html/src/main/java/ru/noties/markwon/html/impl/AppendableUtils.java diff --git a/markwon/src/main/java/ru/noties/markwon/renderer/html2/CssInlineStyleParser.java b/markwon-html/src/main/java/ru/noties/markwon/html/impl/CssInlineStyleParser.java similarity index 99% rename from markwon/src/main/java/ru/noties/markwon/renderer/html2/CssInlineStyleParser.java rename to markwon-html/src/main/java/ru/noties/markwon/html/impl/CssInlineStyleParser.java index 9670d018..a42fab05 100644 --- a/markwon/src/main/java/ru/noties/markwon/renderer/html2/CssInlineStyleParser.java +++ b/markwon-html/src/main/java/ru/noties/markwon/html/impl/CssInlineStyleParser.java @@ -1,4 +1,4 @@ -package ru.noties.markwon.renderer.html2; +package ru.noties.markwon.html.impl; import android.support.annotation.NonNull; import android.support.annotation.Nullable; diff --git a/markwon/src/main/java/ru/noties/markwon/renderer/html2/CssProperty.java b/markwon-html/src/main/java/ru/noties/markwon/html/impl/CssProperty.java similarity index 94% rename from markwon/src/main/java/ru/noties/markwon/renderer/html2/CssProperty.java rename to markwon-html/src/main/java/ru/noties/markwon/html/impl/CssProperty.java index aa490361..405d7c61 100644 --- a/markwon/src/main/java/ru/noties/markwon/renderer/html2/CssProperty.java +++ b/markwon-html/src/main/java/ru/noties/markwon/html/impl/CssProperty.java @@ -1,4 +1,4 @@ -package ru.noties.markwon.renderer.html2; +package ru.noties.markwon.html.impl; import android.support.annotation.NonNull; diff --git a/markwon-html-parser-impl/src/main/java/ru/noties/markwon/html/impl/HtmlEmptyTagReplacement.java b/markwon-html/src/main/java/ru/noties/markwon/html/impl/HtmlEmptyTagReplacement.java similarity index 97% rename from markwon-html-parser-impl/src/main/java/ru/noties/markwon/html/impl/HtmlEmptyTagReplacement.java rename to markwon-html/src/main/java/ru/noties/markwon/html/impl/HtmlEmptyTagReplacement.java index c0d304dc..6604242b 100644 --- a/markwon-html-parser-impl/src/main/java/ru/noties/markwon/html/impl/HtmlEmptyTagReplacement.java +++ b/markwon-html/src/main/java/ru/noties/markwon/html/impl/HtmlEmptyTagReplacement.java @@ -3,7 +3,7 @@ package ru.noties.markwon.html.impl; import android.support.annotation.NonNull; import android.support.annotation.Nullable; -import ru.noties.markwon.html.api.HtmlTag; +import ru.noties.markwon.html.HtmlTag; /** * This class will be used to append some text to output in order to diff --git a/markwon-html-parser-impl/src/main/java/ru/noties/markwon/html/impl/HtmlTagImpl.java b/markwon-html/src/main/java/ru/noties/markwon/html/impl/HtmlTagImpl.java similarity index 99% rename from markwon-html-parser-impl/src/main/java/ru/noties/markwon/html/impl/HtmlTagImpl.java rename to markwon-html/src/main/java/ru/noties/markwon/html/impl/HtmlTagImpl.java index 01466106..8cedc767 100644 --- a/markwon-html-parser-impl/src/main/java/ru/noties/markwon/html/impl/HtmlTagImpl.java +++ b/markwon-html/src/main/java/ru/noties/markwon/html/impl/HtmlTagImpl.java @@ -7,7 +7,7 @@ import java.util.Collections; import java.util.List; import java.util.Map; -import ru.noties.markwon.html.api.HtmlTag; +import ru.noties.markwon.html.HtmlTag; abstract class HtmlTagImpl implements HtmlTag { diff --git a/markwon-html-parser-impl/src/main/java/ru/noties/markwon/html/impl/MarkwonHtmlParserImpl.java b/markwon-html/src/main/java/ru/noties/markwon/html/impl/MarkwonHtmlParserImpl.java similarity index 98% rename from markwon-html-parser-impl/src/main/java/ru/noties/markwon/html/impl/MarkwonHtmlParserImpl.java rename to markwon-html/src/main/java/ru/noties/markwon/html/impl/MarkwonHtmlParserImpl.java index 86f985a2..1bd639c6 100644 --- a/markwon-html-parser-impl/src/main/java/ru/noties/markwon/html/impl/MarkwonHtmlParserImpl.java +++ b/markwon-html/src/main/java/ru/noties/markwon/html/impl/MarkwonHtmlParserImpl.java @@ -14,10 +14,10 @@ import java.util.Locale; import java.util.Map; import java.util.Set; -import ru.noties.markwon.html.api.HtmlTag; -import ru.noties.markwon.html.api.HtmlTag.Block; -import ru.noties.markwon.html.api.HtmlTag.Inline; -import ru.noties.markwon.html.api.MarkwonHtmlParser; +import ru.noties.markwon.html.HtmlTag; +import ru.noties.markwon.html.HtmlTag.Block; +import ru.noties.markwon.html.HtmlTag.Inline; +import ru.noties.markwon.html.MarkwonHtmlParser; import ru.noties.markwon.html.impl.jsoup.nodes.Attribute; import ru.noties.markwon.html.impl.jsoup.nodes.Attributes; import ru.noties.markwon.html.impl.jsoup.parser.CharacterReader; diff --git a/markwon-html/src/main/java/ru/noties/markwon/html/impl/MarkwonHtmlRendererImpl.java b/markwon-html/src/main/java/ru/noties/markwon/html/impl/MarkwonHtmlRendererImpl.java new file mode 100644 index 00000000..6ec8f26b --- /dev/null +++ b/markwon-html/src/main/java/ru/noties/markwon/html/impl/MarkwonHtmlRendererImpl.java @@ -0,0 +1,167 @@ +package ru.noties.markwon.html.impl; + +import android.support.annotation.NonNull; +import android.support.annotation.Nullable; + +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Locale; +import java.util.Map; + +import ru.noties.markwon.MarkwonConfiguration; +import ru.noties.markwon.SpannableBuilder; +import ru.noties.markwon.html.HtmlTag; +import ru.noties.markwon.html.MarkwonHtmlParser; +import ru.noties.markwon.html.MarkwonHtmlRenderer; +import ru.noties.markwon.html.TagHandler; +import ru.noties.markwon.html.impl.tag.BlockquoteHandler; +import ru.noties.markwon.html.impl.tag.EmphasisHandler; +import ru.noties.markwon.html.impl.tag.HeadingHandler; +import ru.noties.markwon.html.impl.tag.ImageHandler; +import ru.noties.markwon.html.impl.tag.LinkHandler; +import ru.noties.markwon.html.impl.tag.ListHandler; +import ru.noties.markwon.html.impl.tag.StrikeHandler; +import ru.noties.markwon.html.impl.tag.StrongEmphasisHandler; +import ru.noties.markwon.html.impl.tag.SubScriptHandler; +import ru.noties.markwon.html.impl.tag.SuperScriptHandler; +import ru.noties.markwon.html.impl.tag.UnderlineHandler; + +public class MarkwonHtmlRendererImpl extends MarkwonHtmlRenderer { + + @NonNull + public static MarkwonHtmlRendererImpl create() { + return builderWithDefaults().build(); + } + + @NonNull + public static Builder builderWithDefaults() { + + final EmphasisHandler emphasisHandler = new EmphasisHandler(); + final StrongEmphasisHandler strongEmphasisHandler = new StrongEmphasisHandler(); + final StrikeHandler strikeHandler = new StrikeHandler(); + final UnderlineHandler underlineHandler = new UnderlineHandler(); + final ListHandler listHandler = new ListHandler(); + + return builder() + .handler("i", emphasisHandler) + .handler("em", emphasisHandler) + .handler("cite", emphasisHandler) + .handler("dfn", emphasisHandler) + .handler("b", strongEmphasisHandler) + .handler("strong", strongEmphasisHandler) + .handler("sup", new SuperScriptHandler()) + .handler("sub", new SubScriptHandler()) + .handler("u", underlineHandler) + .handler("ins", underlineHandler) + .handler("del", strikeHandler) + .handler("s", strikeHandler) + .handler("strike", strikeHandler) + .handler("a", new LinkHandler()) + .handler("ul", listHandler) + .handler("ol", listHandler) + .handler("img", ImageHandler.create()) + .handler("blockquote", new BlockquoteHandler()) + .handler("h1", new HeadingHandler(1)) + .handler("h2", new HeadingHandler(2)) + .handler("h3", new HeadingHandler(3)) + .handler("h4", new HeadingHandler(4)) + .handler("h5", new HeadingHandler(5)) + .handler("h6", new HeadingHandler(6)); + } + + @NonNull + public static Builder builder() { + return new Builder(); + } + + public static final float SCRIPT_DEF_TEXT_SIZE_RATIO = .75F; + + private final Map tagHandlers; + + private MarkwonHtmlRendererImpl(@NonNull Map tagHandlers) { + this.tagHandlers = tagHandlers; + } + + @Override + public void render( + @NonNull final MarkwonConfiguration configuration, + @NonNull final SpannableBuilder builder, + @NonNull MarkwonHtmlParser parser) { + + final int end; + if (!configuration.htmlAllowNonClosedTags()) { + end = HtmlTag.NO_END; + } else { + end = builder.length(); + } + + parser.flushInlineTags(end, new MarkwonHtmlParser.FlushAction() { + @Override + public void apply(@NonNull List tags) { + + TagHandler handler; + + for (HtmlTag.Inline inline : tags) { + + // if tag is not closed -> do not render + if (!inline.isClosed()) { + continue; + } + + handler = tagHandler(inline.name()); + if (handler != null) { + handler.handle(configuration, builder, inline); + } + } + } + }); + + parser.flushBlockTags(end, new MarkwonHtmlParser.FlushAction() { + @Override + public void apply(@NonNull List tags) { + + TagHandler handler; + + for (HtmlTag.Block block : tags) { + + if (!block.isClosed()) { + continue; + } + + handler = tagHandler(block.name()); + if (handler != null) { + handler.handle(configuration, builder, block); + } else { + // see if any of children can be handled + apply(block.children()); + } + } + } + }); + + parser.reset(); + } + + @Nullable + @Override + public TagHandler tagHandler(@NonNull String tagName) { + return tagHandlers.get(tagName); + } + + public static class Builder { + + private final Map tagHandlers = new HashMap<>(2); + + @NonNull + public Builder handler(@NonNull String tagName, @NonNull TagHandler tagHandler) { + tagHandlers.put(tagName.toLowerCase(Locale.US), tagHandler); + return this; + } + + @NonNull + public MarkwonHtmlRendererImpl build() { + return new MarkwonHtmlRendererImpl(Collections.unmodifiableMap(tagHandlers)); + } + } +} diff --git a/markwon-html-parser-impl/src/main/java/ru/noties/markwon/html/impl/TrimmingAppender.java b/markwon-html/src/main/java/ru/noties/markwon/html/impl/TrimmingAppender.java similarity index 100% rename from markwon-html-parser-impl/src/main/java/ru/noties/markwon/html/impl/TrimmingAppender.java rename to markwon-html/src/main/java/ru/noties/markwon/html/impl/TrimmingAppender.java diff --git a/markwon-html-parser-impl/src/main/java/ru/noties/markwon/html/impl/jsoup/UncheckedIOException.java b/markwon-html/src/main/java/ru/noties/markwon/html/impl/jsoup/UncheckedIOException.java similarity index 100% rename from markwon-html-parser-impl/src/main/java/ru/noties/markwon/html/impl/jsoup/UncheckedIOException.java rename to markwon-html/src/main/java/ru/noties/markwon/html/impl/jsoup/UncheckedIOException.java diff --git a/markwon-html-parser-impl/src/main/java/ru/noties/markwon/html/impl/jsoup/helper/Normalizer.java b/markwon-html/src/main/java/ru/noties/markwon/html/impl/jsoup/helper/Normalizer.java similarity index 100% rename from markwon-html-parser-impl/src/main/java/ru/noties/markwon/html/impl/jsoup/helper/Normalizer.java rename to markwon-html/src/main/java/ru/noties/markwon/html/impl/jsoup/helper/Normalizer.java diff --git a/markwon-html-parser-impl/src/main/java/ru/noties/markwon/html/impl/jsoup/helper/Validate.java b/markwon-html/src/main/java/ru/noties/markwon/html/impl/jsoup/helper/Validate.java similarity index 100% rename from markwon-html-parser-impl/src/main/java/ru/noties/markwon/html/impl/jsoup/helper/Validate.java rename to markwon-html/src/main/java/ru/noties/markwon/html/impl/jsoup/helper/Validate.java diff --git a/markwon-html-parser-impl/src/main/java/ru/noties/markwon/html/impl/jsoup/nodes/Attribute.java b/markwon-html/src/main/java/ru/noties/markwon/html/impl/jsoup/nodes/Attribute.java similarity index 100% rename from markwon-html-parser-impl/src/main/java/ru/noties/markwon/html/impl/jsoup/nodes/Attribute.java rename to markwon-html/src/main/java/ru/noties/markwon/html/impl/jsoup/nodes/Attribute.java diff --git a/markwon-html-parser-impl/src/main/java/ru/noties/markwon/html/impl/jsoup/nodes/Attributes.java b/markwon-html/src/main/java/ru/noties/markwon/html/impl/jsoup/nodes/Attributes.java similarity index 100% rename from markwon-html-parser-impl/src/main/java/ru/noties/markwon/html/impl/jsoup/nodes/Attributes.java rename to markwon-html/src/main/java/ru/noties/markwon/html/impl/jsoup/nodes/Attributes.java diff --git a/markwon-html-parser-impl/src/main/java/ru/noties/markwon/html/impl/jsoup/nodes/CommonMarkEntities.java b/markwon-html/src/main/java/ru/noties/markwon/html/impl/jsoup/nodes/CommonMarkEntities.java similarity index 100% rename from markwon-html-parser-impl/src/main/java/ru/noties/markwon/html/impl/jsoup/nodes/CommonMarkEntities.java rename to markwon-html/src/main/java/ru/noties/markwon/html/impl/jsoup/nodes/CommonMarkEntities.java diff --git a/markwon-html-parser-impl/src/main/java/ru/noties/markwon/html/impl/jsoup/nodes/DocumentType.java b/markwon-html/src/main/java/ru/noties/markwon/html/impl/jsoup/nodes/DocumentType.java similarity index 100% rename from markwon-html-parser-impl/src/main/java/ru/noties/markwon/html/impl/jsoup/nodes/DocumentType.java rename to markwon-html/src/main/java/ru/noties/markwon/html/impl/jsoup/nodes/DocumentType.java diff --git a/markwon-html-parser-impl/src/main/java/ru/noties/markwon/html/impl/jsoup/parser/CharacterReader.java b/markwon-html/src/main/java/ru/noties/markwon/html/impl/jsoup/parser/CharacterReader.java similarity index 100% rename from markwon-html-parser-impl/src/main/java/ru/noties/markwon/html/impl/jsoup/parser/CharacterReader.java rename to markwon-html/src/main/java/ru/noties/markwon/html/impl/jsoup/parser/CharacterReader.java diff --git a/markwon-html-parser-impl/src/main/java/ru/noties/markwon/html/impl/jsoup/parser/ParseError.java b/markwon-html/src/main/java/ru/noties/markwon/html/impl/jsoup/parser/ParseError.java similarity index 100% rename from markwon-html-parser-impl/src/main/java/ru/noties/markwon/html/impl/jsoup/parser/ParseError.java rename to markwon-html/src/main/java/ru/noties/markwon/html/impl/jsoup/parser/ParseError.java diff --git a/markwon-html-parser-impl/src/main/java/ru/noties/markwon/html/impl/jsoup/parser/ParseErrorList.java b/markwon-html/src/main/java/ru/noties/markwon/html/impl/jsoup/parser/ParseErrorList.java similarity index 100% rename from markwon-html-parser-impl/src/main/java/ru/noties/markwon/html/impl/jsoup/parser/ParseErrorList.java rename to markwon-html/src/main/java/ru/noties/markwon/html/impl/jsoup/parser/ParseErrorList.java diff --git a/markwon-html-parser-impl/src/main/java/ru/noties/markwon/html/impl/jsoup/parser/Token.java b/markwon-html/src/main/java/ru/noties/markwon/html/impl/jsoup/parser/Token.java similarity index 100% rename from markwon-html-parser-impl/src/main/java/ru/noties/markwon/html/impl/jsoup/parser/Token.java rename to markwon-html/src/main/java/ru/noties/markwon/html/impl/jsoup/parser/Token.java diff --git a/markwon-html-parser-impl/src/main/java/ru/noties/markwon/html/impl/jsoup/parser/Tokeniser.java b/markwon-html/src/main/java/ru/noties/markwon/html/impl/jsoup/parser/Tokeniser.java similarity index 100% rename from markwon-html-parser-impl/src/main/java/ru/noties/markwon/html/impl/jsoup/parser/Tokeniser.java rename to markwon-html/src/main/java/ru/noties/markwon/html/impl/jsoup/parser/Tokeniser.java diff --git a/markwon-html-parser-impl/src/main/java/ru/noties/markwon/html/impl/jsoup/parser/TokeniserState.java b/markwon-html/src/main/java/ru/noties/markwon/html/impl/jsoup/parser/TokeniserState.java similarity index 100% rename from markwon-html-parser-impl/src/main/java/ru/noties/markwon/html/impl/jsoup/parser/TokeniserState.java rename to markwon-html/src/main/java/ru/noties/markwon/html/impl/jsoup/parser/TokeniserState.java diff --git a/markwon-html/src/main/java/ru/noties/markwon/html/impl/span/SubScriptSpan.java b/markwon-html/src/main/java/ru/noties/markwon/html/impl/span/SubScriptSpan.java new file mode 100644 index 00000000..8eca8fdc --- /dev/null +++ b/markwon-html/src/main/java/ru/noties/markwon/html/impl/span/SubScriptSpan.java @@ -0,0 +1,25 @@ +package ru.noties.markwon.html.impl.span; + +import android.support.annotation.NonNull; +import android.text.TextPaint; +import android.text.style.MetricAffectingSpan; + +import ru.noties.markwon.html.impl.MarkwonHtmlRendererImpl; + +public class SubScriptSpan extends MetricAffectingSpan { + + @Override + public void updateDrawState(TextPaint tp) { + apply(tp); + } + + @Override + public void updateMeasureState(@NonNull TextPaint tp) { + apply(tp); + } + + private void apply(TextPaint paint) { + paint.setTextSize(paint.getTextSize() * MarkwonHtmlRendererImpl.SCRIPT_DEF_TEXT_SIZE_RATIO); + paint.baselineShift -= (int) (paint.ascent() / 2); + } +} diff --git a/markwon-html/src/main/java/ru/noties/markwon/html/impl/span/SuperScriptSpan.java b/markwon-html/src/main/java/ru/noties/markwon/html/impl/span/SuperScriptSpan.java new file mode 100644 index 00000000..d33a2bea --- /dev/null +++ b/markwon-html/src/main/java/ru/noties/markwon/html/impl/span/SuperScriptSpan.java @@ -0,0 +1,25 @@ +package ru.noties.markwon.html.impl.span; + +import android.support.annotation.NonNull; +import android.text.TextPaint; +import android.text.style.MetricAffectingSpan; + +import ru.noties.markwon.html.impl.MarkwonHtmlRendererImpl; + +public class SuperScriptSpan extends MetricAffectingSpan { + + @Override + public void updateDrawState(TextPaint tp) { + apply(tp); + } + + @Override + public void updateMeasureState(@NonNull TextPaint tp) { + apply(tp); + } + + private void apply(TextPaint paint) { + paint.setTextSize(paint.getTextSize() * MarkwonHtmlRendererImpl.SCRIPT_DEF_TEXT_SIZE_RATIO); + paint.baselineShift += (int) (paint.ascent() / 2); + } +} diff --git a/markwon/src/main/java/ru/noties/markwon/renderer/html2/tag/BlockquoteHandler.java b/markwon-html/src/main/java/ru/noties/markwon/html/impl/tag/BlockquoteHandler.java similarity index 84% rename from markwon/src/main/java/ru/noties/markwon/renderer/html2/tag/BlockquoteHandler.java rename to markwon-html/src/main/java/ru/noties/markwon/html/impl/tag/BlockquoteHandler.java index 9f90dc93..2687ea94 100644 --- a/markwon/src/main/java/ru/noties/markwon/renderer/html2/tag/BlockquoteHandler.java +++ b/markwon-html/src/main/java/ru/noties/markwon/html/impl/tag/BlockquoteHandler.java @@ -1,10 +1,11 @@ -package ru.noties.markwon.renderer.html2.tag; +package ru.noties.markwon.html.impl.tag; import android.support.annotation.NonNull; -import ru.noties.markwon.SpannableBuilder; import ru.noties.markwon.MarkwonConfiguration; -import ru.noties.markwon.html.api.HtmlTag; +import ru.noties.markwon.SpannableBuilder; +import ru.noties.markwon.html.HtmlTag; +import ru.noties.markwon.html.TagHandler; public class BlockquoteHandler extends TagHandler { diff --git a/markwon/src/main/java/ru/noties/markwon/renderer/html2/tag/EmphasisHandler.java b/markwon-html/src/main/java/ru/noties/markwon/html/impl/tag/EmphasisHandler.java similarity index 80% rename from markwon/src/main/java/ru/noties/markwon/renderer/html2/tag/EmphasisHandler.java rename to markwon-html/src/main/java/ru/noties/markwon/html/impl/tag/EmphasisHandler.java index aa452ebf..0385266a 100644 --- a/markwon/src/main/java/ru/noties/markwon/renderer/html2/tag/EmphasisHandler.java +++ b/markwon-html/src/main/java/ru/noties/markwon/html/impl/tag/EmphasisHandler.java @@ -1,10 +1,10 @@ -package ru.noties.markwon.renderer.html2.tag; +package ru.noties.markwon.html.impl.tag; import android.support.annotation.NonNull; import android.support.annotation.Nullable; import ru.noties.markwon.MarkwonConfiguration; -import ru.noties.markwon.html.api.HtmlTag; +import ru.noties.markwon.html.HtmlTag; public class EmphasisHandler extends SimpleTagHandler { @Nullable diff --git a/markwon/src/main/java/ru/noties/markwon/renderer/html2/tag/HeadingHandler.java b/markwon-html/src/main/java/ru/noties/markwon/html/impl/tag/HeadingHandler.java similarity index 85% rename from markwon/src/main/java/ru/noties/markwon/renderer/html2/tag/HeadingHandler.java rename to markwon-html/src/main/java/ru/noties/markwon/html/impl/tag/HeadingHandler.java index 99626259..d675351e 100644 --- a/markwon/src/main/java/ru/noties/markwon/renderer/html2/tag/HeadingHandler.java +++ b/markwon-html/src/main/java/ru/noties/markwon/html/impl/tag/HeadingHandler.java @@ -1,10 +1,10 @@ -package ru.noties.markwon.renderer.html2.tag; +package ru.noties.markwon.html.impl.tag; import android.support.annotation.NonNull; import android.support.annotation.Nullable; import ru.noties.markwon.MarkwonConfiguration; -import ru.noties.markwon.html.api.HtmlTag; +import ru.noties.markwon.html.HtmlTag; public class HeadingHandler extends SimpleTagHandler { diff --git a/markwon/src/main/java/ru/noties/markwon/renderer/html2/tag/ImageHandler.java b/markwon-html/src/main/java/ru/noties/markwon/html/impl/tag/ImageHandler.java similarity index 92% rename from markwon/src/main/java/ru/noties/markwon/renderer/html2/tag/ImageHandler.java rename to markwon-html/src/main/java/ru/noties/markwon/html/impl/tag/ImageHandler.java index fe41f9d1..adbf745e 100644 --- a/markwon/src/main/java/ru/noties/markwon/renderer/html2/tag/ImageHandler.java +++ b/markwon-html/src/main/java/ru/noties/markwon/html/impl/tag/ImageHandler.java @@ -1,4 +1,4 @@ -package ru.noties.markwon.renderer.html2.tag; +package ru.noties.markwon.html.impl.tag; import android.support.annotation.NonNull; import android.support.annotation.Nullable; @@ -7,9 +7,9 @@ import android.text.TextUtils; import java.util.Map; import ru.noties.markwon.MarkwonConfiguration; -import ru.noties.markwon.html.api.HtmlTag; +import ru.noties.markwon.html.HtmlTag; +import ru.noties.markwon.html.impl.CssInlineStyleParser; import ru.noties.markwon.renderer.ImageSize; -import ru.noties.markwon.renderer.html2.CssInlineStyleParser; public class ImageHandler extends SimpleTagHandler { diff --git a/markwon/src/main/java/ru/noties/markwon/renderer/html2/tag/ImageSizeParserImpl.java b/markwon-html/src/main/java/ru/noties/markwon/html/impl/tag/ImageSizeParserImpl.java similarity index 94% rename from markwon/src/main/java/ru/noties/markwon/renderer/html2/tag/ImageSizeParserImpl.java rename to markwon-html/src/main/java/ru/noties/markwon/html/impl/tag/ImageSizeParserImpl.java index 56ad13c0..04556327 100644 --- a/markwon/src/main/java/ru/noties/markwon/renderer/html2/tag/ImageSizeParserImpl.java +++ b/markwon-html/src/main/java/ru/noties/markwon/html/impl/tag/ImageSizeParserImpl.java @@ -1,4 +1,4 @@ -package ru.noties.markwon.renderer.html2.tag; +package ru.noties.markwon.html.impl.tag; import android.support.annotation.NonNull; import android.support.annotation.Nullable; @@ -7,9 +7,9 @@ import android.text.TextUtils; import java.util.Map; +import ru.noties.markwon.html.impl.CssInlineStyleParser; +import ru.noties.markwon.html.impl.CssProperty; import ru.noties.markwon.renderer.ImageSize; -import ru.noties.markwon.renderer.html2.CssInlineStyleParser; -import ru.noties.markwon.renderer.html2.CssProperty; class ImageSizeParserImpl implements ImageHandler.ImageSizeParser { diff --git a/markwon/src/main/java/ru/noties/markwon/renderer/html2/tag/LinkHandler.java b/markwon-html/src/main/java/ru/noties/markwon/html/impl/tag/LinkHandler.java similarity index 89% rename from markwon/src/main/java/ru/noties/markwon/renderer/html2/tag/LinkHandler.java rename to markwon-html/src/main/java/ru/noties/markwon/html/impl/tag/LinkHandler.java index 74ac3c00..1faba510 100644 --- a/markwon/src/main/java/ru/noties/markwon/renderer/html2/tag/LinkHandler.java +++ b/markwon-html/src/main/java/ru/noties/markwon/html/impl/tag/LinkHandler.java @@ -1,11 +1,11 @@ -package ru.noties.markwon.renderer.html2.tag; +package ru.noties.markwon.html.impl.tag; import android.support.annotation.NonNull; import android.support.annotation.Nullable; import android.text.TextUtils; import ru.noties.markwon.MarkwonConfiguration; -import ru.noties.markwon.html.api.HtmlTag; +import ru.noties.markwon.html.HtmlTag; public class LinkHandler extends SimpleTagHandler { @Nullable diff --git a/markwon/src/main/java/ru/noties/markwon/renderer/html2/tag/ListHandler.java b/markwon-html/src/main/java/ru/noties/markwon/html/impl/tag/ListHandler.java similarity index 93% rename from markwon/src/main/java/ru/noties/markwon/renderer/html2/tag/ListHandler.java rename to markwon-html/src/main/java/ru/noties/markwon/html/impl/tag/ListHandler.java index 671e2297..80094b5c 100644 --- a/markwon/src/main/java/ru/noties/markwon/renderer/html2/tag/ListHandler.java +++ b/markwon-html/src/main/java/ru/noties/markwon/html/impl/tag/ListHandler.java @@ -1,10 +1,11 @@ -package ru.noties.markwon.renderer.html2.tag; +package ru.noties.markwon.html.impl.tag; import android.support.annotation.NonNull; import ru.noties.markwon.MarkwonConfiguration; import ru.noties.markwon.SpannableBuilder; -import ru.noties.markwon.html.api.HtmlTag; +import ru.noties.markwon.html.HtmlTag; +import ru.noties.markwon.html.TagHandler; public class ListHandler extends TagHandler { diff --git a/markwon/src/main/java/ru/noties/markwon/renderer/html2/tag/SimpleTagHandler.java b/markwon-html/src/main/java/ru/noties/markwon/html/impl/tag/SimpleTagHandler.java similarity index 84% rename from markwon/src/main/java/ru/noties/markwon/renderer/html2/tag/SimpleTagHandler.java rename to markwon-html/src/main/java/ru/noties/markwon/html/impl/tag/SimpleTagHandler.java index d6fd93dc..ad68ef30 100644 --- a/markwon/src/main/java/ru/noties/markwon/renderer/html2/tag/SimpleTagHandler.java +++ b/markwon-html/src/main/java/ru/noties/markwon/html/impl/tag/SimpleTagHandler.java @@ -1,11 +1,12 @@ -package ru.noties.markwon.renderer.html2.tag; +package ru.noties.markwon.html.impl.tag; import android.support.annotation.NonNull; import android.support.annotation.Nullable; -import ru.noties.markwon.SpannableBuilder; import ru.noties.markwon.MarkwonConfiguration; -import ru.noties.markwon.html.api.HtmlTag; +import ru.noties.markwon.SpannableBuilder; +import ru.noties.markwon.html.HtmlTag; +import ru.noties.markwon.html.TagHandler; public abstract class SimpleTagHandler extends TagHandler { diff --git a/markwon/src/main/java/ru/noties/markwon/renderer/html2/tag/StrikeHandler.java b/markwon-html/src/main/java/ru/noties/markwon/html/impl/tag/StrikeHandler.java similarity index 84% rename from markwon/src/main/java/ru/noties/markwon/renderer/html2/tag/StrikeHandler.java rename to markwon-html/src/main/java/ru/noties/markwon/html/impl/tag/StrikeHandler.java index 9b8cad2d..4d59b152 100644 --- a/markwon/src/main/java/ru/noties/markwon/renderer/html2/tag/StrikeHandler.java +++ b/markwon-html/src/main/java/ru/noties/markwon/html/impl/tag/StrikeHandler.java @@ -1,10 +1,11 @@ -package ru.noties.markwon.renderer.html2.tag; +package ru.noties.markwon.html.impl.tag; import android.support.annotation.NonNull; import ru.noties.markwon.MarkwonConfiguration; import ru.noties.markwon.SpannableBuilder; -import ru.noties.markwon.html.api.HtmlTag; +import ru.noties.markwon.html.HtmlTag; +import ru.noties.markwon.html.TagHandler; public class StrikeHandler extends TagHandler { diff --git a/markwon/src/main/java/ru/noties/markwon/renderer/html2/tag/StrongEmphasisHandler.java b/markwon-html/src/main/java/ru/noties/markwon/html/impl/tag/StrongEmphasisHandler.java similarity index 81% rename from markwon/src/main/java/ru/noties/markwon/renderer/html2/tag/StrongEmphasisHandler.java rename to markwon-html/src/main/java/ru/noties/markwon/html/impl/tag/StrongEmphasisHandler.java index 7e50bc72..6b5f8230 100644 --- a/markwon/src/main/java/ru/noties/markwon/renderer/html2/tag/StrongEmphasisHandler.java +++ b/markwon-html/src/main/java/ru/noties/markwon/html/impl/tag/StrongEmphasisHandler.java @@ -1,10 +1,10 @@ -package ru.noties.markwon.renderer.html2.tag; +package ru.noties.markwon.html.impl.tag; import android.support.annotation.NonNull; import android.support.annotation.Nullable; import ru.noties.markwon.MarkwonConfiguration; -import ru.noties.markwon.html.api.HtmlTag; +import ru.noties.markwon.html.HtmlTag; public class StrongEmphasisHandler extends SimpleTagHandler { @Nullable diff --git a/markwon/src/main/java/ru/noties/markwon/renderer/html2/tag/SubScriptHandler.java b/markwon-html/src/main/java/ru/noties/markwon/html/impl/tag/SubScriptHandler.java similarity index 65% rename from markwon/src/main/java/ru/noties/markwon/renderer/html2/tag/SubScriptHandler.java rename to markwon-html/src/main/java/ru/noties/markwon/html/impl/tag/SubScriptHandler.java index 145cf261..9adf5684 100644 --- a/markwon/src/main/java/ru/noties/markwon/renderer/html2/tag/SubScriptHandler.java +++ b/markwon-html/src/main/java/ru/noties/markwon/html/impl/tag/SubScriptHandler.java @@ -1,15 +1,16 @@ -package ru.noties.markwon.renderer.html2.tag; +package ru.noties.markwon.html.impl.tag; import android.support.annotation.NonNull; import android.support.annotation.Nullable; import ru.noties.markwon.MarkwonConfiguration; -import ru.noties.markwon.html.api.HtmlTag; +import ru.noties.markwon.html.HtmlTag; +import ru.noties.markwon.html.impl.span.SubScriptSpan; public class SubScriptHandler extends SimpleTagHandler { @Nullable @Override public Object getSpans(@NonNull MarkwonConfiguration configuration, @NonNull HtmlTag tag) { - return configuration.factory().subScript(configuration.theme()); + return new SubScriptSpan(); } } diff --git a/markwon/src/main/java/ru/noties/markwon/renderer/html2/tag/SuperScriptHandler.java b/markwon-html/src/main/java/ru/noties/markwon/html/impl/tag/SuperScriptHandler.java similarity index 65% rename from markwon/src/main/java/ru/noties/markwon/renderer/html2/tag/SuperScriptHandler.java rename to markwon-html/src/main/java/ru/noties/markwon/html/impl/tag/SuperScriptHandler.java index 60da420d..42e00075 100644 --- a/markwon/src/main/java/ru/noties/markwon/renderer/html2/tag/SuperScriptHandler.java +++ b/markwon-html/src/main/java/ru/noties/markwon/html/impl/tag/SuperScriptHandler.java @@ -1,15 +1,16 @@ -package ru.noties.markwon.renderer.html2.tag; +package ru.noties.markwon.html.impl.tag; import android.support.annotation.NonNull; import android.support.annotation.Nullable; import ru.noties.markwon.MarkwonConfiguration; -import ru.noties.markwon.html.api.HtmlTag; +import ru.noties.markwon.html.HtmlTag; +import ru.noties.markwon.html.impl.span.SuperScriptSpan; public class SuperScriptHandler extends SimpleTagHandler { @Nullable @Override public Object getSpans(@NonNull MarkwonConfiguration configuration, @NonNull HtmlTag tag) { - return configuration.factory().superScript(configuration.theme()); + return new SuperScriptSpan(); } } diff --git a/markwon/src/main/java/ru/noties/markwon/renderer/html2/tag/UnderlineHandler.java b/markwon-html/src/main/java/ru/noties/markwon/html/impl/tag/UnderlineHandler.java similarity index 78% rename from markwon/src/main/java/ru/noties/markwon/renderer/html2/tag/UnderlineHandler.java rename to markwon-html/src/main/java/ru/noties/markwon/html/impl/tag/UnderlineHandler.java index d9e03e2a..fb492dda 100644 --- a/markwon/src/main/java/ru/noties/markwon/renderer/html2/tag/UnderlineHandler.java +++ b/markwon-html/src/main/java/ru/noties/markwon/html/impl/tag/UnderlineHandler.java @@ -1,10 +1,12 @@ -package ru.noties.markwon.renderer.html2.tag; +package ru.noties.markwon.html.impl.tag; import android.support.annotation.NonNull; +import android.text.style.UnderlineSpan; -import ru.noties.markwon.SpannableBuilder; import ru.noties.markwon.MarkwonConfiguration; -import ru.noties.markwon.html.api.HtmlTag; +import ru.noties.markwon.SpannableBuilder; +import ru.noties.markwon.html.HtmlTag; +import ru.noties.markwon.html.TagHandler; public class UnderlineHandler extends TagHandler { @@ -23,7 +25,7 @@ public class UnderlineHandler extends TagHandler { SpannableBuilder.setSpans( builder, - configuration.factory().underline(), + new UnderlineSpan(), tag.start(), tag.end() ); diff --git a/markwon-html/src/test/java/ru/noties/markwon/html/impl/CssInlineStyleParserTest.java b/markwon-html/src/test/java/ru/noties/markwon/html/impl/CssInlineStyleParserTest.java new file mode 100644 index 00000000..36c7d798 --- /dev/null +++ b/markwon-html/src/test/java/ru/noties/markwon/html/impl/CssInlineStyleParserTest.java @@ -0,0 +1,239 @@ +package ru.noties.markwon.html.impl; + +import android.support.annotation.NonNull; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.robolectric.RobolectricTestRunner; +import org.robolectric.annotation.Config; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import ix.Ix; +import ix.IxFunction; +import ru.noties.markwon.test.TestUtils; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static ru.noties.markwon.test.TestUtils.with; + +@RunWith(RobolectricTestRunner.class) +@Config(manifest = Config.NONE) +public class CssInlineStyleParserTest { + + private CssInlineStyleParser.Impl impl; + + @Before + public void before() { + impl = new CssInlineStyleParser.Impl(); + } + + @Test + public void simple_single_pair() { + + final String input = "key: value;"; + + final List list = listProperties(input); + + assertEquals(1, list.size()); + + with(list.get(0), new TestUtils.Action() { + @Override + public void apply(@NonNull CssProperty cssProperty) { + assertEquals("key", cssProperty.key()); + assertEquals("value", cssProperty.value()); + } + }); + } + + @Test + public void simple_two_pairs() { + + final String input = "key1: value1; key2: value2;"; + + final List list = listProperties(input); + + assertEquals(2, list.size()); + + with(list.get(0), new TestUtils.Action() { + @Override + public void apply(@NonNull CssProperty cssProperty) { + assertEquals("key1", cssProperty.key()); + assertEquals("value1", cssProperty.value()); + } + }); + + with(list.get(1), new TestUtils.Action() { + @Override + public void apply(@NonNull CssProperty cssProperty) { + assertEquals("key2", cssProperty.key()); + assertEquals("value2", cssProperty.value()); + } + }); + } + + @Test + public void one_pair_eof() { + + final String input = "key: value"; + final List list = listProperties(input); + assertEquals(1, list.size()); + + with(list.get(0), new TestUtils.Action() { + @Override + public void apply(@NonNull CssProperty cssProperty) { + assertEquals("key", cssProperty.key()); + assertEquals("value", cssProperty.value()); + } + }); + } + + @Test + public void one_pair_eof_whitespaces() { + + final String input = "key: value \n\n\t"; + final List list = listProperties(input); + assertEquals(1, list.size()); + + with(list.get(0), new TestUtils.Action() { + @Override + public void apply(@NonNull CssProperty cssProperty) { + assertEquals("key", cssProperty.key()); + assertEquals("value", cssProperty.value()); + } + }); + } + + @Test + public void white_spaces() { + + final String input = "\n\n\n\t \t key1 \n\n\n\t : \n\n\n\n \t value1 \n\n\n\n ; \n key2\n : \n value2 \n ; "; + final List list = listProperties(input); + assertEquals(2, list.size()); + + with(list.get(0), new TestUtils.Action() { + @Override + public void apply(@NonNull CssProperty cssProperty) { + assertEquals("key1", cssProperty.key()); + assertEquals("value1", cssProperty.value()); + } + }); + + with(list.get(1), new TestUtils.Action() { + @Override + public void apply(@NonNull CssProperty cssProperty) { + assertEquals("key2", cssProperty.key()); + assertEquals("value2", cssProperty.value()); + } + }); + } + + @Test + public void list_of_keys() { + + final String input = "key1 key2 key3 key4"; + final List list = listProperties(input); + + assertEquals(0, list.size()); + } + + @Test + public void list_of_keys_and_value() { + + final String input = "key1 key2 key3 key4: value4"; + final List list = listProperties(input); + assertEquals(1, list.size()); + + with(list.get(0), new TestUtils.Action() { + @Override + public void apply(@NonNull CssProperty cssProperty) { + assertEquals("key4", cssProperty.key()); + assertEquals("value4", cssProperty.value()); + } + }); + } + + @Test + public void list_of_keys_separated_by_semi_colon() { + + final String input = "key1;key2;key3;key4;"; + final List list = listProperties(input); + assertEquals(0, list.size()); + } + + @Test + public void key_value_with_invalid_between() { + + final String input = "key1: value1; key2 key3: value3;"; + final List list = listProperties(input); + + assertEquals(2, list.size()); + + with(list.get(0), new TestUtils.Action() { + @Override + public void apply(@NonNull CssProperty cssProperty) { + assertEquals("key1", cssProperty.key()); + assertEquals("value1", cssProperty.value()); + } + }); + + with(list.get(1), new TestUtils.Action() { + @Override + public void apply(@NonNull CssProperty cssProperty) { + assertEquals("key3", cssProperty.key()); + assertEquals("value3", cssProperty.value()); + } + }); + } + + @Test + public void css_functions() { + + final Map map = new HashMap() {{ + put("attr", "\" (\" attr(href) \")\""); + put("calc", "calc(100% - 100px)"); + put("cubic-bezier", "cubic-bezier(0.1, 0.7, 1.0, 0.1)"); + put("hsl", "hsl(120,100%,50%)"); + put("hsla", "hsla(120,100%,50%,0.3)"); + put("linear-gradient", "linear-gradient(red, yellow, blue)"); + put("radial-gradient", "radial-gradient(red, green, blue)"); + put("repeating-linear-gradient", "repeating-linear-gradient(red, yellow 10%, green 20%)"); + put("repeating-radial-gradient", "repeating-radial-gradient(red, yellow 10%, green 15%)"); + put("rgb", "rgb(255,0,0)"); + put("rgba", "rgba(255,0,0,0.3)"); + put("var", "var(--some-variable)"); + put("url", "url(\"url.gif\")"); + }}; + + final StringBuilder builder = new StringBuilder(); + for (Map.Entry entry: map.entrySet()) { + builder.append(entry.getKey()) + .append(':') + .append(entry.getValue()) + .append(';'); + } + + for (CssProperty cssProperty: impl.parse(builder.toString())) { + final String value = map.remove(cssProperty.key()); + assertNotNull(cssProperty.key(), value); + assertEquals(cssProperty.key(), value, cssProperty.value()); + } + + assertEquals(0, map.size()); + } + + @NonNull + private List listProperties(@NonNull String input) { + return Ix.from(impl.parse(input)) + .map(new IxFunction() { + @Override + public CssProperty apply(CssProperty cssProperty) { + return cssProperty.mutate(); + } + }) + .toList(); + } +} \ No newline at end of file diff --git a/markwon-html-parser-impl/src/test/java/ru/noties/markwon/html/impl/HtmlEmptyTagReplacementTest.java b/markwon-html/src/test/java/ru/noties/markwon/html/impl/HtmlEmptyTagReplacementTest.java similarity index 100% rename from markwon-html-parser-impl/src/test/java/ru/noties/markwon/html/impl/HtmlEmptyTagReplacementTest.java rename to markwon-html/src/test/java/ru/noties/markwon/html/impl/HtmlEmptyTagReplacementTest.java diff --git a/markwon-html-parser-impl/src/test/java/ru/noties/markwon/html/impl/MarkwonHtmlParserImplTest.java b/markwon-html/src/test/java/ru/noties/markwon/html/impl/MarkwonHtmlParserImplTest.java similarity index 100% rename from markwon-html-parser-impl/src/test/java/ru/noties/markwon/html/impl/MarkwonHtmlParserImplTest.java rename to markwon-html/src/test/java/ru/noties/markwon/html/impl/MarkwonHtmlParserImplTest.java diff --git a/markwon-html-parser-impl/src/test/java/ru/noties/markwon/html/impl/TrimmingAppenderTest.java b/markwon-html/src/test/java/ru/noties/markwon/html/impl/TrimmingAppenderTest.java similarity index 100% rename from markwon-html-parser-impl/src/test/java/ru/noties/markwon/html/impl/TrimmingAppenderTest.java rename to markwon-html/src/test/java/ru/noties/markwon/html/impl/TrimmingAppenderTest.java diff --git a/markwon-html-parser-impl/src/test/java/ru/noties/markwon/html/impl/jsoup/nodes/CommonMarkEntitiesTest.java b/markwon-html/src/test/java/ru/noties/markwon/html/impl/jsoup/nodes/CommonMarkEntitiesTest.java similarity index 100% rename from markwon-html-parser-impl/src/test/java/ru/noties/markwon/html/impl/jsoup/nodes/CommonMarkEntitiesTest.java rename to markwon-html/src/test/java/ru/noties/markwon/html/impl/jsoup/nodes/CommonMarkEntitiesTest.java diff --git a/markwon-html/src/test/java/ru/noties/markwon/html/impl/tag/ImageSizeParserImplTest.java b/markwon-html/src/test/java/ru/noties/markwon/html/impl/tag/ImageSizeParserImplTest.java new file mode 100644 index 00000000..006427e8 --- /dev/null +++ b/markwon-html/src/test/java/ru/noties/markwon/html/impl/tag/ImageSizeParserImplTest.java @@ -0,0 +1,186 @@ +package ru.noties.markwon.html.impl.tag; + +import android.support.annotation.NonNull; +import android.support.annotation.Nullable; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.robolectric.RobolectricTestRunner; +import org.robolectric.annotation.Config; + +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; + +import ru.noties.markwon.renderer.ImageSize; +import ru.noties.markwon.renderer.html2.CssInlineStyleParser; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; + +@RunWith(RobolectricTestRunner.class) +@Config(manifest = Config.NONE) +public class ImageSizeParserImplTest { + + private static final float DELTA = 1e-7F; + + private ImageSizeParserImpl impl; + + @Before + public void before() { + impl = new ImageSizeParserImpl(CssInlineStyleParser.create()); + } + + @Test + public void nothing() { + assertNull(impl.parse(Collections.emptyMap())); + } + + @Test + public void width_height_from_style() { + + final String style = "width: 123; height: 321"; + + assertImageSize( + new ImageSize(dimension(123, null), dimension(321, null)), + impl.parse(Collections.singletonMap("style", style)) + ); + } + + @Test + public void style_has_higher_priority_width() { + + // if property is found in styles, do not lookup raw attribute + final Map attributes = new HashMap() {{ + put("style", "width: 43"); + put("width", "991"); + }}; + + assertImageSize( + new ImageSize(dimension(43, null), null), + impl.parse(attributes) + ); + } + + @Test + public void style_has_higher_priority_height() { + + // if property is found in styles, do not lookup raw attribute + final Map attributes = new HashMap() {{ + put("style", "height: 177"); + put("height", "8"); + }}; + + assertImageSize( + new ImageSize(null, dimension(177, null)), + impl.parse(attributes) + ); + } + + @Test + public void width_style_height_attributes() { + + final Map attributes = new HashMap() {{ + put("style", "width: 99"); + put("height", "7"); + }}; + + assertImageSize( + new ImageSize(dimension(99, null), dimension(7, null)), + impl.parse(attributes) + ); + } + + @Test + public void height_style_width_attributes() { + + final Map attributes = new HashMap() {{ + put("style", "height: 15"); + put("width", "88"); + }}; + + assertImageSize( + new ImageSize(dimension(88, null), dimension(15, null)), + impl.parse(attributes) + ); + } + + @Test + public void non_empty_styles_width_height_attributes() { + + final Map attributes = new HashMap() {{ + put("style", "key1: value1; width0: 123; height0: 99"); + put("width", "40"); + put("height", "77"); + }}; + + assertImageSize( + new ImageSize(dimension(40, null), dimension(77, null)), + impl.parse(attributes) + ); + } + + @Test + public void dimension_units() { + + final Map map = new HashMap() {{ + put("100", dimension(100, null)); + put("100%", dimension(100, "%")); + put("1%", dimension(1, "%")); + put("0.2em", dimension(0.2F, "em")); + put("155px", dimension(155, "px")); + put("67blah", dimension(67, "blah")); + put("-1", dimension(-1, null)); + put("-0.01pt", dimension(-0.01F, "pt")); + }}; + + for (Map.Entry entry : map.entrySet()) { + assertDimension(entry.getKey(), entry.getValue(), impl.dimension(entry.getKey())); + } + } + + @Test + public void bad_dimension() { + + final String[] dimensions = { + "calc(5px + 10rem)", + "whataver6", + "165 165", + "!@#$%^&*(%" + }; + + for (String dimension : dimensions) { + assertNull(dimension, impl.dimension(dimension)); + } + } + + private static void assertImageSize(@Nullable ImageSize expected, @Nullable ImageSize actual) { + if (expected == null) { + assertNull(actual); + } else { + assertNotNull(actual); + assertDimension("width", expected.width, actual.width); + assertDimension("height", expected.height, actual.height); + } + } + + private static void assertDimension( + @NonNull String name, + @Nullable ImageSize.Dimension expected, + @Nullable ImageSize.Dimension actual) { + if (expected == null) { + assertNull(name, actual); + } else { + assertNotNull(name, actual); + assertEquals(name, expected.value, actual.value, DELTA); + assertEquals(name, expected.unit, actual.unit); + } + } + + @NonNull + private static ImageSize.Dimension dimension(float value, @Nullable String unit) { + return new ImageSize.Dimension(value, unit); + } +} \ No newline at end of file diff --git a/markwon-image-loader/build.gradle b/markwon-image-loader/build.gradle deleted file mode 100644 index fd4293c8..00000000 --- a/markwon-image-loader/build.gradle +++ /dev/null @@ -1,37 +0,0 @@ -apply plugin: 'com.android.library' - -android { - - compileSdkVersion config['compile-sdk'] - buildToolsVersion config['build-tools'] - - defaultConfig { - minSdkVersion config['min-sdk'] - targetSdkVersion config['target-sdk'] - versionCode 1 - versionName version - } - - lintOptions { - // okio.... - disable 'InvalidPackage' - } -} - -dependencies { - - api project(':markwon') - - deps.with { - api it['android-svg'] - api it['android-gif'] - api it['okhttp'] - } - - deps['test'].with { - testImplementation it['junit'] - testImplementation it['robolectric'] - } -} - -registerArtifact(this) diff --git a/markwon-image-loader/gradle.properties b/markwon-image-loader/gradle.properties deleted file mode 100644 index 4dbec709..00000000 --- a/markwon-image-loader/gradle.properties +++ /dev/null @@ -1,3 +0,0 @@ -POM_NAME=Markwon-Image-Loader -POM_ARTIFACT_ID=markwon-image-loader -POM_PACKAGING=aar \ No newline at end of file diff --git a/markwon-image-loader/src/main/AndroidManifest.xml b/markwon-image-loader/src/main/AndroidManifest.xml deleted file mode 100644 index 35da8e8a..00000000 --- a/markwon-image-loader/src/main/AndroidManifest.xml +++ /dev/null @@ -1 +0,0 @@ - diff --git a/markwon-image-loader/src/main/java/ru/noties/markwon/il/AsyncDrawableLoader.java b/markwon-image-loader/src/main/java/ru/noties/markwon/il/AsyncDrawableLoader.java deleted file mode 100644 index 5f7a5f01..00000000 --- a/markwon-image-loader/src/main/java/ru/noties/markwon/il/AsyncDrawableLoader.java +++ /dev/null @@ -1,405 +0,0 @@ -package ru.noties.markwon.il; - -import android.content.res.Resources; -import android.graphics.drawable.Drawable; -import android.net.Uri; -import android.os.Handler; -import android.os.Looper; -import android.support.annotation.NonNull; -import android.support.annotation.Nullable; - -import java.io.IOException; -import java.io.InputStream; -import java.lang.ref.WeakReference; -import java.util.ArrayList; -import java.util.HashMap; -import java.util.List; -import java.util.Map; -import java.util.concurrent.ExecutorService; -import java.util.concurrent.Executors; -import java.util.concurrent.Future; - -import okhttp3.OkHttpClient; -import ru.noties.markwon.image.AsyncDrawable; - -public class AsyncDrawableLoader implements AsyncDrawable.Loader { - - @NonNull - public static AsyncDrawableLoader create() { - return builder().build(); - } - - @NonNull - public static AsyncDrawableLoader.Builder builder() { - return new Builder(); - } - - private final ExecutorService executorService; - private final Handler mainThread; - private final Drawable errorDrawable; - private final Map schemeHandlers; - private final List mediaDecoders; - - private final Map> requests; - - AsyncDrawableLoader(Builder builder) { - this.executorService = builder.executorService; - this.mainThread = new Handler(Looper.getMainLooper()); - this.errorDrawable = builder.errorDrawable; - this.schemeHandlers = builder.schemeHandlers; - this.mediaDecoders = builder.mediaDecoders; - this.requests = new HashMap<>(3); - } - - - @Override - public void load(@NonNull String destination, @NonNull AsyncDrawable drawable) { - // if drawable is not a link -> show loading placeholder... - requests.put(destination, execute(destination, drawable)); - } - - @Override - public void cancel(@NonNull String destination) { - - final Future request = requests.remove(destination); - if (request != null) { - request.cancel(true); - } - - for (SchemeHandler schemeHandler : schemeHandlers.values()) { - schemeHandler.cancel(destination); - } - } - - private Future execute(@NonNull final String destination, @NonNull AsyncDrawable drawable) { - - final WeakReference reference = new WeakReference(drawable); - - // todo: should we cancel pending request for the same destination? - // we _could_ but there is possibility that one resource is request in multiple places - - // todo: error handing (simply applying errorDrawable is not a good solution - // as reason for an error is unclear (no scheme handler, no input data, error decoding, etc) - - // todo: more efficient ImageMediaDecoder... BitmapFactory.decodeStream is a bit not optimal - // for big images for sure. We _could_ introduce internal Drawable that will check for - // image bounds (but we will need to cache inputStream in order to inspect and optimize - // input image...) - - return executorService.submit(new Runnable() { - @Override - public void run() { - - final ImageItem item; - - final Uri uri = Uri.parse(destination); - - final SchemeHandler schemeHandler = schemeHandlers.get(uri.getScheme()); - if (schemeHandler != null) { - item = schemeHandler.handle(destination, uri); - } else { - item = null; - } - - final InputStream inputStream = item != null - ? item.inputStream() - : null; - - Drawable result = null; - - if (inputStream != null) { - try { - - final String fileName = item.fileName(); - final MediaDecoder mediaDecoder = fileName != null - ? mediaDecoderFromFile(fileName) - : mediaDecoderFromContentType(item.contentType()); - - if (mediaDecoder != null) { - result = mediaDecoder.decode(inputStream); - } - - } finally { - try { - inputStream.close(); - } catch (IOException e) { - // ignored - } - } - } - - // if result is null, we assume it's an error - if (result == null) { - result = errorDrawable; - } - - if (result != null) { - final Drawable out = result; - mainThread.post(new Runnable() { - @Override - public void run() { - final AsyncDrawable asyncDrawable = reference.get(); - if (asyncDrawable != null && asyncDrawable.isAttached()) { - asyncDrawable.setResult(out); - } - } - }); - } - - requests.remove(destination); - } - }); - } - - @Nullable - private MediaDecoder mediaDecoderFromFile(@NonNull String fileName) { - - MediaDecoder out = null; - - for (MediaDecoder mediaDecoder : mediaDecoders) { - if (mediaDecoder.canDecodeByFileName(fileName)) { - out = mediaDecoder; - break; - } - } - - return out; - } - - @Nullable - private MediaDecoder mediaDecoderFromContentType(@Nullable String contentType) { - - MediaDecoder out = null; - - for (MediaDecoder mediaDecoder : mediaDecoders) { - if (mediaDecoder.canDecodeByContentType(contentType)) { - out = mediaDecoder; - break; - } - } - - return out; - } - - // todo: as now we have different layers of abstraction (for scheme handling and media decoding) - // we no longer should add dependencies implicitly, it would be way better to allow adding - // multiple artifacts (file, data, network, svg, gif)... at least, maybe we can extract API - // for this module (without implementations), but keep _all-in_ (fat) artifact with all of these. - public static class Builder { - - /** - * @deprecated 2.0.0 add {@link NetworkSchemeHandler} directly - */ - @Deprecated - private OkHttpClient client; - - /** - * @deprecated 2.0.0 construct {@link MediaDecoder} and {@link SchemeHandler} appropriately - */ - @Deprecated - private Resources resources; - - private ExecutorService executorService; - private Drawable errorDrawable; - - // @since 1.1.0 - private final List mediaDecoders = new ArrayList<>(3); - - // @since 2.0.0 - private final Map schemeHandlers = new HashMap<>(3); - - /** - * @deprecated 2.0.0 add {@link NetworkSchemeHandler} directly - */ - @NonNull - @Deprecated - public Builder client(@NonNull OkHttpClient client) { - this.client = client; - return this; - } - - /** - * Supplied resources argument will be used to open files from assets directory - * and to create default {@link MediaDecoder}\'s which require resources instance - * - * @return self - */ - @NonNull - public Builder resources(@NonNull Resources resources) { - this.resources = resources; - return this; - } - - @NonNull - public Builder executorService(@NonNull ExecutorService executorService) { - this.executorService = executorService; - return this; - } - - @NonNull - public Builder errorDrawable(@NonNull Drawable errorDrawable) { - this.errorDrawable = errorDrawable; - return this; - } - - /** - * @since 2.0.0 - */ - @SuppressWarnings("UnusedReturnValue") - @NonNull - public Builder addSchemeHandler(@NonNull SchemeHandler schemeHandler) { - - SchemeHandler previous; - - for (String scheme : schemeHandler.schemes()) { - previous = schemeHandlers.put(scheme, schemeHandler); - if (previous != null) { - throw new IllegalStateException(String.format("Multiple scheme handlers handle " + - "the same scheme: `%s`, %s %s", scheme, previous, schemeHandler)); - } - } - - return this; - } - - /** - * @see #addMediaDecoder(MediaDecoder) - * @see #addMediaDecoders(MediaDecoder...) - * @see #addMediaDecoders(Iterable) - * @since 1.1.0 - * @deprecated 2.0.0 - */ - @Deprecated - @NonNull - public Builder mediaDecoders(@NonNull List mediaDecoders) { - - // previously it was clearing before adding - - for (MediaDecoder mediaDecoder : mediaDecoders) { - this.mediaDecoders.add(requireNonNull(mediaDecoder)); - } - - return this; - } - - /** - * @see #addMediaDecoder(MediaDecoder) - * @see #addMediaDecoders(MediaDecoder...) - * @see #addMediaDecoders(Iterable) - * @since 1.1.0 - * @deprecated 2.0.0 - */ - @NonNull - @Deprecated - public Builder mediaDecoders(MediaDecoder... mediaDecoders) { - - // previously it was clearing before adding - - final int length = mediaDecoders != null - ? mediaDecoders.length - : 0; - - if (length > 0) { - for (int i = 0; i < length; i++) { - this.mediaDecoders.add(requireNonNull(mediaDecoders[i])); - } - } - - return this; - } - - /** - * @see SvgMediaDecoder - * @see GifMediaDecoder - * @see ImageMediaDecoder - * @since 2.0.0 - */ - @NonNull - public Builder addMediaDecoder(@NonNull MediaDecoder mediaDecoder) { - mediaDecoders.add(mediaDecoder); - return this; - } - - /** - * @see SvgMediaDecoder - * @see GifMediaDecoder - * @see ImageMediaDecoder - * @since 2.0.0 - */ - @NonNull - public Builder addMediaDecoders(@NonNull Iterable mediaDecoders) { - for (MediaDecoder mediaDecoder : mediaDecoders) { - this.mediaDecoders.add(requireNonNull(mediaDecoder)); - } - return this; - } - - /** - * @see SvgMediaDecoder - * @see GifMediaDecoder - * @see ImageMediaDecoder - * @since 2.0.0 - */ - @NonNull - public Builder addMediaDecoders(MediaDecoder... mediaDecoders) { - - final int length = mediaDecoders != null - ? mediaDecoders.length - : 0; - - if (length > 0) { - for (int i = 0; i < length; i++) { - this.mediaDecoders.add(requireNonNull(mediaDecoders[i])); - } - } - - return this; - } - - @NonNull - public AsyncDrawableLoader build() { - - // I think we should deprecate this... - if (resources == null) { - resources = Resources.getSystem(); - } - - if (executorService == null) { - // @since 2.0.0 we are using newCachedThreadPool instead - // of `okHttpClient.dispatcher().executorService()` - executorService = Executors.newCachedThreadPool(); - } - - // @since 2.0.0 - // put default scheme handlers (to mimic previous behavior) - // remove in 3.0.0 with plugins - if (schemeHandlers.size() == 0) { - if (client == null) { - client = new OkHttpClient(); - } - addSchemeHandler(NetworkSchemeHandler.create(client)); - addSchemeHandler(FileSchemeHandler.createWithAssets(resources.getAssets())); - addSchemeHandler(DataUriSchemeHandler.create()); - } - - // add default media decoders if not specified - // remove in 3.0.0 with plugins - if (mediaDecoders.size() == 0) { - mediaDecoders.add(SvgMediaDecoder.create(resources)); - mediaDecoders.add(GifMediaDecoder.create(true)); - mediaDecoders.add(ImageMediaDecoder.create(resources)); - } - - return new AsyncDrawableLoader(this); - } - } - - // @since 2.0.0 - @NonNull - private static T requireNonNull(@Nullable T t) { - if (t == null) { - throw new NullPointerException(); - } - return t; - } -} diff --git a/markwon-image-loader/src/main/java/ru/noties/markwon/il/DataUri.java b/markwon-image-loader/src/main/java/ru/noties/markwon/il/DataUri.java deleted file mode 100644 index 697b7b2e..00000000 --- a/markwon-image-loader/src/main/java/ru/noties/markwon/il/DataUri.java +++ /dev/null @@ -1,60 +0,0 @@ -package ru.noties.markwon.il; - -import android.support.annotation.Nullable; - -public class DataUri { - - private final String contentType; - private final boolean base64; - private final String data; - - public DataUri(@Nullable String contentType, boolean base64, @Nullable String data) { - this.contentType = contentType; - this.base64 = base64; - this.data = data; - } - - @Nullable - public String contentType() { - return contentType; - } - - public boolean base64() { - return base64; - } - - @Nullable - public String data() { - return data; - } - - @Override - public String toString() { - return "DataUri{" + - "contentType='" + contentType + '\'' + - ", base64=" + base64 + - ", data='" + data + '\'' + - '}'; - } - - @Override - public boolean equals(Object o) { - if (this == o) return true; - if (o == null || getClass() != o.getClass()) return false; - - DataUri dataUri = (DataUri) o; - - if (base64 != dataUri.base64) return false; - if (contentType != null ? !contentType.equals(dataUri.contentType) : dataUri.contentType != null) - return false; - return data != null ? data.equals(dataUri.data) : dataUri.data == null; - } - - @Override - public int hashCode() { - int result = contentType != null ? contentType.hashCode() : 0; - result = 31 * result + (base64 ? 1 : 0); - result = 31 * result + (data != null ? data.hashCode() : 0); - return result; - } -} diff --git a/markwon-image-loader/src/main/java/ru/noties/markwon/il/DataUriDecoder.java b/markwon-image-loader/src/main/java/ru/noties/markwon/il/DataUriDecoder.java deleted file mode 100644 index ffb0d840..00000000 --- a/markwon-image-loader/src/main/java/ru/noties/markwon/il/DataUriDecoder.java +++ /dev/null @@ -1,41 +0,0 @@ -package ru.noties.markwon.il; - -import android.support.annotation.NonNull; -import android.support.annotation.Nullable; -import android.text.TextUtils; -import android.util.Base64; - -public abstract class DataUriDecoder { - - @Nullable - public abstract byte[] decode(@NonNull DataUri dataUri); - - @NonNull - public static DataUriDecoder create() { - return new Impl(); - } - - static class Impl extends DataUriDecoder { - - @Nullable - @Override - public byte[] decode(@NonNull DataUri dataUri) { - - final String data = dataUri.data(); - - if (!TextUtils.isEmpty(data)) { - try { - if (dataUri.base64()) { - return Base64.decode(data.getBytes("UTF-8"), Base64.DEFAULT); - } else { - return data.getBytes("UTF-8"); - } - } catch (Throwable t) { - return null; - } - } else { - return null; - } - } - } -} diff --git a/markwon-image-loader/src/main/java/ru/noties/markwon/il/DataUriParser.java b/markwon-image-loader/src/main/java/ru/noties/markwon/il/DataUriParser.java deleted file mode 100644 index 63744a42..00000000 --- a/markwon-image-loader/src/main/java/ru/noties/markwon/il/DataUriParser.java +++ /dev/null @@ -1,79 +0,0 @@ -package ru.noties.markwon.il; - -import android.support.annotation.NonNull; -import android.support.annotation.Nullable; - -public abstract class DataUriParser { - - @Nullable - public abstract DataUri parse(@NonNull String input); - - - @NonNull - public static DataUriParser create() { - return new Impl(); - } - - static class Impl extends DataUriParser { - - @Nullable - @Override - public DataUri parse(@NonNull String input) { - - final int index = input.indexOf(','); - // we expect exactly one comma - if (index < 0) { - return null; - } - - final String contentType; - final boolean base64; - - if (index > 0) { - final String part = input.substring(0, index); - final String[] parts = part.split(";"); - final int length = parts.length; - if (length > 0) { - // if one: either content-type or base64 - if (length == 1) { - final String value = parts[0]; - if ("base64".equals(value)) { - contentType = null; - base64 = true; - } else { - contentType = value.indexOf('/') > -1 - ? value - : null; - base64 = false; - } - } else { - contentType = parts[0].indexOf('/') > -1 - ? parts[0] - : null; - base64 = "base64".equals(parts[length - 1]); - } - } else { - contentType = null; - base64 = false; - } - } else { - contentType = null; - base64 = false; - } - - final String data; - if (index < input.length()) { - final String value = input.substring(index + 1, input.length()).replaceAll("\n", ""); - if (value.length() == 0) { - data = null; - } else { - data = value; - } - } else { - data = null; - } - - return new DataUri(contentType, base64, data); - } - } -} diff --git a/markwon-image-loader/src/main/java/ru/noties/markwon/il/DataUriSchemeHandler.java b/markwon-image-loader/src/main/java/ru/noties/markwon/il/DataUriSchemeHandler.java deleted file mode 100644 index c70ea863..00000000 --- a/markwon-image-loader/src/main/java/ru/noties/markwon/il/DataUriSchemeHandler.java +++ /dev/null @@ -1,74 +0,0 @@ -package ru.noties.markwon.il; - -import android.net.Uri; -import android.support.annotation.NonNull; -import android.support.annotation.Nullable; - -import java.io.ByteArrayInputStream; -import java.util.Collection; -import java.util.Collections; - -/** - * @since 2.0.0 - */ -public class DataUriSchemeHandler extends SchemeHandler { - - @NonNull - public static DataUriSchemeHandler create() { - return new DataUriSchemeHandler(DataUriParser.create(), DataUriDecoder.create()); - } - - private static final String START = "data:"; - - private final DataUriParser uriParser; - private final DataUriDecoder uriDecoder; - - @SuppressWarnings("WeakerAccess") - DataUriSchemeHandler(@NonNull DataUriParser uriParser, @NonNull DataUriDecoder uriDecoder) { - this.uriParser = uriParser; - this.uriDecoder = uriDecoder; - } - - @Nullable - @Override - public ImageItem handle(@NonNull String raw, @NonNull Uri uri) { - - if (!raw.startsWith(START)) { - return null; - } - - String part = raw.substring(START.length()); - - // this part is added to support `data://` with which this functionality was released - if (part.startsWith("//")) { - part = part.substring(2); - } - - final DataUri dataUri = uriParser.parse(part); - if (dataUri == null) { - return null; - } - - final byte[] bytes = uriDecoder.decode(dataUri); - if (bytes == null) { - return null; - } - - return new ImageItem( - dataUri.contentType(), - new ByteArrayInputStream(bytes), - null - ); - } - - @Override - public void cancel(@NonNull String raw) { - // no op - } - - @NonNull - @Override - public Collection schemes() { - return Collections.singleton("data"); - } -} diff --git a/markwon-image-loader/src/main/java/ru/noties/markwon/il/DrawableUtils.java b/markwon-image-loader/src/main/java/ru/noties/markwon/il/DrawableUtils.java deleted file mode 100644 index f2aef636..00000000 --- a/markwon-image-loader/src/main/java/ru/noties/markwon/il/DrawableUtils.java +++ /dev/null @@ -1,13 +0,0 @@ -package ru.noties.markwon.il; - -import android.graphics.drawable.Drawable; -import android.support.annotation.NonNull; - -abstract class DrawableUtils { - - static void intrinsicBounds(@NonNull Drawable drawable) { - drawable.setBounds(0, 0, drawable.getIntrinsicWidth(), drawable.getIntrinsicHeight()); - } - - private DrawableUtils() {} -} diff --git a/markwon-image-loader/src/main/java/ru/noties/markwon/il/FileSchemeHandler.java b/markwon-image-loader/src/main/java/ru/noties/markwon/il/FileSchemeHandler.java deleted file mode 100644 index 437bbf7a..00000000 --- a/markwon-image-loader/src/main/java/ru/noties/markwon/il/FileSchemeHandler.java +++ /dev/null @@ -1,109 +0,0 @@ -package ru.noties.markwon.il; - -import android.content.res.AssetManager; -import android.net.Uri; -import android.support.annotation.NonNull; -import android.support.annotation.Nullable; - -import java.io.BufferedInputStream; -import java.io.File; -import java.io.FileInputStream; -import java.io.FileNotFoundException; -import java.io.IOException; -import java.io.InputStream; -import java.util.Collection; -import java.util.Collections; -import java.util.List; - -/** - * @since 2.0.0 - */ -public class FileSchemeHandler extends SchemeHandler { - - @NonNull - public static FileSchemeHandler createWithAssets(@NonNull AssetManager assetManager) { - return new FileSchemeHandler(assetManager); - } - - @NonNull - public static FileSchemeHandler create() { - return new FileSchemeHandler(null); - } - - private static final String FILE_ANDROID_ASSETS = "android_asset"; - - @Nullable - private final AssetManager assetManager; - - @SuppressWarnings("WeakerAccess") - FileSchemeHandler(@Nullable AssetManager assetManager) { - this.assetManager = assetManager; - } - - @Nullable - @Override - public ImageItem handle(@NonNull String raw, @NonNull Uri uri) { - - final List segments = uri.getPathSegments(); - if (segments == null - || segments.size() == 0) { - // pointing to file & having no path segments is no use - return null; - } - - final ImageItem out; - - InputStream inputStream = null; - - final boolean assets = FILE_ANDROID_ASSETS.equals(segments.get(0)); - final String fileName = uri.getLastPathSegment(); - - if (assets) { - - // no handling of assets here if we have no assetsManager - if (assetManager != null) { - - final StringBuilder path = new StringBuilder(); - for (int i = 1, size = segments.size(); i < size; i++) { - if (i != 1) { - path.append('/'); - } - path.append(segments.get(i)); - } - // load assets - - try { - inputStream = assetManager.open(path.toString()); - } catch (IOException e) { - e.printStackTrace(); - } - } - - } else { - try { - inputStream = new BufferedInputStream(new FileInputStream(new File(uri.getPath()))); - } catch (FileNotFoundException e) { - e.printStackTrace(); - } - } - - if (inputStream != null) { - out = new ImageItem(fileName, inputStream, fileName); - } else { - out = null; - } - - return out; - } - - @Override - public void cancel(@NonNull String raw) { - // no op - } - - @NonNull - @Override - public Collection schemes() { - return Collections.singleton("file"); - } -} diff --git a/markwon-image-loader/src/main/java/ru/noties/markwon/il/GifMediaDecoder.java b/markwon-image-loader/src/main/java/ru/noties/markwon/il/GifMediaDecoder.java deleted file mode 100644 index 8342e7d5..00000000 --- a/markwon-image-loader/src/main/java/ru/noties/markwon/il/GifMediaDecoder.java +++ /dev/null @@ -1,91 +0,0 @@ -package ru.noties.markwon.il; - -import android.graphics.drawable.Drawable; -import android.support.annotation.NonNull; -import android.support.annotation.Nullable; - -import java.io.ByteArrayOutputStream; -import java.io.IOException; -import java.io.InputStream; - -import pl.droidsonroids.gif.GifDrawable; - -/** - * @since 1.1.0 - */ -@SuppressWarnings("WeakerAccess") -public class GifMediaDecoder extends MediaDecoder { - - protected static final String CONTENT_TYPE_GIF = "image/gif"; - protected static final String FILE_EXTENSION_GIF = ".gif"; - - @NonNull - public static GifMediaDecoder create(boolean autoPlayGif) { - return new GifMediaDecoder(autoPlayGif); - } - - private final boolean autoPlayGif; - - protected GifMediaDecoder(boolean autoPlayGif) { - this.autoPlayGif = autoPlayGif; - } - - @Override - public boolean canDecodeByContentType(@Nullable String contentType) { - return CONTENT_TYPE_GIF.equals(contentType); - } - - @Override - public boolean canDecodeByFileName(@NonNull String fileName) { - return fileName.endsWith(FILE_EXTENSION_GIF); - } - - @Nullable - @Override - public Drawable decode(@NonNull InputStream inputStream) { - - Drawable out = null; - - final byte[] bytes = readBytes(inputStream); - if (bytes != null) { - try { - out = newGifDrawable(bytes); - DrawableUtils.intrinsicBounds(out); - - if (!autoPlayGif) { - ((GifDrawable) out).pause(); - } - } catch (IOException e) { - e.printStackTrace(); - } - } - - return out; - } - - @NonNull - protected Drawable newGifDrawable(@NonNull byte[] bytes) throws IOException { - return new GifDrawable(bytes); - } - - @Nullable - protected static byte[] readBytes(@NonNull InputStream stream) { - - byte[] out = null; - - try { - final ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); - final int length = 1024 * 8; - final byte[] buffer = new byte[length]; - int read; - while ((read = stream.read(buffer, 0, length)) != -1) { - outputStream.write(buffer, 0, read); - } - out = outputStream.toByteArray(); - } catch (IOException e) { - e.printStackTrace(); - } - - return out; - } -} diff --git a/markwon-image-loader/src/main/java/ru/noties/markwon/il/ImageItem.java b/markwon-image-loader/src/main/java/ru/noties/markwon/il/ImageItem.java deleted file mode 100644 index 3ac9e9ec..00000000 --- a/markwon-image-loader/src/main/java/ru/noties/markwon/il/ImageItem.java +++ /dev/null @@ -1,39 +0,0 @@ -package ru.noties.markwon.il; - -import android.support.annotation.Nullable; - -import java.io.InputStream; - -/** - * @since 2.0.0 - */ -public class ImageItem { - - private final String contentType; - private final InputStream inputStream; - private final String fileName; - - public ImageItem( - @Nullable String contentType, - @Nullable InputStream inputStream, - @Nullable String fileName) { - this.contentType = contentType; - this.inputStream = inputStream; - this.fileName = fileName; - } - - @Nullable - public String contentType() { - return contentType; - } - - @Nullable - public InputStream inputStream() { - return inputStream; - } - - @Nullable - public String fileName() { - return fileName; - } -} diff --git a/markwon-image-loader/src/main/java/ru/noties/markwon/il/ImageMediaDecoder.java b/markwon-image-loader/src/main/java/ru/noties/markwon/il/ImageMediaDecoder.java deleted file mode 100644 index c36545ea..00000000 --- a/markwon-image-loader/src/main/java/ru/noties/markwon/il/ImageMediaDecoder.java +++ /dev/null @@ -1,60 +0,0 @@ -package ru.noties.markwon.il; - -import android.content.res.Resources; -import android.graphics.Bitmap; -import android.graphics.BitmapFactory; -import android.graphics.drawable.BitmapDrawable; -import android.graphics.drawable.Drawable; -import android.support.annotation.NonNull; -import android.support.annotation.Nullable; - -import java.io.InputStream; - -/** - * This class can be used as the last {@link MediaDecoder} to _try_ to handle all rest cases. - * Here we just assume that supplied InputStream is of image type and try to decode it. - * - * @since 1.1.0 - */ -public class ImageMediaDecoder extends MediaDecoder { - - @NonNull - public static ImageMediaDecoder create(@NonNull Resources resources) { - return new ImageMediaDecoder(resources); - } - - private final Resources resources; - - @SuppressWarnings("WeakerAccess") - ImageMediaDecoder(Resources resources) { - this.resources = resources; - } - - @Override - public boolean canDecodeByContentType(@Nullable String contentType) { - return true; - } - - @Override - public boolean canDecodeByFileName(@NonNull String fileName) { - return true; - } - - @Nullable - @Override - public Drawable decode(@NonNull InputStream inputStream) { - - final Drawable out; - - // absolutely not optimal... thing - final Bitmap bitmap = BitmapFactory.decodeStream(inputStream); - if (bitmap != null) { - out = new BitmapDrawable(resources, bitmap); - DrawableUtils.intrinsicBounds(out); - } else { - out = null; - } - - return out; - } -} diff --git a/markwon-image-loader/src/main/java/ru/noties/markwon/il/MediaDecoder.java b/markwon-image-loader/src/main/java/ru/noties/markwon/il/MediaDecoder.java deleted file mode 100644 index 294b716b..00000000 --- a/markwon-image-loader/src/main/java/ru/noties/markwon/il/MediaDecoder.java +++ /dev/null @@ -1,20 +0,0 @@ -package ru.noties.markwon.il; - -import android.graphics.drawable.Drawable; -import android.support.annotation.NonNull; -import android.support.annotation.Nullable; - -import java.io.InputStream; - -/** - * @since 1.1.0 - */ -public abstract class MediaDecoder { - - public abstract boolean canDecodeByContentType(@Nullable String contentType); - - public abstract boolean canDecodeByFileName(@NonNull String fileName); - - @Nullable - public abstract Drawable decode(@NonNull InputStream inputStream); -} diff --git a/markwon-image-loader/src/main/java/ru/noties/markwon/il/NetworkSchemeHandler.java b/markwon-image-loader/src/main/java/ru/noties/markwon/il/NetworkSchemeHandler.java deleted file mode 100644 index d87c4019..00000000 --- a/markwon-image-loader/src/main/java/ru/noties/markwon/il/NetworkSchemeHandler.java +++ /dev/null @@ -1,89 +0,0 @@ -package ru.noties.markwon.il; - -import android.net.Uri; -import android.support.annotation.NonNull; -import android.support.annotation.Nullable; - -import java.io.IOException; -import java.io.InputStream; -import java.util.Arrays; -import java.util.Collection; -import java.util.List; - -import okhttp3.Call; -import okhttp3.OkHttpClient; -import okhttp3.Request; -import okhttp3.Response; -import okhttp3.ResponseBody; - -/** - * @since 2.0.0 - */ -public class NetworkSchemeHandler extends SchemeHandler { - - @NonNull - public static NetworkSchemeHandler create(@NonNull OkHttpClient client) { - return new NetworkSchemeHandler(client); - } - - private static final String HEADER_CONTENT_TYPE = "Content-Type"; - - private final OkHttpClient client; - - @SuppressWarnings("WeakerAccess") - NetworkSchemeHandler(@NonNull OkHttpClient client) { - this.client = client; - } - - @Nullable - @Override - public ImageItem handle(@NonNull String raw, @NonNull Uri uri) { - - ImageItem out = null; - - final Request request = new Request.Builder() - .url(raw) - .tag(raw) - .build(); - - Response response = null; - try { - response = client.newCall(request).execute(); - } catch (IOException e) { - e.printStackTrace(); - } - - if (response != null) { - final ResponseBody body = response.body(); - if (body != null) { - final InputStream inputStream = body.byteStream(); - if (inputStream != null) { - final String contentType = response.header(HEADER_CONTENT_TYPE); - out = new ImageItem(contentType, inputStream, null); - } - } - } - - return out; - } - - @Override - public void cancel(@NonNull String raw) { - final List calls = client.dispatcher().queuedCalls(); - if (calls != null) { - for (Call call : calls) { - if (!call.isCanceled()) { - if (raw.equals(call.request().tag())) { - call.cancel(); - } - } - } - } - } - - @NonNull - @Override - public Collection schemes() { - return Arrays.asList("http", "https"); - } -} diff --git a/markwon-image-loader/src/main/java/ru/noties/markwon/il/SchemeHandler.java b/markwon-image-loader/src/main/java/ru/noties/markwon/il/SchemeHandler.java deleted file mode 100644 index 6d8a44d1..00000000 --- a/markwon-image-loader/src/main/java/ru/noties/markwon/il/SchemeHandler.java +++ /dev/null @@ -1,25 +0,0 @@ -package ru.noties.markwon.il; - -import android.net.Uri; -import android.support.annotation.NonNull; -import android.support.annotation.Nullable; - -import java.util.Collection; - -/** - * @since 2.0.0 - */ -public abstract class SchemeHandler { - - @Nullable - public abstract ImageItem handle(@NonNull String raw, @NonNull Uri uri); - - public abstract void cancel(@NonNull String raw); - - /** - * Will be called only once during initialization, should return schemes that are - * handled by this handler - */ - @NonNull - public abstract Collection schemes(); -} diff --git a/markwon-image-loader/src/main/java/ru/noties/markwon/il/SvgMediaDecoder.java b/markwon-image-loader/src/main/java/ru/noties/markwon/il/SvgMediaDecoder.java deleted file mode 100644 index ff32255a..00000000 --- a/markwon-image-loader/src/main/java/ru/noties/markwon/il/SvgMediaDecoder.java +++ /dev/null @@ -1,81 +0,0 @@ -package ru.noties.markwon.il; - -import android.content.res.Resources; -import android.graphics.Bitmap; -import android.graphics.Canvas; -import android.graphics.drawable.BitmapDrawable; -import android.graphics.drawable.Drawable; -import android.support.annotation.NonNull; -import android.support.annotation.Nullable; - -import com.caverock.androidsvg.SVG; -import com.caverock.androidsvg.SVGParseException; - -import java.io.InputStream; - -/** - * @since 1.1.0 - */ -public class SvgMediaDecoder extends MediaDecoder { - - private static final String CONTENT_TYPE_SVG = "image/svg+xml"; - private static final String FILE_EXTENSION_SVG = ".svg"; - - @NonNull - public static SvgMediaDecoder create(@NonNull Resources resources) { - return new SvgMediaDecoder(resources); - } - - private final Resources resources; - - @SuppressWarnings("WeakerAccess") - SvgMediaDecoder(Resources resources) { - this.resources = resources; - } - - @Override - public boolean canDecodeByContentType(@Nullable String contentType) { - return contentType != null && contentType.startsWith(CONTENT_TYPE_SVG); - } - - @Override - public boolean canDecodeByFileName(@NonNull String fileName) { - return fileName.endsWith(FILE_EXTENSION_SVG); - } - - @Nullable - @Override - public Drawable decode(@NonNull InputStream inputStream) { - - final Drawable out; - - SVG svg = null; - try { - svg = SVG.getFromInputStream(inputStream); - } catch (SVGParseException e) { - e.printStackTrace(); - } - - if (svg == null) { - out = null; - } else { - - final float w = svg.getDocumentWidth(); - final float h = svg.getDocumentHeight(); - final float density = resources.getDisplayMetrics().density; - - final int width = (int) (w * density + .5F); - final int height = (int) (h * density + .5F); - - final Bitmap bitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_4444); - final Canvas canvas = new Canvas(bitmap); - canvas.scale(density, density); - svg.renderToCanvas(canvas); - - out = new BitmapDrawable(resources, bitmap); - DrawableUtils.intrinsicBounds(out); - } - - return out; - } -} diff --git a/markwon-image-loader/src/test/java/ru/noties/markwon/il/DataUriParserTest.java b/markwon-image-loader/src/test/java/ru/noties/markwon/il/DataUriParserTest.java deleted file mode 100644 index 6de01af5..00000000 --- a/markwon-image-loader/src/test/java/ru/noties/markwon/il/DataUriParserTest.java +++ /dev/null @@ -1,119 +0,0 @@ -package ru.noties.markwon.il; - -import org.junit.Before; -import org.junit.Test; -import org.junit.runner.RunWith; -import org.robolectric.RobolectricTestRunner; -import org.robolectric.annotation.Config; - -import java.util.LinkedHashMap; -import java.util.Map; - -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertNull; - -@RunWith(RobolectricTestRunner.class) -@Config(manifest = Config.NONE) -public class DataUriParserTest { - - private DataUriParser.Impl impl; - - @Before - public void before() { - impl = new DataUriParser.Impl(); - } - - @Test - public void test() { - - final Map data = new LinkedHashMap() {{ - put(",", new DataUri(null, false, null)); - put("image/svg+xml;base64,!@#$%^&*(", new DataUri("image/svg+xml", true, "!@#$%^&*(")); - put("text/vnd-example+xyz;foo=bar;base64,R0lGODdh", new DataUri("text/vnd-example+xyz", true, "R0lGODdh")); - put("text/plain;charset=UTF-8;page=21,the%20data:1234,5678", new DataUri("text/plain", false, "the%20data:1234,5678")); - }}; - - for (Map.Entry entry : data.entrySet()) { - assertEquals(entry.getKey(), entry.getValue(), impl.parse(entry.getKey())); - } - } - - @Test - public void data_new_lines_are_ignored() { - - final String input = "image/png;base64,iVBORw0KGgoAAA\n" + - "ANSUhEUgAAAAUAAAAFCAYAAACNbyblAAAAHElEQVQI12P4\n" + - "//8/w38GIAXDIBKE0DHxgljNBAAO9TXL0Y4OHwAAAABJRU\n" + - "5ErkJggg=="; - - assertEquals( - new DataUri("image/png", true, "iVBORw0KGgoAAAANSUhEUgAAAAUAAAAFCAYAAACNbyblAAAAHElEQVQI12P4//8/w38GIAXDIBKE0DHxgljNBAAO9TXL0Y4OHwAAAABJRU5ErkJggg=="), - impl.parse(input) - ); - } - - @Test - public void no_comma_returns_null() { - - final String[] inputs = { - "", - "what-ever", - ";;;;;;;", - "some crazy data" - }; - - for (String input : inputs) { - assertNull(input, impl.parse(input)); - } - } - - @Test - public void two_commas() { - final String input = ",,"; // <- second one would be considered data... - assertEquals( - input, - new DataUri(null, false, ","), - impl.parse(input) - ); - } - - @Test - public void more_commas() { - final String input = "first,second,third"; // <- first is just a value (will be ignored) - assertEquals( - input, - new DataUri(null, false, "second,third"), - impl.parse(input) - ); - } - - @Test - public void base64_no_content_type() { - final String input = ";base64,12345"; - assertEquals( - input, - new DataUri(null, true, "12345"), - impl.parse(input) - ); - } - - @Test - public void not_base64_no_content_type() { - final String input = ",qweRTY"; - assertEquals( - input, - new DataUri(null, false, "qweRTY"), - impl.parse(input) - ); - } - - @Test - public void content_type_data_no_base64() { - final String input = "image/png,aSdFg"; - assertEquals( - input, - new DataUri("image/png", false, "aSdFg"), - impl.parse(input) - ); - } -} \ No newline at end of file diff --git a/markwon-image-loader/src/test/java/ru/noties/markwon/il/DataUriSchemeHandlerTest.java b/markwon-image-loader/src/test/java/ru/noties/markwon/il/DataUriSchemeHandlerTest.java deleted file mode 100644 index 1473744a..00000000 --- a/markwon-image-loader/src/test/java/ru/noties/markwon/il/DataUriSchemeHandlerTest.java +++ /dev/null @@ -1,112 +0,0 @@ -package ru.noties.markwon.il; - -import android.net.Uri; -import android.support.annotation.NonNull; - -import org.junit.Before; -import org.junit.Test; -import org.junit.runner.RunWith; -import org.robolectric.RobolectricTestRunner; -import org.robolectric.annotation.Config; - -import java.io.InputStream; -import java.util.HashMap; -import java.util.Map; -import java.util.Scanner; - -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertNotNull; -import static org.junit.Assert.assertNull; - -@RunWith(RobolectricTestRunner.class) -@Config(manifest = Config.NONE) -public class DataUriSchemeHandlerTest { - - private DataUriSchemeHandler handler; - - @Before - public void before() { - handler = DataUriSchemeHandler.create(); - } - - @Test - public void scheme_specific_part_is_empty() { - assertNull(handler.handle("data:", Uri.parse("data:"))); - } - - @Test - public void data_uri_is_empty() { - assertNull(handler.handle("data://whatever", Uri.parse("data://whatever"))); - } - - @Test - public void no_data() { - assertNull(handler.handle("data://,", Uri.parse("data://,"))); - } - - @Test - public void correct() { - - final class Item { - - final String contentType; - final String data; - - Item(String contentType, String data) { - this.contentType = contentType; - this.data = data; - } - } - - final Map expected = new HashMap() {{ - put("data://text/plain;,123", new Item("text/plain", "123")); - put("data://image/svg+xml;base64,MTIz", new Item("image/svg+xml", "123")); - }}; - - for (Map.Entry entry : expected.entrySet()) { - final ImageItem item = handler.handle(entry.getKey(), Uri.parse(entry.getKey())); - assertNotNull(entry.getKey(), item); - assertEquals(entry.getKey(), entry.getValue().contentType, item.contentType()); - assertEquals(entry.getKey(), entry.getValue().data, readStream(item.inputStream())); - } - } - - @Test - public void correct_real() { - - final class Item { - - final String contentType; - final String data; - - Item(String contentType, String data) { - this.contentType = contentType; - this.data = data; - } - } - - final Map expected = new HashMap() {{ - put("data:text/plain;,123", new Item("text/plain", "123")); - put("data:image/svg+xml;base64,MTIz", new Item("image/svg+xml", "123")); - }}; - - for (Map.Entry entry : expected.entrySet()) { - final ImageItem item = handler.handle(entry.getKey(), Uri.parse(entry.getKey())); - assertNotNull(entry.getKey(), item); - assertEquals(entry.getKey(), entry.getValue().contentType, item.contentType()); - assertEquals(entry.getKey(), entry.getValue().data, readStream(item.inputStream())); - } - } - - @NonNull - private static String readStream(@NonNull InputStream stream) { - try { - final Scanner scanner = new Scanner(stream, "UTF-8").useDelimiter("\\A"); - return scanner.hasNext() - ? scanner.next() - : ""; - } catch (Throwable t) { - throw new RuntimeException(t); - } - } -} \ No newline at end of file diff --git a/markwon-syntax-highlight/src/main/java/ru/noties/markwon/syntax/Prism4jThemeDarkula.java b/markwon-syntax-highlight/src/main/java/ru/noties/markwon/syntax/Prism4jThemeDarkula.java index 9a941951..13bfe48b 100644 --- a/markwon-syntax-highlight/src/main/java/ru/noties/markwon/syntax/Prism4jThemeDarkula.java +++ b/markwon-syntax-highlight/src/main/java/ru/noties/markwon/syntax/Prism4jThemeDarkula.java @@ -16,6 +16,10 @@ public class Prism4jThemeDarkula extends Prism4jThemeBase { return new Prism4jThemeDarkula(0xFF2d2d2d); } + /** + * @param background color + * @since 3.0.0 + */ @NonNull public static Prism4jThemeDarkula create(@ColorInt int background) { return new Prism4jThemeDarkula(background); diff --git a/markwon/build.gradle b/markwon/build.gradle index fc3dc8ff..3cb54118 100644 --- a/markwon/build.gradle +++ b/markwon/build.gradle @@ -15,9 +15,6 @@ android { dependencies { - api project(':markwon-html-parser-api') - api project(':markwon-html-parser-impl') - deps.with { api it['support-annotations'] api it['commonmark'] diff --git a/markwon/src/main/java/ru/noties/markwon/MarkwonConfiguration.java b/markwon/src/main/java/ru/noties/markwon/MarkwonConfiguration.java index 1f6e2216..7c9fad92 100644 --- a/markwon/src/main/java/ru/noties/markwon/MarkwonConfiguration.java +++ b/markwon/src/main/java/ru/noties/markwon/MarkwonConfiguration.java @@ -3,12 +3,12 @@ package ru.noties.markwon; import android.content.Context; import android.support.annotation.NonNull; -import ru.noties.markwon.html.api.MarkwonHtmlParser; +import ru.noties.markwon.html.MarkwonHtmlParser; +import ru.noties.markwon.html.MarkwonHtmlRenderer; import ru.noties.markwon.image.AsyncDrawableLoader; import ru.noties.markwon.image.AsyncDrawableLoaderNoOp; import ru.noties.markwon.renderer.ImageSizeResolver; import ru.noties.markwon.renderer.ImageSizeResolverDef; -import ru.noties.markwon.renderer.html2.MarkwonHtmlRenderer; import ru.noties.markwon.spans.LinkSpan; import ru.noties.markwon.spans.MarkwonTheme; @@ -37,7 +37,6 @@ public class MarkwonConfiguration { private final UrlProcessor urlProcessor; private final ImageSizeResolver imageSizeResolver; private final SpannableFactory factory; // @since 1.1.0 - private final boolean softBreakAddsNewLine; // @since 1.1.1 private final MarkwonHtmlParser htmlParser; // @since 2.0.0 private final MarkwonHtmlRenderer htmlRenderer; // @since 2.0.0 private final boolean htmlAllowNonClosedTags; // @since 2.0.0 @@ -50,7 +49,6 @@ public class MarkwonConfiguration { this.urlProcessor = builder.urlProcessor; this.imageSizeResolver = builder.imageSizeResolver; this.factory = builder.factory; - this.softBreakAddsNewLine = builder.softBreakAddsNewLine; this.htmlParser = builder.htmlParser; this.htmlRenderer = builder.htmlRenderer; this.htmlAllowNonClosedTags = builder.htmlAllowNonClosedTags; @@ -99,15 +97,6 @@ public class MarkwonConfiguration { return factory; } - /** - * @return a flag indicating if soft break should be treated as a hard - * break and thus adding a new line instead of adding a white space - * @since 1.1.1 - */ - public boolean softBreakAddsNewLine() { - return softBreakAddsNewLine; - } - /** * @since 2.0.0 */ @@ -143,7 +132,6 @@ public class MarkwonConfiguration { private UrlProcessor urlProcessor; private ImageSizeResolver imageSizeResolver; private SpannableFactory factory; // @since 1.1.0 - private boolean softBreakAddsNewLine; // @since 1.1.1 private MarkwonHtmlParser htmlParser; // @since 2.0.0 private MarkwonHtmlRenderer htmlRenderer; // @since 2.0.0 private boolean htmlAllowNonClosedTags; // @since 2.0.0 @@ -161,7 +149,6 @@ public class MarkwonConfiguration { this.urlProcessor = configuration.urlProcessor; this.imageSizeResolver = configuration.imageSizeResolver; this.factory = configuration.factory; - this.softBreakAddsNewLine = configuration.softBreakAddsNewLine; this.htmlParser = configuration.htmlParser; this.htmlRenderer = configuration.htmlRenderer; this.htmlAllowNonClosedTags = configuration.htmlAllowNonClosedTags; @@ -203,19 +190,6 @@ public class MarkwonConfiguration { return this; } - /** - * @param softBreakAddsNewLine a flag indicating if soft break should be treated as a hard - * break and thus adding a new line instead of adding a white space - * @return self - * @see spec - * @since 1.1.1 - */ - @NonNull - public Builder softBreakAddsNewLine(boolean softBreakAddsNewLine) { - this.softBreakAddsNewLine = softBreakAddsNewLine; - return this; - } - /** * @since 2.0.0 */ @@ -276,17 +250,12 @@ public class MarkwonConfiguration { // @since 2.0.0 if (htmlParser == null) { - try { - // if impl artifact was excluded -> fallback to no-op implementation - htmlParser = ru.noties.markwon.html.impl.MarkwonHtmlParserImpl.create(); - } catch (Throwable t) { - htmlParser = MarkwonHtmlParser.noOp(); - } + htmlParser = MarkwonHtmlParser.noOp(); } // @since 2.0.0 if (htmlRenderer == null) { - htmlRenderer = MarkwonHtmlRenderer.create(); + htmlRenderer = MarkwonHtmlRenderer.noOp(); } return new MarkwonConfiguration(this); diff --git a/markwon/src/main/java/ru/noties/markwon/SpannableFactory.java b/markwon/src/main/java/ru/noties/markwon/SpannableFactory.java index 13a64cf4..3efec181 100644 --- a/markwon/src/main/java/ru/noties/markwon/SpannableFactory.java +++ b/markwon/src/main/java/ru/noties/markwon/SpannableFactory.java @@ -63,16 +63,4 @@ public interface SpannableFactory { @NonNull MarkwonTheme theme, @NonNull String destination, @NonNull LinkSpan.Resolver resolver); - - // Currently used by HTML parser - @Nullable - Object superScript(@NonNull MarkwonTheme theme); - - // Currently used by HTML parser - @Nullable - Object subScript(@NonNull MarkwonTheme theme); - - // Currently used by HTML parser - @Nullable - Object underline(); } diff --git a/markwon/src/main/java/ru/noties/markwon/SpannableFactoryDef.java b/markwon/src/main/java/ru/noties/markwon/SpannableFactoryDef.java index 21074983..75726b2a 100644 --- a/markwon/src/main/java/ru/noties/markwon/SpannableFactoryDef.java +++ b/markwon/src/main/java/ru/noties/markwon/SpannableFactoryDef.java @@ -118,21 +118,4 @@ public class SpannableFactoryDef implements SpannableFactory { public Object link(@NonNull MarkwonTheme theme, @NonNull String destination, @NonNull LinkSpan.Resolver resolver) { return new LinkSpan(theme, destination, resolver); } - - @Nullable - @Override - public Object superScript(@NonNull MarkwonTheme theme) { - return new SuperScriptSpan(theme); - } - - @Override - public Object subScript(@NonNull MarkwonTheme theme) { - return new SubScriptSpan(theme); - } - - @Nullable - @Override - public Object underline() { - return new UnderlineSpan(); - } } diff --git a/markwon-html-parser-api/src/main/java/ru/noties/markwon/html/api/HtmlTag.java b/markwon/src/main/java/ru/noties/markwon/html/HtmlTag.java similarity index 98% rename from markwon-html-parser-api/src/main/java/ru/noties/markwon/html/api/HtmlTag.java rename to markwon/src/main/java/ru/noties/markwon/html/HtmlTag.java index f3245876..fbe417e9 100644 --- a/markwon-html-parser-api/src/main/java/ru/noties/markwon/html/api/HtmlTag.java +++ b/markwon/src/main/java/ru/noties/markwon/html/HtmlTag.java @@ -1,4 +1,4 @@ -package ru.noties.markwon.html.api; +package ru.noties.markwon.html; import android.support.annotation.NonNull; import android.support.annotation.Nullable; diff --git a/markwon-html-parser-api/src/main/java/ru/noties/markwon/html/api/MarkwonHtmlParser.java b/markwon/src/main/java/ru/noties/markwon/html/MarkwonHtmlParser.java similarity index 92% rename from markwon-html-parser-api/src/main/java/ru/noties/markwon/html/api/MarkwonHtmlParser.java rename to markwon/src/main/java/ru/noties/markwon/html/MarkwonHtmlParser.java index 8d168a72..01bea86a 100644 --- a/markwon-html-parser-api/src/main/java/ru/noties/markwon/html/api/MarkwonHtmlParser.java +++ b/markwon/src/main/java/ru/noties/markwon/html/MarkwonHtmlParser.java @@ -1,4 +1,4 @@ -package ru.noties.markwon.html.api; +package ru.noties.markwon.html; import android.support.annotation.NonNull; @@ -34,7 +34,7 @@ public abstract class MarkwonHtmlParser { * If you wish to keep them open (do not force close at the end of a * document pass here {@link HtmlTag#NO_END}. Later non-closed tags * can be detected by calling {@link HtmlTag#isClosed()} - * @param action {@link FlushAction} to be called with resulting tags ({@link ru.noties.markwon.html.api.HtmlTag.Inline}) + * @param action {@link FlushAction} to be called with resulting tags ({@link HtmlTag.Inline}) */ public abstract void flushInlineTags( int documentLength, @@ -49,7 +49,7 @@ public abstract class MarkwonHtmlParser { * If you wish to keep them open (do not force close at the end of a * document pass here {@link HtmlTag#NO_END}. Later non-closed tags * can be detected by calling {@link HtmlTag#isClosed()} - * @param action {@link FlushAction} to be called with resulting tags ({@link ru.noties.markwon.html.api.HtmlTag.Block}) + * @param action {@link FlushAction} to be called with resulting tags ({@link HtmlTag.Block}) */ public abstract void flushBlockTags( int documentLength, diff --git a/markwon-html-parser-api/src/main/java/ru/noties/markwon/html/api/MarkwonHtmlParserNoOp.java b/markwon/src/main/java/ru/noties/markwon/html/MarkwonHtmlParserNoOp.java similarity index 95% rename from markwon-html-parser-api/src/main/java/ru/noties/markwon/html/api/MarkwonHtmlParserNoOp.java rename to markwon/src/main/java/ru/noties/markwon/html/MarkwonHtmlParserNoOp.java index 0a024865..3b49528a 100644 --- a/markwon-html-parser-api/src/main/java/ru/noties/markwon/html/api/MarkwonHtmlParserNoOp.java +++ b/markwon/src/main/java/ru/noties/markwon/html/MarkwonHtmlParserNoOp.java @@ -1,4 +1,4 @@ -package ru.noties.markwon.html.api; +package ru.noties.markwon.html; import android.support.annotation.NonNull; diff --git a/markwon/src/main/java/ru/noties/markwon/html/MarkwonHtmlRenderer.java b/markwon/src/main/java/ru/noties/markwon/html/MarkwonHtmlRenderer.java new file mode 100644 index 00000000..9e9d222f --- /dev/null +++ b/markwon/src/main/java/ru/noties/markwon/html/MarkwonHtmlRenderer.java @@ -0,0 +1,30 @@ +package ru.noties.markwon.html; + +import android.support.annotation.NonNull; +import android.support.annotation.Nullable; + +import ru.noties.markwon.MarkwonConfiguration; +import ru.noties.markwon.SpannableBuilder; + +/** + * @since 2.0.0 + */ +public abstract class MarkwonHtmlRenderer { + + /** + * @since 3.0.0 + */ + @NonNull + public static MarkwonHtmlRenderer noOp() { + return new MarkwonHtmlRendererNoOp(); + } + + public abstract void render( + @NonNull MarkwonConfiguration configuration, + @NonNull SpannableBuilder builder, + @NonNull MarkwonHtmlParser parser + ); + + @Nullable + public abstract TagHandler tagHandler(@NonNull String tagName); +} diff --git a/markwon/src/main/java/ru/noties/markwon/html/MarkwonHtmlRendererNoOp.java b/markwon/src/main/java/ru/noties/markwon/html/MarkwonHtmlRendererNoOp.java new file mode 100644 index 00000000..7df5027c --- /dev/null +++ b/markwon/src/main/java/ru/noties/markwon/html/MarkwonHtmlRendererNoOp.java @@ -0,0 +1,21 @@ +package ru.noties.markwon.html; + +import android.support.annotation.NonNull; +import android.support.annotation.Nullable; + +import ru.noties.markwon.MarkwonConfiguration; +import ru.noties.markwon.SpannableBuilder; + +class MarkwonHtmlRendererNoOp extends MarkwonHtmlRenderer { + + @Override + public void render(@NonNull MarkwonConfiguration configuration, @NonNull SpannableBuilder builder, @NonNull MarkwonHtmlParser parser) { + + } + + @Nullable + @Override + public TagHandler tagHandler(@NonNull String tagName) { + return null; + } +} diff --git a/markwon/src/main/java/ru/noties/markwon/renderer/html2/tag/TagHandler.java b/markwon/src/main/java/ru/noties/markwon/html/TagHandler.java similarity index 91% rename from markwon/src/main/java/ru/noties/markwon/renderer/html2/tag/TagHandler.java rename to markwon/src/main/java/ru/noties/markwon/html/TagHandler.java index bdf9b134..50af33d8 100644 --- a/markwon/src/main/java/ru/noties/markwon/renderer/html2/tag/TagHandler.java +++ b/markwon/src/main/java/ru/noties/markwon/html/TagHandler.java @@ -1,10 +1,9 @@ -package ru.noties.markwon.renderer.html2.tag; +package ru.noties.markwon.html; import android.support.annotation.NonNull; import ru.noties.markwon.MarkwonConfiguration; import ru.noties.markwon.SpannableBuilder; -import ru.noties.markwon.html.api.HtmlTag; public abstract class TagHandler { diff --git a/markwon/src/main/java/ru/noties/markwon/renderer/html2/MarkwonHtmlRenderer.java b/markwon/src/main/java/ru/noties/markwon/renderer/html2/MarkwonHtmlRenderer.java deleted file mode 100644 index b66045e6..00000000 --- a/markwon/src/main/java/ru/noties/markwon/renderer/html2/MarkwonHtmlRenderer.java +++ /dev/null @@ -1,101 +0,0 @@ -package ru.noties.markwon.renderer.html2; - -import android.support.annotation.NonNull; -import android.support.annotation.Nullable; - -import java.util.Collections; -import java.util.HashMap; -import java.util.Locale; -import java.util.Map; - -import ru.noties.markwon.SpannableBuilder; -import ru.noties.markwon.MarkwonConfiguration; -import ru.noties.markwon.html.api.MarkwonHtmlParser; -import ru.noties.markwon.renderer.html2.tag.BlockquoteHandler; -import ru.noties.markwon.renderer.html2.tag.EmphasisHandler; -import ru.noties.markwon.renderer.html2.tag.HeadingHandler; -import ru.noties.markwon.renderer.html2.tag.ImageHandler; -import ru.noties.markwon.renderer.html2.tag.LinkHandler; -import ru.noties.markwon.renderer.html2.tag.ListHandler; -import ru.noties.markwon.renderer.html2.tag.StrikeHandler; -import ru.noties.markwon.renderer.html2.tag.StrongEmphasisHandler; -import ru.noties.markwon.renderer.html2.tag.SubScriptHandler; -import ru.noties.markwon.renderer.html2.tag.SuperScriptHandler; -import ru.noties.markwon.renderer.html2.tag.TagHandler; -import ru.noties.markwon.renderer.html2.tag.UnderlineHandler; - -/** - * @since 2.0.0 - */ -public abstract class MarkwonHtmlRenderer { - - public abstract void render( - @NonNull MarkwonConfiguration configuration, - @NonNull SpannableBuilder builder, - @NonNull MarkwonHtmlParser parser - ); - - @Nullable - public abstract TagHandler tagHandler(@NonNull String tagName); - - @NonNull - public static MarkwonHtmlRenderer create() { - return builderWithDefaults().build(); - } - - @NonNull - public static Builder builderWithDefaults() { - - final EmphasisHandler emphasisHandler = new EmphasisHandler(); - final StrongEmphasisHandler strongEmphasisHandler = new StrongEmphasisHandler(); - final StrikeHandler strikeHandler = new StrikeHandler(); - final UnderlineHandler underlineHandler = new UnderlineHandler(); - final ListHandler listHandler = new ListHandler(); - - return builder() - .handler("i", emphasisHandler) - .handler("em", emphasisHandler) - .handler("cite", emphasisHandler) - .handler("dfn", emphasisHandler) - .handler("b", strongEmphasisHandler) - .handler("strong", strongEmphasisHandler) - .handler("sup", new SuperScriptHandler()) - .handler("sub", new SubScriptHandler()) - .handler("u", underlineHandler) - .handler("ins", underlineHandler) - .handler("del", strikeHandler) - .handler("s", strikeHandler) - .handler("strike", strikeHandler) - .handler("a", new LinkHandler()) - .handler("ul", listHandler) - .handler("ol", listHandler) - .handler("img", ImageHandler.create()) - .handler("blockquote", new BlockquoteHandler()) - .handler("h1", new HeadingHandler(1)) - .handler("h2", new HeadingHandler(2)) - .handler("h3", new HeadingHandler(3)) - .handler("h4", new HeadingHandler(4)) - .handler("h5", new HeadingHandler(5)) - .handler("h6", new HeadingHandler(6)); - } - - @NonNull - public static Builder builder() { - return new Builder(); - } - - public static class Builder { - - private final Map tagHandlers = new HashMap<>(2); - - public Builder handler(@NonNull String tagName, @NonNull TagHandler tagHandler) { - tagHandlers.put(tagName.toLowerCase(Locale.US), tagHandler); - return this; - } - - @NonNull - public MarkwonHtmlRenderer build() { - return new MarkwonHtmlRendererImpl(Collections.unmodifiableMap(tagHandlers)); - } - } -} diff --git a/markwon/src/main/java/ru/noties/markwon/renderer/html2/MarkwonHtmlRendererImpl.java b/markwon/src/main/java/ru/noties/markwon/renderer/html2/MarkwonHtmlRendererImpl.java deleted file mode 100644 index cc34f55b..00000000 --- a/markwon/src/main/java/ru/noties/markwon/renderer/html2/MarkwonHtmlRendererImpl.java +++ /dev/null @@ -1,88 +0,0 @@ -package ru.noties.markwon.renderer.html2; - -import android.support.annotation.NonNull; -import android.support.annotation.Nullable; - -import java.util.List; -import java.util.Map; - -import ru.noties.markwon.SpannableBuilder; -import ru.noties.markwon.MarkwonConfiguration; -import ru.noties.markwon.html.api.HtmlTag; -import ru.noties.markwon.html.api.MarkwonHtmlParser; -import ru.noties.markwon.renderer.html2.tag.TagHandler; - -class MarkwonHtmlRendererImpl extends MarkwonHtmlRenderer { - - private final Map tagHandlers; - - MarkwonHtmlRendererImpl(@NonNull Map tagHandlers) { - this.tagHandlers = tagHandlers; - } - - @Override - public void render( - @NonNull final MarkwonConfiguration configuration, - @NonNull final SpannableBuilder builder, - @NonNull MarkwonHtmlParser parser) { - - final int end; - if (!configuration.htmlAllowNonClosedTags()) { - end = HtmlTag.NO_END; - } else { - end = builder.length(); - } - - parser.flushInlineTags(end, new MarkwonHtmlParser.FlushAction() { - @Override - public void apply(@NonNull List tags) { - - TagHandler handler; - - for (HtmlTag.Inline inline : tags) { - - // if tag is not closed -> do not render - if (!inline.isClosed()) { - continue; - } - - handler = tagHandler(inline.name()); - if (handler != null) { - handler.handle(configuration, builder, inline); - } - } - } - }); - - parser.flushBlockTags(end, new MarkwonHtmlParser.FlushAction() { - @Override - public void apply(@NonNull List tags) { - - TagHandler handler; - - for (HtmlTag.Block block : tags) { - - if (!block.isClosed()) { - continue; - } - - handler = tagHandler(block.name()); - if (handler != null) { - handler.handle(configuration, builder, block); - } else { - // see if any of children can be handled - apply(block.children()); - } - } - } - }); - - parser.reset(); - } - - @Nullable - @Override - public TagHandler tagHandler(@NonNull String tagName) { - return tagHandlers.get(tagName); - } -} diff --git a/markwon/src/test/java/ru/noties/markwon/renderer/MarkwonConfigurationTest.java b/markwon/src/test/java/ru/noties/markwon/renderer/MarkwonConfigurationTest.java index 108df62d..11f539b3 100644 --- a/markwon/src/test/java/ru/noties/markwon/renderer/MarkwonConfigurationTest.java +++ b/markwon/src/test/java/ru/noties/markwon/renderer/MarkwonConfigurationTest.java @@ -7,7 +7,7 @@ import ru.noties.markwon.SpannableFactory; import ru.noties.markwon.SyntaxHighlight; import ru.noties.markwon.UrlProcessor; import ru.noties.markwon.html.api.MarkwonHtmlParser; -import ru.noties.markwon.renderer.html2.MarkwonHtmlRenderer; +import ru.noties.markwon.html.MarkwonHtmlRenderer; import ru.noties.markwon.image.AsyncDrawable; import ru.noties.markwon.spans.LinkSpan; import ru.noties.markwon.spans.MarkwonTheme; diff --git a/settings.gradle b/settings.gradle index 3a1678e4..ca458b18 100644 --- a/settings.gradle +++ b/settings.gradle @@ -1,3 +1,3 @@ rootProject.name = 'MarkwonProject' include ':app', ':markwon', ':markwon-view', ':sample-custom-extension', ':sample-latex-math', ':markwon-image-svg', ':markwon-image-gif', - ':markwon-syntax-highlight', ':markwon-html-parser-api', ':markwon-html-parser-impl' + ':markwon-syntax-highlight', ':markwon-html' From 66bb33a76b6cac7076c32411988406551e834606 Mon Sep 17 00:00:00 2001 From: Dimitry Ivanov Date: Mon, 26 Nov 2018 15:51:16 +0300 Subject: [PATCH 021/103] Create HtmlPlugin --- app/build.gradle | 1 + .../java/ru/noties/markwon/GifProcessor.java | 13 +++ .../ru/noties/markwon/MarkdownRenderer.java | 6 ++ .../noties/markwon/html/impl/HtmlPlugin.java | 85 +++++++++++++++++++ .../html/impl/MarkwonHtmlRendererImpl.java | 40 ++++++++- .../noties/markwon/MarkwonConfiguration.java | 44 +++++----- .../renderer/SpannableMarkdownVisitor.java | 77 +++++++---------- 7 files changed, 197 insertions(+), 69 deletions(-) create mode 100644 markwon-html/src/main/java/ru/noties/markwon/html/impl/HtmlPlugin.java diff --git a/app/build.gradle b/app/build.gradle index 9512e8d2..ef215520 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -29,6 +29,7 @@ android { dependencies { implementation project(':markwon') + implementation project(':markwon-html') implementation project(':markwon-image-gif') implementation project(':markwon-image-svg') implementation project(':markwon-syntax-highlight') diff --git a/app/src/main/java/ru/noties/markwon/GifProcessor.java b/app/src/main/java/ru/noties/markwon/GifProcessor.java index 7d2cd7c6..6c8bd35f 100644 --- a/app/src/main/java/ru/noties/markwon/GifProcessor.java +++ b/app/src/main/java/ru/noties/markwon/GifProcessor.java @@ -9,6 +9,7 @@ import android.view.View; import android.widget.TextView; import pl.droidsonroids.gif.GifDrawable; +import ru.noties.debug.Debug; import ru.noties.markwon.spans.AsyncDrawableSpan; public abstract class GifProcessor { @@ -25,16 +26,21 @@ public abstract class GifProcessor { @Override public void process(@NonNull final TextView textView) { + Debug.i("textView: %s", textView); + // here is what we will do additionally: // we query for all asyncDrawableSpans // we check if they are inside clickableSpan // if not we apply onGifListener final Spannable spannable = spannable(textView); + Debug.i(spannable); if (spannable == null) { return; } + Debug.i(spannable); + final AsyncDrawableSpan[] asyncDrawableSpans = spannable.getSpans(0, spannable.length(), AsyncDrawableSpan.class); if (asyncDrawableSpans == null @@ -42,6 +48,8 @@ public abstract class GifProcessor { return; } + Debug.i(asyncDrawableSpans); + int start; int end; ClickableSpan[] clickableSpans; @@ -51,6 +59,8 @@ public abstract class GifProcessor { start = spannable.getSpanStart(asyncDrawableSpan); end = spannable.getSpanEnd(asyncDrawableSpan); + Debug.i(asyncDrawableSpan, start, end); + if (start < 0 || end < 0) { continue; @@ -74,6 +84,7 @@ public abstract class GifProcessor { @Nullable private static Spannable spannable(@NonNull TextView textView) { final CharSequence charSequence = textView.getText(); + Debug.i("type: %s, spanned: %s, spannable: %s", charSequence.getClass().getName(), charSequence instanceof Spanned, charSequence instanceof Spannable); if (charSequence instanceof Spannable) { return (Spannable) charSequence; } @@ -85,6 +96,8 @@ public abstract class GifProcessor { @NonNull AsyncDrawableSpan span, @NonNull GifAwareAsyncDrawable drawable) { + Debug.i("textView: %s, span: %s, drawable: %s", textView, span, drawable); + // important thing here is to obtain new spannable from textView // as with each `setText()` new spannable is created and keeping reference // to an older one won't affect textView diff --git a/app/src/main/java/ru/noties/markwon/MarkdownRenderer.java b/app/src/main/java/ru/noties/markwon/MarkdownRenderer.java index fc6af948..bd09825f 100644 --- a/app/src/main/java/ru/noties/markwon/MarkdownRenderer.java +++ b/app/src/main/java/ru/noties/markwon/MarkdownRenderer.java @@ -14,6 +14,7 @@ import javax.inject.Inject; import ru.noties.debug.Debug; import ru.noties.markwon.core.CorePlugin; +import ru.noties.markwon.html.impl.HtmlPlugin; import ru.noties.markwon.image.ImagesPlugin; import ru.noties.markwon.image.gif.GifPlugin; import ru.noties.markwon.image.svg.SvgPlugin; @@ -21,6 +22,8 @@ import ru.noties.markwon.syntax.Prism4jTheme; import ru.noties.markwon.syntax.Prism4jThemeDarkula; import ru.noties.markwon.syntax.Prism4jThemeDefault; import ru.noties.markwon.syntax.SyntaxHighlightPlugin; +import ru.noties.markwon.table.TablePlugin; +import ru.noties.markwon.tasklist.TaskListPlugin; import ru.noties.prism4j.Prism4j; @ActivityScope @@ -84,6 +87,9 @@ public class MarkdownRenderer { .use(GifPlugin.create(false)) .use(SyntaxHighlightPlugin.create(prism4j, prism4jTheme)) .use(GifAwarePlugin.create(context)) + .use(TablePlugin.create(context)) + .use(TaskListPlugin.create(context)) + .use(HtmlPlugin.create()) .use(new AbstractMarkwonPlugin() { @Override public void configureConfiguration(@NonNull MarkwonConfiguration.Builder builder) { diff --git a/markwon-html/src/main/java/ru/noties/markwon/html/impl/HtmlPlugin.java b/markwon-html/src/main/java/ru/noties/markwon/html/impl/HtmlPlugin.java new file mode 100644 index 00000000..6e8935df --- /dev/null +++ b/markwon-html/src/main/java/ru/noties/markwon/html/impl/HtmlPlugin.java @@ -0,0 +1,85 @@ +package ru.noties.markwon.html.impl; + +import android.support.annotation.NonNull; +import android.support.annotation.Nullable; + +import org.commonmark.node.Document; +import org.commonmark.node.HtmlBlock; +import org.commonmark.node.HtmlInline; + +import ru.noties.markwon.AbstractMarkwonPlugin; +import ru.noties.markwon.MarkwonConfiguration; +import ru.noties.markwon.MarkwonVisitor; +import ru.noties.markwon.html.MarkwonHtmlParser; +import ru.noties.markwon.html.MarkwonHtmlRenderer; + +public class HtmlPlugin extends AbstractMarkwonPlugin { + + @NonNull + public static HtmlPlugin create() { + return create(MarkwonHtmlRendererImpl.create(), MarkwonHtmlParserImpl.create()); + } + + @NonNull + public static HtmlPlugin create(@NonNull MarkwonHtmlRenderer renderer) { + return create(renderer, MarkwonHtmlParserImpl.create()); + } + + @NonNull + public static HtmlPlugin create(@NonNull MarkwonHtmlParser parser) { + return create(MarkwonHtmlRendererImpl.create(), parser); + } + + @NonNull + public static HtmlPlugin create(@NonNull MarkwonHtmlRenderer renderer, @NonNull MarkwonHtmlParser parser) { + return new HtmlPlugin(renderer, parser); + } + + private final MarkwonHtmlRenderer renderer; + private final MarkwonHtmlParser parser; + + public HtmlPlugin(@NonNull MarkwonHtmlRenderer renderer, @NonNull MarkwonHtmlParser parser) { + this.renderer = renderer; + this.parser = parser; + } + + @Override + public void configureConfiguration(@NonNull MarkwonConfiguration.Builder builder) { + builder + .htmlParser(parser) + .htmlRenderer(renderer); + } + + @Override + public void configureVisitor(@NonNull MarkwonVisitor.Builder builder) { + builder + .on(Document.class, new MarkwonVisitor.NodeVisitor() { + @Override + public void visit(@NonNull MarkwonVisitor visitor, @NonNull Document document) { + + visitor.visitChildren(document); + + final MarkwonConfiguration configuration = visitor.configuration(); + configuration.htmlRenderer().render(configuration, visitor.builder(), configuration.htmlParser()); + } + }) + .on(HtmlBlock.class, new MarkwonVisitor.NodeVisitor() { + @Override + public void visit(@NonNull MarkwonVisitor visitor, @NonNull HtmlBlock htmlBlock) { + visitHtml(visitor, htmlBlock.getLiteral()); + } + }) + .on(HtmlInline.class, new MarkwonVisitor.NodeVisitor() { + @Override + public void visit(@NonNull MarkwonVisitor visitor, @NonNull HtmlInline htmlInline) { + visitHtml(visitor, htmlInline.getLiteral()); + } + }); + } + + private void visitHtml(@NonNull MarkwonVisitor visitor, @Nullable String html) { + if (html != null) { + visitor.configuration().htmlParser().processFragment(visitor.builder(), html); + } + } +} diff --git a/markwon-html/src/main/java/ru/noties/markwon/html/impl/MarkwonHtmlRendererImpl.java b/markwon-html/src/main/java/ru/noties/markwon/html/impl/MarkwonHtmlRendererImpl.java index 6ec8f26b..b6b2851d 100644 --- a/markwon-html/src/main/java/ru/noties/markwon/html/impl/MarkwonHtmlRendererImpl.java +++ b/markwon-html/src/main/java/ru/noties/markwon/html/impl/MarkwonHtmlRendererImpl.java @@ -34,8 +34,24 @@ public class MarkwonHtmlRendererImpl extends MarkwonHtmlRenderer { return builderWithDefaults().build(); } + /** + * @since 3.0.0 + */ + @NonNull + public static MarkwonHtmlRendererImpl create(boolean allowNonClosedTags) { + return builderWithDefaults(allowNonClosedTags).build(); + } + @NonNull public static Builder builderWithDefaults() { + return builderWithDefaults(false); + } + + /** + * @since 3.0.0 + */ + @NonNull + public static Builder builderWithDefaults(boolean allowNonClosedTags) { final EmphasisHandler emphasisHandler = new EmphasisHandler(); final StrongEmphasisHandler strongEmphasisHandler = new StrongEmphasisHandler(); @@ -44,6 +60,7 @@ public class MarkwonHtmlRendererImpl extends MarkwonHtmlRenderer { final ListHandler listHandler = new ListHandler(); return builder() + .allowNonClosedTags(allowNonClosedTags) .handler("i", emphasisHandler) .handler("em", emphasisHandler) .handler("cite", emphasisHandler) @@ -77,9 +94,11 @@ public class MarkwonHtmlRendererImpl extends MarkwonHtmlRenderer { public static final float SCRIPT_DEF_TEXT_SIZE_RATIO = .75F; + private final boolean allowNonClosedTags; private final Map tagHandlers; - private MarkwonHtmlRendererImpl(@NonNull Map tagHandlers) { + private MarkwonHtmlRendererImpl(boolean allowNonClosedTags, @NonNull Map tagHandlers) { + this.allowNonClosedTags = allowNonClosedTags; this.tagHandlers = tagHandlers; } @@ -90,7 +109,7 @@ public class MarkwonHtmlRendererImpl extends MarkwonHtmlRenderer { @NonNull MarkwonHtmlParser parser) { final int end; - if (!configuration.htmlAllowNonClosedTags()) { + if (!allowNonClosedTags) { end = HtmlTag.NO_END; } else { end = builder.length(); @@ -152,6 +171,7 @@ public class MarkwonHtmlRendererImpl extends MarkwonHtmlRenderer { public static class Builder { private final Map tagHandlers = new HashMap<>(2); + private boolean allowNonClosedTags; @NonNull public Builder handler(@NonNull String tagName, @NonNull TagHandler tagHandler) { @@ -159,9 +179,23 @@ public class MarkwonHtmlRendererImpl extends MarkwonHtmlRenderer { return this; } + /** + * @param allowNonClosedTags that indicates if non-closed html tags should be rendered. + * If this argument is true then all non-closed HTML tags + * will be closed at the end of a document. Otherwise they will + * be delivered non-closed {@code HtmlTag#isClosed()} and thus not + * rendered at all + * @since 3.0.0 + */ + @NonNull + public Builder allowNonClosedTags(boolean allowNonClosedTags) { + this.allowNonClosedTags = allowNonClosedTags; + return this; + } + @NonNull public MarkwonHtmlRendererImpl build() { - return new MarkwonHtmlRendererImpl(Collections.unmodifiableMap(tagHandlers)); + return new MarkwonHtmlRendererImpl(allowNonClosedTags, Collections.unmodifiableMap(tagHandlers)); } } } diff --git a/markwon/src/main/java/ru/noties/markwon/MarkwonConfiguration.java b/markwon/src/main/java/ru/noties/markwon/MarkwonConfiguration.java index 7c9fad92..33be0601 100644 --- a/markwon/src/main/java/ru/noties/markwon/MarkwonConfiguration.java +++ b/markwon/src/main/java/ru/noties/markwon/MarkwonConfiguration.java @@ -39,7 +39,7 @@ public class MarkwonConfiguration { private final SpannableFactory factory; // @since 1.1.0 private final MarkwonHtmlParser htmlParser; // @since 2.0.0 private final MarkwonHtmlRenderer htmlRenderer; // @since 2.0.0 - private final boolean htmlAllowNonClosedTags; // @since 2.0.0 +// private final boolean htmlAllowNonClosedTags; // @since 2.0.0 private MarkwonConfiguration(@NonNull Builder builder) { this.theme = builder.theme; @@ -51,7 +51,7 @@ public class MarkwonConfiguration { this.factory = builder.factory; this.htmlParser = builder.htmlParser; this.htmlRenderer = builder.htmlRenderer; - this.htmlAllowNonClosedTags = builder.htmlAllowNonClosedTags; +// this.htmlAllowNonClosedTags = builder.htmlAllowNonClosedTags; } /** @@ -113,12 +113,12 @@ public class MarkwonConfiguration { return htmlRenderer; } - /** - * @since 2.0.0 - */ - public boolean htmlAllowNonClosedTags() { - return htmlAllowNonClosedTags; - } +// /** +// * @since 2.0.0 +// */ +// public boolean htmlAllowNonClosedTags() { +// return htmlAllowNonClosedTags; +// } @SuppressWarnings("unused") public static class Builder { @@ -134,7 +134,7 @@ public class MarkwonConfiguration { private SpannableFactory factory; // @since 1.1.0 private MarkwonHtmlParser htmlParser; // @since 2.0.0 private MarkwonHtmlRenderer htmlRenderer; // @since 2.0.0 - private boolean htmlAllowNonClosedTags; // @since 2.0.0 +// private boolean htmlAllowNonClosedTags; // @since 2.0.0 Builder(@NonNull Context context) { this.context = context; @@ -151,7 +151,7 @@ public class MarkwonConfiguration { this.factory = configuration.factory; this.htmlParser = configuration.htmlParser; this.htmlRenderer = configuration.htmlRenderer; - this.htmlAllowNonClosedTags = configuration.htmlAllowNonClosedTags; +// this.htmlAllowNonClosedTags = configuration.htmlAllowNonClosedTags; } @NonNull @@ -208,18 +208,18 @@ public class MarkwonConfiguration { return this; } - /** - * @param htmlAllowNonClosedTags that indicates if non-closed html tags should be rendered. - * If this argument is true then all non-closed HTML tags - * will be closed at the end of a document. Otherwise they will - * be delivered non-closed {@code HtmlTag#isClosed()} - * @since 2.0.0 - */ - @NonNull - public Builder htmlAllowNonClosedTags(boolean htmlAllowNonClosedTags) { - this.htmlAllowNonClosedTags = htmlAllowNonClosedTags; - return this; - } +// /** +// * @param htmlAllowNonClosedTags that indicates if non-closed html tags should be rendered. +// * If this argument is true then all non-closed HTML tags +// * will be closed at the end of a document. Otherwise they will +// * be delivered non-closed {@code HtmlTag#isClosed()} +// * @since 2.0.0 +// */ +// @NonNull +// public Builder htmlAllowNonClosedTags(boolean htmlAllowNonClosedTags) { +// this.htmlAllowNonClosedTags = htmlAllowNonClosedTags; +// return this; +// } @NonNull public MarkwonConfiguration build(@NonNull MarkwonTheme theme, @NonNull AsyncDrawableLoader asyncDrawableLoader) { diff --git a/markwon/src/main/java/ru/noties/markwon/renderer/SpannableMarkdownVisitor.java b/markwon/src/main/java/ru/noties/markwon/renderer/SpannableMarkdownVisitor.java index 6a3a94e3..18d02c19 100644 --- a/markwon/src/main/java/ru/noties/markwon/renderer/SpannableMarkdownVisitor.java +++ b/markwon/src/main/java/ru/noties/markwon/renderer/SpannableMarkdownVisitor.java @@ -4,23 +4,16 @@ import android.support.annotation.NonNull; import android.support.annotation.Nullable; import org.commonmark.ext.gfm.strikethrough.Strikethrough; -import org.commonmark.ext.gfm.tables.TableBody; -import org.commonmark.ext.gfm.tables.TableCell; -import org.commonmark.ext.gfm.tables.TableHead; -import org.commonmark.ext.gfm.tables.TableRow; import org.commonmark.node.AbstractVisitor; import org.commonmark.node.BlockQuote; import org.commonmark.node.BulletList; import org.commonmark.node.Code; import org.commonmark.node.CustomBlock; import org.commonmark.node.CustomNode; -import org.commonmark.node.Document; import org.commonmark.node.Emphasis; import org.commonmark.node.FencedCodeBlock; import org.commonmark.node.HardLineBreak; import org.commonmark.node.Heading; -import org.commonmark.node.HtmlBlock; -import org.commonmark.node.HtmlInline; import org.commonmark.node.Image; import org.commonmark.node.IndentedCodeBlock; import org.commonmark.node.Link; @@ -29,29 +22,25 @@ import org.commonmark.node.ListItem; import org.commonmark.node.Node; import org.commonmark.node.OrderedList; import org.commonmark.node.Paragraph; -import org.commonmark.node.SoftLineBreak; import org.commonmark.node.StrongEmphasis; import org.commonmark.node.Text; import org.commonmark.node.ThematicBreak; -import java.util.ArrayList; import java.util.List; -import ru.noties.markwon.SpannableBuilder; import ru.noties.markwon.MarkwonConfiguration; +import ru.noties.markwon.SpannableBuilder; import ru.noties.markwon.SpannableFactory; -import ru.noties.markwon.html.api.MarkwonHtmlParser; import ru.noties.markwon.spans.MarkwonTheme; import ru.noties.markwon.table.TableRowSpan; import ru.noties.markwon.tasklist.TaskListBlock; -import ru.noties.markwon.tasklist.TaskListItem; @SuppressWarnings("WeakerAccess") public class SpannableMarkdownVisitor extends AbstractVisitor { private final MarkwonConfiguration configuration; private final SpannableBuilder builder; - private final MarkwonHtmlParser htmlParser; +// private final MarkwonHtmlParser htmlParser; private final MarkwonTheme theme; private final SpannableFactory factory; @@ -69,18 +58,18 @@ public class SpannableMarkdownVisitor extends AbstractVisitor { ) { this.configuration = configuration; this.builder = builder; - this.htmlParser = configuration.htmlParser(); +// this.htmlParser = configuration.htmlParser(); this.theme = configuration.theme(); this.factory = configuration.factory(); } - @Override - public void visit(Document document) { - super.visit(document); - - configuration.htmlRenderer().render(configuration, builder, htmlParser); - } +// @Override +// public void visit(Document document) { +// super.visit(document); +// +// configuration.htmlRenderer().render(configuration, builder, htmlParser); +// } @Override public void visit(Text text) { @@ -276,15 +265,15 @@ public class SpannableMarkdownVisitor extends AbstractVisitor { } } - @Override - public void visit(SoftLineBreak softLineBreak) { - // @since 1.1.1 there is an option to treat soft break as a hard break (thus adding new line) - if (configuration.softBreakAddsNewLine()) { - newLine(); - } else { - builder.append(' '); - } - } +// @Override +// public void visit(SoftLineBreak softLineBreak) { +// // @since 1.1.1 there is an option to treat soft break as a hard break (thus adding new line) +// if (configuration.softBreakAddsNewLine()) { +// newLine(); +// } else { +// builder.append(' '); +// } +// } @Override public void visit(HardLineBreak hardLineBreak) { @@ -463,21 +452,21 @@ public class SpannableMarkdownVisitor extends AbstractVisitor { // user can open it in external viewer? } - @Override - public void visit(HtmlBlock htmlBlock) { - visitHtml(htmlBlock.getLiteral()); - } - - @Override - public void visit(HtmlInline htmlInline) { - visitHtml(htmlInline.getLiteral()); - } - - private void visitHtml(@Nullable String html) { - if (html != null) { - htmlParser.processFragment(builder, html); - } - } +// @Override +// public void visit(HtmlBlock htmlBlock) { +// visitHtml(htmlBlock.getLiteral()); +// } +// +// @Override +// public void visit(HtmlInline htmlInline) { +// visitHtml(htmlInline.getLiteral()); +// } +// +// private void visitHtml(@Nullable String html) { +// if (html != null) { +// htmlParser.processFragment(builder, html); +// } +// } @Override public void visit(Link link) { From 6eb8e64d7580c22b93e479769f47a1aa3def4ab2 Mon Sep 17 00:00:00 2001 From: Dimitry Ivanov Date: Mon, 26 Nov 2018 16:11:16 +0300 Subject: [PATCH 022/103] Add bufferType Markwon option and fix GIF in sample --- .../main/java/ru/noties/markwon/GifProcessor.java | 15 ++------------- .../main/java/ru/noties/markwon/MainActivity.java | 3 +++ .../src/main/java/ru/noties/markwon/Markwon2.java | 9 +++++++++ .../ru/noties/markwon/MarkwonBuilderImpl.java | 10 ++++++++++ .../main/java/ru/noties/markwon/MarkwonImpl.java | 5 ++++- 5 files changed, 28 insertions(+), 14 deletions(-) diff --git a/app/src/main/java/ru/noties/markwon/GifProcessor.java b/app/src/main/java/ru/noties/markwon/GifProcessor.java index 6c8bd35f..5be9cd1b 100644 --- a/app/src/main/java/ru/noties/markwon/GifProcessor.java +++ b/app/src/main/java/ru/noties/markwon/GifProcessor.java @@ -9,7 +9,6 @@ import android.view.View; import android.widget.TextView; import pl.droidsonroids.gif.GifDrawable; -import ru.noties.debug.Debug; import ru.noties.markwon.spans.AsyncDrawableSpan; public abstract class GifProcessor { @@ -26,21 +25,17 @@ public abstract class GifProcessor { @Override public void process(@NonNull final TextView textView) { - Debug.i("textView: %s", textView); - // here is what we will do additionally: // we query for all asyncDrawableSpans // we check if they are inside clickableSpan // if not we apply onGifListener final Spannable spannable = spannable(textView); - Debug.i(spannable); + if (spannable == null) { return; } - Debug.i(spannable); - final AsyncDrawableSpan[] asyncDrawableSpans = spannable.getSpans(0, spannable.length(), AsyncDrawableSpan.class); if (asyncDrawableSpans == null @@ -48,8 +43,6 @@ public abstract class GifProcessor { return; } - Debug.i(asyncDrawableSpans); - int start; int end; ClickableSpan[] clickableSpans; @@ -59,8 +52,6 @@ public abstract class GifProcessor { start = spannable.getSpanStart(asyncDrawableSpan); end = spannable.getSpanEnd(asyncDrawableSpan); - Debug.i(asyncDrawableSpan, start, end); - if (start < 0 || end < 0) { continue; @@ -84,7 +75,6 @@ public abstract class GifProcessor { @Nullable private static Spannable spannable(@NonNull TextView textView) { final CharSequence charSequence = textView.getText(); - Debug.i("type: %s, spanned: %s, spannable: %s", charSequence.getClass().getName(), charSequence instanceof Spanned, charSequence instanceof Spannable); if (charSequence instanceof Spannable) { return (Spannable) charSequence; } @@ -96,12 +86,11 @@ public abstract class GifProcessor { @NonNull AsyncDrawableSpan span, @NonNull GifAwareAsyncDrawable drawable) { - Debug.i("textView: %s, span: %s, drawable: %s", textView, span, drawable); - // important thing here is to obtain new spannable from textView // as with each `setText()` new spannable is created and keeping reference // to an older one won't affect textView final Spannable spannable = spannable(textView); + if (spannable == null) { return; } diff --git a/app/src/main/java/ru/noties/markwon/MainActivity.java b/app/src/main/java/ru/noties/markwon/MainActivity.java index 84904934..34ed71de 100644 --- a/app/src/main/java/ru/noties/markwon/MainActivity.java +++ b/app/src/main/java/ru/noties/markwon/MainActivity.java @@ -6,6 +6,7 @@ import android.net.Uri; import android.os.Bundle; import android.support.annotation.NonNull; import android.support.annotation.Nullable; +import android.text.method.LinkMovementMethod; import android.view.View; import android.widget.TextView; @@ -66,6 +67,8 @@ public class MainActivity extends Activity { appBarRenderer.render(appBarState()); + textView.setMovementMethod(LinkMovementMethod.getInstance()); + markdownLoader.load(uri(), new MarkdownLoader.OnMarkdownTextLoaded() { @Override public void apply(final String text) { diff --git a/markwon/src/main/java/ru/noties/markwon/Markwon2.java b/markwon/src/main/java/ru/noties/markwon/Markwon2.java index 734a9ee3..5d4f3847 100644 --- a/markwon/src/main/java/ru/noties/markwon/Markwon2.java +++ b/markwon/src/main/java/ru/noties/markwon/Markwon2.java @@ -29,6 +29,15 @@ public abstract class Markwon2 { public interface Builder { + /** + * Specify bufferType when applying text to a TextView {@code textView.setText(CharSequence,BufferType)}. + * By default `BufferType.SPANNABLE` is used + * + * @param bufferType BufferType + */ + @NonNull + Builder bufferType(@NonNull TextView.BufferType bufferType); + @NonNull Builder use(@NonNull MarkwonPlugin plugin); diff --git a/markwon/src/main/java/ru/noties/markwon/MarkwonBuilderImpl.java b/markwon/src/main/java/ru/noties/markwon/MarkwonBuilderImpl.java index 5bbe9fa5..cc607322 100644 --- a/markwon/src/main/java/ru/noties/markwon/MarkwonBuilderImpl.java +++ b/markwon/src/main/java/ru/noties/markwon/MarkwonBuilderImpl.java @@ -2,6 +2,7 @@ package ru.noties.markwon; import android.content.Context; import android.support.annotation.NonNull; +import android.widget.TextView; import org.commonmark.parser.Parser; @@ -17,11 +18,19 @@ class MarkwonBuilderImpl implements Markwon2.Builder { private final Context context; private final List plugins = new ArrayList<>(3); + private TextView.BufferType bufferType = TextView.BufferType.SPANNABLE; MarkwonBuilderImpl(@NonNull Context context) { this.context = context; } + @NonNull + @Override + public Markwon2.Builder bufferType(@NonNull TextView.BufferType bufferType) { + this.bufferType = bufferType; + return this; + } + @NonNull @Override public Markwon2.Builder use(@NonNull MarkwonPlugin plugin) { @@ -52,6 +61,7 @@ class MarkwonBuilderImpl implements Markwon2.Builder { asyncDrawableLoaderBuilder.build()); return new MarkwonImpl( + bufferType, parserBuilder.build(), visitorBuilder.build(configuration), Collections.unmodifiableList(plugins) diff --git a/markwon/src/main/java/ru/noties/markwon/MarkwonImpl.java b/markwon/src/main/java/ru/noties/markwon/MarkwonImpl.java index f92881e2..af604f26 100644 --- a/markwon/src/main/java/ru/noties/markwon/MarkwonImpl.java +++ b/markwon/src/main/java/ru/noties/markwon/MarkwonImpl.java @@ -10,14 +10,17 @@ import java.util.List; class MarkwonImpl extends Markwon2 { + private final TextView.BufferType bufferType; private final Parser parser; private final MarkwonVisitor visitor; private final List plugins; MarkwonImpl( + @NonNull TextView.BufferType bufferType, @NonNull Parser parser, @NonNull MarkwonVisitor visitor, @NonNull List plugins) { + this.bufferType = bufferType; this.parser = parser; this.visitor = visitor; this.plugins = plugins; @@ -59,7 +62,7 @@ class MarkwonImpl extends Markwon2 { plugin.beforeSetText(textView, markdown); } - textView.setText(markdown); + textView.setText(markdown, bufferType); for (MarkwonPlugin plugin : plugins) { plugin.afterSetText(textView); From d48b33e9a559a93efef4018f11873870645d3a6c Mon Sep 17 00:00:00 2001 From: Dimitry Ivanov Date: Mon, 26 Nov 2018 16:46:55 +0300 Subject: [PATCH 023/103] Add extension modules --- app/build.gradle | 3 + .../debug/DebugCheckboxDrawableView.java | 2 +- .../java/ru/noties/markwon/MainActivity.java | 7 +- .../ru/noties/markwon/MarkdownRenderer.java | 15 +- .../{ => gif}/GifAwareAsyncDrawable.java | 6 +- .../markwon/{ => gif}/GifAwarePlugin.java | 6 +- .../{ => gif}/GifAwareSpannableFactory.java | 8 +- .../markwon/{ => gif}/GifPlaceholder.java | 2 +- .../markwon/{ => gif}/GifProcessor.java | 2 +- markwon-ext-strikethrough/build.gradle | 25 + .../src/main/AndroidManifest.xml | 1 + .../strikethrough/StrikethroughPlugin.java | 38 ++ markwon-ext-tables/build.gradle | 25 + .../src/main/AndroidManifest.xml | 1 + .../markwon/ext/tables}/TablePlugin.java | 2 +- .../markwon/ext/tables}/TableRowSpan.java | 2 +- .../ext/tables}/TableRowsScheduler.java | 2 +- .../markwon/ext/tables}/TableTheme.java | 2 +- markwon-ext-tasklist/build.gradle | 20 + .../src/main/AndroidManifest.xml | 1 + .../markwon/ext}/tasklist/TaskListBlock.java | 2 +- .../ext}/tasklist/TaskListBlockParser.java | 2 +- .../ext}/tasklist/TaskListDrawable.java | 2 +- .../markwon/ext}/tasklist/TaskListItem.java | 2 +- .../markwon/ext}/tasklist/TaskListPlugin.java | 2 +- .../markwon/ext}/tasklist/TaskListSpan.java | 2 +- .../markwon/html/impl/tag/ImageHandler.java | 2 +- .../html/impl/tag/ImageSizeParserImpl.java | 2 +- .../markwon/html/impl/tag/StrikeHandler.java | 3 +- .../impl/tag/ImageSizeParserImplTest.java | 2 +- .../markwon/syntax/Prism4jThemeDefault.java | 18 +- markwon/build.gradle | 2 - .../main/java/ru/noties/markwon/Markwon.java | 218 +------- .../main/java/ru/noties/markwon/Markwon2.java | 47 -- .../ru/noties/markwon/MarkwonBuilderImpl.java | 8 +- .../noties/markwon/MarkwonConfiguration.java | 28 +- .../java/ru/noties/markwon/MarkwonImpl.java | 2 +- .../ru/noties/markwon/SpannableFactory.java | 7 +- .../noties/markwon/SpannableFactoryDef.java | 14 +- .../noties/markwon/image/AsyncDrawable.java | 3 - .../{renderer => image}/ImageSize.java | 2 +- .../ImageSizeResolver.java | 2 +- .../ImageSizeResolverDef.java | 2 +- .../renderer/SpannableMarkdownVisitor.java | 529 ------------------ .../markwon/renderer/SpannableRenderer.java | 18 - .../noties/markwon/spans/SubScriptSpan.java | 28 - .../noties/markwon/spans/SuperScriptSpan.java | 28 - .../markwon/tasklist/TaskListExtension.java | 22 - .../renderer/ImageSizeResolverDefTest.java | 8 +- .../renderer/MarkwonConfigurationTest.java | 1 + .../html2/tag/ImageSizeParserImplTest.java | 2 +- .../markwon/renderer/visitor/TestFactory.java | 4 +- .../sample/jlatexmath/MainActivity.java | 2 +- settings.gradle | 14 +- 54 files changed, 231 insertions(+), 969 deletions(-) rename app/src/main/java/ru/noties/markwon/{ => gif}/GifAwareAsyncDrawable.java (93%) rename app/src/main/java/ru/noties/markwon/{ => gif}/GifAwarePlugin.java (86%) rename app/src/main/java/ru/noties/markwon/{ => gif}/GifAwareSpannableFactory.java (87%) rename app/src/main/java/ru/noties/markwon/{ => gif}/GifPlaceholder.java (98%) rename app/src/main/java/ru/noties/markwon/{ => gif}/GifProcessor.java (99%) create mode 100644 markwon-ext-strikethrough/build.gradle create mode 100644 markwon-ext-strikethrough/src/main/AndroidManifest.xml create mode 100644 markwon-ext-strikethrough/src/main/java/ru/noties/markwon/ext/strikethrough/StrikethroughPlugin.java create mode 100644 markwon-ext-tables/build.gradle create mode 100644 markwon-ext-tables/src/main/AndroidManifest.xml rename {markwon/src/main/java/ru/noties/markwon/table => markwon-ext-tables/src/main/java/ru/noties/markwon/ext/tables}/TablePlugin.java (99%) rename {markwon/src/main/java/ru/noties/markwon/table => markwon-ext-tables/src/main/java/ru/noties/markwon/ext/tables}/TableRowSpan.java (99%) rename {markwon/src/main/java/ru/noties/markwon/table => markwon-ext-tables/src/main/java/ru/noties/markwon/ext/tables}/TableRowsScheduler.java (98%) rename {markwon/src/main/java/ru/noties/markwon/table => markwon-ext-tables/src/main/java/ru/noties/markwon/ext/tables}/TableTheme.java (99%) create mode 100644 markwon-ext-tasklist/build.gradle create mode 100644 markwon-ext-tasklist/src/main/AndroidManifest.xml rename {markwon/src/main/java/ru/noties/markwon => markwon-ext-tasklist/src/main/java/ru/noties/markwon/ext}/tasklist/TaskListBlock.java (74%) rename {markwon/src/main/java/ru/noties/markwon => markwon-ext-tasklist/src/main/java/ru/noties/markwon/ext}/tasklist/TaskListBlockParser.java (99%) rename {markwon/src/main/java/ru/noties/markwon => markwon-ext-tasklist/src/main/java/ru/noties/markwon/ext}/tasklist/TaskListDrawable.java (99%) rename {markwon/src/main/java/ru/noties/markwon => markwon-ext-tasklist/src/main/java/ru/noties/markwon/ext}/tasklist/TaskListItem.java (92%) rename {markwon/src/main/java/ru/noties/markwon => markwon-ext-tasklist/src/main/java/ru/noties/markwon/ext}/tasklist/TaskListPlugin.java (99%) rename {markwon/src/main/java/ru/noties/markwon => markwon-ext-tasklist/src/main/java/ru/noties/markwon/ext}/tasklist/TaskListSpan.java (98%) delete mode 100644 markwon/src/main/java/ru/noties/markwon/Markwon2.java rename markwon/src/main/java/ru/noties/markwon/{renderer => image}/ImageSize.java (96%) rename markwon/src/main/java/ru/noties/markwon/{renderer => image}/ImageSizeResolver.java (95%) rename markwon/src/main/java/ru/noties/markwon/{renderer => image}/ImageSizeResolverDef.java (98%) delete mode 100644 markwon/src/main/java/ru/noties/markwon/renderer/SpannableMarkdownVisitor.java delete mode 100644 markwon/src/main/java/ru/noties/markwon/renderer/SpannableRenderer.java delete mode 100644 markwon/src/main/java/ru/noties/markwon/spans/SubScriptSpan.java delete mode 100644 markwon/src/main/java/ru/noties/markwon/spans/SuperScriptSpan.java delete mode 100644 markwon/src/main/java/ru/noties/markwon/tasklist/TaskListExtension.java diff --git a/app/build.gradle b/app/build.gradle index ef215520..e0d0fbb8 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -29,6 +29,9 @@ android { dependencies { implementation project(':markwon') + implementation project(':markwon-ext-strikethrough') + implementation project(':markwon-ext-tables') + implementation project(':markwon-ext-tasklist') implementation project(':markwon-html') implementation project(':markwon-image-gif') implementation project(':markwon-image-svg') diff --git a/app/src/debug/java/ru/noties/markwon/debug/DebugCheckboxDrawableView.java b/app/src/debug/java/ru/noties/markwon/debug/DebugCheckboxDrawableView.java index e99e35fc..4c9027cc 100644 --- a/app/src/debug/java/ru/noties/markwon/debug/DebugCheckboxDrawableView.java +++ b/app/src/debug/java/ru/noties/markwon/debug/DebugCheckboxDrawableView.java @@ -9,7 +9,7 @@ import android.util.AttributeSet; import android.view.View; import ru.noties.markwon.R; -import ru.noties.markwon.tasklist.TaskListDrawable; +import ru.noties.markwon.ext.tasklist.TaskListDrawable; public class DebugCheckboxDrawableView extends View { diff --git a/app/src/main/java/ru/noties/markwon/MainActivity.java b/app/src/main/java/ru/noties/markwon/MainActivity.java index 34ed71de..f55c3ccc 100644 --- a/app/src/main/java/ru/noties/markwon/MainActivity.java +++ b/app/src/main/java/ru/noties/markwon/MainActivity.java @@ -13,9 +13,6 @@ import android.widget.TextView; import javax.inject.Inject; import ru.noties.debug.Debug; -import ru.noties.markwon.core.CorePlugin; -import ru.noties.markwon.tasklist.TaskListDrawable; -import ru.noties.markwon.tasklist.TaskListPlugin; public class MainActivity extends Activity { @@ -74,9 +71,9 @@ public class MainActivity extends Activity { public void apply(final String text) { markdownRenderer.render(MainActivity.this, themes.isLight(), uri(), text, new MarkdownRenderer.MarkdownReadyListener() { @Override - public void onMarkdownReady(@NonNull Markwon2 markwon2, CharSequence markdown) { + public void onMarkdownReady(@NonNull Markwon markwon, CharSequence markdown) { - markwon2.setParsedMarkdown(textView, markdown); + markwon.setParsedMarkdown(textView, markdown); Views.setVisible(progress, false); } diff --git a/app/src/main/java/ru/noties/markwon/MarkdownRenderer.java b/app/src/main/java/ru/noties/markwon/MarkdownRenderer.java index bd09825f..2da031f0 100644 --- a/app/src/main/java/ru/noties/markwon/MarkdownRenderer.java +++ b/app/src/main/java/ru/noties/markwon/MarkdownRenderer.java @@ -14,6 +14,10 @@ import javax.inject.Inject; import ru.noties.debug.Debug; import ru.noties.markwon.core.CorePlugin; +import ru.noties.markwon.ext.strikethrough.StrikethroughPlugin; +import ru.noties.markwon.ext.tables.TablePlugin; +import ru.noties.markwon.ext.tasklist.TaskListPlugin; +import ru.noties.markwon.gif.GifAwarePlugin; import ru.noties.markwon.html.impl.HtmlPlugin; import ru.noties.markwon.image.ImagesPlugin; import ru.noties.markwon.image.gif.GifPlugin; @@ -22,15 +26,13 @@ import ru.noties.markwon.syntax.Prism4jTheme; import ru.noties.markwon.syntax.Prism4jThemeDarkula; import ru.noties.markwon.syntax.Prism4jThemeDefault; import ru.noties.markwon.syntax.SyntaxHighlightPlugin; -import ru.noties.markwon.table.TablePlugin; -import ru.noties.markwon.tasklist.TaskListPlugin; import ru.noties.prism4j.Prism4j; @ActivityScope public class MarkdownRenderer { interface MarkdownReadyListener { - void onMarkdownReady(@NonNull Markwon2 markwon2, CharSequence markdown); + void onMarkdownReady(@NonNull Markwon markwon, CharSequence markdown); } @Inject @@ -80,7 +82,7 @@ public class MarkdownRenderer { ? prism4jThemeDefault : prism4JThemeDarkula; - final Markwon2 markwon2 = Markwon2.builder(context) + final Markwon markwon = Markwon.builder(context) .use(CorePlugin.create()) .use(ImagesPlugin.createWithAssets(context)) .use(SvgPlugin.create(context.getResources())) @@ -89,6 +91,7 @@ public class MarkdownRenderer { .use(GifAwarePlugin.create(context)) .use(TablePlugin.create(context)) .use(TaskListPlugin.create(context)) + .use(StrikethroughPlugin.create()) .use(HtmlPlugin.create()) .use(new AbstractMarkwonPlugin() { @Override @@ -100,7 +103,7 @@ public class MarkdownRenderer { final long start = SystemClock.uptimeMillis(); - final CharSequence text = markwon2.toMarkdown(markdown); + final CharSequence text = markwon.toMarkdown(markdown); final long end = SystemClock.uptimeMillis(); @@ -111,7 +114,7 @@ public class MarkdownRenderer { @Override public void run() { if (!isCancelled()) { - listener.onMarkdownReady(markwon2, text); + listener.onMarkdownReady(markwon, text); task = null; } } diff --git a/app/src/main/java/ru/noties/markwon/GifAwareAsyncDrawable.java b/app/src/main/java/ru/noties/markwon/gif/GifAwareAsyncDrawable.java similarity index 93% rename from app/src/main/java/ru/noties/markwon/GifAwareAsyncDrawable.java rename to app/src/main/java/ru/noties/markwon/gif/GifAwareAsyncDrawable.java index ed16554f..b5ba34ce 100644 --- a/app/src/main/java/ru/noties/markwon/GifAwareAsyncDrawable.java +++ b/app/src/main/java/ru/noties/markwon/gif/GifAwareAsyncDrawable.java @@ -1,4 +1,4 @@ -package ru.noties.markwon; +package ru.noties.markwon.gif; import android.graphics.Canvas; import android.graphics.drawable.Drawable; @@ -7,8 +7,8 @@ import android.support.annotation.Nullable; import pl.droidsonroids.gif.GifDrawable; import ru.noties.markwon.image.AsyncDrawableLoader; -import ru.noties.markwon.renderer.ImageSize; -import ru.noties.markwon.renderer.ImageSizeResolver; +import ru.noties.markwon.image.ImageSize; +import ru.noties.markwon.image.ImageSizeResolver; import ru.noties.markwon.image.AsyncDrawable; public class GifAwareAsyncDrawable extends AsyncDrawable { diff --git a/app/src/main/java/ru/noties/markwon/GifAwarePlugin.java b/app/src/main/java/ru/noties/markwon/gif/GifAwarePlugin.java similarity index 86% rename from app/src/main/java/ru/noties/markwon/GifAwarePlugin.java rename to app/src/main/java/ru/noties/markwon/gif/GifAwarePlugin.java index bb2fafd4..6bc6682b 100644 --- a/app/src/main/java/ru/noties/markwon/GifAwarePlugin.java +++ b/app/src/main/java/ru/noties/markwon/gif/GifAwarePlugin.java @@ -1,9 +1,13 @@ -package ru.noties.markwon; +package ru.noties.markwon.gif; import android.content.Context; import android.support.annotation.NonNull; import android.widget.TextView; +import ru.noties.markwon.AbstractMarkwonPlugin; +import ru.noties.markwon.MarkwonConfiguration; +import ru.noties.markwon.R; + public class GifAwarePlugin extends AbstractMarkwonPlugin { @NonNull diff --git a/app/src/main/java/ru/noties/markwon/GifAwareSpannableFactory.java b/app/src/main/java/ru/noties/markwon/gif/GifAwareSpannableFactory.java similarity index 87% rename from app/src/main/java/ru/noties/markwon/GifAwareSpannableFactory.java rename to app/src/main/java/ru/noties/markwon/gif/GifAwareSpannableFactory.java index 432a379c..bf108477 100644 --- a/app/src/main/java/ru/noties/markwon/GifAwareSpannableFactory.java +++ b/app/src/main/java/ru/noties/markwon/gif/GifAwareSpannableFactory.java @@ -1,12 +1,12 @@ -package ru.noties.markwon; +package ru.noties.markwon.gif; import android.support.annotation.NonNull; import android.support.annotation.Nullable; +import ru.noties.markwon.SpannableFactoryDef; import ru.noties.markwon.image.AsyncDrawableLoader; -import ru.noties.markwon.renderer.ImageSize; -import ru.noties.markwon.renderer.ImageSizeResolver; -import ru.noties.markwon.image.AsyncDrawable; +import ru.noties.markwon.image.ImageSize; +import ru.noties.markwon.image.ImageSizeResolver; import ru.noties.markwon.spans.AsyncDrawableSpan; import ru.noties.markwon.spans.MarkwonTheme; diff --git a/app/src/main/java/ru/noties/markwon/GifPlaceholder.java b/app/src/main/java/ru/noties/markwon/gif/GifPlaceholder.java similarity index 98% rename from app/src/main/java/ru/noties/markwon/GifPlaceholder.java rename to app/src/main/java/ru/noties/markwon/gif/GifPlaceholder.java index 0ee66d0c..7d6dcbe1 100644 --- a/app/src/main/java/ru/noties/markwon/GifPlaceholder.java +++ b/app/src/main/java/ru/noties/markwon/gif/GifPlaceholder.java @@ -1,4 +1,4 @@ -package ru.noties.markwon; +package ru.noties.markwon.gif; import android.graphics.Canvas; import android.graphics.ColorFilter; diff --git a/app/src/main/java/ru/noties/markwon/GifProcessor.java b/app/src/main/java/ru/noties/markwon/gif/GifProcessor.java similarity index 99% rename from app/src/main/java/ru/noties/markwon/GifProcessor.java rename to app/src/main/java/ru/noties/markwon/gif/GifProcessor.java index 5be9cd1b..f50b1094 100644 --- a/app/src/main/java/ru/noties/markwon/GifProcessor.java +++ b/app/src/main/java/ru/noties/markwon/gif/GifProcessor.java @@ -1,4 +1,4 @@ -package ru.noties.markwon; +package ru.noties.markwon.gif; import android.support.annotation.NonNull; import android.support.annotation.Nullable; diff --git a/markwon-ext-strikethrough/build.gradle b/markwon-ext-strikethrough/build.gradle new file mode 100644 index 00000000..b4daa0d1 --- /dev/null +++ b/markwon-ext-strikethrough/build.gradle @@ -0,0 +1,25 @@ +apply plugin: 'com.android.library' + +android { + + compileSdkVersion config['compile-sdk'] + buildToolsVersion config['build-tools'] + + defaultConfig { + minSdkVersion config['min-sdk'] + targetSdkVersion config['target-sdk'] + versionCode 1 + versionName version + } +} + +dependencies { + + api project(':markwon') + + deps.with { + api it['commonmark-strikethrough'] + } +} + +registerArtifact(this) \ No newline at end of file diff --git a/markwon-ext-strikethrough/src/main/AndroidManifest.xml b/markwon-ext-strikethrough/src/main/AndroidManifest.xml new file mode 100644 index 00000000..6c243e1b --- /dev/null +++ b/markwon-ext-strikethrough/src/main/AndroidManifest.xml @@ -0,0 +1 @@ + diff --git a/markwon-ext-strikethrough/src/main/java/ru/noties/markwon/ext/strikethrough/StrikethroughPlugin.java b/markwon-ext-strikethrough/src/main/java/ru/noties/markwon/ext/strikethrough/StrikethroughPlugin.java new file mode 100644 index 00000000..69c55890 --- /dev/null +++ b/markwon-ext-strikethrough/src/main/java/ru/noties/markwon/ext/strikethrough/StrikethroughPlugin.java @@ -0,0 +1,38 @@ +package ru.noties.markwon.ext.strikethrough; + +import android.support.annotation.NonNull; +import android.text.style.StrikethroughSpan; + +import org.commonmark.ext.gfm.strikethrough.Strikethrough; +import org.commonmark.ext.gfm.strikethrough.StrikethroughExtension; +import org.commonmark.parser.Parser; + +import java.util.Collections; + +import ru.noties.markwon.AbstractMarkwonPlugin; +import ru.noties.markwon.MarkwonVisitor; + +public class StrikethroughPlugin extends AbstractMarkwonPlugin { + + @NonNull + public static StrikethroughPlugin create() { + return new StrikethroughPlugin(); + } + + @Override + public void configureParser(@NonNull Parser.Builder builder) { + builder.extensions(Collections.singleton(StrikethroughExtension.create())); + } + + @Override + public void configureVisitor(@NonNull MarkwonVisitor.Builder builder) { + builder.on(Strikethrough.class, new MarkwonVisitor.NodeVisitor() { + @Override + public void visit(@NonNull MarkwonVisitor visitor, @NonNull Strikethrough strikethrough) { + final int length = visitor.length(); + visitor.visitChildren(strikethrough); + visitor.setSpans(length, new StrikethroughSpan()); + } + }); + } +} diff --git a/markwon-ext-tables/build.gradle b/markwon-ext-tables/build.gradle new file mode 100644 index 00000000..b65c0c29 --- /dev/null +++ b/markwon-ext-tables/build.gradle @@ -0,0 +1,25 @@ +apply plugin: 'com.android.library' + +android { + + compileSdkVersion config['compile-sdk'] + buildToolsVersion config['build-tools'] + + defaultConfig { + minSdkVersion config['min-sdk'] + targetSdkVersion config['target-sdk'] + versionCode 1 + versionName version + } +} + +dependencies { + + api project(':markwon') + + deps.with { + api it['commonmark-table'] + } +} + +registerArtifact(this) \ No newline at end of file diff --git a/markwon-ext-tables/src/main/AndroidManifest.xml b/markwon-ext-tables/src/main/AndroidManifest.xml new file mode 100644 index 00000000..f65b9d67 --- /dev/null +++ b/markwon-ext-tables/src/main/AndroidManifest.xml @@ -0,0 +1 @@ + diff --git a/markwon/src/main/java/ru/noties/markwon/table/TablePlugin.java b/markwon-ext-tables/src/main/java/ru/noties/markwon/ext/tables/TablePlugin.java similarity index 99% rename from markwon/src/main/java/ru/noties/markwon/table/TablePlugin.java rename to markwon-ext-tables/src/main/java/ru/noties/markwon/ext/tables/TablePlugin.java index 506b8ff3..b4bbfb00 100644 --- a/markwon/src/main/java/ru/noties/markwon/table/TablePlugin.java +++ b/markwon-ext-tables/src/main/java/ru/noties/markwon/ext/tables/TablePlugin.java @@ -1,4 +1,4 @@ -package ru.noties.markwon.table; +package ru.noties.markwon.ext.tables; import android.content.Context; import android.support.annotation.NonNull; diff --git a/markwon/src/main/java/ru/noties/markwon/table/TableRowSpan.java b/markwon-ext-tables/src/main/java/ru/noties/markwon/ext/tables/TableRowSpan.java similarity index 99% rename from markwon/src/main/java/ru/noties/markwon/table/TableRowSpan.java rename to markwon-ext-tables/src/main/java/ru/noties/markwon/ext/tables/TableRowSpan.java index 903cf5f3..3f9bbace 100644 --- a/markwon/src/main/java/ru/noties/markwon/table/TableRowSpan.java +++ b/markwon-ext-tables/src/main/java/ru/noties/markwon/ext/tables/TableRowSpan.java @@ -1,4 +1,4 @@ -package ru.noties.markwon.table; +package ru.noties.markwon.ext.tables; import android.annotation.SuppressLint; import android.graphics.Canvas; diff --git a/markwon/src/main/java/ru/noties/markwon/table/TableRowsScheduler.java b/markwon-ext-tables/src/main/java/ru/noties/markwon/ext/tables/TableRowsScheduler.java similarity index 98% rename from markwon/src/main/java/ru/noties/markwon/table/TableRowsScheduler.java rename to markwon-ext-tables/src/main/java/ru/noties/markwon/ext/tables/TableRowsScheduler.java index 1d1246b0..2f85cf1f 100644 --- a/markwon/src/main/java/ru/noties/markwon/table/TableRowsScheduler.java +++ b/markwon-ext-tables/src/main/java/ru/noties/markwon/ext/tables/TableRowsScheduler.java @@ -1,4 +1,4 @@ -package ru.noties.markwon.table; +package ru.noties.markwon.ext.tables; import android.support.annotation.NonNull; import android.support.annotation.Nullable; diff --git a/markwon/src/main/java/ru/noties/markwon/table/TableTheme.java b/markwon-ext-tables/src/main/java/ru/noties/markwon/ext/tables/TableTheme.java similarity index 99% rename from markwon/src/main/java/ru/noties/markwon/table/TableTheme.java rename to markwon-ext-tables/src/main/java/ru/noties/markwon/ext/tables/TableTheme.java index 56a72321..a868c7c7 100644 --- a/markwon/src/main/java/ru/noties/markwon/table/TableTheme.java +++ b/markwon-ext-tables/src/main/java/ru/noties/markwon/ext/tables/TableTheme.java @@ -1,4 +1,4 @@ -package ru.noties.markwon.table; +package ru.noties.markwon.ext.tables; import android.content.Context; import android.graphics.Paint; diff --git a/markwon-ext-tasklist/build.gradle b/markwon-ext-tasklist/build.gradle new file mode 100644 index 00000000..798dd0b3 --- /dev/null +++ b/markwon-ext-tasklist/build.gradle @@ -0,0 +1,20 @@ +apply plugin: 'com.android.library' + +android { + + compileSdkVersion config['compile-sdk'] + buildToolsVersion config['build-tools'] + + defaultConfig { + minSdkVersion config['min-sdk'] + targetSdkVersion config['target-sdk'] + versionCode 1 + versionName version + } +} + +dependencies { + api project(':markwon') +} + +registerArtifact(this) \ No newline at end of file diff --git a/markwon-ext-tasklist/src/main/AndroidManifest.xml b/markwon-ext-tasklist/src/main/AndroidManifest.xml new file mode 100644 index 00000000..14f5d738 --- /dev/null +++ b/markwon-ext-tasklist/src/main/AndroidManifest.xml @@ -0,0 +1 @@ + diff --git a/markwon/src/main/java/ru/noties/markwon/tasklist/TaskListBlock.java b/markwon-ext-tasklist/src/main/java/ru/noties/markwon/ext/tasklist/TaskListBlock.java similarity index 74% rename from markwon/src/main/java/ru/noties/markwon/tasklist/TaskListBlock.java rename to markwon-ext-tasklist/src/main/java/ru/noties/markwon/ext/tasklist/TaskListBlock.java index 3f6df97d..4e3cc8b4 100644 --- a/markwon/src/main/java/ru/noties/markwon/tasklist/TaskListBlock.java +++ b/markwon-ext-tasklist/src/main/java/ru/noties/markwon/ext/tasklist/TaskListBlock.java @@ -1,4 +1,4 @@ -package ru.noties.markwon.tasklist; +package ru.noties.markwon.ext.tasklist; import org.commonmark.node.CustomBlock; diff --git a/markwon/src/main/java/ru/noties/markwon/tasklist/TaskListBlockParser.java b/markwon-ext-tasklist/src/main/java/ru/noties/markwon/ext/tasklist/TaskListBlockParser.java similarity index 99% rename from markwon/src/main/java/ru/noties/markwon/tasklist/TaskListBlockParser.java rename to markwon-ext-tasklist/src/main/java/ru/noties/markwon/ext/tasklist/TaskListBlockParser.java index 4c2ab99a..842c53ca 100644 --- a/markwon/src/main/java/ru/noties/markwon/tasklist/TaskListBlockParser.java +++ b/markwon-ext-tasklist/src/main/java/ru/noties/markwon/ext/tasklist/TaskListBlockParser.java @@ -1,4 +1,4 @@ -package ru.noties.markwon.tasklist; +package ru.noties.markwon.ext.tasklist; import android.support.annotation.NonNull; import android.support.annotation.Nullable; diff --git a/markwon/src/main/java/ru/noties/markwon/tasklist/TaskListDrawable.java b/markwon-ext-tasklist/src/main/java/ru/noties/markwon/ext/tasklist/TaskListDrawable.java similarity index 99% rename from markwon/src/main/java/ru/noties/markwon/tasklist/TaskListDrawable.java rename to markwon-ext-tasklist/src/main/java/ru/noties/markwon/ext/tasklist/TaskListDrawable.java index 15924e8f..4a02c6c4 100644 --- a/markwon/src/main/java/ru/noties/markwon/tasklist/TaskListDrawable.java +++ b/markwon-ext-tasklist/src/main/java/ru/noties/markwon/ext/tasklist/TaskListDrawable.java @@ -1,4 +1,4 @@ -package ru.noties.markwon.tasklist; +package ru.noties.markwon.ext.tasklist; import android.graphics.Canvas; import android.graphics.ColorFilter; diff --git a/markwon/src/main/java/ru/noties/markwon/tasklist/TaskListItem.java b/markwon-ext-tasklist/src/main/java/ru/noties/markwon/ext/tasklist/TaskListItem.java similarity index 92% rename from markwon/src/main/java/ru/noties/markwon/tasklist/TaskListItem.java rename to markwon-ext-tasklist/src/main/java/ru/noties/markwon/ext/tasklist/TaskListItem.java index 2c012ee3..65b969db 100644 --- a/markwon/src/main/java/ru/noties/markwon/tasklist/TaskListItem.java +++ b/markwon-ext-tasklist/src/main/java/ru/noties/markwon/ext/tasklist/TaskListItem.java @@ -1,4 +1,4 @@ -package ru.noties.markwon.tasklist; +package ru.noties.markwon.ext.tasklist; import org.commonmark.node.CustomNode; diff --git a/markwon/src/main/java/ru/noties/markwon/tasklist/TaskListPlugin.java b/markwon-ext-tasklist/src/main/java/ru/noties/markwon/ext/tasklist/TaskListPlugin.java similarity index 99% rename from markwon/src/main/java/ru/noties/markwon/tasklist/TaskListPlugin.java rename to markwon-ext-tasklist/src/main/java/ru/noties/markwon/ext/tasklist/TaskListPlugin.java index 77c865c4..dc81b8f3 100644 --- a/markwon/src/main/java/ru/noties/markwon/tasklist/TaskListPlugin.java +++ b/markwon-ext-tasklist/src/main/java/ru/noties/markwon/ext/tasklist/TaskListPlugin.java @@ -1,4 +1,4 @@ -package ru.noties.markwon.tasklist; +package ru.noties.markwon.ext.tasklist; import android.content.Context; import android.content.res.TypedArray; diff --git a/markwon/src/main/java/ru/noties/markwon/tasklist/TaskListSpan.java b/markwon-ext-tasklist/src/main/java/ru/noties/markwon/ext/tasklist/TaskListSpan.java similarity index 98% rename from markwon/src/main/java/ru/noties/markwon/tasklist/TaskListSpan.java rename to markwon-ext-tasklist/src/main/java/ru/noties/markwon/ext/tasklist/TaskListSpan.java index 5c59b167..efddfe25 100644 --- a/markwon/src/main/java/ru/noties/markwon/tasklist/TaskListSpan.java +++ b/markwon-ext-tasklist/src/main/java/ru/noties/markwon/ext/tasklist/TaskListSpan.java @@ -1,4 +1,4 @@ -package ru.noties.markwon.tasklist; +package ru.noties.markwon.ext.tasklist; import android.graphics.Canvas; import android.graphics.Paint; diff --git a/markwon-html/src/main/java/ru/noties/markwon/html/impl/tag/ImageHandler.java b/markwon-html/src/main/java/ru/noties/markwon/html/impl/tag/ImageHandler.java index adbf745e..0fc9b6f2 100644 --- a/markwon-html/src/main/java/ru/noties/markwon/html/impl/tag/ImageHandler.java +++ b/markwon-html/src/main/java/ru/noties/markwon/html/impl/tag/ImageHandler.java @@ -9,7 +9,7 @@ import java.util.Map; import ru.noties.markwon.MarkwonConfiguration; import ru.noties.markwon.html.HtmlTag; import ru.noties.markwon.html.impl.CssInlineStyleParser; -import ru.noties.markwon.renderer.ImageSize; +import ru.noties.markwon.image.ImageSize; public class ImageHandler extends SimpleTagHandler { diff --git a/markwon-html/src/main/java/ru/noties/markwon/html/impl/tag/ImageSizeParserImpl.java b/markwon-html/src/main/java/ru/noties/markwon/html/impl/tag/ImageSizeParserImpl.java index 04556327..e8d32367 100644 --- a/markwon-html/src/main/java/ru/noties/markwon/html/impl/tag/ImageSizeParserImpl.java +++ b/markwon-html/src/main/java/ru/noties/markwon/html/impl/tag/ImageSizeParserImpl.java @@ -9,7 +9,7 @@ import java.util.Map; import ru.noties.markwon.html.impl.CssInlineStyleParser; import ru.noties.markwon.html.impl.CssProperty; -import ru.noties.markwon.renderer.ImageSize; +import ru.noties.markwon.image.ImageSize; class ImageSizeParserImpl implements ImageHandler.ImageSizeParser { diff --git a/markwon-html/src/main/java/ru/noties/markwon/html/impl/tag/StrikeHandler.java b/markwon-html/src/main/java/ru/noties/markwon/html/impl/tag/StrikeHandler.java index 4d59b152..54497822 100644 --- a/markwon-html/src/main/java/ru/noties/markwon/html/impl/tag/StrikeHandler.java +++ b/markwon-html/src/main/java/ru/noties/markwon/html/impl/tag/StrikeHandler.java @@ -1,6 +1,7 @@ package ru.noties.markwon.html.impl.tag; import android.support.annotation.NonNull; +import android.text.style.StrikethroughSpan; import ru.noties.markwon.MarkwonConfiguration; import ru.noties.markwon.SpannableBuilder; @@ -21,7 +22,7 @@ public class StrikeHandler extends TagHandler { SpannableBuilder.setSpans( builder, - configuration.factory().strikethrough(), + new StrikethroughSpan(), tag.start(), tag.end() ); diff --git a/markwon-html/src/test/java/ru/noties/markwon/html/impl/tag/ImageSizeParserImplTest.java b/markwon-html/src/test/java/ru/noties/markwon/html/impl/tag/ImageSizeParserImplTest.java index 006427e8..37a10c8d 100644 --- a/markwon-html/src/test/java/ru/noties/markwon/html/impl/tag/ImageSizeParserImplTest.java +++ b/markwon-html/src/test/java/ru/noties/markwon/html/impl/tag/ImageSizeParserImplTest.java @@ -13,7 +13,7 @@ import java.util.Collections; import java.util.HashMap; import java.util.Map; -import ru.noties.markwon.renderer.ImageSize; +import ru.noties.markwon.image.ImageSize; import ru.noties.markwon.renderer.html2.CssInlineStyleParser; import static org.junit.Assert.assertEquals; diff --git a/markwon-syntax-highlight/src/main/java/ru/noties/markwon/syntax/Prism4jThemeDefault.java b/markwon-syntax-highlight/src/main/java/ru/noties/markwon/syntax/Prism4jThemeDefault.java index 22354dce..4356e13c 100644 --- a/markwon-syntax-highlight/src/main/java/ru/noties/markwon/syntax/Prism4jThemeDefault.java +++ b/markwon-syntax-highlight/src/main/java/ru/noties/markwon/syntax/Prism4jThemeDefault.java @@ -14,12 +14,26 @@ public class Prism4jThemeDefault extends Prism4jThemeBase { @NonNull public static Prism4jThemeDefault create() { - return new Prism4jThemeDefault(); + return new Prism4jThemeDefault(0xFFf5f2f0); + } + + /** + * @since 3.0.0 + */ + @NonNull + public static Prism4jThemeDefault create(@ColorInt int background) { + return new Prism4jThemeDefault(background); + } + + private final int background; + + public Prism4jThemeDefault(@ColorInt int background) { + this.background = background; } @Override public int background() { - return 0xFFf5f2f0; + return background; } @Override diff --git a/markwon/build.gradle b/markwon/build.gradle index 3cb54118..e1d9db87 100644 --- a/markwon/build.gradle +++ b/markwon/build.gradle @@ -18,8 +18,6 @@ dependencies { deps.with { api it['support-annotations'] api it['commonmark'] - api it['commonmark-strikethrough'] - api it['commonmark-table'] } deps['test'].with { diff --git a/markwon/src/main/java/ru/noties/markwon/Markwon.java b/markwon/src/main/java/ru/noties/markwon/Markwon.java index 022c3ca8..b9fe2dad 100644 --- a/markwon/src/main/java/ru/noties/markwon/Markwon.java +++ b/markwon/src/main/java/ru/noties/markwon/Markwon.java @@ -2,216 +2,46 @@ package ru.noties.markwon; import android.content.Context; import android.support.annotation.NonNull; -import android.support.annotation.Nullable; -import android.text.method.LinkMovementMethod; -import android.text.method.MovementMethod; import android.widget.TextView; -import org.commonmark.ext.gfm.strikethrough.StrikethroughExtension; -import org.commonmark.ext.gfm.tables.TablesExtension; import org.commonmark.node.Node; -import org.commonmark.parser.Parser; -import java.util.Arrays; - -import ru.noties.markwon.image.AsyncDrawable; -//import ru.noties.markwon.image.DrawablesScheduler; -import ru.noties.markwon.renderer.SpannableRenderer; -import ru.noties.markwon.spans.OrderedListItemSpan; -import ru.noties.markwon.table.TableRowSpan; -import ru.noties.markwon.tasklist.TaskListExtension; - -@SuppressWarnings({"WeakerAccess", "unused"}) public abstract class Markwon { - /** - * Helper method to obtain a Parser with registered strike-through & table extensions - * & task lists (added in 1.0.1) - * - * @return a Parser instance that is supported by this library - * @since 1.0.0 - */ @NonNull - public static Parser createParser() { - return new Parser.Builder() - .extensions(Arrays.asList( - StrikethroughExtension.create(), - TablesExtension.create(), - TaskListExtension.create() - )) - .build(); + public static Builder builder(@NonNull Context context) { + return new MarkwonBuilderImpl(context); } - /** - * @see #setMarkdown(TextView, MarkwonConfiguration, String) - * @since 1.0.0 - */ - public static void setMarkdown(@NonNull TextView view, @NonNull String markdown) { - setMarkdown(view, MarkwonConfiguration.create(view.getContext()), markdown); - } - - /** - * Parses submitted raw toMarkdown, converts it to CharSequence (with Spannables) - * and applies it to view - * - * @param view {@link TextView} to set toMarkdown into - * @param configuration a {@link MarkwonConfiguration} instance - * @param markdown raw toMarkdown String (for example: {@code `**Hello**`}) - * @see #markdown(MarkwonConfiguration, String) - * @see #setText(TextView, CharSequence) - * @see MarkwonConfiguration - * @since 1.0.0 - */ - public static void setMarkdown( - @NonNull TextView view, - @NonNull MarkwonConfiguration configuration, - @NonNull String markdown - ) { - - setText(view, markdown(configuration, markdown)); - } - - /** - * Helper method to apply parsed toMarkdown. - *

- * Since 1.0.6 redirects it\'s call to {@link #setText(TextView, CharSequence, MovementMethod)} - * with LinkMovementMethod as an argument to preserve current API. - * - * @param view {@link TextView} to set toMarkdown into - * @param text parsed toMarkdown - * @see #setText(TextView, CharSequence, MovementMethod) - * @since 1.0.0 - */ - public static void setText(@NonNull TextView view, CharSequence text) { - setText(view, text, LinkMovementMethod.getInstance()); - } - - /** - * Helper method to apply parsed toMarkdown with additional argument of a MovementMethod. Used - * to workaround problems that occur when using system LinkMovementMethod (for example: - * https://issuetracker.google.com/issues/37068143). As a better alternative to it consider - * using: https://github.com/saket/Better-Link-Movement-Method - * - * @param view TextView to set toMarkdown into - * @param text parsed toMarkdown - * @param movementMethod an implementation if MovementMethod or null - * @see #scheduleDrawables(TextView) - * @see #scheduleTableRows(TextView) - * @since 1.0.6 - */ - public static void setText(@NonNull TextView view, CharSequence text, @Nullable MovementMethod movementMethod) { - - unscheduleDrawables(view); - unscheduleTableRows(view); - - // @since 2.0.1 we must measure ordered-list-item-spans before applying text to a TextView. - // if toMarkdown has a lot of ordered list items (or text size is relatively big, or block-margin - // is relatively small) then this list won't be rendered properly: it will take correct - // layout (width and margin) but will be clipped if margin is not _consistent_ between calls. - OrderedListItemSpan.measure(view, text); - - // update movement method (for links to be clickable) - view.setMovementMethod(movementMethod); - view.setText(text); - - // schedule drawables (dynamic drawables that can change bounds/animate will be correctly updated) - scheduleDrawables(view); - scheduleTableRows(view); - } - - /** - * Returns parsed toMarkdown with default {@link MarkwonConfiguration} obtained from {@link Context} - * - * @param context {@link Context} - * @param markdown raw toMarkdown - * @return parsed toMarkdown - * @since 1.0.0 - */ @NonNull - public static CharSequence markdown(@NonNull Context context, @NonNull String markdown) { - final MarkwonConfiguration configuration = MarkwonConfiguration.create(context); - return markdown(configuration, markdown); - } + public abstract Node parse(@NonNull String input); - /** - * Returns parsed toMarkdown with provided {@link MarkwonConfiguration} - * - * @param configuration a {@link MarkwonConfiguration} - * @param markdown raw toMarkdown - * @return parsed toMarkdown - * @see MarkwonConfiguration - * @since 1.0.0 - */ @NonNull - public static CharSequence markdown(@NonNull MarkwonConfiguration configuration, @NonNull String markdown) { - final Parser parser = createParser(); - final Node node = parser.parse(markdown); - final SpannableRenderer renderer = new SpannableRenderer(); - return renderer.render(configuration, node); - } + public abstract CharSequence render(@NonNull Node node); - /** - * This method adds support for {@link AsyncDrawable} to be used. As - * textView seems not to support drawables that change bounds (and gives no means - * to update the layout), we create own {@link android.graphics.drawable.Drawable.Callback} - * and apply it. So, textView can display drawables, that are: async (loading from disk, network); - * dynamic (requires `invalidate`) - GIF, animations. - * Please note, that this method should be preceded with {@link #unscheduleDrawables(TextView)} - * in order to avoid keeping drawables in memory after they have been removed from layout - * - * @param view a {@link TextView} - * @see AsyncDrawable - * @see ru.noties.markwon.spans.AsyncDrawableSpan - * @see DrawablesScheduler#schedule(TextView) - * @see DrawablesScheduler#unschedule(TextView) - * @since 1.0.0 - */ - public static void scheduleDrawables(@NonNull TextView view) { -// DrawablesScheduler.schedule(view); - } + // parse + render + @NonNull + public abstract CharSequence toMarkdown(@NonNull String input); - /** - * De-references previously scheduled {@link ru.noties.markwon.spans.AsyncDrawableSpan}'s - * - * @param view a {@link TextView} - * @see #scheduleDrawables(TextView) - * @since 1.0.0 - */ - public static void unscheduleDrawables(@NonNull TextView view) { -// DrawablesScheduler.unschedule(view); - } + public abstract void setMarkdown(@NonNull TextView textView, @NonNull String markdown); - /** - * This method is required in order to use tables. A bit of background: - * this library uses a {@link android.text.style.ReplacementSpan} to - * render tables, but the flow is not really flexible. We are required - * to return `size` (width) of our replacement, but we are not provided - * with the total one (canvas width). In order to correctly calculate height of our - * table cell text, we must have available width first. This method gives - * ability for {@link TableRowSpan} to invalidate - * `view` when it encounters such a situation (when available width is not known or have changed). - * Precede this call with {@link #unscheduleTableRows(TextView)} in order to - * de-reference previously scheduled {@link TableRowSpan}'s - * - * @param view a {@link TextView} - * @see #unscheduleTableRows(TextView) - * @since 1.0.0 - */ - public static void scheduleTableRows(@NonNull TextView view) { -// TableRowsScheduler.schedule(view); - } + public abstract void setParsedMarkdown(@NonNull TextView textView, @NonNull CharSequence markdown); - /** - * De-references previously scheduled {@link TableRowSpan}'s - * - * @param view a {@link TextView} - * @see #scheduleTableRows(TextView) - * @since 1.0.0 - */ - public static void unscheduleTableRows(@NonNull TextView view) { -// TableRowsScheduler.unschedule(view); - } + public interface Builder { - private Markwon() { + /** + * Specify bufferType when applying text to a TextView {@code textView.setText(CharSequence,BufferType)}. + * By default `BufferType.SPANNABLE` is used + * + * @param bufferType BufferType + */ + @NonNull + Builder bufferType(@NonNull TextView.BufferType bufferType); + + @NonNull + Builder use(@NonNull MarkwonPlugin plugin); + + @NonNull + Markwon build(); } } diff --git a/markwon/src/main/java/ru/noties/markwon/Markwon2.java b/markwon/src/main/java/ru/noties/markwon/Markwon2.java deleted file mode 100644 index 5d4f3847..00000000 --- a/markwon/src/main/java/ru/noties/markwon/Markwon2.java +++ /dev/null @@ -1,47 +0,0 @@ -package ru.noties.markwon; - -import android.content.Context; -import android.support.annotation.NonNull; -import android.widget.TextView; - -import org.commonmark.node.Node; - -public abstract class Markwon2 { - - @NonNull - public static Builder builder(@NonNull Context context) { - return new MarkwonBuilderImpl(context); - } - - @NonNull - public abstract Node parse(@NonNull String input); - - @NonNull - public abstract CharSequence render(@NonNull Node node); - - // parse + render - @NonNull - public abstract CharSequence toMarkdown(@NonNull String input); - - public abstract void setMarkdown(@NonNull TextView textView, @NonNull String markdown); - - public abstract void setParsedMarkdown(@NonNull TextView textView, @NonNull CharSequence markdown); - - public interface Builder { - - /** - * Specify bufferType when applying text to a TextView {@code textView.setText(CharSequence,BufferType)}. - * By default `BufferType.SPANNABLE` is used - * - * @param bufferType BufferType - */ - @NonNull - Builder bufferType(@NonNull TextView.BufferType bufferType); - - @NonNull - Builder use(@NonNull MarkwonPlugin plugin); - - @NonNull - Markwon2 build(); - } -} diff --git a/markwon/src/main/java/ru/noties/markwon/MarkwonBuilderImpl.java b/markwon/src/main/java/ru/noties/markwon/MarkwonBuilderImpl.java index cc607322..5c65c6a9 100644 --- a/markwon/src/main/java/ru/noties/markwon/MarkwonBuilderImpl.java +++ b/markwon/src/main/java/ru/noties/markwon/MarkwonBuilderImpl.java @@ -13,7 +13,7 @@ import java.util.List; import ru.noties.markwon.image.AsyncDrawableLoader; import ru.noties.markwon.spans.MarkwonTheme; -class MarkwonBuilderImpl implements Markwon2.Builder { +class MarkwonBuilderImpl implements Markwon.Builder { private final Context context; @@ -26,21 +26,21 @@ class MarkwonBuilderImpl implements Markwon2.Builder { @NonNull @Override - public Markwon2.Builder bufferType(@NonNull TextView.BufferType bufferType) { + public Markwon.Builder bufferType(@NonNull TextView.BufferType bufferType) { this.bufferType = bufferType; return this; } @NonNull @Override - public Markwon2.Builder use(@NonNull MarkwonPlugin plugin) { + public Markwon.Builder use(@NonNull MarkwonPlugin plugin) { plugins.add(plugin); return this; } @NonNull @Override - public Markwon2 build() { + public Markwon build() { final Parser.Builder parserBuilder = new Parser.Builder(); final MarkwonTheme.Builder themeBuilder = MarkwonTheme.builderWithDefaults(context); diff --git a/markwon/src/main/java/ru/noties/markwon/MarkwonConfiguration.java b/markwon/src/main/java/ru/noties/markwon/MarkwonConfiguration.java index 33be0601..df083e42 100644 --- a/markwon/src/main/java/ru/noties/markwon/MarkwonConfiguration.java +++ b/markwon/src/main/java/ru/noties/markwon/MarkwonConfiguration.java @@ -7,8 +7,8 @@ import ru.noties.markwon.html.MarkwonHtmlParser; import ru.noties.markwon.html.MarkwonHtmlRenderer; import ru.noties.markwon.image.AsyncDrawableLoader; import ru.noties.markwon.image.AsyncDrawableLoaderNoOp; -import ru.noties.markwon.renderer.ImageSizeResolver; -import ru.noties.markwon.renderer.ImageSizeResolverDef; +import ru.noties.markwon.image.ImageSizeResolver; +import ru.noties.markwon.image.ImageSizeResolverDef; import ru.noties.markwon.spans.LinkSpan; import ru.noties.markwon.spans.MarkwonTheme; @@ -39,7 +39,6 @@ public class MarkwonConfiguration { private final SpannableFactory factory; // @since 1.1.0 private final MarkwonHtmlParser htmlParser; // @since 2.0.0 private final MarkwonHtmlRenderer htmlRenderer; // @since 2.0.0 -// private final boolean htmlAllowNonClosedTags; // @since 2.0.0 private MarkwonConfiguration(@NonNull Builder builder) { this.theme = builder.theme; @@ -51,7 +50,6 @@ public class MarkwonConfiguration { this.factory = builder.factory; this.htmlParser = builder.htmlParser; this.htmlRenderer = builder.htmlRenderer; -// this.htmlAllowNonClosedTags = builder.htmlAllowNonClosedTags; } /** @@ -113,13 +111,6 @@ public class MarkwonConfiguration { return htmlRenderer; } -// /** -// * @since 2.0.0 -// */ -// public boolean htmlAllowNonClosedTags() { -// return htmlAllowNonClosedTags; -// } - @SuppressWarnings("unused") public static class Builder { @@ -134,7 +125,6 @@ public class MarkwonConfiguration { private SpannableFactory factory; // @since 1.1.0 private MarkwonHtmlParser htmlParser; // @since 2.0.0 private MarkwonHtmlRenderer htmlRenderer; // @since 2.0.0 -// private boolean htmlAllowNonClosedTags; // @since 2.0.0 Builder(@NonNull Context context) { this.context = context; @@ -151,7 +141,6 @@ public class MarkwonConfiguration { this.factory = configuration.factory; this.htmlParser = configuration.htmlParser; this.htmlRenderer = configuration.htmlRenderer; -// this.htmlAllowNonClosedTags = configuration.htmlAllowNonClosedTags; } @NonNull @@ -208,19 +197,6 @@ public class MarkwonConfiguration { return this; } -// /** -// * @param htmlAllowNonClosedTags that indicates if non-closed html tags should be rendered. -// * If this argument is true then all non-closed HTML tags -// * will be closed at the end of a document. Otherwise they will -// * be delivered non-closed {@code HtmlTag#isClosed()} -// * @since 2.0.0 -// */ -// @NonNull -// public Builder htmlAllowNonClosedTags(boolean htmlAllowNonClosedTags) { -// this.htmlAllowNonClosedTags = htmlAllowNonClosedTags; -// return this; -// } - @NonNull public MarkwonConfiguration build(@NonNull MarkwonTheme theme, @NonNull AsyncDrawableLoader asyncDrawableLoader) { diff --git a/markwon/src/main/java/ru/noties/markwon/MarkwonImpl.java b/markwon/src/main/java/ru/noties/markwon/MarkwonImpl.java index af604f26..77e68b49 100644 --- a/markwon/src/main/java/ru/noties/markwon/MarkwonImpl.java +++ b/markwon/src/main/java/ru/noties/markwon/MarkwonImpl.java @@ -8,7 +8,7 @@ import org.commonmark.parser.Parser; import java.util.List; -class MarkwonImpl extends Markwon2 { +class MarkwonImpl extends Markwon { private final TextView.BufferType bufferType; private final Parser parser; diff --git a/markwon/src/main/java/ru/noties/markwon/SpannableFactory.java b/markwon/src/main/java/ru/noties/markwon/SpannableFactory.java index 3efec181..abca9c86 100644 --- a/markwon/src/main/java/ru/noties/markwon/SpannableFactory.java +++ b/markwon/src/main/java/ru/noties/markwon/SpannableFactory.java @@ -4,8 +4,8 @@ import android.support.annotation.NonNull; import android.support.annotation.Nullable; import ru.noties.markwon.image.AsyncDrawableLoader; -import ru.noties.markwon.renderer.ImageSize; -import ru.noties.markwon.renderer.ImageSizeResolver; +import ru.noties.markwon.image.ImageSize; +import ru.noties.markwon.image.ImageSizeResolver; import ru.noties.markwon.spans.LinkSpan; import ru.noties.markwon.spans.MarkwonTheme; @@ -40,9 +40,6 @@ public interface SpannableFactory { @Nullable Object heading(@NonNull MarkwonTheme theme, int level); - @Nullable - Object strikethrough(); - /** * @since 1.1.1 */ diff --git a/markwon/src/main/java/ru/noties/markwon/SpannableFactoryDef.java b/markwon/src/main/java/ru/noties/markwon/SpannableFactoryDef.java index 75726b2a..999a42da 100644 --- a/markwon/src/main/java/ru/noties/markwon/SpannableFactoryDef.java +++ b/markwon/src/main/java/ru/noties/markwon/SpannableFactoryDef.java @@ -2,13 +2,11 @@ package ru.noties.markwon; import android.support.annotation.NonNull; import android.support.annotation.Nullable; -import android.text.style.StrikethroughSpan; -import android.text.style.UnderlineSpan; import ru.noties.markwon.image.AsyncDrawable; import ru.noties.markwon.image.AsyncDrawableLoader; -import ru.noties.markwon.renderer.ImageSize; -import ru.noties.markwon.renderer.ImageSizeResolver; +import ru.noties.markwon.image.ImageSize; +import ru.noties.markwon.image.ImageSizeResolver; import ru.noties.markwon.spans.AsyncDrawableSpan; import ru.noties.markwon.spans.BlockQuoteSpan; import ru.noties.markwon.spans.BulletListItemSpan; @@ -19,8 +17,6 @@ import ru.noties.markwon.spans.LinkSpan; import ru.noties.markwon.spans.MarkwonTheme; import ru.noties.markwon.spans.OrderedListItemSpan; import ru.noties.markwon.spans.StrongEmphasisSpan; -import ru.noties.markwon.spans.SubScriptSpan; -import ru.noties.markwon.spans.SuperScriptSpan; import ru.noties.markwon.spans.ThematicBreakSpan; /** @@ -82,12 +78,6 @@ public class SpannableFactoryDef implements SpannableFactory { return new HeadingSpan(theme, level); } - @Nullable - @Override - public Object strikethrough() { - return new StrikethroughSpan(); - } - /** * @since 1.1.1 */ diff --git a/markwon/src/main/java/ru/noties/markwon/image/AsyncDrawable.java b/markwon/src/main/java/ru/noties/markwon/image/AsyncDrawable.java index 3184f599..205830c0 100644 --- a/markwon/src/main/java/ru/noties/markwon/image/AsyncDrawable.java +++ b/markwon/src/main/java/ru/noties/markwon/image/AsyncDrawable.java @@ -10,9 +10,6 @@ import android.support.annotation.IntRange; import android.support.annotation.NonNull; import android.support.annotation.Nullable; -import ru.noties.markwon.renderer.ImageSize; -import ru.noties.markwon.renderer.ImageSizeResolver; - public class AsyncDrawable extends Drawable { private final String destination; diff --git a/markwon/src/main/java/ru/noties/markwon/renderer/ImageSize.java b/markwon/src/main/java/ru/noties/markwon/image/ImageSize.java similarity index 96% rename from markwon/src/main/java/ru/noties/markwon/renderer/ImageSize.java rename to markwon/src/main/java/ru/noties/markwon/image/ImageSize.java index 074ce880..ec44ed76 100644 --- a/markwon/src/main/java/ru/noties/markwon/renderer/ImageSize.java +++ b/markwon/src/main/java/ru/noties/markwon/image/ImageSize.java @@ -1,4 +1,4 @@ -package ru.noties.markwon.renderer; +package ru.noties.markwon.image; import android.support.annotation.Nullable; diff --git a/markwon/src/main/java/ru/noties/markwon/renderer/ImageSizeResolver.java b/markwon/src/main/java/ru/noties/markwon/image/ImageSizeResolver.java similarity index 95% rename from markwon/src/main/java/ru/noties/markwon/renderer/ImageSizeResolver.java rename to markwon/src/main/java/ru/noties/markwon/image/ImageSizeResolver.java index a7241914..57284a41 100644 --- a/markwon/src/main/java/ru/noties/markwon/renderer/ImageSizeResolver.java +++ b/markwon/src/main/java/ru/noties/markwon/image/ImageSizeResolver.java @@ -1,4 +1,4 @@ -package ru.noties.markwon.renderer; +package ru.noties.markwon.image; import android.graphics.Rect; import android.support.annotation.NonNull; diff --git a/markwon/src/main/java/ru/noties/markwon/renderer/ImageSizeResolverDef.java b/markwon/src/main/java/ru/noties/markwon/image/ImageSizeResolverDef.java similarity index 98% rename from markwon/src/main/java/ru/noties/markwon/renderer/ImageSizeResolverDef.java rename to markwon/src/main/java/ru/noties/markwon/image/ImageSizeResolverDef.java index c825002a..bdf7ae48 100644 --- a/markwon/src/main/java/ru/noties/markwon/renderer/ImageSizeResolverDef.java +++ b/markwon/src/main/java/ru/noties/markwon/image/ImageSizeResolverDef.java @@ -1,4 +1,4 @@ -package ru.noties.markwon.renderer; +package ru.noties.markwon.image; import android.graphics.Rect; import android.support.annotation.NonNull; diff --git a/markwon/src/main/java/ru/noties/markwon/renderer/SpannableMarkdownVisitor.java b/markwon/src/main/java/ru/noties/markwon/renderer/SpannableMarkdownVisitor.java deleted file mode 100644 index 18d02c19..00000000 --- a/markwon/src/main/java/ru/noties/markwon/renderer/SpannableMarkdownVisitor.java +++ /dev/null @@ -1,529 +0,0 @@ -package ru.noties.markwon.renderer; - -import android.support.annotation.NonNull; -import android.support.annotation.Nullable; - -import org.commonmark.ext.gfm.strikethrough.Strikethrough; -import org.commonmark.node.AbstractVisitor; -import org.commonmark.node.BlockQuote; -import org.commonmark.node.BulletList; -import org.commonmark.node.Code; -import org.commonmark.node.CustomBlock; -import org.commonmark.node.CustomNode; -import org.commonmark.node.Emphasis; -import org.commonmark.node.FencedCodeBlock; -import org.commonmark.node.HardLineBreak; -import org.commonmark.node.Heading; -import org.commonmark.node.Image; -import org.commonmark.node.IndentedCodeBlock; -import org.commonmark.node.Link; -import org.commonmark.node.ListBlock; -import org.commonmark.node.ListItem; -import org.commonmark.node.Node; -import org.commonmark.node.OrderedList; -import org.commonmark.node.Paragraph; -import org.commonmark.node.StrongEmphasis; -import org.commonmark.node.Text; -import org.commonmark.node.ThematicBreak; - -import java.util.List; - -import ru.noties.markwon.MarkwonConfiguration; -import ru.noties.markwon.SpannableBuilder; -import ru.noties.markwon.SpannableFactory; -import ru.noties.markwon.spans.MarkwonTheme; -import ru.noties.markwon.table.TableRowSpan; -import ru.noties.markwon.tasklist.TaskListBlock; - -@SuppressWarnings("WeakerAccess") -public class SpannableMarkdownVisitor extends AbstractVisitor { - - private final MarkwonConfiguration configuration; - private final SpannableBuilder builder; -// private final MarkwonHtmlParser htmlParser; - - private final MarkwonTheme theme; - private final SpannableFactory factory; - - private int blockQuoteIndent; - private int listLevel; - - private List pendingTableRow; - private boolean tableRowIsHeader; - private int tableRows; - - public SpannableMarkdownVisitor( - @NonNull MarkwonConfiguration configuration, - @NonNull SpannableBuilder builder - ) { - this.configuration = configuration; - this.builder = builder; -// this.htmlParser = configuration.htmlParser(); - - this.theme = configuration.theme(); - this.factory = configuration.factory(); - } - -// @Override -// public void visit(Document document) { -// super.visit(document); -// -// configuration.htmlRenderer().render(configuration, builder, htmlParser); -// } - - @Override - public void visit(Text text) { - builder.append(text.getLiteral()); - } - - @Override - public void visit(StrongEmphasis strongEmphasis) { - final int length = builder.length(); - visitChildren(strongEmphasis); - setSpan(length, factory.strongEmphasis()); - } - - @Override - public void visit(Emphasis emphasis) { - final int length = builder.length(); - visitChildren(emphasis); - setSpan(length, factory.emphasis()); - } - - @Override - public void visit(BlockQuote blockQuote) { - - newLine(); - if (blockQuoteIndent != 0) { - builder.append('\n'); - } - - final int length = builder.length(); - - blockQuoteIndent += 1; - - visitChildren(blockQuote); - - setSpan(length, factory.blockQuote(theme)); - - blockQuoteIndent -= 1; - - if (hasNext(blockQuote)) { - newLine(); - if (blockQuoteIndent == 0) { - builder.append('\n'); - } - } - } - - @Override - public void visit(Code code) { - - final int length = builder.length(); - - // NB, in order to provide a _padding_ feeling code is wrapped inside two unbreakable spaces - // unfortunately we cannot use this for multiline code as we cannot control where a new line break will be inserted - builder.append('\u00a0'); - builder.append(code.getLiteral()); - builder.append('\u00a0'); - - setSpan(length, factory.code(theme, false)); - } - - @Override - public void visit(FencedCodeBlock fencedCodeBlock) { - // @since 1.0.4 - visitCodeBlock(fencedCodeBlock.getInfo(), fencedCodeBlock.getLiteral(), fencedCodeBlock); - } - - /** - * @since 1.0.4 - */ - @Override - public void visit(IndentedCodeBlock indentedCodeBlock) { - visitCodeBlock(null, indentedCodeBlock.getLiteral(), indentedCodeBlock); - } - - /** - * @param info tag of a code block - * @param code content of a code block - * @since 1.0.4 - */ - private void visitCodeBlock(@Nullable String info, @NonNull String code, @NonNull Node node) { - - newLine(); - - final int length = builder.length(); - - // empty lines on top & bottom - builder.append('\u00a0').append('\n'); - builder.append( - configuration.syntaxHighlight() - .highlight(info, code) - ); - - newLine(); - builder.append('\u00a0'); - - setSpan(length, factory.code(theme, true)); - - if (hasNext(node)) { - newLine(); - builder.append('\n'); - } - } - - @Override - public void visit(BulletList bulletList) { - visitList(bulletList); - } - - @Override - public void visit(OrderedList orderedList) { - visitList(orderedList); - } - - private void visitList(Node node) { - - newLine(); - - visitChildren(node); - - if (hasNext(node)) { - newLine(); - if (listLevel == 0 && blockQuoteIndent == 0) { - builder.append('\n'); - } - } - } - - @Override - public void visit(ListItem listItem) { - - final int length = builder.length(); - - blockQuoteIndent += 1; - listLevel += 1; - - final Node parent = listItem.getParent(); - if (parent instanceof OrderedList) { - - final int start = ((OrderedList) parent).getStartNumber(); - - visitChildren(listItem); - - setSpan(length, factory.orderedListItem(theme, start)); - - // after we have visited the children increment start number - final OrderedList orderedList = (OrderedList) parent; - orderedList.setStartNumber(orderedList.getStartNumber() + 1); - - } else { - - visitChildren(listItem); - - setSpan(length, factory.bulletListItem(theme, listLevel - 1)); - } - - blockQuoteIndent -= 1; - listLevel -= 1; - - if (hasNext(listItem)) { - newLine(); - } - } - - @Override - public void visit(ThematicBreak thematicBreak) { - - newLine(); - - final int length = builder.length(); - builder.append('\u00a0'); // without space it won't render - - setSpan(length, factory.thematicBreak(theme)); - - if (hasNext(thematicBreak)) { - newLine(); - builder.append('\n'); - } - } - - @Override - public void visit(Heading heading) { - - newLine(); - - final int length = builder.length(); - visitChildren(heading); - setSpan(length, factory.heading(theme, heading.getLevel())); - - if (hasNext(heading)) { - newLine(); - // after heading we add another line anyway (no additional checks) - builder.append('\n'); - } - } - -// @Override -// public void visit(SoftLineBreak softLineBreak) { -// // @since 1.1.1 there is an option to treat soft break as a hard break (thus adding new line) -// if (configuration.softBreakAddsNewLine()) { -// newLine(); -// } else { -// builder.append(' '); -// } -// } - - @Override - public void visit(HardLineBreak hardLineBreak) { - newLine(); - } - - /** - * @since 1.0.1 - */ - @Override - public void visit(CustomBlock customBlock) { - - if (customBlock instanceof TaskListBlock) { - blockQuoteIndent += 1; - visitChildren(customBlock); - blockQuoteIndent -= 1; - - if (hasNext(customBlock)) { - newLine(); - builder.append('\n'); - } - - } else { - super.visit(customBlock); - } - } - - @Override - public void visit(CustomNode customNode) { - - if (customNode instanceof Strikethrough) { - - final int length = builder.length(); - visitChildren(customNode); - setSpan(length, factory.strikethrough()); - - } else { - super.visit(customNode); - } - } - -// private boolean handleTableNodes(CustomNode node) { -// -// final boolean handled; -// -// if (node instanceof TableBody) { -// -// visitChildren(node); -// tableRows = 0; -// handled = true; -// -// if (hasNext(node)) { -// newLine(); -// builder.append('\n'); -// } -// -// } else if (node instanceof TableRow || node instanceof TableHead) { -// -// final int length = builder.length(); -// -// visitChildren(node); -// -// if (pendingTableRow != null) { -// -// // @since 2.0.0 -// // we cannot rely on hasNext(TableHead) as it's not reliable -// // we must apply new line manually and then exclude it from tableRow span -// final boolean addNewLine; -// { -// final int builderLength = builder.length(); -// addNewLine = builderLength > 0 -// && '\n' != builder.charAt(builderLength - 1); -// } -// if (addNewLine) { -// builder.append('\n'); -// } -// -// // @since 1.0.4 Replace table char with non-breakable space -// // we need this because if table is at the end of the text, then it will be -// // trimmed from the final result -// builder.append('\u00a0'); -// -// final Object span = factory.tableRow( -// theme, -// pendingTableRow, -// tableRowIsHeader, -// tableRows % 2 == 1); -// -// tableRows = tableRowIsHeader -// ? 0 -// : tableRows + 1; -// -// setSpan(addNewLine ? length + 1 : length, span); -// -// pendingTableRow = null; -// } -// -// handled = true; -// -// } else if (node instanceof TableCell) { -// -// final TableCell cell = (TableCell) node; -// final int length = builder.length(); -// visitChildren(cell); -// if (pendingTableRow == null) { -// pendingTableRow = new ArrayList<>(2); -// } -// -// pendingTableRow.add(new TableRowSpan.Cell( -// tableCellAlignment(cell.getAlignment()), -// builder.removeFromEnd(length) -// )); -// -// tableRowIsHeader = cell.isHeader(); -// -// handled = true; -// } else { -// handled = false; -// } -// -// return handled; -// } - - @Override - public void visit(Paragraph paragraph) { - - final boolean inTightList = isInTightList(paragraph); - - if (!inTightList) { - newLine(); - } - - final int length = builder.length(); - visitChildren(paragraph); - - // @since 1.1.1 apply paragraph span - setSpan(length, factory.paragraph(inTightList)); - - if (hasNext(paragraph) && !inTightList) { - newLine(); - if (blockQuoteIndent == 0) { - builder.append('\n'); - } - } - } - - @Override - public void visit(Image image) { - - final int length = builder.length(); - - visitChildren(image); - - // we must check if anything _was_ added, as we need at least one char to render - if (length == builder.length()) { - builder.append('\uFFFC'); - } - - final Node parent = image.getParent(); - final boolean link = parent != null && parent instanceof Link; - final String destination = configuration.urlProcessor().process(image.getDestination()); - - setSpan( - length, - factory.image( - theme, - destination, - configuration.asyncDrawableLoader(), - configuration.imageSizeResolver(), - null, - link - ) - ); - - // todo, maybe, if image is not inside a link, we should make it clickable, so - // user can open it in external viewer? - } - -// @Override -// public void visit(HtmlBlock htmlBlock) { -// visitHtml(htmlBlock.getLiteral()); -// } -// -// @Override -// public void visit(HtmlInline htmlInline) { -// visitHtml(htmlInline.getLiteral()); -// } -// -// private void visitHtml(@Nullable String html) { -// if (html != null) { -// htmlParser.processFragment(builder, html); -// } -// } - - @Override - public void visit(Link link) { - final int length = builder.length(); - visitChildren(link); - final String destination = configuration.urlProcessor().process(link.getDestination()); - setSpan(length, factory.link(theme, destination, configuration.linkResolver())); - } - - private void setSpan(int start, @Nullable Object span) { - SpannableBuilder.setSpans(builder, span, start, builder.length()); - } - - private void newLine() { - if (builder.length() > 0 - && '\n' != builder.lastChar()) { - builder.append('\n'); - } - } - - private boolean isInTightList(Paragraph paragraph) { - final Node parent = paragraph.getParent(); - if (parent != null) { - final Node gramps = parent.getParent(); - if (gramps != null && gramps instanceof ListBlock) { - ListBlock list = (ListBlock) gramps; - return list.isTight(); - } - } - return false; - } - -// @TableRowSpan.Alignment -// private static int tableCellAlignment(TableCell.Alignment alignment) { -// final int out; -// if (alignment != null) { -// switch (alignment) { -// case CENTER: -// out = TableRowSpan.ALIGN_CENTER; -// break; -// case RIGHT: -// out = TableRowSpan.ALIGN_RIGHT; -// break; -// default: -// out = TableRowSpan.ALIGN_LEFT; -// break; -// } -// } else { -// out = TableRowSpan.ALIGN_LEFT; -// } -// return out; -// } - - /** - * @since 2.0.0 - */ - protected static boolean hasNext(@NonNull Node node) { - return node.getNext() != null; - } -} diff --git a/markwon/src/main/java/ru/noties/markwon/renderer/SpannableRenderer.java b/markwon/src/main/java/ru/noties/markwon/renderer/SpannableRenderer.java deleted file mode 100644 index b78f9c72..00000000 --- a/markwon/src/main/java/ru/noties/markwon/renderer/SpannableRenderer.java +++ /dev/null @@ -1,18 +0,0 @@ -package ru.noties.markwon.renderer; - -import android.support.annotation.NonNull; - -import org.commonmark.node.Node; - -import ru.noties.markwon.MarkwonConfiguration; -import ru.noties.markwon.SpannableBuilder; - -public class SpannableRenderer { - - @NonNull - public CharSequence render(@NonNull MarkwonConfiguration configuration, @NonNull Node node) { - final SpannableBuilder builder = new SpannableBuilder(); - node.accept(new SpannableMarkdownVisitor(configuration, builder)); - return builder.text(); - } -} diff --git a/markwon/src/main/java/ru/noties/markwon/spans/SubScriptSpan.java b/markwon/src/main/java/ru/noties/markwon/spans/SubScriptSpan.java deleted file mode 100644 index 318697b1..00000000 --- a/markwon/src/main/java/ru/noties/markwon/spans/SubScriptSpan.java +++ /dev/null @@ -1,28 +0,0 @@ -package ru.noties.markwon.spans; - -import android.support.annotation.NonNull; -import android.text.TextPaint; -import android.text.style.MetricAffectingSpan; - -public class SubScriptSpan extends MetricAffectingSpan { - - private final MarkwonTheme theme; - - public SubScriptSpan(@NonNull MarkwonTheme theme) { - this.theme = theme; - } - - @Override - public void updateDrawState(TextPaint tp) { - apply(tp); - } - - @Override - public void updateMeasureState(TextPaint tp) { - apply(tp); - } - - private void apply(TextPaint paint) { - theme.applySubScriptStyle(paint); - } -} diff --git a/markwon/src/main/java/ru/noties/markwon/spans/SuperScriptSpan.java b/markwon/src/main/java/ru/noties/markwon/spans/SuperScriptSpan.java deleted file mode 100644 index d1aa172b..00000000 --- a/markwon/src/main/java/ru/noties/markwon/spans/SuperScriptSpan.java +++ /dev/null @@ -1,28 +0,0 @@ -package ru.noties.markwon.spans; - -import android.support.annotation.NonNull; -import android.text.TextPaint; -import android.text.style.MetricAffectingSpan; - -public class SuperScriptSpan extends MetricAffectingSpan { - - private final MarkwonTheme theme; - - public SuperScriptSpan(@NonNull MarkwonTheme theme) { - this.theme = theme; - } - - @Override - public void updateDrawState(TextPaint tp) { - apply(tp); - } - - @Override - public void updateMeasureState(TextPaint tp) { - apply(tp); - } - - private void apply(TextPaint paint) { - theme.applySuperScriptStyle(paint); - } -} diff --git a/markwon/src/main/java/ru/noties/markwon/tasklist/TaskListExtension.java b/markwon/src/main/java/ru/noties/markwon/tasklist/TaskListExtension.java deleted file mode 100644 index a721c297..00000000 --- a/markwon/src/main/java/ru/noties/markwon/tasklist/TaskListExtension.java +++ /dev/null @@ -1,22 +0,0 @@ -package ru.noties.markwon.tasklist; - -import android.support.annotation.NonNull; - -import org.commonmark.parser.Parser; - -/** - * @since 1.0.1 - */ -@Deprecated -public class TaskListExtension implements Parser.ParserExtension { - - @NonNull - public static TaskListExtension create() { - return new TaskListExtension(); - } - - @Override - public void extend(Parser.Builder parserBuilder) { - parserBuilder.customBlockParserFactory(new TaskListBlockParser.Factory()); - } -} diff --git a/markwon/src/test/java/ru/noties/markwon/renderer/ImageSizeResolverDefTest.java b/markwon/src/test/java/ru/noties/markwon/renderer/ImageSizeResolverDefTest.java index f0945554..b5121d39 100644 --- a/markwon/src/test/java/ru/noties/markwon/renderer/ImageSizeResolverDefTest.java +++ b/markwon/src/test/java/ru/noties/markwon/renderer/ImageSizeResolverDefTest.java @@ -8,11 +8,13 @@ import org.junit.runner.RunWith; import org.robolectric.RobolectricTestRunner; import org.robolectric.annotation.Config; -import ru.noties.markwon.renderer.ImageSize.Dimension; +import ru.noties.markwon.image.ImageSize; +import ru.noties.markwon.image.ImageSize.Dimension; +import ru.noties.markwon.image.ImageSizeResolverDef; import static org.junit.Assert.assertEquals; -import static ru.noties.markwon.renderer.ImageSizeResolverDef.UNIT_EM; -import static ru.noties.markwon.renderer.ImageSizeResolverDef.UNIT_PERCENT; +import static ru.noties.markwon.image.ImageSizeResolverDef.UNIT_EM; +import static ru.noties.markwon.image.ImageSizeResolverDef.UNIT_PERCENT; @RunWith(RobolectricTestRunner.class) @Config(manifest = Config.NONE) diff --git a/markwon/src/test/java/ru/noties/markwon/renderer/MarkwonConfigurationTest.java b/markwon/src/test/java/ru/noties/markwon/renderer/MarkwonConfigurationTest.java index 11f539b3..71cd0145 100644 --- a/markwon/src/test/java/ru/noties/markwon/renderer/MarkwonConfigurationTest.java +++ b/markwon/src/test/java/ru/noties/markwon/renderer/MarkwonConfigurationTest.java @@ -9,6 +9,7 @@ import ru.noties.markwon.UrlProcessor; import ru.noties.markwon.html.api.MarkwonHtmlParser; import ru.noties.markwon.html.MarkwonHtmlRenderer; import ru.noties.markwon.image.AsyncDrawable; +import ru.noties.markwon.image.ImageSizeResolver; import ru.noties.markwon.spans.LinkSpan; import ru.noties.markwon.spans.MarkwonTheme; diff --git a/markwon/src/test/java/ru/noties/markwon/renderer/html2/tag/ImageSizeParserImplTest.java b/markwon/src/test/java/ru/noties/markwon/renderer/html2/tag/ImageSizeParserImplTest.java index 23d7eb93..f7ccca03 100644 --- a/markwon/src/test/java/ru/noties/markwon/renderer/html2/tag/ImageSizeParserImplTest.java +++ b/markwon/src/test/java/ru/noties/markwon/renderer/html2/tag/ImageSizeParserImplTest.java @@ -13,7 +13,7 @@ import java.util.Collections; import java.util.HashMap; import java.util.Map; -import ru.noties.markwon.renderer.ImageSize; +import ru.noties.markwon.image.ImageSize; import ru.noties.markwon.renderer.html2.CssInlineStyleParser; import static org.junit.Assert.assertEquals; diff --git a/markwon/src/test/java/ru/noties/markwon/renderer/visitor/TestFactory.java b/markwon/src/test/java/ru/noties/markwon/renderer/visitor/TestFactory.java index 6e6feb30..b8cd7c8d 100644 --- a/markwon/src/test/java/ru/noties/markwon/renderer/visitor/TestFactory.java +++ b/markwon/src/test/java/ru/noties/markwon/renderer/visitor/TestFactory.java @@ -9,8 +9,8 @@ import java.util.List; import java.util.Map; import ru.noties.markwon.SpannableFactory; -import ru.noties.markwon.renderer.ImageSize; -import ru.noties.markwon.renderer.ImageSizeResolver; +import ru.noties.markwon.image.ImageSize; +import ru.noties.markwon.image.ImageSizeResolver; import ru.noties.markwon.image.AsyncDrawable; import ru.noties.markwon.spans.LinkSpan; import ru.noties.markwon.spans.MarkwonTheme; diff --git a/sample-latex-math/src/main/java/ru/noties/markwon/sample/jlatexmath/MainActivity.java b/sample-latex-math/src/main/java/ru/noties/markwon/sample/jlatexmath/MainActivity.java index b613eac5..5f2ea561 100644 --- a/sample-latex-math/src/main/java/ru/noties/markwon/sample/jlatexmath/MainActivity.java +++ b/sample-latex-math/src/main/java/ru/noties/markwon/sample/jlatexmath/MainActivity.java @@ -13,7 +13,7 @@ import ru.noties.markwon.Markwon; import ru.noties.markwon.MarkwonConfiguration; import ru.noties.markwon.SpannableBuilder; import ru.noties.markwon.il.AsyncDrawableLoader; -import ru.noties.markwon.renderer.ImageSize; +import ru.noties.markwon.image.ImageSize; import ru.noties.markwon.renderer.SpannableMarkdownVisitor; public class MainActivity extends Activity { diff --git a/settings.gradle b/settings.gradle index ca458b18..13fb6c63 100644 --- a/settings.gradle +++ b/settings.gradle @@ -1,3 +1,13 @@ rootProject.name = 'MarkwonProject' -include ':app', ':markwon', ':markwon-view', ':sample-custom-extension', ':sample-latex-math', ':markwon-image-svg', ':markwon-image-gif', - ':markwon-syntax-highlight', ':markwon-html' +include ':app', + ':markwon', + ':markwon-ext-strikethrough', + ':markwon-ext-tables', + ':markwon-ext-tasklist', + ':markwon-image-svg', + ':markwon-image-gif', + ':markwon-syntax-highlight', + ':markwon-html', + ':markwon-view', + ':sample-custom-extension', + ':sample-latex-math' From dc9a4dbf5638d44595f720c4bef6f6280b9bede7 Mon Sep 17 00:00:00 2001 From: Dimitry Ivanov Date: Mon, 26 Nov 2018 17:21:24 +0300 Subject: [PATCH 024/103] Removed html from core artifact --- .../ru/noties/markwon/MarkdownRenderer.java | 2 +- markwon-html/src/main/AndroidManifest.xml | 2 +- .../html/{impl => }/AppendableUtils.java | 2 +- .../html/{impl => }/CssInlineStyleParser.java | 2 +- .../markwon/html/{impl => }/CssProperty.java | 2 +- .../{impl => }/HtmlEmptyTagReplacement.java | 4 +- .../markwon/html/{impl => }/HtmlPlugin.java | 17 +----- .../java/ru/noties/markwon/html/HtmlTag.java | 0 .../markwon/html/{impl => }/HtmlTagImpl.java | 4 +- .../markwon/html/MarkwonHtmlParser.java | 0 .../{impl => }/MarkwonHtmlParserImpl.java | 18 +++--- .../markwon/html/MarkwonHtmlParserNoOp.java | 0 .../markwon/html/MarkwonHtmlRenderer.java | 0 .../{impl => }/MarkwonHtmlRendererImpl.java | 32 +++++----- .../markwon/html/MarkwonHtmlRendererNoOp.java | 0 .../ru/noties/markwon/html/TagHandler.java | 8 ++- .../html/{impl => }/TrimmingAppender.java | 4 +- .../jsoup/UncheckedIOException.java | 2 +- .../{impl => }/jsoup/helper/Normalizer.java | 2 +- .../{impl => }/jsoup/helper/Validate.java | 2 +- .../{impl => }/jsoup/nodes/Attribute.java | 4 +- .../{impl => }/jsoup/nodes/Attributes.java | 9 +-- .../jsoup/nodes/CommonMarkEntities.java | 2 +- .../{impl => }/jsoup/nodes/DocumentType.java | 2 +- .../jsoup/parser/CharacterReader.java | 8 +-- .../{impl => }/jsoup/parser/ParseError.java | 2 +- .../jsoup/parser/ParseErrorList.java | 2 +- .../html/{impl => }/jsoup/parser/Token.java | 8 +-- .../{impl => }/jsoup/parser/Tokeniser.java | 6 +- .../jsoup/parser/TokeniserState.java | 4 +- .../html/{impl => }/span/SubScriptSpan.java | 4 +- .../html/{impl => }/span/SuperScriptSpan.java | 4 +- .../{impl => }/tag/BlockquoteHandler.java | 6 +- .../html/{impl => }/tag/EmphasisHandler.java | 2 +- .../html/{impl => }/tag/HeadingHandler.java | 2 +- .../html/{impl => }/tag/ImageHandler.java | 4 +- .../{impl => }/tag/ImageSizeParserImpl.java | 6 +- .../html/{impl => }/tag/LinkHandler.java | 2 +- .../html/{impl => }/tag/ListHandler.java | 6 +- .../html/{impl => }/tag/SimpleTagHandler.java | 5 +- .../html/{impl => }/tag/StrikeHandler.java | 6 +- .../{impl => }/tag/StrongEmphasisHandler.java | 2 +- .../html/{impl => }/tag/SubScriptHandler.java | 4 +- .../{impl => }/tag/SuperScriptHandler.java | 4 +- .../html/{impl => }/tag/UnderlineHandler.java | 6 +- .../html/impl/CssInlineStyleParserTest.java | 2 + .../impl/HtmlEmptyTagReplacementTest.java | 3 +- .../html/impl/MarkwonHtmlParserImplTest.java | 2 + .../html/impl/TrimmingAppenderTest.java | 2 + .../jsoup/nodes/CommonMarkEntitiesTest.java | 2 + .../impl/tag/ImageSizeParserImplTest.java | 1 + .../noties/markwon/MarkwonConfiguration.java | 58 +------------------ .../markwon/image/AsyncDrawableLoader.java | 16 +++++ .../image/AsyncDrawableLoaderNoOp.java | 2 +- 54 files changed, 129 insertions(+), 172 deletions(-) rename markwon-html/src/main/java/ru/noties/markwon/html/{impl => }/AppendableUtils.java (95%) rename markwon-html/src/main/java/ru/noties/markwon/html/{impl => }/CssInlineStyleParser.java (99%) rename markwon-html/src/main/java/ru/noties/markwon/html/{impl => }/CssProperty.java (95%) rename markwon-html/src/main/java/ru/noties/markwon/html/{impl => }/HtmlEmptyTagReplacement.java (95%) rename markwon-html/src/main/java/ru/noties/markwon/html/{impl => }/HtmlPlugin.java (78%) rename {markwon => markwon-html}/src/main/java/ru/noties/markwon/html/HtmlTag.java (100%) rename markwon-html/src/main/java/ru/noties/markwon/html/{impl => }/HtmlTagImpl.java (98%) rename {markwon => markwon-html}/src/main/java/ru/noties/markwon/html/MarkwonHtmlParser.java (100%) rename markwon-html/src/main/java/ru/noties/markwon/html/{impl => }/MarkwonHtmlParserImpl.java (96%) rename {markwon => markwon-html}/src/main/java/ru/noties/markwon/html/MarkwonHtmlParserNoOp.java (100%) rename {markwon => markwon-html}/src/main/java/ru/noties/markwon/html/MarkwonHtmlRenderer.java (100%) rename markwon-html/src/main/java/ru/noties/markwon/html/{impl => }/MarkwonHtmlRendererImpl.java (86%) rename {markwon => markwon-html}/src/main/java/ru/noties/markwon/html/MarkwonHtmlRendererNoOp.java (100%) rename {markwon => markwon-html}/src/main/java/ru/noties/markwon/html/TagHandler.java (73%) rename markwon-html/src/main/java/ru/noties/markwon/html/{impl => }/TrimmingAppender.java (94%) rename markwon-html/src/main/java/ru/noties/markwon/html/{impl => }/jsoup/UncheckedIOException.java (85%) rename markwon-html/src/main/java/ru/noties/markwon/html/{impl => }/jsoup/helper/Normalizer.java (89%) rename markwon-html/src/main/java/ru/noties/markwon/html/{impl => }/jsoup/helper/Validate.java (98%) rename markwon-html/src/main/java/ru/noties/markwon/html/{impl => }/jsoup/nodes/Attribute.java (98%) rename markwon-html/src/main/java/ru/noties/markwon/html/{impl => }/jsoup/nodes/Attributes.java (98%) rename markwon-html/src/main/java/ru/noties/markwon/html/{impl => }/jsoup/nodes/CommonMarkEntities.java (96%) rename markwon-html/src/main/java/ru/noties/markwon/html/{impl => }/jsoup/nodes/DocumentType.java (98%) rename markwon-html/src/main/java/ru/noties/markwon/html/{impl => }/jsoup/parser/CharacterReader.java (98%) rename markwon-html/src/main/java/ru/noties/markwon/html/{impl => }/jsoup/parser/ParseError.java (94%) rename markwon-html/src/main/java/ru/noties/markwon/html/{impl => }/jsoup/parser/ParseErrorList.java (93%) rename markwon-html/src/main/java/ru/noties/markwon/html/{impl => }/jsoup/parser/Token.java (97%) rename markwon-html/src/main/java/ru/noties/markwon/html/{impl => }/jsoup/parser/Tokeniser.java (98%) rename markwon-html/src/main/java/ru/noties/markwon/html/{impl => }/jsoup/parser/TokeniserState.java (99%) rename markwon-html/src/main/java/ru/noties/markwon/html/{impl => }/span/SubScriptSpan.java (85%) rename markwon-html/src/main/java/ru/noties/markwon/html/{impl => }/span/SuperScriptSpan.java (85%) rename markwon-html/src/main/java/ru/noties/markwon/html/{impl => }/tag/BlockquoteHandler.java (76%) rename markwon-html/src/main/java/ru/noties/markwon/html/{impl => }/tag/EmphasisHandler.java (91%) rename markwon-html/src/main/java/ru/noties/markwon/html/{impl => }/tag/HeadingHandler.java (93%) rename markwon-html/src/main/java/ru/noties/markwon/html/{impl => }/tag/ImageHandler.java (94%) rename markwon-html/src/main/java/ru/noties/markwon/html/{impl => }/tag/ImageSizeParserImpl.java (95%) rename markwon-html/src/main/java/ru/noties/markwon/html/{impl => }/tag/LinkHandler.java (94%) rename markwon-html/src/main/java/ru/noties/markwon/html/{impl => }/tag/ListHandler.java (90%) rename markwon-html/src/main/java/ru/noties/markwon/html/{impl => }/tag/SimpleTagHandler.java (78%) rename markwon-html/src/main/java/ru/noties/markwon/html/{impl => }/tag/StrikeHandler.java (76%) rename markwon-html/src/main/java/ru/noties/markwon/html/{impl => }/tag/StrongEmphasisHandler.java (91%) rename markwon-html/src/main/java/ru/noties/markwon/html/{impl => }/tag/SubScriptHandler.java (80%) rename markwon-html/src/main/java/ru/noties/markwon/html/{impl => }/tag/SuperScriptHandler.java (80%) rename markwon-html/src/main/java/ru/noties/markwon/html/{impl => }/tag/UnderlineHandler.java (79%) diff --git a/app/src/main/java/ru/noties/markwon/MarkdownRenderer.java b/app/src/main/java/ru/noties/markwon/MarkdownRenderer.java index 2da031f0..007b754f 100644 --- a/app/src/main/java/ru/noties/markwon/MarkdownRenderer.java +++ b/app/src/main/java/ru/noties/markwon/MarkdownRenderer.java @@ -18,7 +18,7 @@ import ru.noties.markwon.ext.strikethrough.StrikethroughPlugin; import ru.noties.markwon.ext.tables.TablePlugin; import ru.noties.markwon.ext.tasklist.TaskListPlugin; import ru.noties.markwon.gif.GifAwarePlugin; -import ru.noties.markwon.html.impl.HtmlPlugin; +import ru.noties.markwon.html.HtmlPlugin; import ru.noties.markwon.image.ImagesPlugin; import ru.noties.markwon.image.gif.GifPlugin; import ru.noties.markwon.image.svg.SvgPlugin; diff --git a/markwon-html/src/main/AndroidManifest.xml b/markwon-html/src/main/AndroidManifest.xml index 14bee867..6d886e0e 100644 --- a/markwon-html/src/main/AndroidManifest.xml +++ b/markwon-html/src/main/AndroidManifest.xml @@ -1 +1 @@ - + diff --git a/markwon-html/src/main/java/ru/noties/markwon/html/impl/AppendableUtils.java b/markwon-html/src/main/java/ru/noties/markwon/html/AppendableUtils.java similarity index 95% rename from markwon-html/src/main/java/ru/noties/markwon/html/impl/AppendableUtils.java rename to markwon-html/src/main/java/ru/noties/markwon/html/AppendableUtils.java index 39b6bf73..5e890174 100644 --- a/markwon-html/src/main/java/ru/noties/markwon/html/impl/AppendableUtils.java +++ b/markwon-html/src/main/java/ru/noties/markwon/html/AppendableUtils.java @@ -1,4 +1,4 @@ -package ru.noties.markwon.html.impl; +package ru.noties.markwon.html; import android.support.annotation.NonNull; diff --git a/markwon-html/src/main/java/ru/noties/markwon/html/impl/CssInlineStyleParser.java b/markwon-html/src/main/java/ru/noties/markwon/html/CssInlineStyleParser.java similarity index 99% rename from markwon-html/src/main/java/ru/noties/markwon/html/impl/CssInlineStyleParser.java rename to markwon-html/src/main/java/ru/noties/markwon/html/CssInlineStyleParser.java index a42fab05..c9d7c1fc 100644 --- a/markwon-html/src/main/java/ru/noties/markwon/html/impl/CssInlineStyleParser.java +++ b/markwon-html/src/main/java/ru/noties/markwon/html/CssInlineStyleParser.java @@ -1,4 +1,4 @@ -package ru.noties.markwon.html.impl; +package ru.noties.markwon.html; import android.support.annotation.NonNull; import android.support.annotation.Nullable; diff --git a/markwon-html/src/main/java/ru/noties/markwon/html/impl/CssProperty.java b/markwon-html/src/main/java/ru/noties/markwon/html/CssProperty.java similarity index 95% rename from markwon-html/src/main/java/ru/noties/markwon/html/impl/CssProperty.java rename to markwon-html/src/main/java/ru/noties/markwon/html/CssProperty.java index 405d7c61..70bc6d88 100644 --- a/markwon-html/src/main/java/ru/noties/markwon/html/impl/CssProperty.java +++ b/markwon-html/src/main/java/ru/noties/markwon/html/CssProperty.java @@ -1,4 +1,4 @@ -package ru.noties.markwon.html.impl; +package ru.noties.markwon.html; import android.support.annotation.NonNull; diff --git a/markwon-html/src/main/java/ru/noties/markwon/html/impl/HtmlEmptyTagReplacement.java b/markwon-html/src/main/java/ru/noties/markwon/html/HtmlEmptyTagReplacement.java similarity index 95% rename from markwon-html/src/main/java/ru/noties/markwon/html/impl/HtmlEmptyTagReplacement.java rename to markwon-html/src/main/java/ru/noties/markwon/html/HtmlEmptyTagReplacement.java index 6604242b..c191b59d 100644 --- a/markwon-html/src/main/java/ru/noties/markwon/html/impl/HtmlEmptyTagReplacement.java +++ b/markwon-html/src/main/java/ru/noties/markwon/html/HtmlEmptyTagReplacement.java @@ -1,10 +1,8 @@ -package ru.noties.markwon.html.impl; +package ru.noties.markwon.html; import android.support.annotation.NonNull; import android.support.annotation.Nullable; -import ru.noties.markwon.html.HtmlTag; - /** * This class will be used to append some text to output in order to * apply a Span for this tag. Please note that this class will be used for diff --git a/markwon-html/src/main/java/ru/noties/markwon/html/impl/HtmlPlugin.java b/markwon-html/src/main/java/ru/noties/markwon/html/HtmlPlugin.java similarity index 78% rename from markwon-html/src/main/java/ru/noties/markwon/html/impl/HtmlPlugin.java rename to markwon-html/src/main/java/ru/noties/markwon/html/HtmlPlugin.java index 6e8935df..3852c772 100644 --- a/markwon-html/src/main/java/ru/noties/markwon/html/impl/HtmlPlugin.java +++ b/markwon-html/src/main/java/ru/noties/markwon/html/HtmlPlugin.java @@ -1,4 +1,4 @@ -package ru.noties.markwon.html.impl; +package ru.noties.markwon.html; import android.support.annotation.NonNull; import android.support.annotation.Nullable; @@ -8,10 +8,7 @@ import org.commonmark.node.HtmlBlock; import org.commonmark.node.HtmlInline; import ru.noties.markwon.AbstractMarkwonPlugin; -import ru.noties.markwon.MarkwonConfiguration; import ru.noties.markwon.MarkwonVisitor; -import ru.noties.markwon.html.MarkwonHtmlParser; -import ru.noties.markwon.html.MarkwonHtmlRenderer; public class HtmlPlugin extends AbstractMarkwonPlugin { @@ -43,13 +40,6 @@ public class HtmlPlugin extends AbstractMarkwonPlugin { this.parser = parser; } - @Override - public void configureConfiguration(@NonNull MarkwonConfiguration.Builder builder) { - builder - .htmlParser(parser) - .htmlRenderer(renderer); - } - @Override public void configureVisitor(@NonNull MarkwonVisitor.Builder builder) { builder @@ -59,8 +49,7 @@ public class HtmlPlugin extends AbstractMarkwonPlugin { visitor.visitChildren(document); - final MarkwonConfiguration configuration = visitor.configuration(); - configuration.htmlRenderer().render(configuration, visitor.builder(), configuration.htmlParser()); + renderer.render(visitor.configuration(), visitor.builder(), parser); } }) .on(HtmlBlock.class, new MarkwonVisitor.NodeVisitor() { @@ -79,7 +68,7 @@ public class HtmlPlugin extends AbstractMarkwonPlugin { private void visitHtml(@NonNull MarkwonVisitor visitor, @Nullable String html) { if (html != null) { - visitor.configuration().htmlParser().processFragment(visitor.builder(), html); + parser.processFragment(visitor.builder(), html); } } } diff --git a/markwon/src/main/java/ru/noties/markwon/html/HtmlTag.java b/markwon-html/src/main/java/ru/noties/markwon/html/HtmlTag.java similarity index 100% rename from markwon/src/main/java/ru/noties/markwon/html/HtmlTag.java rename to markwon-html/src/main/java/ru/noties/markwon/html/HtmlTag.java diff --git a/markwon-html/src/main/java/ru/noties/markwon/html/impl/HtmlTagImpl.java b/markwon-html/src/main/java/ru/noties/markwon/html/HtmlTagImpl.java similarity index 98% rename from markwon-html/src/main/java/ru/noties/markwon/html/impl/HtmlTagImpl.java rename to markwon-html/src/main/java/ru/noties/markwon/html/HtmlTagImpl.java index 8cedc767..7c07360a 100644 --- a/markwon-html/src/main/java/ru/noties/markwon/html/impl/HtmlTagImpl.java +++ b/markwon-html/src/main/java/ru/noties/markwon/html/HtmlTagImpl.java @@ -1,4 +1,4 @@ -package ru.noties.markwon.html.impl; +package ru.noties.markwon.html; import android.support.annotation.NonNull; import android.support.annotation.Nullable; @@ -7,8 +7,6 @@ import java.util.Collections; import java.util.List; import java.util.Map; -import ru.noties.markwon.html.HtmlTag; - abstract class HtmlTagImpl implements HtmlTag { final String name; diff --git a/markwon/src/main/java/ru/noties/markwon/html/MarkwonHtmlParser.java b/markwon-html/src/main/java/ru/noties/markwon/html/MarkwonHtmlParser.java similarity index 100% rename from markwon/src/main/java/ru/noties/markwon/html/MarkwonHtmlParser.java rename to markwon-html/src/main/java/ru/noties/markwon/html/MarkwonHtmlParser.java diff --git a/markwon-html/src/main/java/ru/noties/markwon/html/impl/MarkwonHtmlParserImpl.java b/markwon-html/src/main/java/ru/noties/markwon/html/MarkwonHtmlParserImpl.java similarity index 96% rename from markwon-html/src/main/java/ru/noties/markwon/html/impl/MarkwonHtmlParserImpl.java rename to markwon-html/src/main/java/ru/noties/markwon/html/MarkwonHtmlParserImpl.java index 1bd639c6..6d715510 100644 --- a/markwon-html/src/main/java/ru/noties/markwon/html/impl/MarkwonHtmlParserImpl.java +++ b/markwon-html/src/main/java/ru/noties/markwon/html/MarkwonHtmlParserImpl.java @@ -1,4 +1,4 @@ -package ru.noties.markwon.html.impl; +package ru.noties.markwon.html; import android.support.annotation.NonNull; import android.support.annotation.Nullable; @@ -14,18 +14,16 @@ import java.util.Locale; import java.util.Map; import java.util.Set; -import ru.noties.markwon.html.HtmlTag; import ru.noties.markwon.html.HtmlTag.Block; import ru.noties.markwon.html.HtmlTag.Inline; -import ru.noties.markwon.html.MarkwonHtmlParser; -import ru.noties.markwon.html.impl.jsoup.nodes.Attribute; -import ru.noties.markwon.html.impl.jsoup.nodes.Attributes; -import ru.noties.markwon.html.impl.jsoup.parser.CharacterReader; -import ru.noties.markwon.html.impl.jsoup.parser.ParseErrorList; -import ru.noties.markwon.html.impl.jsoup.parser.Token; -import ru.noties.markwon.html.impl.jsoup.parser.Tokeniser; +import ru.noties.markwon.html.jsoup.nodes.Attribute; +import ru.noties.markwon.html.jsoup.nodes.Attributes; +import ru.noties.markwon.html.jsoup.parser.CharacterReader; +import ru.noties.markwon.html.jsoup.parser.ParseErrorList; +import ru.noties.markwon.html.jsoup.parser.Token; +import ru.noties.markwon.html.jsoup.parser.Tokeniser; -import static ru.noties.markwon.html.impl.AppendableUtils.appendQuietly; +import static ru.noties.markwon.html.AppendableUtils.appendQuietly; /** * @since 2.0.0 diff --git a/markwon/src/main/java/ru/noties/markwon/html/MarkwonHtmlParserNoOp.java b/markwon-html/src/main/java/ru/noties/markwon/html/MarkwonHtmlParserNoOp.java similarity index 100% rename from markwon/src/main/java/ru/noties/markwon/html/MarkwonHtmlParserNoOp.java rename to markwon-html/src/main/java/ru/noties/markwon/html/MarkwonHtmlParserNoOp.java diff --git a/markwon/src/main/java/ru/noties/markwon/html/MarkwonHtmlRenderer.java b/markwon-html/src/main/java/ru/noties/markwon/html/MarkwonHtmlRenderer.java similarity index 100% rename from markwon/src/main/java/ru/noties/markwon/html/MarkwonHtmlRenderer.java rename to markwon-html/src/main/java/ru/noties/markwon/html/MarkwonHtmlRenderer.java diff --git a/markwon-html/src/main/java/ru/noties/markwon/html/impl/MarkwonHtmlRendererImpl.java b/markwon-html/src/main/java/ru/noties/markwon/html/MarkwonHtmlRendererImpl.java similarity index 86% rename from markwon-html/src/main/java/ru/noties/markwon/html/impl/MarkwonHtmlRendererImpl.java rename to markwon-html/src/main/java/ru/noties/markwon/html/MarkwonHtmlRendererImpl.java index b6b2851d..399d58ca 100644 --- a/markwon-html/src/main/java/ru/noties/markwon/html/impl/MarkwonHtmlRendererImpl.java +++ b/markwon-html/src/main/java/ru/noties/markwon/html/MarkwonHtmlRendererImpl.java @@ -1,4 +1,4 @@ -package ru.noties.markwon.html.impl; +package ru.noties.markwon.html; import android.support.annotation.NonNull; import android.support.annotation.Nullable; @@ -11,21 +11,17 @@ import java.util.Map; import ru.noties.markwon.MarkwonConfiguration; import ru.noties.markwon.SpannableBuilder; -import ru.noties.markwon.html.HtmlTag; -import ru.noties.markwon.html.MarkwonHtmlParser; -import ru.noties.markwon.html.MarkwonHtmlRenderer; -import ru.noties.markwon.html.TagHandler; -import ru.noties.markwon.html.impl.tag.BlockquoteHandler; -import ru.noties.markwon.html.impl.tag.EmphasisHandler; -import ru.noties.markwon.html.impl.tag.HeadingHandler; -import ru.noties.markwon.html.impl.tag.ImageHandler; -import ru.noties.markwon.html.impl.tag.LinkHandler; -import ru.noties.markwon.html.impl.tag.ListHandler; -import ru.noties.markwon.html.impl.tag.StrikeHandler; -import ru.noties.markwon.html.impl.tag.StrongEmphasisHandler; -import ru.noties.markwon.html.impl.tag.SubScriptHandler; -import ru.noties.markwon.html.impl.tag.SuperScriptHandler; -import ru.noties.markwon.html.impl.tag.UnderlineHandler; +import ru.noties.markwon.html.tag.BlockquoteHandler; +import ru.noties.markwon.html.tag.EmphasisHandler; +import ru.noties.markwon.html.tag.HeadingHandler; +import ru.noties.markwon.html.tag.ImageHandler; +import ru.noties.markwon.html.tag.LinkHandler; +import ru.noties.markwon.html.tag.ListHandler; +import ru.noties.markwon.html.tag.StrikeHandler; +import ru.noties.markwon.html.tag.StrongEmphasisHandler; +import ru.noties.markwon.html.tag.SubScriptHandler; +import ru.noties.markwon.html.tag.SuperScriptHandler; +import ru.noties.markwon.html.tag.UnderlineHandler; public class MarkwonHtmlRendererImpl extends MarkwonHtmlRenderer { @@ -130,7 +126,7 @@ public class MarkwonHtmlRendererImpl extends MarkwonHtmlRenderer { handler = tagHandler(inline.name()); if (handler != null) { - handler.handle(configuration, builder, inline); + handler.handle(configuration, MarkwonHtmlRendererImpl.this, builder, inline); } } } @@ -150,7 +146,7 @@ public class MarkwonHtmlRendererImpl extends MarkwonHtmlRenderer { handler = tagHandler(block.name()); if (handler != null) { - handler.handle(configuration, builder, block); + handler.handle(configuration, MarkwonHtmlRendererImpl.this, builder, block); } else { // see if any of children can be handled apply(block.children()); diff --git a/markwon/src/main/java/ru/noties/markwon/html/MarkwonHtmlRendererNoOp.java b/markwon-html/src/main/java/ru/noties/markwon/html/MarkwonHtmlRendererNoOp.java similarity index 100% rename from markwon/src/main/java/ru/noties/markwon/html/MarkwonHtmlRendererNoOp.java rename to markwon-html/src/main/java/ru/noties/markwon/html/MarkwonHtmlRendererNoOp.java diff --git a/markwon/src/main/java/ru/noties/markwon/html/TagHandler.java b/markwon-html/src/main/java/ru/noties/markwon/html/TagHandler.java similarity index 73% rename from markwon/src/main/java/ru/noties/markwon/html/TagHandler.java rename to markwon-html/src/main/java/ru/noties/markwon/html/TagHandler.java index 50af33d8..881e882f 100644 --- a/markwon/src/main/java/ru/noties/markwon/html/TagHandler.java +++ b/markwon-html/src/main/java/ru/noties/markwon/html/TagHandler.java @@ -9,12 +9,14 @@ public abstract class TagHandler { public abstract void handle( @NonNull MarkwonConfiguration configuration, + @NonNull MarkwonHtmlRenderer renderer, @NonNull SpannableBuilder builder, @NonNull HtmlTag tag ); protected static void visitChildren( @NonNull MarkwonConfiguration configuration, + @NonNull MarkwonHtmlRenderer renderer, @NonNull SpannableBuilder builder, @NonNull HtmlTag.Block block) { @@ -26,11 +28,11 @@ public abstract class TagHandler { continue; } - handler = configuration.htmlRenderer().tagHandler(child.name()); + handler = renderer.tagHandler(child.name()); if (handler != null) { - handler.handle(configuration, builder, child); + handler.handle(configuration, renderer, builder, child); } else { - visitChildren(configuration, builder, child); + visitChildren(configuration, renderer, builder, child); } } } diff --git a/markwon-html/src/main/java/ru/noties/markwon/html/impl/TrimmingAppender.java b/markwon-html/src/main/java/ru/noties/markwon/html/TrimmingAppender.java similarity index 94% rename from markwon-html/src/main/java/ru/noties/markwon/html/impl/TrimmingAppender.java rename to markwon-html/src/main/java/ru/noties/markwon/html/TrimmingAppender.java index c29c93b9..4f884b00 100644 --- a/markwon-html/src/main/java/ru/noties/markwon/html/impl/TrimmingAppender.java +++ b/markwon-html/src/main/java/ru/noties/markwon/html/TrimmingAppender.java @@ -1,8 +1,8 @@ -package ru.noties.markwon.html.impl; +package ru.noties.markwon.html; import android.support.annotation.NonNull; -import static ru.noties.markwon.html.impl.AppendableUtils.appendQuietly; +import static ru.noties.markwon.html.AppendableUtils.appendQuietly; abstract class TrimmingAppender { diff --git a/markwon-html/src/main/java/ru/noties/markwon/html/impl/jsoup/UncheckedIOException.java b/markwon-html/src/main/java/ru/noties/markwon/html/jsoup/UncheckedIOException.java similarity index 85% rename from markwon-html/src/main/java/ru/noties/markwon/html/impl/jsoup/UncheckedIOException.java rename to markwon-html/src/main/java/ru/noties/markwon/html/jsoup/UncheckedIOException.java index c59b725b..9548bdf4 100644 --- a/markwon-html/src/main/java/ru/noties/markwon/html/impl/jsoup/UncheckedIOException.java +++ b/markwon-html/src/main/java/ru/noties/markwon/html/jsoup/UncheckedIOException.java @@ -1,4 +1,4 @@ -package ru.noties.markwon.html.impl.jsoup; +package ru.noties.markwon.html.jsoup; import java.io.IOException; diff --git a/markwon-html/src/main/java/ru/noties/markwon/html/impl/jsoup/helper/Normalizer.java b/markwon-html/src/main/java/ru/noties/markwon/html/jsoup/helper/Normalizer.java similarity index 89% rename from markwon-html/src/main/java/ru/noties/markwon/html/impl/jsoup/helper/Normalizer.java rename to markwon-html/src/main/java/ru/noties/markwon/html/jsoup/helper/Normalizer.java index 024e5350..a0df7dd4 100644 --- a/markwon-html/src/main/java/ru/noties/markwon/html/impl/jsoup/helper/Normalizer.java +++ b/markwon-html/src/main/java/ru/noties/markwon/html/jsoup/helper/Normalizer.java @@ -1,4 +1,4 @@ -package ru.noties.markwon.html.impl.jsoup.helper; +package ru.noties.markwon.html.jsoup.helper; import java.util.Locale; diff --git a/markwon-html/src/main/java/ru/noties/markwon/html/impl/jsoup/helper/Validate.java b/markwon-html/src/main/java/ru/noties/markwon/html/jsoup/helper/Validate.java similarity index 98% rename from markwon-html/src/main/java/ru/noties/markwon/html/impl/jsoup/helper/Validate.java rename to markwon-html/src/main/java/ru/noties/markwon/html/jsoup/helper/Validate.java index e8effd8c..0d00249b 100644 --- a/markwon-html/src/main/java/ru/noties/markwon/html/impl/jsoup/helper/Validate.java +++ b/markwon-html/src/main/java/ru/noties/markwon/html/jsoup/helper/Validate.java @@ -1,4 +1,4 @@ -package ru.noties.markwon.html.impl.jsoup.helper; +package ru.noties.markwon.html.jsoup.helper; /** * Simple validation methods. Designed for jsoup internal use diff --git a/markwon-html/src/main/java/ru/noties/markwon/html/impl/jsoup/nodes/Attribute.java b/markwon-html/src/main/java/ru/noties/markwon/html/jsoup/nodes/Attribute.java similarity index 98% rename from markwon-html/src/main/java/ru/noties/markwon/html/impl/jsoup/nodes/Attribute.java rename to markwon-html/src/main/java/ru/noties/markwon/html/jsoup/nodes/Attribute.java index 3ece793d..934dc364 100644 --- a/markwon-html/src/main/java/ru/noties/markwon/html/impl/jsoup/nodes/Attribute.java +++ b/markwon-html/src/main/java/ru/noties/markwon/html/jsoup/nodes/Attribute.java @@ -1,8 +1,8 @@ -package ru.noties.markwon.html.impl.jsoup.nodes; +package ru.noties.markwon.html.jsoup.nodes; import java.util.Map; -import ru.noties.markwon.html.impl.jsoup.helper.Validate; +import ru.noties.markwon.html.jsoup.helper.Validate; /** A single key + value attribute. (Only used for presentation.) diff --git a/markwon-html/src/main/java/ru/noties/markwon/html/impl/jsoup/nodes/Attributes.java b/markwon-html/src/main/java/ru/noties/markwon/html/jsoup/nodes/Attributes.java similarity index 98% rename from markwon-html/src/main/java/ru/noties/markwon/html/impl/jsoup/nodes/Attributes.java rename to markwon-html/src/main/java/ru/noties/markwon/html/jsoup/nodes/Attributes.java index 71494812..ced993b0 100644 --- a/markwon-html/src/main/java/ru/noties/markwon/html/impl/jsoup/nodes/Attributes.java +++ b/markwon-html/src/main/java/ru/noties/markwon/html/jsoup/nodes/Attributes.java @@ -1,14 +1,11 @@ -package ru.noties.markwon.html.impl.jsoup.nodes; +package ru.noties.markwon.html.jsoup.nodes; -import java.util.ArrayList; import java.util.Arrays; -import java.util.Collections; import java.util.Iterator; -import java.util.List; -import ru.noties.markwon.html.impl.jsoup.helper.Validate; +import ru.noties.markwon.html.jsoup.helper.Validate; -import static ru.noties.markwon.html.impl.jsoup.helper.Normalizer.lowerCase; +import static ru.noties.markwon.html.jsoup.helper.Normalizer.lowerCase; /** * The attributes of an Element. diff --git a/markwon-html/src/main/java/ru/noties/markwon/html/impl/jsoup/nodes/CommonMarkEntities.java b/markwon-html/src/main/java/ru/noties/markwon/html/jsoup/nodes/CommonMarkEntities.java similarity index 96% rename from markwon-html/src/main/java/ru/noties/markwon/html/impl/jsoup/nodes/CommonMarkEntities.java rename to markwon-html/src/main/java/ru/noties/markwon/html/jsoup/nodes/CommonMarkEntities.java index abb49575..ec362312 100644 --- a/markwon-html/src/main/java/ru/noties/markwon/html/impl/jsoup/nodes/CommonMarkEntities.java +++ b/markwon-html/src/main/java/ru/noties/markwon/html/jsoup/nodes/CommonMarkEntities.java @@ -1,4 +1,4 @@ -package ru.noties.markwon.html.impl.jsoup.nodes; +package ru.noties.markwon.html.jsoup.nodes; import android.support.annotation.NonNull; diff --git a/markwon-html/src/main/java/ru/noties/markwon/html/impl/jsoup/nodes/DocumentType.java b/markwon-html/src/main/java/ru/noties/markwon/html/jsoup/nodes/DocumentType.java similarity index 98% rename from markwon-html/src/main/java/ru/noties/markwon/html/impl/jsoup/nodes/DocumentType.java rename to markwon-html/src/main/java/ru/noties/markwon/html/jsoup/nodes/DocumentType.java index 0b1240df..dc11e537 100644 --- a/markwon-html/src/main/java/ru/noties/markwon/html/impl/jsoup/nodes/DocumentType.java +++ b/markwon-html/src/main/java/ru/noties/markwon/html/jsoup/nodes/DocumentType.java @@ -1,4 +1,4 @@ -package ru.noties.markwon.html.impl.jsoup.nodes; +package ru.noties.markwon.html.jsoup.nodes; /** * A {@code } node. diff --git a/markwon-html/src/main/java/ru/noties/markwon/html/impl/jsoup/parser/CharacterReader.java b/markwon-html/src/main/java/ru/noties/markwon/html/jsoup/parser/CharacterReader.java similarity index 98% rename from markwon-html/src/main/java/ru/noties/markwon/html/impl/jsoup/parser/CharacterReader.java rename to markwon-html/src/main/java/ru/noties/markwon/html/jsoup/parser/CharacterReader.java index f7b7d892..1839cc2b 100644 --- a/markwon-html/src/main/java/ru/noties/markwon/html/impl/jsoup/parser/CharacterReader.java +++ b/markwon-html/src/main/java/ru/noties/markwon/html/jsoup/parser/CharacterReader.java @@ -1,6 +1,4 @@ -package ru.noties.markwon.html.impl.jsoup.parser; - -import android.support.annotation.NonNull; +package ru.noties.markwon.html.jsoup.parser; import java.io.IOException; import java.io.Reader; @@ -8,8 +6,8 @@ import java.io.StringReader; import java.util.Arrays; import java.util.Locale; -import ru.noties.markwon.html.impl.jsoup.UncheckedIOException; -import ru.noties.markwon.html.impl.jsoup.helper.Validate; +import ru.noties.markwon.html.jsoup.UncheckedIOException; +import ru.noties.markwon.html.jsoup.helper.Validate; /** * CharacterReader consumes tokens off a string. Used internally by jsoup. API subject to changes. diff --git a/markwon-html/src/main/java/ru/noties/markwon/html/impl/jsoup/parser/ParseError.java b/markwon-html/src/main/java/ru/noties/markwon/html/jsoup/parser/ParseError.java similarity index 94% rename from markwon-html/src/main/java/ru/noties/markwon/html/impl/jsoup/parser/ParseError.java rename to markwon-html/src/main/java/ru/noties/markwon/html/jsoup/parser/ParseError.java index f43c3007..533f9aee 100644 --- a/markwon-html/src/main/java/ru/noties/markwon/html/impl/jsoup/parser/ParseError.java +++ b/markwon-html/src/main/java/ru/noties/markwon/html/jsoup/parser/ParseError.java @@ -1,4 +1,4 @@ -package ru.noties.markwon.html.impl.jsoup.parser; +package ru.noties.markwon.html.jsoup.parser; /** * A Parse Error records an error in the input HTML that occurs in either the tokenisation or the tree building phase. diff --git a/markwon-html/src/main/java/ru/noties/markwon/html/impl/jsoup/parser/ParseErrorList.java b/markwon-html/src/main/java/ru/noties/markwon/html/jsoup/parser/ParseErrorList.java similarity index 93% rename from markwon-html/src/main/java/ru/noties/markwon/html/impl/jsoup/parser/ParseErrorList.java rename to markwon-html/src/main/java/ru/noties/markwon/html/jsoup/parser/ParseErrorList.java index 032b7571..a3e42a08 100644 --- a/markwon-html/src/main/java/ru/noties/markwon/html/impl/jsoup/parser/ParseErrorList.java +++ b/markwon-html/src/main/java/ru/noties/markwon/html/jsoup/parser/ParseErrorList.java @@ -1,4 +1,4 @@ -package ru.noties.markwon.html.impl.jsoup.parser; +package ru.noties.markwon.html.jsoup.parser; import java.util.ArrayList; diff --git a/markwon-html/src/main/java/ru/noties/markwon/html/impl/jsoup/parser/Token.java b/markwon-html/src/main/java/ru/noties/markwon/html/jsoup/parser/Token.java similarity index 97% rename from markwon-html/src/main/java/ru/noties/markwon/html/impl/jsoup/parser/Token.java rename to markwon-html/src/main/java/ru/noties/markwon/html/jsoup/parser/Token.java index a70da0e7..887cc61c 100644 --- a/markwon-html/src/main/java/ru/noties/markwon/html/impl/jsoup/parser/Token.java +++ b/markwon-html/src/main/java/ru/noties/markwon/html/jsoup/parser/Token.java @@ -1,11 +1,11 @@ -package ru.noties.markwon.html.impl.jsoup.parser; +package ru.noties.markwon.html.jsoup.parser; import android.support.annotation.NonNull; -import ru.noties.markwon.html.impl.jsoup.helper.Validate; -import ru.noties.markwon.html.impl.jsoup.nodes.Attributes; +import ru.noties.markwon.html.jsoup.helper.Validate; +import ru.noties.markwon.html.jsoup.nodes.Attributes; -import static ru.noties.markwon.html.impl.jsoup.helper.Normalizer.lowerCase; +import static ru.noties.markwon.html.jsoup.helper.Normalizer.lowerCase; /** * Parse tokens for the Tokeniser. diff --git a/markwon-html/src/main/java/ru/noties/markwon/html/impl/jsoup/parser/Tokeniser.java b/markwon-html/src/main/java/ru/noties/markwon/html/jsoup/parser/Tokeniser.java similarity index 98% rename from markwon-html/src/main/java/ru/noties/markwon/html/impl/jsoup/parser/Tokeniser.java rename to markwon-html/src/main/java/ru/noties/markwon/html/jsoup/parser/Tokeniser.java index 77df2b7c..3b871776 100644 --- a/markwon-html/src/main/java/ru/noties/markwon/html/impl/jsoup/parser/Tokeniser.java +++ b/markwon-html/src/main/java/ru/noties/markwon/html/jsoup/parser/Tokeniser.java @@ -1,9 +1,9 @@ -package ru.noties.markwon.html.impl.jsoup.parser; +package ru.noties.markwon.html.jsoup.parser; import java.util.Arrays; -import ru.noties.markwon.html.impl.jsoup.helper.Validate; -import ru.noties.markwon.html.impl.jsoup.nodes.CommonMarkEntities; +import ru.noties.markwon.html.jsoup.helper.Validate; +import ru.noties.markwon.html.jsoup.nodes.CommonMarkEntities; /** * Readers the input stream into tokens. diff --git a/markwon-html/src/main/java/ru/noties/markwon/html/impl/jsoup/parser/TokeniserState.java b/markwon-html/src/main/java/ru/noties/markwon/html/jsoup/parser/TokeniserState.java similarity index 99% rename from markwon-html/src/main/java/ru/noties/markwon/html/impl/jsoup/parser/TokeniserState.java rename to markwon-html/src/main/java/ru/noties/markwon/html/jsoup/parser/TokeniserState.java index 4c3e4405..01a98958 100644 --- a/markwon-html/src/main/java/ru/noties/markwon/html/impl/jsoup/parser/TokeniserState.java +++ b/markwon-html/src/main/java/ru/noties/markwon/html/jsoup/parser/TokeniserState.java @@ -1,6 +1,6 @@ -package ru.noties.markwon.html.impl.jsoup.parser; +package ru.noties.markwon.html.jsoup.parser; -import ru.noties.markwon.html.impl.jsoup.nodes.DocumentType; +import ru.noties.markwon.html.jsoup.nodes.DocumentType; /** * States and transition activations for the Tokeniser. diff --git a/markwon-html/src/main/java/ru/noties/markwon/html/impl/span/SubScriptSpan.java b/markwon-html/src/main/java/ru/noties/markwon/html/span/SubScriptSpan.java similarity index 85% rename from markwon-html/src/main/java/ru/noties/markwon/html/impl/span/SubScriptSpan.java rename to markwon-html/src/main/java/ru/noties/markwon/html/span/SubScriptSpan.java index 8eca8fdc..d106c1a0 100644 --- a/markwon-html/src/main/java/ru/noties/markwon/html/impl/span/SubScriptSpan.java +++ b/markwon-html/src/main/java/ru/noties/markwon/html/span/SubScriptSpan.java @@ -1,10 +1,10 @@ -package ru.noties.markwon.html.impl.span; +package ru.noties.markwon.html.span; import android.support.annotation.NonNull; import android.text.TextPaint; import android.text.style.MetricAffectingSpan; -import ru.noties.markwon.html.impl.MarkwonHtmlRendererImpl; +import ru.noties.markwon.html.MarkwonHtmlRendererImpl; public class SubScriptSpan extends MetricAffectingSpan { diff --git a/markwon-html/src/main/java/ru/noties/markwon/html/impl/span/SuperScriptSpan.java b/markwon-html/src/main/java/ru/noties/markwon/html/span/SuperScriptSpan.java similarity index 85% rename from markwon-html/src/main/java/ru/noties/markwon/html/impl/span/SuperScriptSpan.java rename to markwon-html/src/main/java/ru/noties/markwon/html/span/SuperScriptSpan.java index d33a2bea..9d43c563 100644 --- a/markwon-html/src/main/java/ru/noties/markwon/html/impl/span/SuperScriptSpan.java +++ b/markwon-html/src/main/java/ru/noties/markwon/html/span/SuperScriptSpan.java @@ -1,10 +1,10 @@ -package ru.noties.markwon.html.impl.span; +package ru.noties.markwon.html.span; import android.support.annotation.NonNull; import android.text.TextPaint; import android.text.style.MetricAffectingSpan; -import ru.noties.markwon.html.impl.MarkwonHtmlRendererImpl; +import ru.noties.markwon.html.MarkwonHtmlRendererImpl; public class SuperScriptSpan extends MetricAffectingSpan { diff --git a/markwon-html/src/main/java/ru/noties/markwon/html/impl/tag/BlockquoteHandler.java b/markwon-html/src/main/java/ru/noties/markwon/html/tag/BlockquoteHandler.java similarity index 76% rename from markwon-html/src/main/java/ru/noties/markwon/html/impl/tag/BlockquoteHandler.java rename to markwon-html/src/main/java/ru/noties/markwon/html/tag/BlockquoteHandler.java index 2687ea94..4cd93fa0 100644 --- a/markwon-html/src/main/java/ru/noties/markwon/html/impl/tag/BlockquoteHandler.java +++ b/markwon-html/src/main/java/ru/noties/markwon/html/tag/BlockquoteHandler.java @@ -1,10 +1,11 @@ -package ru.noties.markwon.html.impl.tag; +package ru.noties.markwon.html.tag; import android.support.annotation.NonNull; import ru.noties.markwon.MarkwonConfiguration; import ru.noties.markwon.SpannableBuilder; import ru.noties.markwon.html.HtmlTag; +import ru.noties.markwon.html.MarkwonHtmlRenderer; import ru.noties.markwon.html.TagHandler; public class BlockquoteHandler extends TagHandler { @@ -12,11 +13,12 @@ public class BlockquoteHandler extends TagHandler { @Override public void handle( @NonNull MarkwonConfiguration configuration, + @NonNull MarkwonHtmlRenderer renderer, @NonNull SpannableBuilder builder, @NonNull HtmlTag tag) { if (tag.isBlock()) { - visitChildren(configuration, builder, tag.getAsBlock()); + visitChildren(configuration, renderer, builder, tag.getAsBlock()); } SpannableBuilder.setSpans( diff --git a/markwon-html/src/main/java/ru/noties/markwon/html/impl/tag/EmphasisHandler.java b/markwon-html/src/main/java/ru/noties/markwon/html/tag/EmphasisHandler.java similarity index 91% rename from markwon-html/src/main/java/ru/noties/markwon/html/impl/tag/EmphasisHandler.java rename to markwon-html/src/main/java/ru/noties/markwon/html/tag/EmphasisHandler.java index 0385266a..b0809be6 100644 --- a/markwon-html/src/main/java/ru/noties/markwon/html/impl/tag/EmphasisHandler.java +++ b/markwon-html/src/main/java/ru/noties/markwon/html/tag/EmphasisHandler.java @@ -1,4 +1,4 @@ -package ru.noties.markwon.html.impl.tag; +package ru.noties.markwon.html.tag; import android.support.annotation.NonNull; import android.support.annotation.Nullable; diff --git a/markwon-html/src/main/java/ru/noties/markwon/html/impl/tag/HeadingHandler.java b/markwon-html/src/main/java/ru/noties/markwon/html/tag/HeadingHandler.java similarity index 93% rename from markwon-html/src/main/java/ru/noties/markwon/html/impl/tag/HeadingHandler.java rename to markwon-html/src/main/java/ru/noties/markwon/html/tag/HeadingHandler.java index d675351e..79848625 100644 --- a/markwon-html/src/main/java/ru/noties/markwon/html/impl/tag/HeadingHandler.java +++ b/markwon-html/src/main/java/ru/noties/markwon/html/tag/HeadingHandler.java @@ -1,4 +1,4 @@ -package ru.noties.markwon.html.impl.tag; +package ru.noties.markwon.html.tag; import android.support.annotation.NonNull; import android.support.annotation.Nullable; diff --git a/markwon-html/src/main/java/ru/noties/markwon/html/impl/tag/ImageHandler.java b/markwon-html/src/main/java/ru/noties/markwon/html/tag/ImageHandler.java similarity index 94% rename from markwon-html/src/main/java/ru/noties/markwon/html/impl/tag/ImageHandler.java rename to markwon-html/src/main/java/ru/noties/markwon/html/tag/ImageHandler.java index 0fc9b6f2..2faf4826 100644 --- a/markwon-html/src/main/java/ru/noties/markwon/html/impl/tag/ImageHandler.java +++ b/markwon-html/src/main/java/ru/noties/markwon/html/tag/ImageHandler.java @@ -1,4 +1,4 @@ -package ru.noties.markwon.html.impl.tag; +package ru.noties.markwon.html.tag; import android.support.annotation.NonNull; import android.support.annotation.Nullable; @@ -8,7 +8,7 @@ import java.util.Map; import ru.noties.markwon.MarkwonConfiguration; import ru.noties.markwon.html.HtmlTag; -import ru.noties.markwon.html.impl.CssInlineStyleParser; +import ru.noties.markwon.html.CssInlineStyleParser; import ru.noties.markwon.image.ImageSize; public class ImageHandler extends SimpleTagHandler { diff --git a/markwon-html/src/main/java/ru/noties/markwon/html/impl/tag/ImageSizeParserImpl.java b/markwon-html/src/main/java/ru/noties/markwon/html/tag/ImageSizeParserImpl.java similarity index 95% rename from markwon-html/src/main/java/ru/noties/markwon/html/impl/tag/ImageSizeParserImpl.java rename to markwon-html/src/main/java/ru/noties/markwon/html/tag/ImageSizeParserImpl.java index e8d32367..236f53be 100644 --- a/markwon-html/src/main/java/ru/noties/markwon/html/impl/tag/ImageSizeParserImpl.java +++ b/markwon-html/src/main/java/ru/noties/markwon/html/tag/ImageSizeParserImpl.java @@ -1,4 +1,4 @@ -package ru.noties.markwon.html.impl.tag; +package ru.noties.markwon.html.tag; import android.support.annotation.NonNull; import android.support.annotation.Nullable; @@ -7,8 +7,8 @@ import android.text.TextUtils; import java.util.Map; -import ru.noties.markwon.html.impl.CssInlineStyleParser; -import ru.noties.markwon.html.impl.CssProperty; +import ru.noties.markwon.html.CssInlineStyleParser; +import ru.noties.markwon.html.CssProperty; import ru.noties.markwon.image.ImageSize; class ImageSizeParserImpl implements ImageHandler.ImageSizeParser { diff --git a/markwon-html/src/main/java/ru/noties/markwon/html/impl/tag/LinkHandler.java b/markwon-html/src/main/java/ru/noties/markwon/html/tag/LinkHandler.java similarity index 94% rename from markwon-html/src/main/java/ru/noties/markwon/html/impl/tag/LinkHandler.java rename to markwon-html/src/main/java/ru/noties/markwon/html/tag/LinkHandler.java index 1faba510..8bf78ff0 100644 --- a/markwon-html/src/main/java/ru/noties/markwon/html/impl/tag/LinkHandler.java +++ b/markwon-html/src/main/java/ru/noties/markwon/html/tag/LinkHandler.java @@ -1,4 +1,4 @@ -package ru.noties.markwon.html.impl.tag; +package ru.noties.markwon.html.tag; import android.support.annotation.NonNull; import android.support.annotation.Nullable; diff --git a/markwon-html/src/main/java/ru/noties/markwon/html/impl/tag/ListHandler.java b/markwon-html/src/main/java/ru/noties/markwon/html/tag/ListHandler.java similarity index 90% rename from markwon-html/src/main/java/ru/noties/markwon/html/impl/tag/ListHandler.java rename to markwon-html/src/main/java/ru/noties/markwon/html/tag/ListHandler.java index 80094b5c..3a4dfd7c 100644 --- a/markwon-html/src/main/java/ru/noties/markwon/html/impl/tag/ListHandler.java +++ b/markwon-html/src/main/java/ru/noties/markwon/html/tag/ListHandler.java @@ -1,10 +1,11 @@ -package ru.noties.markwon.html.impl.tag; +package ru.noties.markwon.html.tag; import android.support.annotation.NonNull; import ru.noties.markwon.MarkwonConfiguration; import ru.noties.markwon.SpannableBuilder; import ru.noties.markwon.html.HtmlTag; +import ru.noties.markwon.html.MarkwonHtmlRenderer; import ru.noties.markwon.html.TagHandler; public class ListHandler extends TagHandler { @@ -12,6 +13,7 @@ public class ListHandler extends TagHandler { @Override public void handle( @NonNull MarkwonConfiguration configuration, + @NonNull MarkwonHtmlRenderer renderer, @NonNull SpannableBuilder builder, @NonNull HtmlTag tag) { @@ -34,7 +36,7 @@ public class ListHandler extends TagHandler { for (HtmlTag.Block child : block.children()) { - visitChildren(configuration, builder, child); + visitChildren(configuration, renderer, builder, child); if ("li".equals(child.name())) { // insert list item here diff --git a/markwon-html/src/main/java/ru/noties/markwon/html/impl/tag/SimpleTagHandler.java b/markwon-html/src/main/java/ru/noties/markwon/html/tag/SimpleTagHandler.java similarity index 78% rename from markwon-html/src/main/java/ru/noties/markwon/html/impl/tag/SimpleTagHandler.java rename to markwon-html/src/main/java/ru/noties/markwon/html/tag/SimpleTagHandler.java index ad68ef30..784ef8c9 100644 --- a/markwon-html/src/main/java/ru/noties/markwon/html/impl/tag/SimpleTagHandler.java +++ b/markwon-html/src/main/java/ru/noties/markwon/html/tag/SimpleTagHandler.java @@ -1,4 +1,4 @@ -package ru.noties.markwon.html.impl.tag; +package ru.noties.markwon.html.tag; import android.support.annotation.NonNull; import android.support.annotation.Nullable; @@ -6,6 +6,7 @@ import android.support.annotation.Nullable; import ru.noties.markwon.MarkwonConfiguration; import ru.noties.markwon.SpannableBuilder; import ru.noties.markwon.html.HtmlTag; +import ru.noties.markwon.html.MarkwonHtmlRenderer; import ru.noties.markwon.html.TagHandler; public abstract class SimpleTagHandler extends TagHandler { @@ -14,7 +15,7 @@ public abstract class SimpleTagHandler extends TagHandler { public abstract Object getSpans(@NonNull MarkwonConfiguration configuration, @NonNull HtmlTag tag); @Override - public void handle(@NonNull MarkwonConfiguration configuration, @NonNull SpannableBuilder builder, @NonNull HtmlTag tag) { + public void handle(@NonNull MarkwonConfiguration configuration, @NonNull MarkwonHtmlRenderer renderer, @NonNull SpannableBuilder builder, @NonNull HtmlTag tag) { final Object spans = getSpans(configuration, tag); if (spans != null) { SpannableBuilder.setSpans(builder, spans, tag.start(), tag.end()); diff --git a/markwon-html/src/main/java/ru/noties/markwon/html/impl/tag/StrikeHandler.java b/markwon-html/src/main/java/ru/noties/markwon/html/tag/StrikeHandler.java similarity index 76% rename from markwon-html/src/main/java/ru/noties/markwon/html/impl/tag/StrikeHandler.java rename to markwon-html/src/main/java/ru/noties/markwon/html/tag/StrikeHandler.java index 54497822..6df59f28 100644 --- a/markwon-html/src/main/java/ru/noties/markwon/html/impl/tag/StrikeHandler.java +++ b/markwon-html/src/main/java/ru/noties/markwon/html/tag/StrikeHandler.java @@ -1,4 +1,4 @@ -package ru.noties.markwon.html.impl.tag; +package ru.noties.markwon.html.tag; import android.support.annotation.NonNull; import android.text.style.StrikethroughSpan; @@ -6,6 +6,7 @@ import android.text.style.StrikethroughSpan; import ru.noties.markwon.MarkwonConfiguration; import ru.noties.markwon.SpannableBuilder; import ru.noties.markwon.html.HtmlTag; +import ru.noties.markwon.html.MarkwonHtmlRenderer; import ru.noties.markwon.html.TagHandler; public class StrikeHandler extends TagHandler { @@ -13,11 +14,12 @@ public class StrikeHandler extends TagHandler { @Override public void handle( @NonNull MarkwonConfiguration configuration, + @NonNull MarkwonHtmlRenderer renderer, @NonNull SpannableBuilder builder, @NonNull HtmlTag tag) { if (tag.isBlock()) { - visitChildren(configuration, builder, tag.getAsBlock()); + visitChildren(configuration, renderer, builder, tag.getAsBlock()); } SpannableBuilder.setSpans( diff --git a/markwon-html/src/main/java/ru/noties/markwon/html/impl/tag/StrongEmphasisHandler.java b/markwon-html/src/main/java/ru/noties/markwon/html/tag/StrongEmphasisHandler.java similarity index 91% rename from markwon-html/src/main/java/ru/noties/markwon/html/impl/tag/StrongEmphasisHandler.java rename to markwon-html/src/main/java/ru/noties/markwon/html/tag/StrongEmphasisHandler.java index 6b5f8230..31f5aff8 100644 --- a/markwon-html/src/main/java/ru/noties/markwon/html/impl/tag/StrongEmphasisHandler.java +++ b/markwon-html/src/main/java/ru/noties/markwon/html/tag/StrongEmphasisHandler.java @@ -1,4 +1,4 @@ -package ru.noties.markwon.html.impl.tag; +package ru.noties.markwon.html.tag; import android.support.annotation.NonNull; import android.support.annotation.Nullable; diff --git a/markwon-html/src/main/java/ru/noties/markwon/html/impl/tag/SubScriptHandler.java b/markwon-html/src/main/java/ru/noties/markwon/html/tag/SubScriptHandler.java similarity index 80% rename from markwon-html/src/main/java/ru/noties/markwon/html/impl/tag/SubScriptHandler.java rename to markwon-html/src/main/java/ru/noties/markwon/html/tag/SubScriptHandler.java index 9adf5684..2dee9f7a 100644 --- a/markwon-html/src/main/java/ru/noties/markwon/html/impl/tag/SubScriptHandler.java +++ b/markwon-html/src/main/java/ru/noties/markwon/html/tag/SubScriptHandler.java @@ -1,11 +1,11 @@ -package ru.noties.markwon.html.impl.tag; +package ru.noties.markwon.html.tag; import android.support.annotation.NonNull; import android.support.annotation.Nullable; import ru.noties.markwon.MarkwonConfiguration; import ru.noties.markwon.html.HtmlTag; -import ru.noties.markwon.html.impl.span.SubScriptSpan; +import ru.noties.markwon.html.span.SubScriptSpan; public class SubScriptHandler extends SimpleTagHandler { @Nullable diff --git a/markwon-html/src/main/java/ru/noties/markwon/html/impl/tag/SuperScriptHandler.java b/markwon-html/src/main/java/ru/noties/markwon/html/tag/SuperScriptHandler.java similarity index 80% rename from markwon-html/src/main/java/ru/noties/markwon/html/impl/tag/SuperScriptHandler.java rename to markwon-html/src/main/java/ru/noties/markwon/html/tag/SuperScriptHandler.java index 42e00075..ccc5c17a 100644 --- a/markwon-html/src/main/java/ru/noties/markwon/html/impl/tag/SuperScriptHandler.java +++ b/markwon-html/src/main/java/ru/noties/markwon/html/tag/SuperScriptHandler.java @@ -1,11 +1,11 @@ -package ru.noties.markwon.html.impl.tag; +package ru.noties.markwon.html.tag; import android.support.annotation.NonNull; import android.support.annotation.Nullable; import ru.noties.markwon.MarkwonConfiguration; import ru.noties.markwon.html.HtmlTag; -import ru.noties.markwon.html.impl.span.SuperScriptSpan; +import ru.noties.markwon.html.span.SuperScriptSpan; public class SuperScriptHandler extends SimpleTagHandler { @Nullable diff --git a/markwon-html/src/main/java/ru/noties/markwon/html/impl/tag/UnderlineHandler.java b/markwon-html/src/main/java/ru/noties/markwon/html/tag/UnderlineHandler.java similarity index 79% rename from markwon-html/src/main/java/ru/noties/markwon/html/impl/tag/UnderlineHandler.java rename to markwon-html/src/main/java/ru/noties/markwon/html/tag/UnderlineHandler.java index fb492dda..882cae1c 100644 --- a/markwon-html/src/main/java/ru/noties/markwon/html/impl/tag/UnderlineHandler.java +++ b/markwon-html/src/main/java/ru/noties/markwon/html/tag/UnderlineHandler.java @@ -1,4 +1,4 @@ -package ru.noties.markwon.html.impl.tag; +package ru.noties.markwon.html.tag; import android.support.annotation.NonNull; import android.text.style.UnderlineSpan; @@ -6,6 +6,7 @@ import android.text.style.UnderlineSpan; import ru.noties.markwon.MarkwonConfiguration; import ru.noties.markwon.SpannableBuilder; import ru.noties.markwon.html.HtmlTag; +import ru.noties.markwon.html.MarkwonHtmlRenderer; import ru.noties.markwon.html.TagHandler; public class UnderlineHandler extends TagHandler { @@ -13,6 +14,7 @@ public class UnderlineHandler extends TagHandler { @Override public void handle( @NonNull MarkwonConfiguration configuration, + @NonNull MarkwonHtmlRenderer renderer, @NonNull SpannableBuilder builder, @NonNull HtmlTag tag) { @@ -20,7 +22,7 @@ public class UnderlineHandler extends TagHandler { // thus doesn't allow children, we must visit them first if (tag.isBlock()) { - visitChildren(configuration, builder, tag.getAsBlock()); + visitChildren(configuration, renderer, builder, tag.getAsBlock()); } SpannableBuilder.setSpans( diff --git a/markwon-html/src/test/java/ru/noties/markwon/html/impl/CssInlineStyleParserTest.java b/markwon-html/src/test/java/ru/noties/markwon/html/impl/CssInlineStyleParserTest.java index 36c7d798..a898d342 100644 --- a/markwon-html/src/test/java/ru/noties/markwon/html/impl/CssInlineStyleParserTest.java +++ b/markwon-html/src/test/java/ru/noties/markwon/html/impl/CssInlineStyleParserTest.java @@ -14,6 +14,8 @@ import java.util.Map; import ix.Ix; import ix.IxFunction; +import ru.noties.markwon.html.CssInlineStyleParser; +import ru.noties.markwon.html.CssProperty; import ru.noties.markwon.test.TestUtils; import static org.junit.Assert.assertEquals; diff --git a/markwon-html/src/test/java/ru/noties/markwon/html/impl/HtmlEmptyTagReplacementTest.java b/markwon-html/src/test/java/ru/noties/markwon/html/impl/HtmlEmptyTagReplacementTest.java index eab3a9f1..62333012 100644 --- a/markwon-html/src/test/java/ru/noties/markwon/html/impl/HtmlEmptyTagReplacementTest.java +++ b/markwon-html/src/test/java/ru/noties/markwon/html/impl/HtmlEmptyTagReplacementTest.java @@ -5,8 +5,9 @@ import org.junit.Test; import java.util.Collections; +import ru.noties.markwon.html.HtmlEmptyTagReplacement; import ru.noties.markwon.html.api.HtmlTag; -import ru.noties.markwon.html.impl.HtmlTagImpl.InlineImpl; +import ru.noties.markwon.html.HtmlTagImpl.InlineImpl; import static org.junit.Assert.assertEquals; diff --git a/markwon-html/src/test/java/ru/noties/markwon/html/impl/MarkwonHtmlParserImplTest.java b/markwon-html/src/test/java/ru/noties/markwon/html/impl/MarkwonHtmlParserImplTest.java index ad0669b3..54adb09f 100644 --- a/markwon-html/src/test/java/ru/noties/markwon/html/impl/MarkwonHtmlParserImplTest.java +++ b/markwon-html/src/test/java/ru/noties/markwon/html/impl/MarkwonHtmlParserImplTest.java @@ -15,6 +15,8 @@ import java.util.List; import java.util.Map; import java.util.Set; +import ru.noties.markwon.html.HtmlEmptyTagReplacement; +import ru.noties.markwon.html.MarkwonHtmlParserImpl; import ru.noties.markwon.html.api.HtmlTag; import ru.noties.markwon.html.api.MarkwonHtmlParser; diff --git a/markwon-html/src/test/java/ru/noties/markwon/html/impl/TrimmingAppenderTest.java b/markwon-html/src/test/java/ru/noties/markwon/html/impl/TrimmingAppenderTest.java index 87923c33..3d5cf4db 100644 --- a/markwon-html/src/test/java/ru/noties/markwon/html/impl/TrimmingAppenderTest.java +++ b/markwon-html/src/test/java/ru/noties/markwon/html/impl/TrimmingAppenderTest.java @@ -6,6 +6,8 @@ import org.junit.runner.RunWith; import org.robolectric.RobolectricTestRunner; import org.robolectric.annotation.Config; +import ru.noties.markwon.html.TrimmingAppender; + import static org.junit.Assert.assertEquals; @RunWith(RobolectricTestRunner.class) diff --git a/markwon-html/src/test/java/ru/noties/markwon/html/impl/jsoup/nodes/CommonMarkEntitiesTest.java b/markwon-html/src/test/java/ru/noties/markwon/html/impl/jsoup/nodes/CommonMarkEntitiesTest.java index ab013578..866b3352 100644 --- a/markwon-html/src/test/java/ru/noties/markwon/html/impl/jsoup/nodes/CommonMarkEntitiesTest.java +++ b/markwon-html/src/test/java/ru/noties/markwon/html/impl/jsoup/nodes/CommonMarkEntitiesTest.java @@ -5,6 +5,8 @@ import org.junit.runner.RunWith; import org.robolectric.RobolectricTestRunner; import org.robolectric.annotation.Config; +import ru.noties.markwon.html.jsoup.nodes.CommonMarkEntities; + import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertTrue; diff --git a/markwon-html/src/test/java/ru/noties/markwon/html/impl/tag/ImageSizeParserImplTest.java b/markwon-html/src/test/java/ru/noties/markwon/html/impl/tag/ImageSizeParserImplTest.java index 37a10c8d..c1549747 100644 --- a/markwon-html/src/test/java/ru/noties/markwon/html/impl/tag/ImageSizeParserImplTest.java +++ b/markwon-html/src/test/java/ru/noties/markwon/html/impl/tag/ImageSizeParserImplTest.java @@ -13,6 +13,7 @@ import java.util.Collections; import java.util.HashMap; import java.util.Map; +import ru.noties.markwon.html.tag.ImageSizeParserImpl; import ru.noties.markwon.image.ImageSize; import ru.noties.markwon.renderer.html2.CssInlineStyleParser; diff --git a/markwon/src/main/java/ru/noties/markwon/MarkwonConfiguration.java b/markwon/src/main/java/ru/noties/markwon/MarkwonConfiguration.java index df083e42..347e2e95 100644 --- a/markwon/src/main/java/ru/noties/markwon/MarkwonConfiguration.java +++ b/markwon/src/main/java/ru/noties/markwon/MarkwonConfiguration.java @@ -3,10 +3,7 @@ package ru.noties.markwon; import android.content.Context; import android.support.annotation.NonNull; -import ru.noties.markwon.html.MarkwonHtmlParser; -import ru.noties.markwon.html.MarkwonHtmlRenderer; import ru.noties.markwon.image.AsyncDrawableLoader; -import ru.noties.markwon.image.AsyncDrawableLoaderNoOp; import ru.noties.markwon.image.ImageSizeResolver; import ru.noties.markwon.image.ImageSizeResolverDef; import ru.noties.markwon.spans.LinkSpan; @@ -20,8 +17,9 @@ public class MarkwonConfiguration { // creates default configuration @NonNull + @Deprecated public static MarkwonConfiguration create(@NonNull Context context) { - return new Builder(context).build(MarkwonTheme.create(context), new AsyncDrawableLoaderNoOp()); + return new Builder(context).build(MarkwonTheme.create(context), AsyncDrawableLoader.noOp()); } @NonNull @@ -37,8 +35,6 @@ public class MarkwonConfiguration { private final UrlProcessor urlProcessor; private final ImageSizeResolver imageSizeResolver; private final SpannableFactory factory; // @since 1.1.0 - private final MarkwonHtmlParser htmlParser; // @since 2.0.0 - private final MarkwonHtmlRenderer htmlRenderer; // @since 2.0.0 private MarkwonConfiguration(@NonNull Builder builder) { this.theme = builder.theme; @@ -48,8 +44,6 @@ public class MarkwonConfiguration { this.urlProcessor = builder.urlProcessor; this.imageSizeResolver = builder.imageSizeResolver; this.factory = builder.factory; - this.htmlParser = builder.htmlParser; - this.htmlRenderer = builder.htmlRenderer; } /** @@ -95,22 +89,6 @@ public class MarkwonConfiguration { return factory; } - /** - * @since 2.0.0 - */ - @NonNull - public MarkwonHtmlParser htmlParser() { - return htmlParser; - } - - /** - * @since 2.0.0 - */ - @NonNull - public MarkwonHtmlRenderer htmlRenderer() { - return htmlRenderer; - } - @SuppressWarnings("unused") public static class Builder { @@ -123,8 +101,6 @@ public class MarkwonConfiguration { private UrlProcessor urlProcessor; private ImageSizeResolver imageSizeResolver; private SpannableFactory factory; // @since 1.1.0 - private MarkwonHtmlParser htmlParser; // @since 2.0.0 - private MarkwonHtmlRenderer htmlRenderer; // @since 2.0.0 Builder(@NonNull Context context) { this.context = context; @@ -139,8 +115,6 @@ public class MarkwonConfiguration { this.urlProcessor = configuration.urlProcessor; this.imageSizeResolver = configuration.imageSizeResolver; this.factory = configuration.factory; - this.htmlParser = configuration.htmlParser; - this.htmlRenderer = configuration.htmlRenderer; } @NonNull @@ -179,24 +153,6 @@ public class MarkwonConfiguration { return this; } - /** - * @since 2.0.0 - */ - @NonNull - public Builder htmlParser(@NonNull MarkwonHtmlParser htmlParser) { - this.htmlParser = htmlParser; - return this; - } - - /** - * @since 2.0.0 - */ - @NonNull - public Builder htmlRenderer(@NonNull MarkwonHtmlRenderer htmlRenderer) { - this.htmlRenderer = htmlRenderer; - return this; - } - @NonNull public MarkwonConfiguration build(@NonNull MarkwonTheme theme, @NonNull AsyncDrawableLoader asyncDrawableLoader) { @@ -224,16 +180,6 @@ public class MarkwonConfiguration { factory = SpannableFactoryDef.create(); } - // @since 2.0.0 - if (htmlParser == null) { - htmlParser = MarkwonHtmlParser.noOp(); - } - - // @since 2.0.0 - if (htmlRenderer == null) { - htmlRenderer = MarkwonHtmlRenderer.noOp(); - } - return new MarkwonConfiguration(this); } } diff --git a/markwon/src/main/java/ru/noties/markwon/image/AsyncDrawableLoader.java b/markwon/src/main/java/ru/noties/markwon/image/AsyncDrawableLoader.java index 1e841f49..f036aa7b 100644 --- a/markwon/src/main/java/ru/noties/markwon/image/AsyncDrawableLoader.java +++ b/markwon/src/main/java/ru/noties/markwon/image/AsyncDrawableLoader.java @@ -12,6 +12,22 @@ import java.util.concurrent.Executors; public abstract class AsyncDrawableLoader { + /** + * @since 3.0.0 + */ + @NonNull + public static Builder builder() { + return new Builder(); + } + + /** + * @since 3.0.0 + */ + @NonNull + public static AsyncDrawableLoader noOp() { + return new AsyncDrawableLoaderNoOp(); + } + public abstract void load(@NonNull String destination, @NonNull AsyncDrawable drawable); diff --git a/markwon/src/main/java/ru/noties/markwon/image/AsyncDrawableLoaderNoOp.java b/markwon/src/main/java/ru/noties/markwon/image/AsyncDrawableLoaderNoOp.java index 08e1283f..cef634a4 100644 --- a/markwon/src/main/java/ru/noties/markwon/image/AsyncDrawableLoaderNoOp.java +++ b/markwon/src/main/java/ru/noties/markwon/image/AsyncDrawableLoaderNoOp.java @@ -2,7 +2,7 @@ package ru.noties.markwon.image; import android.support.annotation.NonNull; -public class AsyncDrawableLoaderNoOp extends AsyncDrawableLoader { +class AsyncDrawableLoaderNoOp extends AsyncDrawableLoader { @Override public void load(@NonNull String destination, @NonNull AsyncDrawable drawable) { From b6b360b736054005987499a5d9ebc44f9b3e6532 Mon Sep 17 00:00:00 2001 From: Dimitry Ivanov Date: Sun, 2 Dec 2018 13:54:08 +0300 Subject: [PATCH 025/103] Add before and after render methods to plugin --- .../java/ru/noties/markwon/html/HtmlPlugin.java | 15 ++++++--------- .../ru/noties/markwon/AbstractMarkwonPlugin.java | 11 +++++++++++ .../main/java/ru/noties/markwon/MarkwonImpl.java | 10 ++++++++++ .../java/ru/noties/markwon/MarkwonPlugin.java | 7 +++++-- 4 files changed, 32 insertions(+), 11 deletions(-) diff --git a/markwon-html/src/main/java/ru/noties/markwon/html/HtmlPlugin.java b/markwon-html/src/main/java/ru/noties/markwon/html/HtmlPlugin.java index 3852c772..0ee64b5e 100644 --- a/markwon-html/src/main/java/ru/noties/markwon/html/HtmlPlugin.java +++ b/markwon-html/src/main/java/ru/noties/markwon/html/HtmlPlugin.java @@ -6,6 +6,7 @@ import android.support.annotation.Nullable; import org.commonmark.node.Document; import org.commonmark.node.HtmlBlock; import org.commonmark.node.HtmlInline; +import org.commonmark.node.Node; import ru.noties.markwon.AbstractMarkwonPlugin; import ru.noties.markwon.MarkwonVisitor; @@ -40,18 +41,14 @@ public class HtmlPlugin extends AbstractMarkwonPlugin { this.parser = parser; } + @Override + public void afterRender(@NonNull Node node, @NonNull MarkwonVisitor visitor) { + renderer.render(visitor.configuration(), visitor.builder(), parser); + } + @Override public void configureVisitor(@NonNull MarkwonVisitor.Builder builder) { builder - .on(Document.class, new MarkwonVisitor.NodeVisitor() { - @Override - public void visit(@NonNull MarkwonVisitor visitor, @NonNull Document document) { - - visitor.visitChildren(document); - - renderer.render(visitor.configuration(), visitor.builder(), parser); - } - }) .on(HtmlBlock.class, new MarkwonVisitor.NodeVisitor() { @Override public void visit(@NonNull MarkwonVisitor visitor, @NonNull HtmlBlock htmlBlock) { diff --git a/markwon/src/main/java/ru/noties/markwon/AbstractMarkwonPlugin.java b/markwon/src/main/java/ru/noties/markwon/AbstractMarkwonPlugin.java index b1671899..5cf614be 100644 --- a/markwon/src/main/java/ru/noties/markwon/AbstractMarkwonPlugin.java +++ b/markwon/src/main/java/ru/noties/markwon/AbstractMarkwonPlugin.java @@ -3,6 +3,7 @@ package ru.noties.markwon; import android.support.annotation.NonNull; import android.widget.TextView; +import org.commonmark.node.Node; import org.commonmark.parser.Parser; import ru.noties.markwon.image.AsyncDrawableLoader; @@ -40,6 +41,16 @@ public abstract class AbstractMarkwonPlugin implements MarkwonPlugin { return markdown; } + @Override + public void beforeRender(@NonNull Node node) { + + } + + @Override + public void afterRender(@NonNull Node node, @NonNull MarkwonVisitor visitor) { + + } + @Override public void beforeSetText(@NonNull TextView textView, @NonNull CharSequence markdown) { diff --git a/markwon/src/main/java/ru/noties/markwon/MarkwonImpl.java b/markwon/src/main/java/ru/noties/markwon/MarkwonImpl.java index 77e68b49..fe6747c7 100644 --- a/markwon/src/main/java/ru/noties/markwon/MarkwonImpl.java +++ b/markwon/src/main/java/ru/noties/markwon/MarkwonImpl.java @@ -40,7 +40,17 @@ class MarkwonImpl extends Markwon { @NonNull @Override public CharSequence render(@NonNull Node node) { + + for (MarkwonPlugin plugin : plugins) { + plugin.beforeRender(node); + } + node.accept(visitor); + + for (MarkwonPlugin plugin : plugins) { + plugin.afterRender(node, visitor); + } + return visitor.builder().text(); } diff --git a/markwon/src/main/java/ru/noties/markwon/MarkwonPlugin.java b/markwon/src/main/java/ru/noties/markwon/MarkwonPlugin.java index 690c8f97..4a1c33b9 100644 --- a/markwon/src/main/java/ru/noties/markwon/MarkwonPlugin.java +++ b/markwon/src/main/java/ru/noties/markwon/MarkwonPlugin.java @@ -3,6 +3,7 @@ package ru.noties.markwon; import android.support.annotation.NonNull; import android.widget.TextView; +import org.commonmark.node.Node; import org.commonmark.parser.Parser; import ru.noties.markwon.image.AsyncDrawableLoader; @@ -20,11 +21,13 @@ public interface MarkwonPlugin { void configureVisitor(@NonNull MarkwonVisitor.Builder builder); - // html - @NonNull String processMarkdown(@NonNull String markdown); + void beforeRender(@NonNull Node node); + + void afterRender(@NonNull Node node, @NonNull MarkwonVisitor visitor); + void beforeSetText(@NonNull TextView textView, @NonNull CharSequence markdown); // this method do not receive markdown like `beforeSetText` does because at this From ca231ab2d83c6fd7503d6e5095276048d32bc718 Mon Sep 17 00:00:00 2001 From: Dimitry Ivanov Date: Mon, 26 Nov 2018 18:20:05 +0300 Subject: [PATCH 026/103] AsyncDrawable fix no dimensions bug --- .../noties/markwon/image/AsyncDrawable.java | 20 +++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/markwon/src/main/java/ru/noties/markwon/image/AsyncDrawable.java b/markwon/src/main/java/ru/noties/markwon/image/AsyncDrawable.java index 205830c0..a9bde47a 100644 --- a/markwon/src/main/java/ru/noties/markwon/image/AsyncDrawable.java +++ b/markwon/src/main/java/ru/noties/markwon/image/AsyncDrawable.java @@ -23,6 +23,9 @@ public class AsyncDrawable extends Drawable { private int canvasWidth; private float textSize; + // @since 2.0.1 for use-cases when image is loaded faster than span is drawn and knows canvas width + private boolean waitingForDemensions; + /** * @since 1.0.1 */ @@ -88,6 +91,19 @@ public class AsyncDrawable extends Drawable { this.result = result; this.result.setCallback(callback); + initBounds(); + } + + private void initBounds() { + + if (canvasWidth == 0) { + // we still have no bounds - wait for them + waitingForDemensions = true; + return; + } + + waitingForDemensions = false; + final Rect bounds = resolveBounds(); result.setBounds(bounds); setBounds(bounds); @@ -102,6 +118,10 @@ public class AsyncDrawable extends Drawable { public void initWithKnownDimensions(int width, float textSize) { this.canvasWidth = width; this.textSize = textSize; + + if (waitingForDemensions) { + initBounds(); + } } @Override From b52de58fbdefb8d6a73fd5cce08bc27203fcf60c Mon Sep 17 00:00:00 2001 From: Dimitry Ivanov Date: Sun, 2 Dec 2018 14:02:04 +0300 Subject: [PATCH 027/103] Applying fix from 2.0.1 to blockQuote and paragraph (fac23ef) --- .../main/java/ru/noties/markwon/core/CorePlugin.java | 12 ++---------- 1 file changed, 2 insertions(+), 10 deletions(-) diff --git a/markwon/src/main/java/ru/noties/markwon/core/CorePlugin.java b/markwon/src/main/java/ru/noties/markwon/core/CorePlugin.java index e2893c53..929391b9 100644 --- a/markwon/src/main/java/ru/noties/markwon/core/CorePlugin.java +++ b/markwon/src/main/java/ru/noties/markwon/core/CorePlugin.java @@ -125,10 +125,6 @@ public class CorePlugin extends AbstractMarkwonPlugin { visitor.ensureNewLine(); - if (visitor.blockQuoteIndent() > 0) { - visitor.forceNewLine(); - } - final int length = visitor.length(); visitor.incrementBlockQuoteIndent(); visitor.visitChildren(blockQuote); @@ -137,9 +133,7 @@ public class CorePlugin extends AbstractMarkwonPlugin { if (visitor.hasNext(blockQuote)) { visitor.ensureNewLine(); - if (visitor.blockQuoteIndent() > 0) { - visitor.forceNewLine(); - } + visitor.forceNewLine(); } } }); @@ -363,9 +357,7 @@ public class CorePlugin extends AbstractMarkwonPlugin { if (!inTightList && visitor.hasNext(paragraph)) { visitor.ensureNewLine(); - if (visitor.blockQuoteIndent() == 0) { - visitor.forceNewLine(); - } + visitor.forceNewLine(); } } }); From 42aab2280e5bddd1d2e5d96d201fc3ec95b1bc91 Mon Sep 17 00:00:00 2001 From: Dimitry Ivanov Date: Sun, 2 Dec 2018 14:04:58 +0300 Subject: [PATCH 028/103] Rename blockQuoteIndent to blockIndent --- .../noties/markwon/ext/tasklist/TaskListPlugin.java | 12 ++++++------ .../main/java/ru/noties/markwon/MarkwonVisitor.java | 8 ++++---- .../java/ru/noties/markwon/MarkwonVisitorImpl.java | 10 +++++----- .../main/java/ru/noties/markwon/core/CorePlugin.java | 10 +++++----- .../markwon/core/visitor/BlockQuoteNodeVisitor.java | 4 ++++ .../markwon/core/visitor/EmphasisNodeVisitor.java | 4 ++++ .../core/visitor/StrongEmphasisNodeVisitor.java | 4 ++++ .../noties/markwon/core/visitor/TextNodeVisitor.java | 4 ++++ 8 files changed, 36 insertions(+), 20 deletions(-) create mode 100644 markwon/src/main/java/ru/noties/markwon/core/visitor/BlockQuoteNodeVisitor.java create mode 100644 markwon/src/main/java/ru/noties/markwon/core/visitor/EmphasisNodeVisitor.java create mode 100644 markwon/src/main/java/ru/noties/markwon/core/visitor/StrongEmphasisNodeVisitor.java create mode 100644 markwon/src/main/java/ru/noties/markwon/core/visitor/TextNodeVisitor.java diff --git a/markwon-ext-tasklist/src/main/java/ru/noties/markwon/ext/tasklist/TaskListPlugin.java b/markwon-ext-tasklist/src/main/java/ru/noties/markwon/ext/tasklist/TaskListPlugin.java index dc81b8f3..4efe4892 100644 --- a/markwon-ext-tasklist/src/main/java/ru/noties/markwon/ext/tasklist/TaskListPlugin.java +++ b/markwon-ext-tasklist/src/main/java/ru/noties/markwon/ext/tasklist/TaskListPlugin.java @@ -70,9 +70,9 @@ public class TaskListPlugin extends AbstractMarkwonPlugin { visitor.ensureNewLine(); - visitor.incrementBlockQuoteIndent(); + visitor.incrementBlockIndent(); visitor.visitChildren(taskListBlock); - visitor.decrementBlockQuoteIndent(); + visitor.decrementBlockIndent(); if (visitor.hasNext(taskListBlock)) { visitor.ensureNewLine(); @@ -86,20 +86,20 @@ public class TaskListPlugin extends AbstractMarkwonPlugin { final int length = visitor.length(); - final int indent = visitor.blockQuoteIndent(); - visitor.blockQuoteIntent(indent + taskListItem.indent()); + final int indent = visitor.blockIndent(); + visitor.blockIntent(indent + taskListItem.indent()); visitor.visitChildren(taskListItem); visitor.setSpans(length, new TaskListSpan( visitor.theme(), drawable, - visitor.blockQuoteIndent(), + visitor.blockIndent(), taskListItem.done())); if (visitor.hasNext(taskListItem)) { visitor.ensureNewLine(); } - visitor.blockQuoteIntent(indent); + visitor.blockIntent(indent); } }); } diff --git a/markwon/src/main/java/ru/noties/markwon/MarkwonVisitor.java b/markwon/src/main/java/ru/noties/markwon/MarkwonVisitor.java index 5bb22998..40d389b4 100644 --- a/markwon/src/main/java/ru/noties/markwon/MarkwonVisitor.java +++ b/markwon/src/main/java/ru/noties/markwon/MarkwonVisitor.java @@ -39,13 +39,13 @@ public interface MarkwonVisitor extends Visitor { boolean hasNext(@NonNull Node node); - void incrementBlockQuoteIndent(); + void incrementBlockIndent(); - void decrementBlockQuoteIndent(); + void decrementBlockIndent(); - void blockQuoteIntent(int blockQuoteIndent); + void blockIntent(int blockIndent); - int blockQuoteIndent(); + int blockIndent(); void incrementListLevel(); diff --git a/markwon/src/main/java/ru/noties/markwon/MarkwonVisitorImpl.java b/markwon/src/main/java/ru/noties/markwon/MarkwonVisitorImpl.java index 07684737..94399509 100644 --- a/markwon/src/main/java/ru/noties/markwon/MarkwonVisitorImpl.java +++ b/markwon/src/main/java/ru/noties/markwon/MarkwonVisitorImpl.java @@ -217,22 +217,22 @@ class MarkwonVisitorImpl implements MarkwonVisitor { } @Override - public void incrementBlockQuoteIndent() { + public void incrementBlockIndent() { blockQuoteIndent += 1; } @Override - public void decrementBlockQuoteIndent() { + public void decrementBlockIndent() { blockQuoteIndent -= 1; } @Override - public void blockQuoteIntent(int blockQuoteIndent) { - this.blockQuoteIndent = blockQuoteIndent; + public void blockIntent(int blockIndent) { + this.blockQuoteIndent = blockIndent; } @Override - public int blockQuoteIndent() { + public int blockIndent() { return blockQuoteIndent; } diff --git a/markwon/src/main/java/ru/noties/markwon/core/CorePlugin.java b/markwon/src/main/java/ru/noties/markwon/core/CorePlugin.java index 929391b9..01c35d37 100644 --- a/markwon/src/main/java/ru/noties/markwon/core/CorePlugin.java +++ b/markwon/src/main/java/ru/noties/markwon/core/CorePlugin.java @@ -126,10 +126,10 @@ public class CorePlugin extends AbstractMarkwonPlugin { visitor.ensureNewLine(); final int length = visitor.length(); - visitor.incrementBlockQuoteIndent(); + visitor.incrementBlockIndent(); visitor.visitChildren(blockQuote); visitor.setSpans(length, visitor.factory().blockQuote(visitor.theme())); - visitor.decrementBlockQuoteIndent(); + visitor.decrementBlockIndent(); if (visitor.hasNext(blockQuote)) { visitor.ensureNewLine(); @@ -229,7 +229,7 @@ public class CorePlugin extends AbstractMarkwonPlugin { if (visitor.hasNext(node)) { visitor.ensureNewLine(); if (visitor.listLevel() == 0 - && visitor.blockQuoteIndent() == 0) { + && visitor.blockIndent() == 0) { visitor.forceNewLine(); } } @@ -242,7 +242,7 @@ public class CorePlugin extends AbstractMarkwonPlugin { final int length = visitor.length(); - visitor.incrementBlockQuoteIndent(); + visitor.incrementBlockIndent(); visitor.incrementListLevel(); final Node parent = listItem.getParent(); @@ -265,7 +265,7 @@ public class CorePlugin extends AbstractMarkwonPlugin { } - visitor.decrementBlockQuoteIndent(); + visitor.decrementBlockIndent(); visitor.decrementListLevel(); if (visitor.hasNext(listItem)) { diff --git a/markwon/src/main/java/ru/noties/markwon/core/visitor/BlockQuoteNodeVisitor.java b/markwon/src/main/java/ru/noties/markwon/core/visitor/BlockQuoteNodeVisitor.java new file mode 100644 index 00000000..46ad855e --- /dev/null +++ b/markwon/src/main/java/ru/noties/markwon/core/visitor/BlockQuoteNodeVisitor.java @@ -0,0 +1,4 @@ +package ru.noties.markwon.core.visitor; + +public class BlockQuoteNodeVisitor { +} diff --git a/markwon/src/main/java/ru/noties/markwon/core/visitor/EmphasisNodeVisitor.java b/markwon/src/main/java/ru/noties/markwon/core/visitor/EmphasisNodeVisitor.java new file mode 100644 index 00000000..bfc5847e --- /dev/null +++ b/markwon/src/main/java/ru/noties/markwon/core/visitor/EmphasisNodeVisitor.java @@ -0,0 +1,4 @@ +package ru.noties.markwon.core.visitor; + +public class EmphasisNodeVisitor { +} diff --git a/markwon/src/main/java/ru/noties/markwon/core/visitor/StrongEmphasisNodeVisitor.java b/markwon/src/main/java/ru/noties/markwon/core/visitor/StrongEmphasisNodeVisitor.java new file mode 100644 index 00000000..64176e0c --- /dev/null +++ b/markwon/src/main/java/ru/noties/markwon/core/visitor/StrongEmphasisNodeVisitor.java @@ -0,0 +1,4 @@ +package ru.noties.markwon.core.visitor; + +public class StrongEmphasisNodeVisitor { +} diff --git a/markwon/src/main/java/ru/noties/markwon/core/visitor/TextNodeVisitor.java b/markwon/src/main/java/ru/noties/markwon/core/visitor/TextNodeVisitor.java new file mode 100644 index 00000000..8f553ace --- /dev/null +++ b/markwon/src/main/java/ru/noties/markwon/core/visitor/TextNodeVisitor.java @@ -0,0 +1,4 @@ +package ru.noties.markwon.core.visitor; + +public class TextNodeVisitor { +} From 19cd94febb8ce34f02350e64b6a3a91a935b7fd9 Mon Sep 17 00:00:00 2001 From: Dimitry Ivanov Date: Sun, 2 Dec 2018 14:29:27 +0300 Subject: [PATCH 029/103] Specific node visitors for core nodes --- .../ru/noties/markwon/MarkwonVisitor.java | 6 +- .../ru/noties/markwon/MarkwonVisitorImpl.java | 9 +- .../ru/noties/markwon/core/CorePlugin.java | 297 ++---------------- .../core/visitor/BlockQuoteNodeVisitor.java | 24 +- .../core/visitor/CodeBlockNodeVisitor.java | 56 ++++ .../markwon/core/visitor/CodeNodeVisitor.java | 24 ++ .../core/visitor/EmphasisNodeVisitor.java | 14 +- .../visitor/HardLineBreakNodeVisitor.java | 14 + .../core/visitor/HeadingNodeVisitor.java | 24 ++ .../markwon/core/visitor/LinkNodeVisitor.java | 19 ++ .../core/visitor/ListBlockNodeVisitor.java | 22 ++ .../core/visitor/ListItemNodeVisitor.java | 48 +++ .../core/visitor/ParagraphNodeVisitor.java | 44 +++ .../visitor/SoftLineBreakNodeVisitor.java | 25 ++ .../visitor/StrongEmphasisNodeVisitor.java | 14 +- .../markwon/core/visitor/TextNodeVisitor.java | 12 +- .../visitor/ThematicBreakNodeVisitor.java | 27 ++ 17 files changed, 406 insertions(+), 273 deletions(-) create mode 100644 markwon/src/main/java/ru/noties/markwon/core/visitor/CodeBlockNodeVisitor.java create mode 100644 markwon/src/main/java/ru/noties/markwon/core/visitor/CodeNodeVisitor.java create mode 100644 markwon/src/main/java/ru/noties/markwon/core/visitor/HardLineBreakNodeVisitor.java create mode 100644 markwon/src/main/java/ru/noties/markwon/core/visitor/HeadingNodeVisitor.java create mode 100644 markwon/src/main/java/ru/noties/markwon/core/visitor/LinkNodeVisitor.java create mode 100644 markwon/src/main/java/ru/noties/markwon/core/visitor/ListBlockNodeVisitor.java create mode 100644 markwon/src/main/java/ru/noties/markwon/core/visitor/ListItemNodeVisitor.java create mode 100644 markwon/src/main/java/ru/noties/markwon/core/visitor/ParagraphNodeVisitor.java create mode 100644 markwon/src/main/java/ru/noties/markwon/core/visitor/SoftLineBreakNodeVisitor.java create mode 100644 markwon/src/main/java/ru/noties/markwon/core/visitor/ThematicBreakNodeVisitor.java diff --git a/markwon/src/main/java/ru/noties/markwon/MarkwonVisitor.java b/markwon/src/main/java/ru/noties/markwon/MarkwonVisitor.java index 40d389b4..1b72218c 100644 --- a/markwon/src/main/java/ru/noties/markwon/MarkwonVisitor.java +++ b/markwon/src/main/java/ru/noties/markwon/MarkwonVisitor.java @@ -17,7 +17,11 @@ public interface MarkwonVisitor extends Visitor { interface Builder { @NonNull - Builder on(@NonNull Class node, @Nullable NodeVisitor nodeVisitor); + Builder on(@NonNull Class node, @Nullable NodeVisitor nodeVisitor); + + // to obtain currently registered one + @Nullable + NodeVisitor registeredVisitor(@NonNull Class node); @NonNull MarkwonVisitor build(@NonNull MarkwonConfiguration configuration); diff --git a/markwon/src/main/java/ru/noties/markwon/MarkwonVisitorImpl.java b/markwon/src/main/java/ru/noties/markwon/MarkwonVisitorImpl.java index 94399509..70684e00 100644 --- a/markwon/src/main/java/ru/noties/markwon/MarkwonVisitorImpl.java +++ b/markwon/src/main/java/ru/noties/markwon/MarkwonVisitorImpl.java @@ -281,7 +281,7 @@ class MarkwonVisitorImpl implements MarkwonVisitor { @NonNull @Override - public Builder on(@NonNull Class node, @Nullable NodeVisitor nodeVisitor) { + public Builder on(@NonNull Class node, @Nullable NodeVisitor nodeVisitor) { // we should allow `null` to exclude node from being visited (for example to disable // some functionality) if (nodeVisitor == null) { @@ -292,6 +292,13 @@ class MarkwonVisitorImpl implements MarkwonVisitor { return this; } + @Nullable + @Override + public NodeVisitor registeredVisitor(@NonNull Class node) { + //noinspection unchecked + return (NodeVisitor) nodes.get(node); + } + @NonNull @Override public MarkwonVisitor build(@NonNull MarkwonConfiguration configuration) { diff --git a/markwon/src/main/java/ru/noties/markwon/core/CorePlugin.java b/markwon/src/main/java/ru/noties/markwon/core/CorePlugin.java index 01c35d37..832536da 100644 --- a/markwon/src/main/java/ru/noties/markwon/core/CorePlugin.java +++ b/markwon/src/main/java/ru/noties/markwon/core/CorePlugin.java @@ -1,7 +1,6 @@ package ru.noties.markwon.core; import android.support.annotation.NonNull; -import android.support.annotation.Nullable; import android.widget.TextView; import org.commonmark.node.BlockQuote; @@ -13,9 +12,7 @@ import org.commonmark.node.HardLineBreak; import org.commonmark.node.Heading; import org.commonmark.node.IndentedCodeBlock; import org.commonmark.node.Link; -import org.commonmark.node.ListBlock; import org.commonmark.node.ListItem; -import org.commonmark.node.Node; import org.commonmark.node.OrderedList; import org.commonmark.node.Paragraph; import org.commonmark.node.SoftLineBreak; @@ -24,21 +21,25 @@ import org.commonmark.node.Text; import org.commonmark.node.ThematicBreak; import ru.noties.markwon.AbstractMarkwonPlugin; -import ru.noties.markwon.MarkwonConfiguration; import ru.noties.markwon.MarkwonVisitor; +import ru.noties.markwon.core.visitor.BlockQuoteNodeVisitor; +import ru.noties.markwon.core.visitor.CodeBlockNodeVisitor; +import ru.noties.markwon.core.visitor.CodeNodeVisitor; +import ru.noties.markwon.core.visitor.EmphasisNodeVisitor; +import ru.noties.markwon.core.visitor.HardLineBreakNodeVisitor; +import ru.noties.markwon.core.visitor.HeadingNodeVisitor; +import ru.noties.markwon.core.visitor.LinkNodeVisitor; +import ru.noties.markwon.core.visitor.ListBlockNodeVisitor; +import ru.noties.markwon.core.visitor.ListItemNodeVisitor; +import ru.noties.markwon.core.visitor.ParagraphNodeVisitor; +import ru.noties.markwon.core.visitor.SoftLineBreakNodeVisitor; +import ru.noties.markwon.core.visitor.StrongEmphasisNodeVisitor; +import ru.noties.markwon.core.visitor.TextNodeVisitor; +import ru.noties.markwon.core.visitor.ThematicBreakNodeVisitor; import ru.noties.markwon.spans.OrderedListItemSpan; public class CorePlugin extends AbstractMarkwonPlugin { - // todo: factory. Logically it must be here only, but in order to make spans - // uniform in HTML (for example) we should expose it... Anyway, this factory _must_ - // include only _core_ spans - - // todo: softBreak adds new line should be here (or maybe removed even?) - - // todo: add a simple HTML handler - // todo: configure primitive images (without okhttp -> just HttpUrlConnection and simple types (static, data) - @NonNull public static CorePlugin create() { return create(false); @@ -72,7 +73,6 @@ public class CorePlugin extends AbstractMarkwonPlugin { softLineBreak(builder); hardLineBreak(builder); paragraph(builder); -// image(builder); link(builder); } @@ -88,303 +88,66 @@ public class CorePlugin extends AbstractMarkwonPlugin { } protected void text(@NonNull MarkwonVisitor.Builder builder) { - builder.on(Text.class, new MarkwonVisitor.NodeVisitor() { - @Override - public void visit(@NonNull MarkwonVisitor visitor, @NonNull Text text) { - visitor.builder().append(text.getLiteral()); - } - }); + builder.on(Text.class, new TextNodeVisitor()); } protected void strongEmphasis(@NonNull MarkwonVisitor.Builder builder) { - builder.on(StrongEmphasis.class, new MarkwonVisitor.NodeVisitor() { - @Override - public void visit(@NonNull MarkwonVisitor visitor, @NonNull StrongEmphasis strongEmphasis) { - final int length = visitor.length(); - visitor.visitChildren(strongEmphasis); - visitor.setSpans(length, visitor.factory().strongEmphasis()); - } - }); + builder.on(StrongEmphasis.class, new StrongEmphasisNodeVisitor()); } protected void emphasis(@NonNull MarkwonVisitor.Builder builder) { - builder.on(Emphasis.class, new MarkwonVisitor.NodeVisitor() { - @Override - public void visit(@NonNull MarkwonVisitor visitor, @NonNull Emphasis emphasis) { - final int length = visitor.length(); - visitor.visitChildren(emphasis); - visitor.setSpans(length, visitor.factory().emphasis()); - } - }); + builder.on(Emphasis.class, new EmphasisNodeVisitor()); } protected void blockQuote(@NonNull MarkwonVisitor.Builder builder) { - builder.on(BlockQuote.class, new MarkwonVisitor.NodeVisitor

() { - @Override - public void visit(@NonNull MarkwonVisitor visitor, @NonNull BlockQuote blockQuote) { - - visitor.ensureNewLine(); - - final int length = visitor.length(); - visitor.incrementBlockIndent(); - visitor.visitChildren(blockQuote); - visitor.setSpans(length, visitor.factory().blockQuote(visitor.theme())); - visitor.decrementBlockIndent(); - - if (visitor.hasNext(blockQuote)) { - visitor.ensureNewLine(); - visitor.forceNewLine(); - } - } - }); + builder.on(BlockQuote.class, new BlockQuoteNodeVisitor()); } protected void code(@NonNull MarkwonVisitor.Builder builder) { - builder.on(Code.class, new MarkwonVisitor.NodeVisitor() { - @Override - public void visit(@NonNull MarkwonVisitor visitor, @NonNull Code code) { - - final int length = visitor.length(); - - // NB, in order to provide a _padding_ feeling code is wrapped inside two unbreakable spaces - // unfortunately we cannot use this for multiline code as we cannot control where a new line break will be inserted - visitor.builder() - .append('\u00a0') - .append(code.getLiteral()) - .append('\u00a0'); - - visitor.setSpans(length, visitor.factory().code(visitor.theme(), false)); - } - }); + builder.on(Code.class, new CodeNodeVisitor()); } protected void fencedCodeBlock(@NonNull MarkwonVisitor.Builder builder) { - builder.on(FencedCodeBlock.class, new MarkwonVisitor.NodeVisitor() { - @Override - public void visit(@NonNull MarkwonVisitor visitor, @NonNull FencedCodeBlock fencedCodeBlock) { - visitCodeBlock(visitor, fencedCodeBlock.getInfo(), fencedCodeBlock.getLiteral(), fencedCodeBlock); - } - }); + builder.on(FencedCodeBlock.class, new CodeBlockNodeVisitor.Fenced()); } protected void indentedCodeBlock(@NonNull MarkwonVisitor.Builder builder) { - builder.on(IndentedCodeBlock.class, new MarkwonVisitor.NodeVisitor() { - @Override - public void visit(@NonNull MarkwonVisitor visitor, @NonNull IndentedCodeBlock indentedCodeBlock) { - visitCodeBlock(visitor, null, indentedCodeBlock.getLiteral(), indentedCodeBlock); - } - }); - } - - protected void visitCodeBlock( - @NonNull MarkwonVisitor visitor, - @Nullable String info, - @NonNull String code, - @NonNull Node node) { - - visitor.ensureNewLine(); - - final int length = visitor.length(); - - visitor.builder() - .append('\u00a0').append('\n') - .append(visitor.configuration().syntaxHighlight().highlight(info, code)); - - visitor.ensureNewLine(); - - visitor.builder().append('\u00a0'); - - visitor.setSpans(length, visitor.factory().code(visitor.theme(), true)); - - if (visitor.hasNext(node)) { - visitor.ensureNewLine(); - visitor.forceNewLine(); - } + builder.on(IndentedCodeBlock.class, new CodeBlockNodeVisitor.Indented()); } protected void bulletList(@NonNull MarkwonVisitor.Builder builder) { - builder.on(BulletList.class, new MarkwonVisitor.NodeVisitor() { - @Override - public void visit(@NonNull MarkwonVisitor visitor, @NonNull BulletList bulletList) { - visitList(visitor, bulletList); - } - }); + builder.on(BulletList.class, new ListBlockNodeVisitor()); } protected void orderedList(@NonNull MarkwonVisitor.Builder builder) { - builder.on(OrderedList.class, new MarkwonVisitor.NodeVisitor() { - @Override - public void visit(@NonNull MarkwonVisitor visitor, @NonNull OrderedList orderedList) { - visitList(visitor, orderedList); - } - }); - } - - protected void visitList(@NonNull MarkwonVisitor visitor, @NonNull Node node) { - - visitor.ensureNewLine(); - - visitor.visitChildren(node); - - if (visitor.hasNext(node)) { - visitor.ensureNewLine(); - if (visitor.listLevel() == 0 - && visitor.blockIndent() == 0) { - visitor.forceNewLine(); - } - } + builder.on(OrderedList.class, new ListBlockNodeVisitor()); } protected void listItem(@NonNull MarkwonVisitor.Builder builder) { - builder.on(ListItem.class, new MarkwonVisitor.NodeVisitor() { - @Override - public void visit(@NonNull MarkwonVisitor visitor, @NonNull ListItem listItem) { - - final int length = visitor.length(); - - visitor.incrementBlockIndent(); - visitor.incrementListLevel(); - - final Node parent = listItem.getParent(); - if (parent instanceof OrderedList) { - - final int start = ((OrderedList) parent).getStartNumber(); - - visitor.visitChildren(listItem); - visitor.setSpans(length, visitor.factory().orderedListItem(visitor.theme(), start)); - - - // after we have visited the children increment start number - final OrderedList orderedList = (OrderedList) parent; - orderedList.setStartNumber(orderedList.getStartNumber() + 1); - - } else { - - visitor.visitChildren(listItem); - visitor.setSpans(length, visitor.factory().bulletListItem(visitor.theme(), visitor.listLevel() - 1)); - - } - - visitor.decrementBlockIndent(); - visitor.decrementListLevel(); - - if (visitor.hasNext(listItem)) { - visitor.ensureNewLine(); - } - } - }); + builder.on(ListItem.class, new ListItemNodeVisitor()); } protected void thematicBreak(@NonNull MarkwonVisitor.Builder builder) { - builder.on(ThematicBreak.class, new MarkwonVisitor.NodeVisitor() { - @Override - public void visit(@NonNull MarkwonVisitor visitor, @NonNull ThematicBreak thematicBreak) { - - visitor.ensureNewLine(); - - final int length = visitor.length(); - - // without space it won't render - visitor.builder().append('\u00a0'); - - visitor.setSpans(length, visitor.factory().thematicBreak(visitor.theme())); - - if (visitor.hasNext(thematicBreak)) { - visitor.ensureNewLine(); - visitor.forceNewLine(); - } - } - }); + builder.on(ThematicBreak.class, new ThematicBreakNodeVisitor()); } protected void heading(@NonNull MarkwonVisitor.Builder builder) { - builder.on(Heading.class, new MarkwonVisitor.NodeVisitor() { - @Override - public void visit(@NonNull MarkwonVisitor visitor, @NonNull Heading heading) { - - visitor.ensureNewLine(); - - final int length = visitor.length(); - visitor.visitChildren(heading); - visitor.setSpans(length, visitor.factory().heading(visitor.theme(), heading.getLevel())); - - if (visitor.hasNext(heading)) { - visitor.ensureNewLine(); - visitor.forceNewLine(); - } - } - }); + builder.on(Heading.class, new HeadingNodeVisitor()); } protected void softLineBreak(@NonNull MarkwonVisitor.Builder builder) { - builder.on(SoftLineBreak.class, new MarkwonVisitor.NodeVisitor() { - @Override - public void visit(@NonNull MarkwonVisitor visitor, @NonNull SoftLineBreak softLineBreak) { - if (softBreakAddsNewLine) { - visitor.ensureNewLine(); - } else { - visitor.builder().append(' '); - } - } - }); + builder.on(SoftLineBreak.class, new SoftLineBreakNodeVisitor(softBreakAddsNewLine)); } protected void hardLineBreak(@NonNull MarkwonVisitor.Builder builder) { - builder.on(HardLineBreak.class, new MarkwonVisitor.NodeVisitor() { - @Override - public void visit(@NonNull MarkwonVisitor visitor, @NonNull HardLineBreak hardLineBreak) { - visitor.ensureNewLine(); - } - }); + builder.on(HardLineBreak.class, new HardLineBreakNodeVisitor()); } protected void paragraph(@NonNull MarkwonVisitor.Builder builder) { - builder.on(Paragraph.class, new MarkwonVisitor.NodeVisitor() { - @Override - public void visit(@NonNull MarkwonVisitor visitor, @NonNull Paragraph paragraph) { - - final boolean inTightList = isInTightList(paragraph); - - if (!inTightList) { - visitor.ensureNewLine(); - } - - final int length = visitor.length(); - visitor.visitChildren(paragraph); - - // @since 1.1.1 apply paragraph span - visitor.setSpans(length, visitor.factory().paragraph(inTightList)); - - if (!inTightList && visitor.hasNext(paragraph)) { - visitor.ensureNewLine(); - visitor.forceNewLine(); - } - } - }); + builder.on(Paragraph.class, new ParagraphNodeVisitor()); } protected void link(@NonNull MarkwonVisitor.Builder builder) { - builder.on(Link.class, new MarkwonVisitor.NodeVisitor() { - @Override - public void visit(@NonNull MarkwonVisitor visitor, @NonNull Link link) { - final int length = visitor.length(); - visitor.visitChildren(link); - final MarkwonConfiguration configuration = visitor.configuration(); - final String destination = configuration.urlProcessor().process(link.getDestination()); - visitor.setSpans(length, visitor.factory().link(visitor.theme(), destination, configuration.linkResolver())); - } - }); - } - - private static boolean isInTightList(@NonNull Paragraph paragraph) { - final Node parent = paragraph.getParent(); - if (parent != null) { - final Node gramps = parent.getParent(); - if (gramps instanceof ListBlock) { - ListBlock list = (ListBlock) gramps; - return list.isTight(); - } - } - return false; + builder.on(Link.class, new LinkNodeVisitor()); } } diff --git a/markwon/src/main/java/ru/noties/markwon/core/visitor/BlockQuoteNodeVisitor.java b/markwon/src/main/java/ru/noties/markwon/core/visitor/BlockQuoteNodeVisitor.java index 46ad855e..08681545 100644 --- a/markwon/src/main/java/ru/noties/markwon/core/visitor/BlockQuoteNodeVisitor.java +++ b/markwon/src/main/java/ru/noties/markwon/core/visitor/BlockQuoteNodeVisitor.java @@ -1,4 +1,26 @@ package ru.noties.markwon.core.visitor; -public class BlockQuoteNodeVisitor { +import android.support.annotation.NonNull; + +import org.commonmark.node.BlockQuote; + +import ru.noties.markwon.MarkwonVisitor; + +public class BlockQuoteNodeVisitor implements MarkwonVisitor.NodeVisitor
{ + @Override + public void visit(@NonNull MarkwonVisitor visitor, @NonNull BlockQuote blockQuote) { + + visitor.ensureNewLine(); + + final int length = visitor.length(); + visitor.incrementBlockIndent(); + visitor.visitChildren(blockQuote); + visitor.setSpans(length, visitor.factory().blockQuote(visitor.theme())); + visitor.decrementBlockIndent(); + + if (visitor.hasNext(blockQuote)) { + visitor.ensureNewLine(); + visitor.forceNewLine(); + } + } } diff --git a/markwon/src/main/java/ru/noties/markwon/core/visitor/CodeBlockNodeVisitor.java b/markwon/src/main/java/ru/noties/markwon/core/visitor/CodeBlockNodeVisitor.java new file mode 100644 index 00000000..491da0e8 --- /dev/null +++ b/markwon/src/main/java/ru/noties/markwon/core/visitor/CodeBlockNodeVisitor.java @@ -0,0 +1,56 @@ +package ru.noties.markwon.core.visitor; + +import android.support.annotation.NonNull; +import android.support.annotation.Nullable; + +import org.commonmark.node.FencedCodeBlock; +import org.commonmark.node.IndentedCodeBlock; +import org.commonmark.node.Node; + +import ru.noties.markwon.MarkwonVisitor; + +public abstract class CodeBlockNodeVisitor { + + public static class Fenced implements MarkwonVisitor.NodeVisitor { + + @Override + public void visit(@NonNull MarkwonVisitor visitor, @NonNull FencedCodeBlock fencedCodeBlock) { + visitCodeBlock(visitor, fencedCodeBlock.getInfo(), fencedCodeBlock.getLiteral(), fencedCodeBlock); + } + } + + public static class Indented implements MarkwonVisitor.NodeVisitor { + + @Override + public void visit(@NonNull MarkwonVisitor visitor, @NonNull IndentedCodeBlock indentedCodeBlock) { + visitCodeBlock(visitor, null, indentedCodeBlock.getLiteral(), indentedCodeBlock); + } + } + + + public static void visitCodeBlock( + @NonNull MarkwonVisitor visitor, + @Nullable String info, + @NonNull String code, + @NonNull Node node) { + + visitor.ensureNewLine(); + + final int length = visitor.length(); + + visitor.builder() + .append('\u00a0').append('\n') + .append(visitor.configuration().syntaxHighlight().highlight(info, code)); + + visitor.ensureNewLine(); + + visitor.builder().append('\u00a0'); + + visitor.setSpans(length, visitor.factory().code(visitor.theme(), true)); + + if (visitor.hasNext(node)) { + visitor.ensureNewLine(); + visitor.forceNewLine(); + } + } +} diff --git a/markwon/src/main/java/ru/noties/markwon/core/visitor/CodeNodeVisitor.java b/markwon/src/main/java/ru/noties/markwon/core/visitor/CodeNodeVisitor.java new file mode 100644 index 00000000..fb893839 --- /dev/null +++ b/markwon/src/main/java/ru/noties/markwon/core/visitor/CodeNodeVisitor.java @@ -0,0 +1,24 @@ +package ru.noties.markwon.core.visitor; + +import android.support.annotation.NonNull; + +import org.commonmark.node.Code; + +import ru.noties.markwon.MarkwonVisitor; + +public class CodeNodeVisitor implements MarkwonVisitor.NodeVisitor { + @Override + public void visit(@NonNull MarkwonVisitor visitor, @NonNull Code code) { + + final int length = visitor.length(); + + // NB, in order to provide a _padding_ feeling code is wrapped inside two unbreakable spaces + // unfortunately we cannot use this for multiline code as we cannot control where a new line break will be inserted + visitor.builder() + .append('\u00a0') + .append(code.getLiteral()) + .append('\u00a0'); + + visitor.setSpans(length, visitor.factory().code(visitor.theme(), false)); + } +} diff --git a/markwon/src/main/java/ru/noties/markwon/core/visitor/EmphasisNodeVisitor.java b/markwon/src/main/java/ru/noties/markwon/core/visitor/EmphasisNodeVisitor.java index bfc5847e..f4010b32 100644 --- a/markwon/src/main/java/ru/noties/markwon/core/visitor/EmphasisNodeVisitor.java +++ b/markwon/src/main/java/ru/noties/markwon/core/visitor/EmphasisNodeVisitor.java @@ -1,4 +1,16 @@ package ru.noties.markwon.core.visitor; -public class EmphasisNodeVisitor { +import android.support.annotation.NonNull; + +import org.commonmark.node.Emphasis; + +import ru.noties.markwon.MarkwonVisitor; + +public class EmphasisNodeVisitor implements MarkwonVisitor.NodeVisitor { + @Override + public void visit(@NonNull MarkwonVisitor visitor, @NonNull Emphasis emphasis) { + final int length = visitor.length(); + visitor.visitChildren(emphasis); + visitor.setSpans(length, visitor.factory().emphasis()); + } } diff --git a/markwon/src/main/java/ru/noties/markwon/core/visitor/HardLineBreakNodeVisitor.java b/markwon/src/main/java/ru/noties/markwon/core/visitor/HardLineBreakNodeVisitor.java new file mode 100644 index 00000000..809638b7 --- /dev/null +++ b/markwon/src/main/java/ru/noties/markwon/core/visitor/HardLineBreakNodeVisitor.java @@ -0,0 +1,14 @@ +package ru.noties.markwon.core.visitor; + +import android.support.annotation.NonNull; + +import org.commonmark.node.HardLineBreak; + +import ru.noties.markwon.MarkwonVisitor; + +public class HardLineBreakNodeVisitor implements MarkwonVisitor.NodeVisitor { + @Override + public void visit(@NonNull MarkwonVisitor visitor, @NonNull HardLineBreak hardLineBreak) { + visitor.ensureNewLine(); + } +} diff --git a/markwon/src/main/java/ru/noties/markwon/core/visitor/HeadingNodeVisitor.java b/markwon/src/main/java/ru/noties/markwon/core/visitor/HeadingNodeVisitor.java new file mode 100644 index 00000000..c82d51fe --- /dev/null +++ b/markwon/src/main/java/ru/noties/markwon/core/visitor/HeadingNodeVisitor.java @@ -0,0 +1,24 @@ +package ru.noties.markwon.core.visitor; + +import android.support.annotation.NonNull; + +import org.commonmark.node.Heading; + +import ru.noties.markwon.MarkwonVisitor; + +public class HeadingNodeVisitor implements MarkwonVisitor.NodeVisitor { + @Override + public void visit(@NonNull MarkwonVisitor visitor, @NonNull Heading heading) { + + visitor.ensureNewLine(); + + final int length = visitor.length(); + visitor.visitChildren(heading); + visitor.setSpans(length, visitor.factory().heading(visitor.theme(), heading.getLevel())); + + if (visitor.hasNext(heading)) { + visitor.ensureNewLine(); + visitor.forceNewLine(); + } + } +} diff --git a/markwon/src/main/java/ru/noties/markwon/core/visitor/LinkNodeVisitor.java b/markwon/src/main/java/ru/noties/markwon/core/visitor/LinkNodeVisitor.java new file mode 100644 index 00000000..cc4dfd6d --- /dev/null +++ b/markwon/src/main/java/ru/noties/markwon/core/visitor/LinkNodeVisitor.java @@ -0,0 +1,19 @@ +package ru.noties.markwon.core.visitor; + +import android.support.annotation.NonNull; + +import org.commonmark.node.Link; + +import ru.noties.markwon.MarkwonConfiguration; +import ru.noties.markwon.MarkwonVisitor; + +public class LinkNodeVisitor implements MarkwonVisitor.NodeVisitor { + @Override + public void visit(@NonNull MarkwonVisitor visitor, @NonNull Link link) { + final int length = visitor.length(); + visitor.visitChildren(link); + final MarkwonConfiguration configuration = visitor.configuration(); + final String destination = configuration.urlProcessor().process(link.getDestination()); + visitor.setSpans(length, visitor.factory().link(visitor.theme(), destination, configuration.linkResolver())); + } +} diff --git a/markwon/src/main/java/ru/noties/markwon/core/visitor/ListBlockNodeVisitor.java b/markwon/src/main/java/ru/noties/markwon/core/visitor/ListBlockNodeVisitor.java new file mode 100644 index 00000000..f06d324b --- /dev/null +++ b/markwon/src/main/java/ru/noties/markwon/core/visitor/ListBlockNodeVisitor.java @@ -0,0 +1,22 @@ +package ru.noties.markwon.core.visitor; + +import android.support.annotation.NonNull; + +import org.commonmark.node.Node; + +import ru.noties.markwon.MarkwonVisitor; + +public class ListBlockNodeVisitor implements MarkwonVisitor.NodeVisitor { + @Override + public void visit(@NonNull MarkwonVisitor visitor, @NonNull Node node) { + + visitor.ensureNewLine(); + + visitor.visitChildren(node); + + if (visitor.hasNext(node)) { + visitor.ensureNewLine(); + visitor.forceNewLine(); + } + } +} diff --git a/markwon/src/main/java/ru/noties/markwon/core/visitor/ListItemNodeVisitor.java b/markwon/src/main/java/ru/noties/markwon/core/visitor/ListItemNodeVisitor.java new file mode 100644 index 00000000..e60dfca6 --- /dev/null +++ b/markwon/src/main/java/ru/noties/markwon/core/visitor/ListItemNodeVisitor.java @@ -0,0 +1,48 @@ +package ru.noties.markwon.core.visitor; + +import android.support.annotation.NonNull; + +import org.commonmark.node.ListItem; +import org.commonmark.node.Node; +import org.commonmark.node.OrderedList; + +import ru.noties.markwon.MarkwonVisitor; + +public class ListItemNodeVisitor implements MarkwonVisitor.NodeVisitor { + + @Override + public void visit(@NonNull MarkwonVisitor visitor, @NonNull ListItem listItem) { + + final int length = visitor.length(); + + visitor.incrementBlockIndent(); + visitor.incrementListLevel(); + + final Node parent = listItem.getParent(); + if (parent instanceof OrderedList) { + + final int start = ((OrderedList) parent).getStartNumber(); + + visitor.visitChildren(listItem); + visitor.setSpans(length, visitor.factory().orderedListItem(visitor.theme(), start)); + + + // after we have visited the children increment start number + final OrderedList orderedList = (OrderedList) parent; + orderedList.setStartNumber(orderedList.getStartNumber() + 1); + + } else { + + visitor.visitChildren(listItem); + visitor.setSpans(length, visitor.factory().bulletListItem(visitor.theme(), visitor.listLevel() - 1)); + + } + + visitor.decrementBlockIndent(); + visitor.decrementListLevel(); + + if (visitor.hasNext(listItem)) { + visitor.ensureNewLine(); + } + } +} diff --git a/markwon/src/main/java/ru/noties/markwon/core/visitor/ParagraphNodeVisitor.java b/markwon/src/main/java/ru/noties/markwon/core/visitor/ParagraphNodeVisitor.java new file mode 100644 index 00000000..b945c501 --- /dev/null +++ b/markwon/src/main/java/ru/noties/markwon/core/visitor/ParagraphNodeVisitor.java @@ -0,0 +1,44 @@ +package ru.noties.markwon.core.visitor; + +import android.support.annotation.NonNull; + +import org.commonmark.node.ListBlock; +import org.commonmark.node.Node; +import org.commonmark.node.Paragraph; + +import ru.noties.markwon.MarkwonVisitor; + +public class ParagraphNodeVisitor implements MarkwonVisitor.NodeVisitor { + @Override + public void visit(@NonNull MarkwonVisitor visitor, @NonNull Paragraph paragraph) { + + final boolean inTightList = isInTightList(paragraph); + + if (!inTightList) { + visitor.ensureNewLine(); + } + + final int length = visitor.length(); + visitor.visitChildren(paragraph); + + // @since 1.1.1 apply paragraph span + visitor.setSpans(length, visitor.factory().paragraph(inTightList)); + + if (!inTightList && visitor.hasNext(paragraph)) { + visitor.ensureNewLine(); + visitor.forceNewLine(); + } + } + + private static boolean isInTightList(@NonNull Paragraph paragraph) { + final Node parent = paragraph.getParent(); + if (parent != null) { + final Node gramps = parent.getParent(); + if (gramps instanceof ListBlock) { + ListBlock list = (ListBlock) gramps; + return list.isTight(); + } + } + return false; + } +} diff --git a/markwon/src/main/java/ru/noties/markwon/core/visitor/SoftLineBreakNodeVisitor.java b/markwon/src/main/java/ru/noties/markwon/core/visitor/SoftLineBreakNodeVisitor.java new file mode 100644 index 00000000..79880bb2 --- /dev/null +++ b/markwon/src/main/java/ru/noties/markwon/core/visitor/SoftLineBreakNodeVisitor.java @@ -0,0 +1,25 @@ +package ru.noties.markwon.core.visitor; + +import android.support.annotation.NonNull; + +import org.commonmark.node.SoftLineBreak; + +import ru.noties.markwon.MarkwonVisitor; + +public class SoftLineBreakNodeVisitor implements MarkwonVisitor.NodeVisitor { + + private final boolean softBreakAddsNewLine; + + public SoftLineBreakNodeVisitor(boolean softBreakAddsNewLine) { + this.softBreakAddsNewLine = softBreakAddsNewLine; + } + + @Override + public void visit(@NonNull MarkwonVisitor visitor, @NonNull SoftLineBreak softLineBreak) { + if (softBreakAddsNewLine) { + visitor.ensureNewLine(); + } else { + visitor.builder().append(' '); + } + } +} diff --git a/markwon/src/main/java/ru/noties/markwon/core/visitor/StrongEmphasisNodeVisitor.java b/markwon/src/main/java/ru/noties/markwon/core/visitor/StrongEmphasisNodeVisitor.java index 64176e0c..ae3c701d 100644 --- a/markwon/src/main/java/ru/noties/markwon/core/visitor/StrongEmphasisNodeVisitor.java +++ b/markwon/src/main/java/ru/noties/markwon/core/visitor/StrongEmphasisNodeVisitor.java @@ -1,4 +1,16 @@ package ru.noties.markwon.core.visitor; -public class StrongEmphasisNodeVisitor { +import android.support.annotation.NonNull; + +import org.commonmark.node.StrongEmphasis; + +import ru.noties.markwon.MarkwonVisitor; + +public class StrongEmphasisNodeVisitor implements MarkwonVisitor.NodeVisitor { + @Override + public void visit(@NonNull MarkwonVisitor visitor, @NonNull StrongEmphasis strongEmphasis) { + final int length = visitor.length(); + visitor.visitChildren(strongEmphasis); + visitor.setSpans(length, visitor.factory().strongEmphasis()); + } } diff --git a/markwon/src/main/java/ru/noties/markwon/core/visitor/TextNodeVisitor.java b/markwon/src/main/java/ru/noties/markwon/core/visitor/TextNodeVisitor.java index 8f553ace..17ebfbb0 100644 --- a/markwon/src/main/java/ru/noties/markwon/core/visitor/TextNodeVisitor.java +++ b/markwon/src/main/java/ru/noties/markwon/core/visitor/TextNodeVisitor.java @@ -1,4 +1,14 @@ package ru.noties.markwon.core.visitor; -public class TextNodeVisitor { +import android.support.annotation.NonNull; + +import org.commonmark.node.Text; + +import ru.noties.markwon.MarkwonVisitor; + +public class TextNodeVisitor implements MarkwonVisitor.NodeVisitor { + @Override + public void visit(@NonNull MarkwonVisitor visitor, @NonNull Text text) { + visitor.builder().append(text.getLiteral()); + } } diff --git a/markwon/src/main/java/ru/noties/markwon/core/visitor/ThematicBreakNodeVisitor.java b/markwon/src/main/java/ru/noties/markwon/core/visitor/ThematicBreakNodeVisitor.java new file mode 100644 index 00000000..2bc2886a --- /dev/null +++ b/markwon/src/main/java/ru/noties/markwon/core/visitor/ThematicBreakNodeVisitor.java @@ -0,0 +1,27 @@ +package ru.noties.markwon.core.visitor; + +import android.support.annotation.NonNull; + +import org.commonmark.node.ThematicBreak; + +import ru.noties.markwon.MarkwonVisitor; + +public class ThematicBreakNodeVisitor implements MarkwonVisitor.NodeVisitor { + @Override + public void visit(@NonNull MarkwonVisitor visitor, @NonNull ThematicBreak thematicBreak) { + + visitor.ensureNewLine(); + + final int length = visitor.length(); + + // without space it won't render + visitor.builder().append('\u00a0'); + + visitor.setSpans(length, visitor.factory().thematicBreak(visitor.theme())); + + if (visitor.hasNext(thematicBreak)) { + visitor.ensureNewLine(); + visitor.forceNewLine(); + } + } +} From 5b61521e30d7ea69dcc8e259f2360a04706c5dce Mon Sep 17 00:00:00 2001 From: Dimitry Ivanov Date: Sun, 2 Dec 2018 15:09:15 +0300 Subject: [PATCH 030/103] Remove increment/decrement blockIndent from visitor --- .../markwon/ext/tasklist/TaskListPlugin.java | 22 ++++++++++----- .../ru/noties/markwon/MarkwonVisitor.java | 12 -------- .../ru/noties/markwon/MarkwonVisitorImpl.java | 28 ------------------- .../core/visitor/BlockQuoteNodeVisitor.java | 3 +- .../core/visitor/ListItemNodeVisitor.java | 2 -- 5 files changed, 16 insertions(+), 51 deletions(-) diff --git a/markwon-ext-tasklist/src/main/java/ru/noties/markwon/ext/tasklist/TaskListPlugin.java b/markwon-ext-tasklist/src/main/java/ru/noties/markwon/ext/tasklist/TaskListPlugin.java index 4efe4892..31a61a1b 100644 --- a/markwon-ext-tasklist/src/main/java/ru/noties/markwon/ext/tasklist/TaskListPlugin.java +++ b/markwon-ext-tasklist/src/main/java/ru/noties/markwon/ext/tasklist/TaskListPlugin.java @@ -8,6 +8,7 @@ import android.support.annotation.ColorInt; import android.support.annotation.NonNull; import android.util.TypedValue; +import org.commonmark.node.Node; import org.commonmark.parser.Parser; import ru.noties.markwon.AbstractMarkwonPlugin; @@ -70,9 +71,7 @@ public class TaskListPlugin extends AbstractMarkwonPlugin { visitor.ensureNewLine(); - visitor.incrementBlockIndent(); visitor.visitChildren(taskListBlock); - visitor.decrementBlockIndent(); if (visitor.hasNext(taskListBlock)) { visitor.ensureNewLine(); @@ -86,20 +85,16 @@ public class TaskListPlugin extends AbstractMarkwonPlugin { final int length = visitor.length(); - final int indent = visitor.blockIndent(); - visitor.blockIntent(indent + taskListItem.indent()); visitor.visitChildren(taskListItem); visitor.setSpans(length, new TaskListSpan( visitor.theme(), drawable, - visitor.blockIndent(), + indent(taskListItem) + taskListItem.indent(), taskListItem.done())); if (visitor.hasNext(taskListItem)) { visitor.ensureNewLine(); } - - visitor.blockIntent(indent); } }); } @@ -114,4 +109,17 @@ public class TaskListPlugin extends AbstractMarkwonPlugin { typedArray.recycle(); } } + + private static int indent(@NonNull Node node) { + int indent = 0; + Node parent = node.getParent(); + if (parent != null) { + parent = parent.getParent(); + while (parent != null) { + indent += 1; + parent = parent.getParent(); + } + } + return indent; + } } diff --git a/markwon/src/main/java/ru/noties/markwon/MarkwonVisitor.java b/markwon/src/main/java/ru/noties/markwon/MarkwonVisitor.java index 1b72218c..3a8587f1 100644 --- a/markwon/src/main/java/ru/noties/markwon/MarkwonVisitor.java +++ b/markwon/src/main/java/ru/noties/markwon/MarkwonVisitor.java @@ -19,10 +19,6 @@ public interface MarkwonVisitor extends Visitor { @NonNull Builder on(@NonNull Class node, @Nullable NodeVisitor nodeVisitor); - // to obtain currently registered one - @Nullable - NodeVisitor registeredVisitor(@NonNull Class node); - @NonNull MarkwonVisitor build(@NonNull MarkwonConfiguration configuration); } @@ -43,14 +39,6 @@ public interface MarkwonVisitor extends Visitor { boolean hasNext(@NonNull Node node); - void incrementBlockIndent(); - - void decrementBlockIndent(); - - void blockIntent(int blockIndent); - - int blockIndent(); - void incrementListLevel(); void decrementListLevel(); diff --git a/markwon/src/main/java/ru/noties/markwon/MarkwonVisitorImpl.java b/markwon/src/main/java/ru/noties/markwon/MarkwonVisitorImpl.java index 70684e00..d7ed0e8a 100644 --- a/markwon/src/main/java/ru/noties/markwon/MarkwonVisitorImpl.java +++ b/markwon/src/main/java/ru/noties/markwon/MarkwonVisitorImpl.java @@ -43,7 +43,6 @@ class MarkwonVisitorImpl implements MarkwonVisitor { private final SpannableBuilder builder = new SpannableBuilder(); - private int blockQuoteIndent; private int listLevel; private MarkwonVisitorImpl( @@ -216,26 +215,6 @@ class MarkwonVisitorImpl implements MarkwonVisitor { return node.getNext() != null; } - @Override - public void incrementBlockIndent() { - blockQuoteIndent += 1; - } - - @Override - public void decrementBlockIndent() { - blockQuoteIndent -= 1; - } - - @Override - public void blockIntent(int blockIndent) { - this.blockQuoteIndent = blockIndent; - } - - @Override - public int blockIndent() { - return blockQuoteIndent; - } - @Override public void incrementListLevel() { listLevel += 1; @@ -292,13 +271,6 @@ class MarkwonVisitorImpl implements MarkwonVisitor { return this; } - @Nullable - @Override - public NodeVisitor registeredVisitor(@NonNull Class node) { - //noinspection unchecked - return (NodeVisitor) nodes.get(node); - } - @NonNull @Override public MarkwonVisitor build(@NonNull MarkwonConfiguration configuration) { diff --git a/markwon/src/main/java/ru/noties/markwon/core/visitor/BlockQuoteNodeVisitor.java b/markwon/src/main/java/ru/noties/markwon/core/visitor/BlockQuoteNodeVisitor.java index 08681545..1c236c42 100644 --- a/markwon/src/main/java/ru/noties/markwon/core/visitor/BlockQuoteNodeVisitor.java +++ b/markwon/src/main/java/ru/noties/markwon/core/visitor/BlockQuoteNodeVisitor.java @@ -13,10 +13,9 @@ public class BlockQuoteNodeVisitor implements MarkwonVisitor.NodeVisitor final int length = visitor.length(); - visitor.incrementBlockIndent(); visitor.incrementListLevel(); final Node parent = listItem.getParent(); @@ -38,7 +37,6 @@ public class ListItemNodeVisitor implements MarkwonVisitor.NodeVisitor } - visitor.decrementBlockIndent(); visitor.decrementListLevel(); if (visitor.hasNext(listItem)) { From f97c852c8a55a1a0835456b64ff851f3a778c033 Mon Sep 17 00:00:00 2001 From: Dimitry Ivanov Date: Sun, 2 Dec 2018 15:22:11 +0300 Subject: [PATCH 031/103] Remove listlevel internal state --- .../ru/noties/markwon/MarkwonVisitor.java | 9 +++---- .../ru/noties/markwon/MarkwonVisitorImpl.java | 24 ++++++------------- .../core/visitor/ListItemNodeVisitor.java | 18 ++++++++++---- 3 files changed, 23 insertions(+), 28 deletions(-) diff --git a/markwon/src/main/java/ru/noties/markwon/MarkwonVisitor.java b/markwon/src/main/java/ru/noties/markwon/MarkwonVisitor.java index 3a8587f1..a124bcb3 100644 --- a/markwon/src/main/java/ru/noties/markwon/MarkwonVisitor.java +++ b/markwon/src/main/java/ru/noties/markwon/MarkwonVisitor.java @@ -39,12 +39,6 @@ public interface MarkwonVisitor extends Visitor { boolean hasNext(@NonNull Node node); - void incrementListLevel(); - - void decrementListLevel(); - - int listLevel(); - void ensureNewLine(); void forceNewLine(); @@ -52,4 +46,7 @@ public interface MarkwonVisitor extends Visitor { int length(); void setSpans(int start, @Nullable Object spans); + + @Nullable + NodeVisitor nodeVisitor(@NonNull Class node); } diff --git a/markwon/src/main/java/ru/noties/markwon/MarkwonVisitorImpl.java b/markwon/src/main/java/ru/noties/markwon/MarkwonVisitorImpl.java index d7ed0e8a..1262d643 100644 --- a/markwon/src/main/java/ru/noties/markwon/MarkwonVisitorImpl.java +++ b/markwon/src/main/java/ru/noties/markwon/MarkwonVisitorImpl.java @@ -43,8 +43,6 @@ class MarkwonVisitorImpl implements MarkwonVisitor { private final SpannableBuilder builder = new SpannableBuilder(); - private int listLevel; - private MarkwonVisitorImpl( @NonNull MarkwonConfiguration configuration, @NonNull Map, NodeVisitor> nodes) { @@ -215,21 +213,6 @@ class MarkwonVisitorImpl implements MarkwonVisitor { return node.getNext() != null; } - @Override - public void incrementListLevel() { - listLevel += 1; - } - - @Override - public void decrementListLevel() { - listLevel -= 1; - } - - @Override - public int listLevel() { - return listLevel; - } - @Override public void ensureNewLine() { if (builder.length() > 0 @@ -253,6 +236,13 @@ class MarkwonVisitorImpl implements MarkwonVisitor { SpannableBuilder.setSpans(builder, spans, start, builder.length()); } + @Nullable + @Override + public NodeVisitor nodeVisitor(@NonNull Class node) { + //noinspection unchecked + return (NodeVisitor) nodes.get(node); + } + static class BuilderImpl implements Builder { private final Map, NodeVisitor> nodes = diff --git a/markwon/src/main/java/ru/noties/markwon/core/visitor/ListItemNodeVisitor.java b/markwon/src/main/java/ru/noties/markwon/core/visitor/ListItemNodeVisitor.java index d21bd766..f362c3ba 100644 --- a/markwon/src/main/java/ru/noties/markwon/core/visitor/ListItemNodeVisitor.java +++ b/markwon/src/main/java/ru/noties/markwon/core/visitor/ListItemNodeVisitor.java @@ -15,8 +15,6 @@ public class ListItemNodeVisitor implements MarkwonVisitor.NodeVisitor final int length = visitor.length(); - visitor.incrementListLevel(); - final Node parent = listItem.getParent(); if (parent instanceof OrderedList) { @@ -33,14 +31,24 @@ public class ListItemNodeVisitor implements MarkwonVisitor.NodeVisitor } else { visitor.visitChildren(listItem); - visitor.setSpans(length, visitor.factory().bulletListItem(visitor.theme(), visitor.listLevel() - 1)); + visitor.setSpans(length, visitor.factory().bulletListItem(visitor.theme(), listLevel(listItem))); } - visitor.decrementListLevel(); - if (visitor.hasNext(listItem)) { visitor.ensureNewLine(); } } + + private static int listLevel(@NonNull Node node) { + int level = 0; + Node parent = node.getParent(); + while (parent != null) { + if (parent instanceof ListItem) { + level += 1; + } + parent = parent.getParent(); + } + return level; + } } From 6a82b75abace14d8749110d93eea383bed3916ad Mon Sep 17 00:00:00 2001 From: Dimitry Ivanov Date: Sun, 2 Dec 2018 15:32:41 +0300 Subject: [PATCH 032/103] Update sample-custome-extension to use plugin --- .../markwon/sample/extension/IconPlugin.java | 58 ++++++++++++++++ .../markwon/sample/extension/IconVisitor.java | 62 ----------------- .../sample/extension/MainActivity.java | 67 +++++-------------- 3 files changed, 73 insertions(+), 114 deletions(-) create mode 100644 sample-custom-extension/src/main/java/ru/noties/markwon/sample/extension/IconPlugin.java delete mode 100644 sample-custom-extension/src/main/java/ru/noties/markwon/sample/extension/IconVisitor.java diff --git a/sample-custom-extension/src/main/java/ru/noties/markwon/sample/extension/IconPlugin.java b/sample-custom-extension/src/main/java/ru/noties/markwon/sample/extension/IconPlugin.java new file mode 100644 index 00000000..eb821000 --- /dev/null +++ b/sample-custom-extension/src/main/java/ru/noties/markwon/sample/extension/IconPlugin.java @@ -0,0 +1,58 @@ +package ru.noties.markwon.sample.extension; + +import android.support.annotation.NonNull; +import android.text.TextUtils; + +import org.commonmark.parser.Parser; + +import ru.noties.markwon.AbstractMarkwonPlugin; +import ru.noties.markwon.MarkwonVisitor; + +public class IconPlugin extends AbstractMarkwonPlugin { + + @NonNull + public static IconPlugin create(@NonNull IconSpanProvider iconSpanProvider) { + return new IconPlugin(iconSpanProvider); + } + + private final IconSpanProvider iconSpanProvider; + + IconPlugin(@NonNull IconSpanProvider iconSpanProvider) { + this.iconSpanProvider = iconSpanProvider; + } + + @Override + public void configureParser(@NonNull Parser.Builder builder) { + builder.customDelimiterProcessor(IconProcessor.create()); + } + + @Override + public void configureVisitor(@NonNull MarkwonVisitor.Builder builder) { + builder.on(IconNode.class, new MarkwonVisitor.NodeVisitor() { + @Override + public void visit(@NonNull MarkwonVisitor visitor, @NonNull IconNode iconNode) { + + final String name = iconNode.name(); + final String color = iconNode.color(); + final String size = iconNode.size(); + + if (!TextUtils.isEmpty(name) + && !TextUtils.isEmpty(color) + && !TextUtils.isEmpty(size)) { + + final int length = visitor.length(); + + visitor.builder().append(name); + visitor.setSpans(length, iconSpanProvider.provide(name, color, size)); + visitor.builder().append(' '); + } + } + }); + } + + @NonNull + @Override + public String processMarkdown(@NonNull String markdown) { + return IconProcessor.prepare(markdown); + } +} diff --git a/sample-custom-extension/src/main/java/ru/noties/markwon/sample/extension/IconVisitor.java b/sample-custom-extension/src/main/java/ru/noties/markwon/sample/extension/IconVisitor.java deleted file mode 100644 index 77c5da2e..00000000 --- a/sample-custom-extension/src/main/java/ru/noties/markwon/sample/extension/IconVisitor.java +++ /dev/null @@ -1,62 +0,0 @@ -package ru.noties.markwon.sample.extension; - -import android.support.annotation.NonNull; -import android.text.TextUtils; - -import org.commonmark.node.CustomNode; - -import ru.noties.markwon.SpannableBuilder; -import ru.noties.markwon.MarkwonConfiguration; -import ru.noties.markwon.renderer.SpannableMarkdownVisitor; - -@SuppressWarnings("WeakerAccess") -public class IconVisitor extends SpannableMarkdownVisitor { - - private final SpannableBuilder builder; - - private final IconSpanProvider iconSpanProvider; - - public IconVisitor( - @NonNull MarkwonConfiguration configuration, - @NonNull SpannableBuilder builder, - @NonNull IconSpanProvider iconSpanProvider - ) { - super(configuration, builder); - this.builder = builder; - this.iconSpanProvider = iconSpanProvider; - } - - @Override - public void visit(CustomNode customNode) { - if (!visitIconNode(customNode)) { - super.visit(customNode); - } - } - - private boolean visitIconNode(@NonNull CustomNode customNode) { - - if (customNode instanceof IconNode) { - - final IconNode node = (IconNode) customNode; - - final String name = node.name(); - final String color = node.color(); - final String size = node.size(); - - if (!TextUtils.isEmpty(name) - && !TextUtils.isEmpty(color) - && !TextUtils.isEmpty(size)) { - - final int length = builder.length(); - - builder.append(name); - builder.setSpan(iconSpanProvider.provide(name, color, size), length); - builder.append(' '); - - return true; - } - } - - return false; - } -} diff --git a/sample-custom-extension/src/main/java/ru/noties/markwon/sample/extension/MainActivity.java b/sample-custom-extension/src/main/java/ru/noties/markwon/sample/extension/MainActivity.java index 9cb46349..6477f5d0 100644 --- a/sample-custom-extension/src/main/java/ru/noties/markwon/sample/extension/MainActivity.java +++ b/sample-custom-extension/src/main/java/ru/noties/markwon/sample/extension/MainActivity.java @@ -3,19 +3,12 @@ package ru.noties.markwon.sample.extension; import android.app.Activity; import android.graphics.Typeface; import android.os.Bundle; +import android.support.annotation.NonNull; import android.widget.TextView; -import org.commonmark.ext.gfm.strikethrough.StrikethroughExtension; -import org.commonmark.ext.gfm.tables.TablesExtension; -import org.commonmark.node.Node; -import org.commonmark.parser.Parser; - -import java.util.Arrays; - -import ru.noties.markwon.MarkwonConfiguration; -import ru.noties.markwon.SpannableBuilder; +import ru.noties.markwon.AbstractMarkwonPlugin; +import ru.noties.markwon.Markwon; import ru.noties.markwon.spans.MarkwonTheme; -import ru.noties.markwon.tasklist.TaskListExtension; public class MainActivity extends Activity { @@ -27,49 +20,19 @@ public class MainActivity extends Activity { final TextView textView = findViewById(R.id.text_view); - // obtain an instance of parser - final Parser parser = new Parser.Builder() - // we will register all known to Markwon extensions - .extensions(Arrays.asList( - StrikethroughExtension.create(), - TablesExtension.create(), - TaskListExtension.create() - )) - // this is the handler for custom icons - .customDelimiterProcessor(IconProcessor.create()) + final Markwon markwon = Markwon.builder(this) + .use(IconPlugin.create(IconSpanProvider.create(this, 0))) + .use(new AbstractMarkwonPlugin() { + @Override + public void configureTheme(@NonNull MarkwonTheme.Builder builder) { + final float[] textSizeMultipliers = new float[]{3f, 2f, 1.5f, 1f, .5f, .25f}; + builder + .headingTypeface(Typeface.MONOSPACE) + .headingTextSizeMultipliers(textSizeMultipliers); + } + }) .build(); - // we process input to wrap icon definitions with `@` on both ends - // if your input already does it, there is not need for `IconProcessor.prepare()` call. - final String markdown = IconProcessor.prepare(getString(R.string.input)); - - final Node node = parser.parse(markdown); - - final SpannableBuilder builder = new SpannableBuilder(); - - // please note that here I am passing `0` as fallback it means that if toMarkdown references - // unknown icon, it will try to load fallback one and will fail with ResourceNotFound. It's - // better to provide a valid fallback option - final IconSpanProvider spanProvider = IconSpanProvider.create(this, 0); - - final float[] textSizeMultipliers = new float[]{3f, 2f, 1.5f, 1f, .5f, .25f}; - MarkwonConfiguration configuration = MarkwonConfiguration.builder(this) - .theme(MarkwonTheme.builder() - .headingTypeface(Typeface.MONOSPACE) - .headingTextSizeMultipliers(textSizeMultipliers) - .build()) - .build(); - // create an instance of visitor to process parsed toMarkdown - final IconVisitor visitor = new IconVisitor( - configuration, - builder, - spanProvider - ); - - // trigger visit - node.accept(visitor); - - // apply - textView.setText(builder.text()); + markwon.setMarkdown(textView, getString(R.string.input)); } } From 577c3fc7820dea76faaeb8d05ca803da5cfbfd4a Mon Sep 17 00:00:00 2001 From: Dimitry Ivanov Date: Sun, 2 Dec 2018 16:13:08 +0300 Subject: [PATCH 033/103] Add markwon-ext-latex module extension --- build.gradle | 1 + markwon-ext-latex/build.gradle | 22 +++ .../src/main/AndroidManifest.xml | 1 + .../markwon/ext/latex}/JLatexMathBlock.java | 2 +- .../ext/latex}/JLatexMathBlockParser.java | 2 +- .../markwon/ext/latex/JLatexMathPlugin.java | 143 ++++++++++++++++++ .../ru/noties/markwon/core/CorePlugin.java | 6 - .../AsyncDrawableScheduler.java | 9 +- .../ru/noties/markwon/image/ImagesPlugin.java | 11 ++ sample-latex-math/build.gradle | 3 +- .../sample/jlatexmath/JLatexMathMedia.java | 138 ----------------- .../sample/jlatexmath/MainActivity.java | 69 ++------- settings.gradle | 1 + 13 files changed, 198 insertions(+), 210 deletions(-) create mode 100644 markwon-ext-latex/build.gradle create mode 100644 markwon-ext-latex/src/main/AndroidManifest.xml rename {sample-latex-math/src/main/java/ru/noties/markwon/sample/jlatexmath => markwon-ext-latex/src/main/java/ru/noties/markwon/ext/latex}/JLatexMathBlock.java (84%) rename {sample-latex-math/src/main/java/ru/noties/markwon/sample/jlatexmath => markwon-ext-latex/src/main/java/ru/noties/markwon/ext/latex}/JLatexMathBlockParser.java (97%) create mode 100644 markwon-ext-latex/src/main/java/ru/noties/markwon/ext/latex/JLatexMathPlugin.java rename markwon/src/main/java/ru/noties/markwon/{core => image}/AsyncDrawableScheduler.java (95%) delete mode 100644 sample-latex-math/src/main/java/ru/noties/markwon/sample/jlatexmath/JLatexMathMedia.java diff --git a/build.gradle b/build.gradle index 6ba06545..f5ccc8a1 100644 --- a/build.gradle +++ b/build.gradle @@ -61,6 +61,7 @@ ext { 'commonmark-table' : "com.atlassian.commonmark:commonmark-ext-gfm-tables:$commonMarkVersion", 'android-svg' : 'com.caverock:androidsvg:1.2.1', 'android-gif' : 'pl.droidsonroids.gif:android-gif-drawable:1.2.14', + 'jlatexmath-android' : 'ru.noties:jlatexmath-android:0.1.0', 'okhttp' : 'com.squareup.okhttp3:okhttp:3.9.0', 'prism4j' : 'ru.noties:prism4j:1.1.0', 'debug' : 'ru.noties:debug:3.0.0@jar', diff --git a/markwon-ext-latex/build.gradle b/markwon-ext-latex/build.gradle new file mode 100644 index 00000000..cd984c60 --- /dev/null +++ b/markwon-ext-latex/build.gradle @@ -0,0 +1,22 @@ +apply plugin: 'com.android.library' + +android { + + compileSdkVersion config['compile-sdk'] + buildToolsVersion config['build-tools'] + + defaultConfig { + minSdkVersion config['min-sdk'] + targetSdkVersion config['target-sdk'] + versionCode 1 + versionName version + } +} + +dependencies { + + api project(':markwon') + api deps['jlatexmath-android'] +} + +registerArtifact(this) \ No newline at end of file diff --git a/markwon-ext-latex/src/main/AndroidManifest.xml b/markwon-ext-latex/src/main/AndroidManifest.xml new file mode 100644 index 00000000..fee2db10 --- /dev/null +++ b/markwon-ext-latex/src/main/AndroidManifest.xml @@ -0,0 +1 @@ + diff --git a/sample-latex-math/src/main/java/ru/noties/markwon/sample/jlatexmath/JLatexMathBlock.java b/markwon-ext-latex/src/main/java/ru/noties/markwon/ext/latex/JLatexMathBlock.java similarity index 84% rename from sample-latex-math/src/main/java/ru/noties/markwon/sample/jlatexmath/JLatexMathBlock.java rename to markwon-ext-latex/src/main/java/ru/noties/markwon/ext/latex/JLatexMathBlock.java index 3e3e4479..d49d108a 100644 --- a/sample-latex-math/src/main/java/ru/noties/markwon/sample/jlatexmath/JLatexMathBlock.java +++ b/markwon-ext-latex/src/main/java/ru/noties/markwon/ext/latex/JLatexMathBlock.java @@ -1,4 +1,4 @@ -package ru.noties.markwon.sample.jlatexmath; +package ru.noties.markwon.ext.latex; import org.commonmark.node.CustomBlock; diff --git a/sample-latex-math/src/main/java/ru/noties/markwon/sample/jlatexmath/JLatexMathBlockParser.java b/markwon-ext-latex/src/main/java/ru/noties/markwon/ext/latex/JLatexMathBlockParser.java similarity index 97% rename from sample-latex-math/src/main/java/ru/noties/markwon/sample/jlatexmath/JLatexMathBlockParser.java rename to markwon-ext-latex/src/main/java/ru/noties/markwon/ext/latex/JLatexMathBlockParser.java index 542b3869..7aac76f2 100644 --- a/sample-latex-math/src/main/java/ru/noties/markwon/sample/jlatexmath/JLatexMathBlockParser.java +++ b/markwon-ext-latex/src/main/java/ru/noties/markwon/ext/latex/JLatexMathBlockParser.java @@ -1,4 +1,4 @@ -package ru.noties.markwon.sample.jlatexmath; +package ru.noties.markwon.ext.latex; import org.commonmark.node.Block; import org.commonmark.parser.block.AbstractBlockParser; diff --git a/markwon-ext-latex/src/main/java/ru/noties/markwon/ext/latex/JLatexMathPlugin.java b/markwon-ext-latex/src/main/java/ru/noties/markwon/ext/latex/JLatexMathPlugin.java new file mode 100644 index 00000000..eec628a9 --- /dev/null +++ b/markwon-ext-latex/src/main/java/ru/noties/markwon/ext/latex/JLatexMathPlugin.java @@ -0,0 +1,143 @@ +package ru.noties.markwon.ext.latex; + +import android.graphics.drawable.Drawable; +import android.net.Uri; +import android.support.annotation.NonNull; +import android.support.annotation.Nullable; + +import org.commonmark.parser.Parser; + +import java.io.ByteArrayInputStream; +import java.io.InputStream; +import java.io.UnsupportedEncodingException; +import java.util.Scanner; + +import ru.noties.jlatexmath.JLatexMathDrawable; +import ru.noties.markwon.AbstractMarkwonPlugin; +import ru.noties.markwon.MarkwonConfiguration; +import ru.noties.markwon.MarkwonVisitor; +import ru.noties.markwon.image.AsyncDrawableLoader; +import ru.noties.markwon.image.ImageItem; +import ru.noties.markwon.image.ImageSize; +import ru.noties.markwon.image.MediaDecoder; +import ru.noties.markwon.image.SchemeHandler; + +public class JLatexMathPlugin extends AbstractMarkwonPlugin { + + @NonNull + public static JLatexMathPlugin create(@NonNull Config config) { + return new JLatexMathPlugin(config); + } + + public static class Config { + + protected final float textSize; + + protected Drawable background; + + @JLatexMathDrawable.Align + protected int align = JLatexMathDrawable.ALIGN_CENTER; + + protected boolean fitCanvas = true; + + protected int padding; + + public Config(float textSize) { + this.textSize = textSize; + } + } + + @NonNull + public static String makeDestination(@NonNull String latex) { + return SCHEME + "://" + latex; + } + + private static final String SCHEME = "jlatexmath"; + private static final String CONTENT_TYPE = "text/jlatexmath"; + + private final Config config; + + JLatexMathPlugin(@NonNull Config config) { + this.config = config; + } + + @Override + public void configureParser(@NonNull Parser.Builder builder) { + builder.customBlockParserFactory(new JLatexMathBlockParser.Factory()); + } + + @Override + public void configureVisitor(@NonNull MarkwonVisitor.Builder builder) { + builder.on(JLatexMathBlock.class, new MarkwonVisitor.NodeVisitor() { + @Override + public void visit(@NonNull MarkwonVisitor visitor, @NonNull JLatexMathBlock jLatexMathBlock) { + + final String latex = jLatexMathBlock.latex(); + + final int length = visitor.length(); + visitor.builder().append(latex); + + final MarkwonConfiguration configuration = visitor.configuration(); + + visitor.setSpans( + length, + configuration.factory().image( + visitor.theme(), + makeDestination(latex), + configuration.asyncDrawableLoader(), + configuration.imageSizeResolver(), + new ImageSize(new ImageSize.Dimension(100, "%"), null), + false + ) + ); + } + }); + } + + @Override + public void configureImages(@NonNull AsyncDrawableLoader.Builder builder) { + builder + .addSchemeHandler(SCHEME, new SchemeHandler() { + @Nullable + @Override + public ImageItem handle(@NonNull String raw, @NonNull Uri uri) { + + ImageItem item = null; + + try { + final byte[] bytes = raw.substring(SCHEME.length()).getBytes("UTF-8"); + item = new ImageItem( + CONTENT_TYPE, + new ByteArrayInputStream(bytes)); + } catch (UnsupportedEncodingException e) { + e.printStackTrace(); + } + + return item; + } + }) + .addMediaDecoder(CONTENT_TYPE, new MediaDecoder() { + @Nullable + @Override + public Drawable decode(@NonNull InputStream inputStream) { + + final Scanner scanner = new Scanner(inputStream, "UTF-8").useDelimiter("\\A"); + final String latex = scanner.hasNext() + ? scanner.next() + : null; + + if (latex == null) { + return null; + } + + return JLatexMathDrawable.builder(latex) + .textSize(config.textSize) + .background(config.background) + .align(config.align) + .fitCanvas(config.fitCanvas) + .padding(config.padding) + .build(); + } + }); + } +} diff --git a/markwon/src/main/java/ru/noties/markwon/core/CorePlugin.java b/markwon/src/main/java/ru/noties/markwon/core/CorePlugin.java index 832536da..37a92f7c 100644 --- a/markwon/src/main/java/ru/noties/markwon/core/CorePlugin.java +++ b/markwon/src/main/java/ru/noties/markwon/core/CorePlugin.java @@ -79,12 +79,6 @@ public class CorePlugin extends AbstractMarkwonPlugin { @Override public void beforeSetText(@NonNull TextView textView, @NonNull CharSequence markdown) { OrderedListItemSpan.measure(textView, markdown); - AsyncDrawableScheduler.unschedule(textView); - } - - @Override - public void afterSetText(@NonNull TextView textView) { - AsyncDrawableScheduler.schedule(textView); } protected void text(@NonNull MarkwonVisitor.Builder builder) { diff --git a/markwon/src/main/java/ru/noties/markwon/core/AsyncDrawableScheduler.java b/markwon/src/main/java/ru/noties/markwon/image/AsyncDrawableScheduler.java similarity index 95% rename from markwon/src/main/java/ru/noties/markwon/core/AsyncDrawableScheduler.java rename to markwon/src/main/java/ru/noties/markwon/image/AsyncDrawableScheduler.java index 3d7fae23..0de99c61 100644 --- a/markwon/src/main/java/ru/noties/markwon/core/AsyncDrawableScheduler.java +++ b/markwon/src/main/java/ru/noties/markwon/image/AsyncDrawableScheduler.java @@ -1,4 +1,4 @@ -package ru.noties.markwon.core; +package ru.noties.markwon.image; import android.graphics.Rect; import android.graphics.drawable.Drawable; @@ -15,12 +15,11 @@ import java.util.Collections; import java.util.List; import ru.noties.markwon.renderer.R; -import ru.noties.markwon.image.AsyncDrawable; import ru.noties.markwon.spans.AsyncDrawableSpan; -abstract class AsyncDrawableScheduler { +public abstract class AsyncDrawableScheduler { - static void schedule(@NonNull final TextView textView) { + public static void schedule(@NonNull final TextView textView) { final List list = extract(textView); if (list.size() > 0) { @@ -50,7 +49,7 @@ abstract class AsyncDrawableScheduler { } // must be called when text manually changed in TextView - static void unschedule(@NonNull TextView view) { + public static void unschedule(@NonNull TextView view) { for (AsyncDrawable drawable : extract(view)) { drawable.setCallback2(null); } diff --git a/markwon/src/main/java/ru/noties/markwon/image/ImagesPlugin.java b/markwon/src/main/java/ru/noties/markwon/image/ImagesPlugin.java index 0154eb8f..5f43ab54 100644 --- a/markwon/src/main/java/ru/noties/markwon/image/ImagesPlugin.java +++ b/markwon/src/main/java/ru/noties/markwon/image/ImagesPlugin.java @@ -2,6 +2,7 @@ package ru.noties.markwon.image; import android.content.Context; import android.support.annotation.NonNull; +import android.widget.TextView; import org.commonmark.node.Image; import org.commonmark.node.Link; @@ -89,4 +90,14 @@ public class ImagesPlugin extends AbstractMarkwonPlugin { } }); } + + @Override + public void beforeSetText(@NonNull TextView textView, @NonNull CharSequence markdown) { + AsyncDrawableScheduler.unschedule(textView); + } + + @Override + public void afterSetText(@NonNull TextView textView) { + AsyncDrawableScheduler.schedule(textView); + } } diff --git a/sample-latex-math/build.gradle b/sample-latex-math/build.gradle index f9f7c1eb..2633b03a 100644 --- a/sample-latex-math/build.gradle +++ b/sample-latex-math/build.gradle @@ -18,6 +18,7 @@ android { dependencies { implementation project(':markwon') + implementation project(':markwon-ext-latex') // implementation project(':markwon-image-loader') - implementation 'ru.noties:jlatexmath-android:0.1.0' +// implementation 'ru.noties:jlatexmath-android:0.1.0' } diff --git a/sample-latex-math/src/main/java/ru/noties/markwon/sample/jlatexmath/JLatexMathMedia.java b/sample-latex-math/src/main/java/ru/noties/markwon/sample/jlatexmath/JLatexMathMedia.java deleted file mode 100644 index bf0b76a5..00000000 --- a/sample-latex-math/src/main/java/ru/noties/markwon/sample/jlatexmath/JLatexMathMedia.java +++ /dev/null @@ -1,138 +0,0 @@ -package ru.noties.markwon.sample.jlatexmath; - -import android.graphics.drawable.Drawable; -import android.net.Uri; -import android.support.annotation.NonNull; -import android.support.annotation.Nullable; - -import java.io.ByteArrayInputStream; -import java.io.InputStream; -import java.io.UnsupportedEncodingException; -import java.util.Collection; -import java.util.Collections; -import java.util.Scanner; - -import ru.noties.jlatexmath.JLatexMathDrawable; -import ru.noties.markwon.il.ImageItem; -import ru.noties.markwon.il.MediaDecoder; -import ru.noties.markwon.il.SchemeHandler; - -public class JLatexMathMedia { - - public static class Config { - - protected final float textSize; - - protected Drawable background; - - @JLatexMathDrawable.Align - protected int align = JLatexMathDrawable.ALIGN_CENTER; - - protected boolean fitCanvas = true; - - protected int padding; - - public Config(float textSize) { - this.textSize = textSize; - } - } - - @NonNull - public static String makeDestination(@NonNull String latex) { - return SCHEME + "://" + latex; - } - - private static final String SCHEME = "jlatexmath"; - private static final String CONTENT_TYPE = "text/jlatexmath"; - - private final Config config; - - public JLatexMathMedia(@NonNull Config config) { - this.config = config; - } - - @NonNull - public SchemeHandler schemeHandler() { - return new SchemeHandlerImpl(); - } - - @NonNull - public MediaDecoder mediaDecoder() { - return new MediaDecoderImpl(config); - } - - static class SchemeHandlerImpl extends SchemeHandler { - - @Nullable - @Override - public ImageItem handle(@NonNull String raw, @NonNull Uri uri) { - - ImageItem item = null; - - try { - final byte[] bytes = raw.substring(SCHEME.length()).getBytes("UTF-8"); - item = new ImageItem( - CONTENT_TYPE, - new ByteArrayInputStream(bytes), - null - ); - } catch (UnsupportedEncodingException e) { - e.printStackTrace(); - } - - return item; - } - - @Override - public void cancel(@NonNull String raw) { - // no op - } - - @NonNull - @Override - public Collection schemes() { - return Collections.singleton(SCHEME); - } - } - - static class MediaDecoderImpl extends MediaDecoder { - - private final Config config; - - MediaDecoderImpl(@NonNull Config config) { - this.config = config; - } - - @Override - public boolean canDecodeByContentType(@Nullable String contentType) { - return CONTENT_TYPE.equals(contentType); - } - - @Override - public boolean canDecodeByFileName(@NonNull String fileName) { - return false; - } - - @Nullable - @Override - public Drawable decode(@NonNull InputStream inputStream) { - - final Scanner scanner = new Scanner(inputStream, "UTF-8").useDelimiter("\\A"); - final String latex = scanner.hasNext() - ? scanner.next() - : null; - - if (latex == null) { - return null; - } - - return JLatexMathDrawable.builder(latex) - .textSize(config.textSize) - .background(config.background) - .align(config.align) - .fitCanvas(config.fitCanvas) - .padding(config.padding) - .build(); - } - } -} diff --git a/sample-latex-math/src/main/java/ru/noties/markwon/sample/jlatexmath/MainActivity.java b/sample-latex-math/src/main/java/ru/noties/markwon/sample/jlatexmath/MainActivity.java index 5f2ea561..01fb2718 100644 --- a/sample-latex-math/src/main/java/ru/noties/markwon/sample/jlatexmath/MainActivity.java +++ b/sample-latex-math/src/main/java/ru/noties/markwon/sample/jlatexmath/MainActivity.java @@ -4,17 +4,10 @@ import android.app.Activity; import android.os.Bundle; import android.widget.TextView; -import org.commonmark.node.CustomBlock; -import org.commonmark.node.Node; -import org.commonmark.parser.Parser; - -import ru.noties.jlatexmath.JLatexMathAndroid; import ru.noties.markwon.Markwon; -import ru.noties.markwon.MarkwonConfiguration; -import ru.noties.markwon.SpannableBuilder; -import ru.noties.markwon.il.AsyncDrawableLoader; -import ru.noties.markwon.image.ImageSize; -import ru.noties.markwon.renderer.SpannableMarkdownVisitor; +import ru.noties.markwon.core.CorePlugin; +import ru.noties.markwon.ext.latex.JLatexMathPlugin; +import ru.noties.markwon.image.ImagesPlugin; public class MainActivity extends Activity { @@ -45,61 +38,21 @@ public class MainActivity extends Activity { // latex += "\\end{array}"; - final JLatexMathMedia.Config config = new JLatexMathMedia.Config(textView.getTextSize()) {{ + final JLatexMathPlugin.Config config = new JLatexMathPlugin.Config(textView.getTextSize()) {{ // align = JLatexMathDrawable.ALIGN_RIGHT; }}; - final JLatexMathMedia jLatexMathMedia = new JLatexMathMedia(config); - - final AsyncDrawableLoader asyncDrawableLoader = AsyncDrawableLoader.builder() - .addSchemeHandler(jLatexMathMedia.schemeHandler()) - .mediaDecoders(jLatexMathMedia.mediaDecoder()) - .build(); - - final MarkwonConfiguration configuration = MarkwonConfiguration.builder(this) - .asyncDrawableLoader(asyncDrawableLoader) - .build(); final String markdown = "# Example of LaTeX\n\n$$" + latex + "$$\n\n something like **this**"; - final Parser parser = new Parser.Builder() - .customBlockParserFactory(new JLatexMathBlockParser.Factory()) + final Markwon markwon = Markwon.builder(this) + .use(CorePlugin.create()) + // strictly speaking this one is not required as long as JLatexMathPlugin schedules + // drawables on it's own + .use(ImagesPlugin.create(this)) + .use(JLatexMathPlugin.create(config)) .build(); - final Node node = parser.parse(markdown); - final SpannableBuilder builder = new SpannableBuilder(); - final SpannableMarkdownVisitor visitor = new SpannableMarkdownVisitor(MarkwonConfiguration.create(this), builder) { - - @Override - public void visit(CustomBlock customBlock) { - - if (!(customBlock instanceof JLatexMathBlock)) { - super.visit(customBlock); - return; - } - - final String latex = ((JLatexMathBlock) customBlock).latex(); - - final int length = builder.length(); - builder.append(latex); - - SpannableBuilder.setSpans( - builder, - configuration.factory().image( - configuration.theme(), - JLatexMathMedia.makeDestination(latex), - configuration.asyncDrawableLoader(), - configuration.imageSizeResolver(), - new ImageSize(new ImageSize.Dimension(100, "%"), null), - false - ), - length, - builder.length() - ); - } - }; - node.accept(visitor); - - Markwon.setText(textView, builder.text()); + markwon.setMarkdown(textView, markdown); } } diff --git a/settings.gradle b/settings.gradle index 13fb6c63..10d8564b 100644 --- a/settings.gradle +++ b/settings.gradle @@ -1,6 +1,7 @@ rootProject.name = 'MarkwonProject' include ':app', ':markwon', + ':markwon-ext-latex', ':markwon-ext-strikethrough', ':markwon-ext-tables', ':markwon-ext-tasklist', From 582b9209d9398b14712ef7edf86ad0ab1ad2b3b3 Mon Sep 17 00:00:00 2001 From: Dimitry Ivanov Date: Mon, 3 Dec 2018 17:19:31 +0300 Subject: [PATCH 034/103] Fix typo in AsyncDrawable waitingForDimensions --- .../noties/markwon/image/AsyncDrawable.java | 8 +- .../markwon/spans/AsyncDrawableTest.java | 107 ++++++++++++++++++ 2 files changed, 111 insertions(+), 4 deletions(-) create mode 100644 markwon/src/test/java/ru/noties/markwon/spans/AsyncDrawableTest.java diff --git a/markwon/src/main/java/ru/noties/markwon/image/AsyncDrawable.java b/markwon/src/main/java/ru/noties/markwon/image/AsyncDrawable.java index a9bde47a..9d38b915 100644 --- a/markwon/src/main/java/ru/noties/markwon/image/AsyncDrawable.java +++ b/markwon/src/main/java/ru/noties/markwon/image/AsyncDrawable.java @@ -24,7 +24,7 @@ public class AsyncDrawable extends Drawable { private float textSize; // @since 2.0.1 for use-cases when image is loaded faster than span is drawn and knows canvas width - private boolean waitingForDemensions; + private boolean waitingForDimensions; /** * @since 1.0.1 @@ -98,11 +98,11 @@ public class AsyncDrawable extends Drawable { if (canvasWidth == 0) { // we still have no bounds - wait for them - waitingForDemensions = true; + waitingForDimensions = true; return; } - waitingForDemensions = false; + waitingForDimensions = false; final Rect bounds = resolveBounds(); result.setBounds(bounds); @@ -119,7 +119,7 @@ public class AsyncDrawable extends Drawable { this.canvasWidth = width; this.textSize = textSize; - if (waitingForDemensions) { + if (waitingForDimensions) { initBounds(); } } diff --git a/markwon/src/test/java/ru/noties/markwon/spans/AsyncDrawableTest.java b/markwon/src/test/java/ru/noties/markwon/spans/AsyncDrawableTest.java new file mode 100644 index 00000000..873cc404 --- /dev/null +++ b/markwon/src/test/java/ru/noties/markwon/spans/AsyncDrawableTest.java @@ -0,0 +1,107 @@ +package ru.noties.markwon.spans; + +import android.graphics.Canvas; +import android.graphics.ColorFilter; +import android.graphics.Rect; +import android.graphics.drawable.Drawable; +import android.support.annotation.NonNull; +import android.support.annotation.Nullable; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.robolectric.RobolectricTestRunner; +import org.robolectric.annotation.Config; + +import ru.noties.markwon.renderer.ImageSize; +import ru.noties.markwon.renderer.ImageSizeResolver; +import ru.noties.markwon.renderer.ImageSizeResolverDef; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertTrue; +import static org.mockito.Mockito.mock; + +@RunWith(RobolectricTestRunner.class) +@Config(manifest = Config.NONE) +public class AsyncDrawableTest { + + private ImageSizeResolver imageSizeResolver; + + @Before + public void before() { + imageSizeResolver = new ImageSizeResolverDef(); + } + + @Test + public void no_dimensions_await() { + // when drawable have no known dimensions yet, it will await for them + + final AsyncDrawable drawable = new AsyncDrawable("", + mock(AsyncDrawable.Loader.class), + imageSizeResolver, + new ImageSize(new ImageSize.Dimension(100.F, "%"), null)); + + final Drawable result = new AbstractDrawable(); + result.setBounds(0, 0, 0, 0); + + assertFalse(drawable.hasResult()); + drawable.setResult(result); + assertTrue(drawable.hasResult()); + + assertTrue(result.getBounds().isEmpty()); + + drawable.initWithKnownDimensions(100, 1); + assertEquals( + new Rect(0, 0, 100, 0), + result.getBounds() + ); + } + + @Test + public void previous_result_detached() { + // when result is present it will be detached (setCallback(null)) + + final AsyncDrawable drawable = new AsyncDrawable("", + mock(AsyncDrawable.Loader.class), + imageSizeResolver, + null); + + drawable.setCallback2(mock(Drawable.Callback.class)); + drawable.initWithKnownDimensions(100, 1); + + final Drawable result1 = new AbstractDrawable(); + final Drawable result2 = new AbstractDrawable(); + + drawable.setResult(result1); + assertNotNull(result1.getCallback()); + drawable.setResult(result2); + assertNull(result1.getCallback()); + assertNotNull(result2.getCallback()); + } + + private static class AbstractDrawable extends Drawable { + + @Override + public void draw(@NonNull Canvas canvas) { + + } + + @Override + public void setAlpha(int alpha) { + + } + + @Override + public void setColorFilter(@Nullable ColorFilter colorFilter) { + + } + + @Override + public int getOpacity() { + return 0; + } + } +} \ No newline at end of file From 078d4dfc3815db205e90dd527e66473a6f25d9fb Mon Sep 17 00:00:00 2001 From: Dimitry Ivanov Date: Mon, 3 Dec 2018 17:58:24 +0300 Subject: [PATCH 035/103] Add README to ext-latex module --- markwon-ext-latex/README.md | 46 +++++++++++++++++++++++++++++++++++++ 1 file changed, 46 insertions(+) create mode 100644 markwon-ext-latex/README.md diff --git a/markwon-ext-latex/README.md b/markwon-ext-latex/README.md new file mode 100644 index 00000000..5937fd6b --- /dev/null +++ b/markwon-ext-latex/README.md @@ -0,0 +1,46 @@ +# LaTeX + +This is a small extension that will help you display LaTeX formulas in your markdown. +Syntax is pretty simple: pre-fix and post-fix your latex with `$$` (double dollar sign). +`$$` should be the first characters in a line. + +```markdown +$$ +\\text{A long division \\longdiv{12345}{13} +$$ +``` + +```markdown +$$\\text{A long division \\longdiv{12345}{13}$$ +``` + +```java +Markwon.builder(context) + .use(CorePlugin.create()) + .use(ImagesPlugin.create(context)) + .use(JLatexMathPlugin.create(new Config(textSize)) + .build(); +``` + +This extension uses [jlatexmath-android](https://github.com/noties/jlatexmath-android) artifact to create LaTeX drawable. Then it +registers special `latex` image scheme handler and uses `AsyncDrawableLoader` to display +final result + +## Config + +```java +public static class Config { + + protected final float textSize; + + protected Drawable background; + + @JLatexMathDrawable.Align + protected int align = JLatexMathDrawable.ALIGN_CENTER; + + protected boolean fitCanvas = true; + + protected int padding; +} +``` + From 5a18aa3a01d1b0ee65fd7756e9711f9164945088 Mon Sep 17 00:00:00 2001 From: Dimitry Ivanov Date: Thu, 13 Dec 2018 19:05:49 +0300 Subject: [PATCH 036/103] Remove ext tests from core module --- app/src/main/AndroidManifest.xml | 15 -- .../ru/noties/markwon/MarkdownRenderer.java | 2 + .../markwon/UrlProcessorInitialReadme.java | 3 + .../markwon/gif/GifAwareSpannableFactory.java | 4 +- .../ru/noties/markwon/gif/GifProcessor.java | 2 +- .../markwon/ext/tasklist/TaskListSpan.java | 2 +- .../syntax/Prism4jSyntaxHighlight.java | 1 - .../markwon/syntax/Prism4jThemeDarkula.java | 4 +- .../markwon/syntax/Prism4jThemeDefault.java | 4 +- .../markwon/syntax/SyntaxHighlightPlugin.java | 2 +- .../debug/DebugConfigurationProvider.java | 2 +- .../markwon/view/MarkwonViewHelper.java | 2 +- .../noties/markwon/AbstractMarkwonPlugin.java | 2 +- .../ru/noties/markwon/LinkResolverDef.java | 2 +- .../ru/noties/markwon/MarkwonBuilderImpl.java | 2 +- .../noties/markwon/MarkwonConfiguration.java | 28 +- .../java/ru/noties/markwon/MarkwonPlugin.java | 2 +- .../ru/noties/markwon/MarkwonVisitor.java | 2 +- .../ru/noties/markwon/MarkwonVisitorImpl.java | 4 +- .../ru/noties/markwon/SpannableFactory.java | 4 +- .../noties/markwon/SpannableFactoryDef.java | 22 +- .../ru/noties/markwon/core/CorePlugin.java | 2 +- .../markwon/{spans => core}/MarkwonTheme.java | 3 +- .../{ => core}/spans/AsyncDrawableSpan.java | 3 +- .../{ => core}/spans/BlockQuoteSpan.java | 4 +- .../{ => core}/spans/BulletListItemSpan.java | 3 +- .../markwon/{ => core}/spans/CodeSpan.java | 4 +- .../{ => core}/spans/EmphasisSpan.java | 2 +- .../markwon/{ => core}/spans/HeadingSpan.java | 3 +- .../markwon/{ => core}/spans/LinkSpan.java | 4 +- .../markwon/{ => core}/spans/ObjectsPool.java | 2 +- .../{ => core}/spans/OrderedListItemSpan.java | 7 +- .../{ => core}/spans/StrongEmphasisSpan.java | 2 +- .../{ => core}/spans/ThematicBreakSpan.java | 4 +- .../markwon/image/AsyncDrawableScheduler.java | 2 +- .../markwon/{ => syntax}/SyntaxHighlight.java | 2 +- .../{ => syntax}/SyntaxHighlightNoOp.java | 4 +- .../{ => urlprocessor}/UrlProcessor.java | 2 +- .../UrlProcessorAndroidAssets.java | 2 +- .../{ => urlprocessor}/UrlProcessorNoOp.java | 2 +- .../UrlProcessorRelativeToAbsolute.java | 2 +- .../java/ru/noties/markwon/utils/Dip.java | 2 - .../markwon/AbstractMarkwonVisitorImpl.java | 16 ++ .../{spans => image}/AsyncDrawableTest.java | 10 +- .../ImageSizeResolverDefTest.java | 2 +- .../renderer/MarkwonConfigurationTest.java | 53 ---- .../html2/CssInlineStyleParserTest.java | 239 ------------------ .../html2/tag/ImageSizeParserImplTest.java | 186 -------------- .../visitor/SpannableMarkdownVisitorTest.java | 52 ++-- .../markwon/renderer/visitor/TestConfig.java | 4 +- .../renderer/visitor/TestDataReader.java | 52 ++-- .../markwon/renderer/visitor/TestFactory.java | 59 +---- .../markwon/renderer/visitor/TestSpan.java | 12 +- .../SyntaxHighlightTest.java | 35 ++- .../UrlProcessorAndroidAssetsTest.java | 6 +- .../UrlProcessorRelativeToAbsoluteTest.java | 4 +- .../test/resources/tests/deeply-nested.yaml | 7 +- .../tests/html-allow-non-closed-tags.yaml | 18 -- .../tests/html-non-closed-ignore.yaml | 12 - markwon/src/test/resources/tests/html.yaml | 105 -------- markwon/src/test/resources/tests/second.yaml | 5 +- .../src/test/resources/tests/single-s.yaml | 4 - .../src/test/resources/tests/single-sub.yaml | 7 - .../src/test/resources/tests/single-sup.yaml | 7 - .../resources/tests/single-task-list.yaml | 6 - .../src/test/resources/tests/single-tr.yaml | 13 - .../src/test/resources/tests/single-u.yaml | 7 - markwon/src/test/resources/tests/table.yaml | 51 ---- .../sample/extension/MainActivity.java | 2 +- 69 files changed, 188 insertions(+), 958 deletions(-) rename markwon/src/main/java/ru/noties/markwon/{spans => core}/MarkwonTheme.java (99%) rename markwon/src/main/java/ru/noties/markwon/{ => core}/spans/AsyncDrawableSpan.java (98%) rename markwon/src/main/java/ru/noties/markwon/{ => core}/spans/BlockQuoteSpan.java (94%) rename markwon/src/main/java/ru/noties/markwon/{ => core}/spans/BulletListItemSpan.java (97%) rename markwon/src/main/java/ru/noties/markwon/{ => core}/spans/CodeSpan.java (95%) rename markwon/src/main/java/ru/noties/markwon/{ => core}/spans/EmphasisSpan.java (90%) rename markwon/src/main/java/ru/noties/markwon/{ => core}/spans/HeadingSpan.java (96%) rename markwon/src/main/java/ru/noties/markwon/{ => core}/spans/LinkSpan.java (90%) rename markwon/src/main/java/ru/noties/markwon/{ => core}/spans/ObjectsPool.java (95%) rename markwon/src/main/java/ru/noties/markwon/{ => core}/spans/OrderedListItemSpan.java (94%) rename markwon/src/main/java/ru/noties/markwon/{ => core}/spans/StrongEmphasisSpan.java (90%) rename markwon/src/main/java/ru/noties/markwon/{ => core}/spans/ThematicBreakSpan.java (94%) rename markwon/src/main/java/ru/noties/markwon/{ => syntax}/SyntaxHighlight.java (87%) rename markwon/src/main/java/ru/noties/markwon/{ => syntax}/SyntaxHighlightNoOp.java (70%) rename markwon/src/main/java/ru/noties/markwon/{ => urlprocessor}/UrlProcessor.java (77%) rename markwon/src/main/java/ru/noties/markwon/{ => urlprocessor}/UrlProcessorAndroidAssets.java (96%) rename markwon/src/main/java/ru/noties/markwon/{ => urlprocessor}/UrlProcessorNoOp.java (84%) rename markwon/src/main/java/ru/noties/markwon/{ => urlprocessor}/UrlProcessorRelativeToAbsolute.java (96%) create mode 100644 markwon/src/test/java/ru/noties/markwon/AbstractMarkwonVisitorImpl.java rename markwon/src/test/java/ru/noties/markwon/{spans => image}/AsyncDrawableTest.java (90%) rename markwon/src/test/java/ru/noties/markwon/{renderer => image}/ImageSizeResolverDefTest.java (99%) delete mode 100644 markwon/src/test/java/ru/noties/markwon/renderer/MarkwonConfigurationTest.java delete mode 100644 markwon/src/test/java/ru/noties/markwon/renderer/html2/CssInlineStyleParserTest.java delete mode 100644 markwon/src/test/java/ru/noties/markwon/renderer/html2/tag/ImageSizeParserImplTest.java rename markwon/src/test/java/ru/noties/markwon/{renderer => syntax}/SyntaxHighlightTest.java (76%) rename markwon/src/test/java/ru/noties/markwon/{ => urlprocessor}/UrlProcessorAndroidAssetsTest.java (86%) rename markwon/src/test/java/ru/noties/markwon/{ => urlprocessor}/UrlProcessorRelativeToAbsoluteTest.java (95%) delete mode 100644 markwon/src/test/resources/tests/html-allow-non-closed-tags.yaml delete mode 100644 markwon/src/test/resources/tests/html-non-closed-ignore.yaml delete mode 100644 markwon/src/test/resources/tests/html.yaml delete mode 100644 markwon/src/test/resources/tests/single-s.yaml delete mode 100644 markwon/src/test/resources/tests/single-sub.yaml delete mode 100644 markwon/src/test/resources/tests/single-sup.yaml delete mode 100644 markwon/src/test/resources/tests/single-task-list.yaml delete mode 100644 markwon/src/test/resources/tests/single-tr.yaml delete mode 100644 markwon/src/test/resources/tests/single-u.yaml delete mode 100644 markwon/src/test/resources/tests/table.yaml diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 96db5266..2446cea2 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -38,21 +38,6 @@ android:host="*" android:scheme="https" /> - - - - - - - - - - - - - - - diff --git a/app/src/main/java/ru/noties/markwon/MarkdownRenderer.java b/app/src/main/java/ru/noties/markwon/MarkdownRenderer.java index 007b754f..df1c0b5d 100644 --- a/app/src/main/java/ru/noties/markwon/MarkdownRenderer.java +++ b/app/src/main/java/ru/noties/markwon/MarkdownRenderer.java @@ -26,6 +26,8 @@ import ru.noties.markwon.syntax.Prism4jTheme; import ru.noties.markwon.syntax.Prism4jThemeDarkula; import ru.noties.markwon.syntax.Prism4jThemeDefault; import ru.noties.markwon.syntax.SyntaxHighlightPlugin; +import ru.noties.markwon.urlprocessor.UrlProcessor; +import ru.noties.markwon.urlprocessor.UrlProcessorRelativeToAbsolute; import ru.noties.prism4j.Prism4j; @ActivityScope diff --git a/app/src/main/java/ru/noties/markwon/UrlProcessorInitialReadme.java b/app/src/main/java/ru/noties/markwon/UrlProcessorInitialReadme.java index 8f18a55e..d9690574 100644 --- a/app/src/main/java/ru/noties/markwon/UrlProcessorInitialReadme.java +++ b/app/src/main/java/ru/noties/markwon/UrlProcessorInitialReadme.java @@ -4,6 +4,9 @@ import android.net.Uri; import android.support.annotation.NonNull; import android.text.TextUtils; +import ru.noties.markwon.urlprocessor.UrlProcessor; +import ru.noties.markwon.urlprocessor.UrlProcessorRelativeToAbsolute; + class UrlProcessorInitialReadme implements UrlProcessor { private static final String GITHUB_BASE = "https://github.com/noties/Markwon/raw/master/"; diff --git a/app/src/main/java/ru/noties/markwon/gif/GifAwareSpannableFactory.java b/app/src/main/java/ru/noties/markwon/gif/GifAwareSpannableFactory.java index bf108477..3cd805b3 100644 --- a/app/src/main/java/ru/noties/markwon/gif/GifAwareSpannableFactory.java +++ b/app/src/main/java/ru/noties/markwon/gif/GifAwareSpannableFactory.java @@ -7,8 +7,8 @@ import ru.noties.markwon.SpannableFactoryDef; import ru.noties.markwon.image.AsyncDrawableLoader; import ru.noties.markwon.image.ImageSize; import ru.noties.markwon.image.ImageSizeResolver; -import ru.noties.markwon.spans.AsyncDrawableSpan; -import ru.noties.markwon.spans.MarkwonTheme; +import ru.noties.markwon.core.spans.AsyncDrawableSpan; +import ru.noties.markwon.core.MarkwonTheme; public class GifAwareSpannableFactory extends SpannableFactoryDef { diff --git a/app/src/main/java/ru/noties/markwon/gif/GifProcessor.java b/app/src/main/java/ru/noties/markwon/gif/GifProcessor.java index f50b1094..6a297f8b 100644 --- a/app/src/main/java/ru/noties/markwon/gif/GifProcessor.java +++ b/app/src/main/java/ru/noties/markwon/gif/GifProcessor.java @@ -9,7 +9,7 @@ import android.view.View; import android.widget.TextView; import pl.droidsonroids.gif.GifDrawable; -import ru.noties.markwon.spans.AsyncDrawableSpan; +import ru.noties.markwon.core.spans.AsyncDrawableSpan; public abstract class GifProcessor { diff --git a/markwon-ext-tasklist/src/main/java/ru/noties/markwon/ext/tasklist/TaskListSpan.java b/markwon-ext-tasklist/src/main/java/ru/noties/markwon/ext/tasklist/TaskListSpan.java index efddfe25..b339360f 100644 --- a/markwon-ext-tasklist/src/main/java/ru/noties/markwon/ext/tasklist/TaskListSpan.java +++ b/markwon-ext-tasklist/src/main/java/ru/noties/markwon/ext/tasklist/TaskListSpan.java @@ -7,7 +7,7 @@ import android.support.annotation.NonNull; import android.text.Layout; import android.text.style.LeadingMarginSpan; -import ru.noties.markwon.spans.MarkwonTheme; +import ru.noties.markwon.core.MarkwonTheme; import ru.noties.markwon.utils.LeadingMarginUtils; /** diff --git a/markwon-syntax-highlight/src/main/java/ru/noties/markwon/syntax/Prism4jSyntaxHighlight.java b/markwon-syntax-highlight/src/main/java/ru/noties/markwon/syntax/Prism4jSyntaxHighlight.java index 4f3c1c04..bb016796 100644 --- a/markwon-syntax-highlight/src/main/java/ru/noties/markwon/syntax/Prism4jSyntaxHighlight.java +++ b/markwon-syntax-highlight/src/main/java/ru/noties/markwon/syntax/Prism4jSyntaxHighlight.java @@ -5,7 +5,6 @@ import android.support.annotation.Nullable; import android.text.SpannableStringBuilder; import android.text.TextUtils; -import ru.noties.markwon.SyntaxHighlight; import ru.noties.prism4j.Prism4j; public class Prism4jSyntaxHighlight implements SyntaxHighlight { diff --git a/markwon-syntax-highlight/src/main/java/ru/noties/markwon/syntax/Prism4jThemeDarkula.java b/markwon-syntax-highlight/src/main/java/ru/noties/markwon/syntax/Prism4jThemeDarkula.java index 13bfe48b..4d4016bf 100644 --- a/markwon-syntax-highlight/src/main/java/ru/noties/markwon/syntax/Prism4jThemeDarkula.java +++ b/markwon-syntax-highlight/src/main/java/ru/noties/markwon/syntax/Prism4jThemeDarkula.java @@ -6,8 +6,8 @@ import android.support.annotation.Nullable; import android.text.SpannableStringBuilder; import android.text.Spanned; -import ru.noties.markwon.spans.EmphasisSpan; -import ru.noties.markwon.spans.StrongEmphasisSpan; +import ru.noties.markwon.core.spans.EmphasisSpan; +import ru.noties.markwon.core.spans.StrongEmphasisSpan; public class Prism4jThemeDarkula extends Prism4jThemeBase { diff --git a/markwon-syntax-highlight/src/main/java/ru/noties/markwon/syntax/Prism4jThemeDefault.java b/markwon-syntax-highlight/src/main/java/ru/noties/markwon/syntax/Prism4jThemeDefault.java index 4356e13c..b729ed76 100644 --- a/markwon-syntax-highlight/src/main/java/ru/noties/markwon/syntax/Prism4jThemeDefault.java +++ b/markwon-syntax-highlight/src/main/java/ru/noties/markwon/syntax/Prism4jThemeDefault.java @@ -7,8 +7,8 @@ import android.text.SpannableStringBuilder; import android.text.Spanned; import android.text.style.BackgroundColorSpan; -import ru.noties.markwon.spans.EmphasisSpan; -import ru.noties.markwon.spans.StrongEmphasisSpan; +import ru.noties.markwon.core.spans.EmphasisSpan; +import ru.noties.markwon.core.spans.StrongEmphasisSpan; public class Prism4jThemeDefault extends Prism4jThemeBase { diff --git a/markwon-syntax-highlight/src/main/java/ru/noties/markwon/syntax/SyntaxHighlightPlugin.java b/markwon-syntax-highlight/src/main/java/ru/noties/markwon/syntax/SyntaxHighlightPlugin.java index 08bf2a73..b07f5c6e 100644 --- a/markwon-syntax-highlight/src/main/java/ru/noties/markwon/syntax/SyntaxHighlightPlugin.java +++ b/markwon-syntax-highlight/src/main/java/ru/noties/markwon/syntax/SyntaxHighlightPlugin.java @@ -5,7 +5,7 @@ import android.support.annotation.Nullable; import ru.noties.markwon.AbstractMarkwonPlugin; import ru.noties.markwon.MarkwonConfiguration; -import ru.noties.markwon.spans.MarkwonTheme; +import ru.noties.markwon.core.MarkwonTheme; import ru.noties.prism4j.Prism4j; public class SyntaxHighlightPlugin extends AbstractMarkwonPlugin { diff --git a/markwon-view/src/debug/java/ru/noties/markwon/view/debug/DebugConfigurationProvider.java b/markwon-view/src/debug/java/ru/noties/markwon/view/debug/DebugConfigurationProvider.java index 50ad7fec..f88ba5ae 100644 --- a/markwon-view/src/debug/java/ru/noties/markwon/view/debug/DebugConfigurationProvider.java +++ b/markwon-view/src/debug/java/ru/noties/markwon/view/debug/DebugConfigurationProvider.java @@ -4,7 +4,7 @@ import android.content.Context; import android.support.annotation.NonNull; import ru.noties.markwon.MarkwonConfiguration; -import ru.noties.markwon.spans.MarkwonTheme; +import ru.noties.markwon.core.MarkwonTheme; import ru.noties.markwon.view.IMarkwonView; public class DebugConfigurationProvider implements IMarkwonView.ConfigurationProvider { diff --git a/markwon-view/src/main/java/ru/noties/markwon/view/MarkwonViewHelper.java b/markwon-view/src/main/java/ru/noties/markwon/view/MarkwonViewHelper.java index 95518023..f3669b77 100644 --- a/markwon-view/src/main/java/ru/noties/markwon/view/MarkwonViewHelper.java +++ b/markwon-view/src/main/java/ru/noties/markwon/view/MarkwonViewHelper.java @@ -60,7 +60,7 @@ public class MarkwonViewHelper implements IMarkwonView { this.provider = provider; this.configuration = provider.provide(textView.getContext()); if (!TextUtils.isEmpty(markdown)) { - // invalidate rendered toMarkdown + // invalidate rendered markdown setMarkdown(markdown); } } diff --git a/markwon/src/main/java/ru/noties/markwon/AbstractMarkwonPlugin.java b/markwon/src/main/java/ru/noties/markwon/AbstractMarkwonPlugin.java index 5cf614be..a873add3 100644 --- a/markwon/src/main/java/ru/noties/markwon/AbstractMarkwonPlugin.java +++ b/markwon/src/main/java/ru/noties/markwon/AbstractMarkwonPlugin.java @@ -7,7 +7,7 @@ import org.commonmark.node.Node; import org.commonmark.parser.Parser; import ru.noties.markwon.image.AsyncDrawableLoader; -import ru.noties.markwon.spans.MarkwonTheme; +import ru.noties.markwon.core.MarkwonTheme; public abstract class AbstractMarkwonPlugin implements MarkwonPlugin { @Override diff --git a/markwon/src/main/java/ru/noties/markwon/LinkResolverDef.java b/markwon/src/main/java/ru/noties/markwon/LinkResolverDef.java index 109af717..4f893761 100644 --- a/markwon/src/main/java/ru/noties/markwon/LinkResolverDef.java +++ b/markwon/src/main/java/ru/noties/markwon/LinkResolverDef.java @@ -9,7 +9,7 @@ import android.support.annotation.NonNull; import android.util.Log; import android.view.View; -import ru.noties.markwon.spans.LinkSpan; +import ru.noties.markwon.core.spans.LinkSpan; public class LinkResolverDef implements LinkSpan.Resolver { @Override diff --git a/markwon/src/main/java/ru/noties/markwon/MarkwonBuilderImpl.java b/markwon/src/main/java/ru/noties/markwon/MarkwonBuilderImpl.java index 5c65c6a9..0e830e00 100644 --- a/markwon/src/main/java/ru/noties/markwon/MarkwonBuilderImpl.java +++ b/markwon/src/main/java/ru/noties/markwon/MarkwonBuilderImpl.java @@ -11,7 +11,7 @@ import java.util.Collections; import java.util.List; import ru.noties.markwon.image.AsyncDrawableLoader; -import ru.noties.markwon.spans.MarkwonTheme; +import ru.noties.markwon.core.MarkwonTheme; class MarkwonBuilderImpl implements Markwon.Builder { diff --git a/markwon/src/main/java/ru/noties/markwon/MarkwonConfiguration.java b/markwon/src/main/java/ru/noties/markwon/MarkwonConfiguration.java index 347e2e95..97f77a69 100644 --- a/markwon/src/main/java/ru/noties/markwon/MarkwonConfiguration.java +++ b/markwon/src/main/java/ru/noties/markwon/MarkwonConfiguration.java @@ -3,11 +3,15 @@ package ru.noties.markwon; import android.content.Context; import android.support.annotation.NonNull; +import ru.noties.markwon.core.MarkwonTheme; +import ru.noties.markwon.core.spans.LinkSpan; import ru.noties.markwon.image.AsyncDrawableLoader; import ru.noties.markwon.image.ImageSizeResolver; import ru.noties.markwon.image.ImageSizeResolverDef; -import ru.noties.markwon.spans.LinkSpan; -import ru.noties.markwon.spans.MarkwonTheme; +import ru.noties.markwon.syntax.SyntaxHighlight; +import ru.noties.markwon.syntax.SyntaxHighlightNoOp; +import ru.noties.markwon.urlprocessor.UrlProcessor; +import ru.noties.markwon.urlprocessor.UrlProcessorNoOp; /** * since 3.0.0 renamed `SpannableConfiguration` -> `MarkwonConfiguration` @@ -27,7 +31,6 @@ public class MarkwonConfiguration { return new Builder(context); } - private final MarkwonTheme theme; private final AsyncDrawableLoader asyncDrawableLoader; private final SyntaxHighlight syntaxHighlight; @@ -46,14 +49,6 @@ public class MarkwonConfiguration { this.factory = builder.factory; } - /** - * Returns a new builder based on this configuration - */ - @NonNull - public Builder newBuilder(@NonNull Context context) { - return new Builder(context, this); - } - @NonNull public MarkwonTheme theme() { return theme; @@ -106,17 +101,6 @@ public class MarkwonConfiguration { this.context = context; } - Builder(@NonNull Context context, @NonNull MarkwonConfiguration configuration) { - this(context); - this.theme = configuration.theme; - this.asyncDrawableLoader = configuration.asyncDrawableLoader; - this.syntaxHighlight = configuration.syntaxHighlight; - this.linkResolver = configuration.linkResolver; - this.urlProcessor = configuration.urlProcessor; - this.imageSizeResolver = configuration.imageSizeResolver; - this.factory = configuration.factory; - } - @NonNull public Builder syntaxHighlight(@NonNull SyntaxHighlight syntaxHighlight) { this.syntaxHighlight = syntaxHighlight; diff --git a/markwon/src/main/java/ru/noties/markwon/MarkwonPlugin.java b/markwon/src/main/java/ru/noties/markwon/MarkwonPlugin.java index 4a1c33b9..0cd959f0 100644 --- a/markwon/src/main/java/ru/noties/markwon/MarkwonPlugin.java +++ b/markwon/src/main/java/ru/noties/markwon/MarkwonPlugin.java @@ -7,7 +7,7 @@ import org.commonmark.node.Node; import org.commonmark.parser.Parser; import ru.noties.markwon.image.AsyncDrawableLoader; -import ru.noties.markwon.spans.MarkwonTheme; +import ru.noties.markwon.core.MarkwonTheme; public interface MarkwonPlugin { diff --git a/markwon/src/main/java/ru/noties/markwon/MarkwonVisitor.java b/markwon/src/main/java/ru/noties/markwon/MarkwonVisitor.java index a124bcb3..becbbbb4 100644 --- a/markwon/src/main/java/ru/noties/markwon/MarkwonVisitor.java +++ b/markwon/src/main/java/ru/noties/markwon/MarkwonVisitor.java @@ -6,7 +6,7 @@ import android.support.annotation.Nullable; import org.commonmark.node.Node; import org.commonmark.node.Visitor; -import ru.noties.markwon.spans.MarkwonTheme; +import ru.noties.markwon.core.MarkwonTheme; public interface MarkwonVisitor extends Visitor { diff --git a/markwon/src/main/java/ru/noties/markwon/MarkwonVisitorImpl.java b/markwon/src/main/java/ru/noties/markwon/MarkwonVisitorImpl.java index 1262d643..60e8af80 100644 --- a/markwon/src/main/java/ru/noties/markwon/MarkwonVisitorImpl.java +++ b/markwon/src/main/java/ru/noties/markwon/MarkwonVisitorImpl.java @@ -31,7 +31,7 @@ import java.util.Collections; import java.util.HashMap; import java.util.Map; -import ru.noties.markwon.spans.MarkwonTheme; +import ru.noties.markwon.core.MarkwonTheme; class MarkwonVisitorImpl implements MarkwonVisitor { @@ -43,7 +43,7 @@ class MarkwonVisitorImpl implements MarkwonVisitor { private final SpannableBuilder builder = new SpannableBuilder(); - private MarkwonVisitorImpl( + MarkwonVisitorImpl( @NonNull MarkwonConfiguration configuration, @NonNull Map, NodeVisitor> nodes) { this.configuration = configuration; diff --git a/markwon/src/main/java/ru/noties/markwon/SpannableFactory.java b/markwon/src/main/java/ru/noties/markwon/SpannableFactory.java index abca9c86..c1b06a57 100644 --- a/markwon/src/main/java/ru/noties/markwon/SpannableFactory.java +++ b/markwon/src/main/java/ru/noties/markwon/SpannableFactory.java @@ -6,8 +6,8 @@ import android.support.annotation.Nullable; import ru.noties.markwon.image.AsyncDrawableLoader; import ru.noties.markwon.image.ImageSize; import ru.noties.markwon.image.ImageSizeResolver; -import ru.noties.markwon.spans.LinkSpan; -import ru.noties.markwon.spans.MarkwonTheme; +import ru.noties.markwon.core.spans.LinkSpan; +import ru.noties.markwon.core.MarkwonTheme; /** * Each method can return null or a Span object or an array of spans diff --git a/markwon/src/main/java/ru/noties/markwon/SpannableFactoryDef.java b/markwon/src/main/java/ru/noties/markwon/SpannableFactoryDef.java index 999a42da..ca3b02cf 100644 --- a/markwon/src/main/java/ru/noties/markwon/SpannableFactoryDef.java +++ b/markwon/src/main/java/ru/noties/markwon/SpannableFactoryDef.java @@ -7,17 +7,17 @@ import ru.noties.markwon.image.AsyncDrawable; import ru.noties.markwon.image.AsyncDrawableLoader; import ru.noties.markwon.image.ImageSize; import ru.noties.markwon.image.ImageSizeResolver; -import ru.noties.markwon.spans.AsyncDrawableSpan; -import ru.noties.markwon.spans.BlockQuoteSpan; -import ru.noties.markwon.spans.BulletListItemSpan; -import ru.noties.markwon.spans.CodeSpan; -import ru.noties.markwon.spans.EmphasisSpan; -import ru.noties.markwon.spans.HeadingSpan; -import ru.noties.markwon.spans.LinkSpan; -import ru.noties.markwon.spans.MarkwonTheme; -import ru.noties.markwon.spans.OrderedListItemSpan; -import ru.noties.markwon.spans.StrongEmphasisSpan; -import ru.noties.markwon.spans.ThematicBreakSpan; +import ru.noties.markwon.core.spans.AsyncDrawableSpan; +import ru.noties.markwon.core.spans.BlockQuoteSpan; +import ru.noties.markwon.core.spans.BulletListItemSpan; +import ru.noties.markwon.core.spans.CodeSpan; +import ru.noties.markwon.core.spans.EmphasisSpan; +import ru.noties.markwon.core.spans.HeadingSpan; +import ru.noties.markwon.core.spans.LinkSpan; +import ru.noties.markwon.core.MarkwonTheme; +import ru.noties.markwon.core.spans.OrderedListItemSpan; +import ru.noties.markwon.core.spans.StrongEmphasisSpan; +import ru.noties.markwon.core.spans.ThematicBreakSpan; /** * @since 1.1.0 diff --git a/markwon/src/main/java/ru/noties/markwon/core/CorePlugin.java b/markwon/src/main/java/ru/noties/markwon/core/CorePlugin.java index 37a92f7c..c6e6c1bb 100644 --- a/markwon/src/main/java/ru/noties/markwon/core/CorePlugin.java +++ b/markwon/src/main/java/ru/noties/markwon/core/CorePlugin.java @@ -36,7 +36,7 @@ import ru.noties.markwon.core.visitor.SoftLineBreakNodeVisitor; import ru.noties.markwon.core.visitor.StrongEmphasisNodeVisitor; import ru.noties.markwon.core.visitor.TextNodeVisitor; import ru.noties.markwon.core.visitor.ThematicBreakNodeVisitor; -import ru.noties.markwon.spans.OrderedListItemSpan; +import ru.noties.markwon.core.spans.OrderedListItemSpan; public class CorePlugin extends AbstractMarkwonPlugin { diff --git a/markwon/src/main/java/ru/noties/markwon/spans/MarkwonTheme.java b/markwon/src/main/java/ru/noties/markwon/core/MarkwonTheme.java similarity index 99% rename from markwon/src/main/java/ru/noties/markwon/spans/MarkwonTheme.java rename to markwon/src/main/java/ru/noties/markwon/core/MarkwonTheme.java index 8f803841..681c6048 100644 --- a/markwon/src/main/java/ru/noties/markwon/spans/MarkwonTheme.java +++ b/markwon/src/main/java/ru/noties/markwon/core/MarkwonTheme.java @@ -1,9 +1,8 @@ -package ru.noties.markwon.spans; +package ru.noties.markwon.core; import android.content.Context; import android.graphics.Paint; import android.graphics.Typeface; -import android.graphics.drawable.Drawable; import android.support.annotation.ColorInt; import android.support.annotation.FloatRange; import android.support.annotation.IntRange; diff --git a/markwon/src/main/java/ru/noties/markwon/spans/AsyncDrawableSpan.java b/markwon/src/main/java/ru/noties/markwon/core/spans/AsyncDrawableSpan.java similarity index 98% rename from markwon/src/main/java/ru/noties/markwon/spans/AsyncDrawableSpan.java rename to markwon/src/main/java/ru/noties/markwon/core/spans/AsyncDrawableSpan.java index 20b5fe9d..b06b2348 100644 --- a/markwon/src/main/java/ru/noties/markwon/spans/AsyncDrawableSpan.java +++ b/markwon/src/main/java/ru/noties/markwon/core/spans/AsyncDrawableSpan.java @@ -1,4 +1,4 @@ -package ru.noties.markwon.spans; +package ru.noties.markwon.core.spans; import android.graphics.Canvas; import android.graphics.Paint; @@ -12,6 +12,7 @@ import android.text.style.ReplacementSpan; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; +import ru.noties.markwon.core.MarkwonTheme; import ru.noties.markwon.image.AsyncDrawable; @SuppressWarnings("WeakerAccess") diff --git a/markwon/src/main/java/ru/noties/markwon/spans/BlockQuoteSpan.java b/markwon/src/main/java/ru/noties/markwon/core/spans/BlockQuoteSpan.java similarity index 94% rename from markwon/src/main/java/ru/noties/markwon/spans/BlockQuoteSpan.java rename to markwon/src/main/java/ru/noties/markwon/core/spans/BlockQuoteSpan.java index 0101199f..ea4e353c 100644 --- a/markwon/src/main/java/ru/noties/markwon/spans/BlockQuoteSpan.java +++ b/markwon/src/main/java/ru/noties/markwon/core/spans/BlockQuoteSpan.java @@ -1,4 +1,4 @@ -package ru.noties.markwon.spans; +package ru.noties.markwon.core.spans; import android.graphics.Canvas; import android.graphics.Paint; @@ -7,6 +7,8 @@ import android.support.annotation.NonNull; import android.text.Layout; import android.text.style.LeadingMarginSpan; +import ru.noties.markwon.core.MarkwonTheme; + public class BlockQuoteSpan implements LeadingMarginSpan { private final MarkwonTheme theme; diff --git a/markwon/src/main/java/ru/noties/markwon/spans/BulletListItemSpan.java b/markwon/src/main/java/ru/noties/markwon/core/spans/BulletListItemSpan.java similarity index 97% rename from markwon/src/main/java/ru/noties/markwon/spans/BulletListItemSpan.java rename to markwon/src/main/java/ru/noties/markwon/core/spans/BulletListItemSpan.java index ee9aefc7..97f3bb32 100644 --- a/markwon/src/main/java/ru/noties/markwon/spans/BulletListItemSpan.java +++ b/markwon/src/main/java/ru/noties/markwon/core/spans/BulletListItemSpan.java @@ -1,4 +1,4 @@ -package ru.noties.markwon.spans; +package ru.noties.markwon.core.spans; import android.graphics.Canvas; import android.graphics.Paint; @@ -9,6 +9,7 @@ import android.support.annotation.NonNull; import android.text.Layout; import android.text.style.LeadingMarginSpan; +import ru.noties.markwon.core.MarkwonTheme; import ru.noties.markwon.utils.LeadingMarginUtils; public class BulletListItemSpan implements LeadingMarginSpan { diff --git a/markwon/src/main/java/ru/noties/markwon/spans/CodeSpan.java b/markwon/src/main/java/ru/noties/markwon/core/spans/CodeSpan.java similarity index 95% rename from markwon/src/main/java/ru/noties/markwon/spans/CodeSpan.java rename to markwon/src/main/java/ru/noties/markwon/core/spans/CodeSpan.java index 7488885d..dc233538 100644 --- a/markwon/src/main/java/ru/noties/markwon/spans/CodeSpan.java +++ b/markwon/src/main/java/ru/noties/markwon/core/spans/CodeSpan.java @@ -1,4 +1,4 @@ -package ru.noties.markwon.spans; +package ru.noties.markwon.core.spans; import android.graphics.Canvas; import android.graphics.Paint; @@ -9,6 +9,8 @@ import android.text.TextPaint; import android.text.style.LeadingMarginSpan; import android.text.style.MetricAffectingSpan; +import ru.noties.markwon.core.MarkwonTheme; + public class CodeSpan extends MetricAffectingSpan implements LeadingMarginSpan { private final MarkwonTheme theme; diff --git a/markwon/src/main/java/ru/noties/markwon/spans/EmphasisSpan.java b/markwon/src/main/java/ru/noties/markwon/core/spans/EmphasisSpan.java similarity index 90% rename from markwon/src/main/java/ru/noties/markwon/spans/EmphasisSpan.java rename to markwon/src/main/java/ru/noties/markwon/core/spans/EmphasisSpan.java index cec49af0..cb7f9ac6 100644 --- a/markwon/src/main/java/ru/noties/markwon/spans/EmphasisSpan.java +++ b/markwon/src/main/java/ru/noties/markwon/core/spans/EmphasisSpan.java @@ -1,4 +1,4 @@ -package ru.noties.markwon.spans; +package ru.noties.markwon.core.spans; import android.text.TextPaint; import android.text.style.MetricAffectingSpan; diff --git a/markwon/src/main/java/ru/noties/markwon/spans/HeadingSpan.java b/markwon/src/main/java/ru/noties/markwon/core/spans/HeadingSpan.java similarity index 96% rename from markwon/src/main/java/ru/noties/markwon/spans/HeadingSpan.java rename to markwon/src/main/java/ru/noties/markwon/core/spans/HeadingSpan.java index c7d8d595..a942cc64 100644 --- a/markwon/src/main/java/ru/noties/markwon/spans/HeadingSpan.java +++ b/markwon/src/main/java/ru/noties/markwon/core/spans/HeadingSpan.java @@ -1,4 +1,4 @@ -package ru.noties.markwon.spans; +package ru.noties.markwon.core.spans; import android.graphics.Canvas; import android.graphics.Paint; @@ -10,6 +10,7 @@ import android.text.TextPaint; import android.text.style.LeadingMarginSpan; import android.text.style.MetricAffectingSpan; +import ru.noties.markwon.core.MarkwonTheme; import ru.noties.markwon.utils.LeadingMarginUtils; public class HeadingSpan extends MetricAffectingSpan implements LeadingMarginSpan { diff --git a/markwon/src/main/java/ru/noties/markwon/spans/LinkSpan.java b/markwon/src/main/java/ru/noties/markwon/core/spans/LinkSpan.java similarity index 90% rename from markwon/src/main/java/ru/noties/markwon/spans/LinkSpan.java rename to markwon/src/main/java/ru/noties/markwon/core/spans/LinkSpan.java index f879c5ec..e8f7d8f7 100644 --- a/markwon/src/main/java/ru/noties/markwon/spans/LinkSpan.java +++ b/markwon/src/main/java/ru/noties/markwon/core/spans/LinkSpan.java @@ -1,10 +1,12 @@ -package ru.noties.markwon.spans; +package ru.noties.markwon.core.spans; import android.support.annotation.NonNull; import android.text.TextPaint; import android.text.style.URLSpan; import android.view.View; +import ru.noties.markwon.core.MarkwonTheme; + public class LinkSpan extends URLSpan { public interface Resolver { diff --git a/markwon/src/main/java/ru/noties/markwon/spans/ObjectsPool.java b/markwon/src/main/java/ru/noties/markwon/core/spans/ObjectsPool.java similarity index 95% rename from markwon/src/main/java/ru/noties/markwon/spans/ObjectsPool.java rename to markwon/src/main/java/ru/noties/markwon/core/spans/ObjectsPool.java index bc56a9d4..de6f0671 100644 --- a/markwon/src/main/java/ru/noties/markwon/spans/ObjectsPool.java +++ b/markwon/src/main/java/ru/noties/markwon/core/spans/ObjectsPool.java @@ -1,4 +1,4 @@ -package ru.noties.markwon.spans; +package ru.noties.markwon.core.spans; import android.graphics.Paint; import android.graphics.Rect; diff --git a/markwon/src/main/java/ru/noties/markwon/spans/OrderedListItemSpan.java b/markwon/src/main/java/ru/noties/markwon/core/spans/OrderedListItemSpan.java similarity index 94% rename from markwon/src/main/java/ru/noties/markwon/spans/OrderedListItemSpan.java rename to markwon/src/main/java/ru/noties/markwon/core/spans/OrderedListItemSpan.java index f7bacfeb..6be46fd5 100644 --- a/markwon/src/main/java/ru/noties/markwon/spans/OrderedListItemSpan.java +++ b/markwon/src/main/java/ru/noties/markwon/core/spans/OrderedListItemSpan.java @@ -1,4 +1,4 @@ -package ru.noties.markwon.spans; +package ru.noties.markwon.core.spans; import android.graphics.Canvas; import android.graphics.Paint; @@ -9,6 +9,7 @@ import android.text.TextPaint; import android.text.style.LeadingMarginSpan; import android.widget.TextView; +import ru.noties.markwon.core.MarkwonTheme; import ru.noties.markwon.utils.LeadingMarginUtils; public class OrderedListItemSpan implements LeadingMarginSpan { @@ -20,8 +21,8 @@ public class OrderedListItemSpan implements LeadingMarginSpan { * NB, this method must be called before setting text to a TextView (`TextView#setText` * internally can trigger new Layout creation which will ask for leading margins right away) * - * @param textView to which toMarkdown will be applied - * @param text parsed toMarkdown to process + * @param textView to which markdown will be applied + * @param text parsed markdown to process * @since 2.0.1 */ public static void measure(@NonNull TextView textView, @NonNull CharSequence text) { diff --git a/markwon/src/main/java/ru/noties/markwon/spans/StrongEmphasisSpan.java b/markwon/src/main/java/ru/noties/markwon/core/spans/StrongEmphasisSpan.java similarity index 90% rename from markwon/src/main/java/ru/noties/markwon/spans/StrongEmphasisSpan.java rename to markwon/src/main/java/ru/noties/markwon/core/spans/StrongEmphasisSpan.java index 32b5e51b..d74ee63e 100644 --- a/markwon/src/main/java/ru/noties/markwon/spans/StrongEmphasisSpan.java +++ b/markwon/src/main/java/ru/noties/markwon/core/spans/StrongEmphasisSpan.java @@ -1,4 +1,4 @@ -package ru.noties.markwon.spans; +package ru.noties.markwon.core.spans; import android.text.TextPaint; import android.text.style.MetricAffectingSpan; diff --git a/markwon/src/main/java/ru/noties/markwon/spans/ThematicBreakSpan.java b/markwon/src/main/java/ru/noties/markwon/core/spans/ThematicBreakSpan.java similarity index 94% rename from markwon/src/main/java/ru/noties/markwon/spans/ThematicBreakSpan.java rename to markwon/src/main/java/ru/noties/markwon/core/spans/ThematicBreakSpan.java index a19f7528..0e06537b 100644 --- a/markwon/src/main/java/ru/noties/markwon/spans/ThematicBreakSpan.java +++ b/markwon/src/main/java/ru/noties/markwon/core/spans/ThematicBreakSpan.java @@ -1,4 +1,4 @@ -package ru.noties.markwon.spans; +package ru.noties.markwon.core.spans; import android.graphics.Canvas; import android.graphics.Paint; @@ -7,6 +7,8 @@ import android.support.annotation.NonNull; import android.text.Layout; import android.text.style.LeadingMarginSpan; +import ru.noties.markwon.core.MarkwonTheme; + public class ThematicBreakSpan implements LeadingMarginSpan { private final MarkwonTheme theme; diff --git a/markwon/src/main/java/ru/noties/markwon/image/AsyncDrawableScheduler.java b/markwon/src/main/java/ru/noties/markwon/image/AsyncDrawableScheduler.java index 0de99c61..27348a76 100644 --- a/markwon/src/main/java/ru/noties/markwon/image/AsyncDrawableScheduler.java +++ b/markwon/src/main/java/ru/noties/markwon/image/AsyncDrawableScheduler.java @@ -15,7 +15,7 @@ import java.util.Collections; import java.util.List; import ru.noties.markwon.renderer.R; -import ru.noties.markwon.spans.AsyncDrawableSpan; +import ru.noties.markwon.core.spans.AsyncDrawableSpan; public abstract class AsyncDrawableScheduler { diff --git a/markwon/src/main/java/ru/noties/markwon/SyntaxHighlight.java b/markwon/src/main/java/ru/noties/markwon/syntax/SyntaxHighlight.java similarity index 87% rename from markwon/src/main/java/ru/noties/markwon/SyntaxHighlight.java rename to markwon/src/main/java/ru/noties/markwon/syntax/SyntaxHighlight.java index 2f3b630f..cf6921ee 100644 --- a/markwon/src/main/java/ru/noties/markwon/SyntaxHighlight.java +++ b/markwon/src/main/java/ru/noties/markwon/syntax/SyntaxHighlight.java @@ -1,4 +1,4 @@ -package ru.noties.markwon; +package ru.noties.markwon.syntax; import android.support.annotation.NonNull; import android.support.annotation.Nullable; diff --git a/markwon/src/main/java/ru/noties/markwon/SyntaxHighlightNoOp.java b/markwon/src/main/java/ru/noties/markwon/syntax/SyntaxHighlightNoOp.java similarity index 70% rename from markwon/src/main/java/ru/noties/markwon/SyntaxHighlightNoOp.java rename to markwon/src/main/java/ru/noties/markwon/syntax/SyntaxHighlightNoOp.java index 84724513..48a80ee2 100644 --- a/markwon/src/main/java/ru/noties/markwon/SyntaxHighlightNoOp.java +++ b/markwon/src/main/java/ru/noties/markwon/syntax/SyntaxHighlightNoOp.java @@ -1,9 +1,9 @@ -package ru.noties.markwon; +package ru.noties.markwon.syntax; import android.support.annotation.NonNull; import android.support.annotation.Nullable; -class SyntaxHighlightNoOp implements SyntaxHighlight { +public class SyntaxHighlightNoOp implements SyntaxHighlight { @NonNull @Override public CharSequence highlight(@Nullable String info, @NonNull String code) { diff --git a/markwon/src/main/java/ru/noties/markwon/UrlProcessor.java b/markwon/src/main/java/ru/noties/markwon/urlprocessor/UrlProcessor.java similarity index 77% rename from markwon/src/main/java/ru/noties/markwon/UrlProcessor.java rename to markwon/src/main/java/ru/noties/markwon/urlprocessor/UrlProcessor.java index b190d110..9ea7919e 100644 --- a/markwon/src/main/java/ru/noties/markwon/UrlProcessor.java +++ b/markwon/src/main/java/ru/noties/markwon/urlprocessor/UrlProcessor.java @@ -1,4 +1,4 @@ -package ru.noties.markwon; +package ru.noties.markwon.urlprocessor; import android.support.annotation.NonNull; diff --git a/markwon/src/main/java/ru/noties/markwon/UrlProcessorAndroidAssets.java b/markwon/src/main/java/ru/noties/markwon/urlprocessor/UrlProcessorAndroidAssets.java similarity index 96% rename from markwon/src/main/java/ru/noties/markwon/UrlProcessorAndroidAssets.java rename to markwon/src/main/java/ru/noties/markwon/urlprocessor/UrlProcessorAndroidAssets.java index 2a1544a9..aa7730de 100644 --- a/markwon/src/main/java/ru/noties/markwon/UrlProcessorAndroidAssets.java +++ b/markwon/src/main/java/ru/noties/markwon/urlprocessor/UrlProcessorAndroidAssets.java @@ -1,4 +1,4 @@ -package ru.noties.markwon; +package ru.noties.markwon.urlprocessor; import android.net.Uri; import android.support.annotation.NonNull; diff --git a/markwon/src/main/java/ru/noties/markwon/UrlProcessorNoOp.java b/markwon/src/main/java/ru/noties/markwon/urlprocessor/UrlProcessorNoOp.java similarity index 84% rename from markwon/src/main/java/ru/noties/markwon/UrlProcessorNoOp.java rename to markwon/src/main/java/ru/noties/markwon/urlprocessor/UrlProcessorNoOp.java index d23af279..9d8560d6 100644 --- a/markwon/src/main/java/ru/noties/markwon/UrlProcessorNoOp.java +++ b/markwon/src/main/java/ru/noties/markwon/urlprocessor/UrlProcessorNoOp.java @@ -1,4 +1,4 @@ -package ru.noties.markwon; +package ru.noties.markwon.urlprocessor; import android.support.annotation.NonNull; diff --git a/markwon/src/main/java/ru/noties/markwon/UrlProcessorRelativeToAbsolute.java b/markwon/src/main/java/ru/noties/markwon/urlprocessor/UrlProcessorRelativeToAbsolute.java similarity index 96% rename from markwon/src/main/java/ru/noties/markwon/UrlProcessorRelativeToAbsolute.java rename to markwon/src/main/java/ru/noties/markwon/urlprocessor/UrlProcessorRelativeToAbsolute.java index fcfd4cab..d99aaf4f 100644 --- a/markwon/src/main/java/ru/noties/markwon/UrlProcessorRelativeToAbsolute.java +++ b/markwon/src/main/java/ru/noties/markwon/urlprocessor/UrlProcessorRelativeToAbsolute.java @@ -1,4 +1,4 @@ -package ru.noties.markwon; +package ru.noties.markwon.urlprocessor; import android.support.annotation.NonNull; import android.support.annotation.Nullable; diff --git a/markwon/src/main/java/ru/noties/markwon/utils/Dip.java b/markwon/src/main/java/ru/noties/markwon/utils/Dip.java index 6899df8f..51d098e0 100644 --- a/markwon/src/main/java/ru/noties/markwon/utils/Dip.java +++ b/markwon/src/main/java/ru/noties/markwon/utils/Dip.java @@ -3,8 +3,6 @@ package ru.noties.markwon.utils; import android.content.Context; import android.support.annotation.NonNull; -import ru.noties.markwon.spans.MarkwonTheme; - public class Dip { @NonNull diff --git a/markwon/src/test/java/ru/noties/markwon/AbstractMarkwonVisitorImpl.java b/markwon/src/test/java/ru/noties/markwon/AbstractMarkwonVisitorImpl.java new file mode 100644 index 00000000..99d183ce --- /dev/null +++ b/markwon/src/test/java/ru/noties/markwon/AbstractMarkwonVisitorImpl.java @@ -0,0 +1,16 @@ +package ru.noties.markwon; + +import android.support.annotation.NonNull; + +import org.commonmark.node.Node; + +import java.util.Map; + +public class AbstractMarkwonVisitorImpl extends MarkwonVisitorImpl { + + public AbstractMarkwonVisitorImpl( + @NonNull MarkwonConfiguration configuration, + @NonNull Map, NodeVisitor> nodes) { + super(configuration, nodes); + } +} diff --git a/markwon/src/test/java/ru/noties/markwon/spans/AsyncDrawableTest.java b/markwon/src/test/java/ru/noties/markwon/image/AsyncDrawableTest.java similarity index 90% rename from markwon/src/test/java/ru/noties/markwon/spans/AsyncDrawableTest.java rename to markwon/src/test/java/ru/noties/markwon/image/AsyncDrawableTest.java index 873cc404..7dad4514 100644 --- a/markwon/src/test/java/ru/noties/markwon/spans/AsyncDrawableTest.java +++ b/markwon/src/test/java/ru/noties/markwon/image/AsyncDrawableTest.java @@ -1,4 +1,4 @@ -package ru.noties.markwon.spans; +package ru.noties.markwon.image; import android.graphics.Canvas; import android.graphics.ColorFilter; @@ -13,10 +13,6 @@ import org.junit.runner.RunWith; import org.robolectric.RobolectricTestRunner; import org.robolectric.annotation.Config; -import ru.noties.markwon.renderer.ImageSize; -import ru.noties.markwon.renderer.ImageSizeResolver; -import ru.noties.markwon.renderer.ImageSizeResolverDef; - import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertNotNull; @@ -40,7 +36,7 @@ public class AsyncDrawableTest { // when drawable have no known dimensions yet, it will await for them final AsyncDrawable drawable = new AsyncDrawable("", - mock(AsyncDrawable.Loader.class), + mock(AsyncDrawableLoader.class), imageSizeResolver, new ImageSize(new ImageSize.Dimension(100.F, "%"), null)); @@ -65,7 +61,7 @@ public class AsyncDrawableTest { // when result is present it will be detached (setCallback(null)) final AsyncDrawable drawable = new AsyncDrawable("", - mock(AsyncDrawable.Loader.class), + mock(AsyncDrawableLoader.class), imageSizeResolver, null); diff --git a/markwon/src/test/java/ru/noties/markwon/renderer/ImageSizeResolverDefTest.java b/markwon/src/test/java/ru/noties/markwon/image/ImageSizeResolverDefTest.java similarity index 99% rename from markwon/src/test/java/ru/noties/markwon/renderer/ImageSizeResolverDefTest.java rename to markwon/src/test/java/ru/noties/markwon/image/ImageSizeResolverDefTest.java index b5121d39..f8649603 100644 --- a/markwon/src/test/java/ru/noties/markwon/renderer/ImageSizeResolverDefTest.java +++ b/markwon/src/test/java/ru/noties/markwon/image/ImageSizeResolverDefTest.java @@ -1,4 +1,4 @@ -package ru.noties.markwon.renderer; +package ru.noties.markwon.image; import android.graphics.Rect; diff --git a/markwon/src/test/java/ru/noties/markwon/renderer/MarkwonConfigurationTest.java b/markwon/src/test/java/ru/noties/markwon/renderer/MarkwonConfigurationTest.java deleted file mode 100644 index 71cd0145..00000000 --- a/markwon/src/test/java/ru/noties/markwon/renderer/MarkwonConfigurationTest.java +++ /dev/null @@ -1,53 +0,0 @@ -package ru.noties.markwon.renderer; - -import org.junit.Test; - -import ru.noties.markwon.MarkwonConfiguration; -import ru.noties.markwon.SpannableFactory; -import ru.noties.markwon.SyntaxHighlight; -import ru.noties.markwon.UrlProcessor; -import ru.noties.markwon.html.api.MarkwonHtmlParser; -import ru.noties.markwon.html.MarkwonHtmlRenderer; -import ru.noties.markwon.image.AsyncDrawable; -import ru.noties.markwon.image.ImageSizeResolver; -import ru.noties.markwon.spans.LinkSpan; -import ru.noties.markwon.spans.MarkwonTheme; - -import static org.junit.Assert.assertEquals; -import static org.mockito.Mockito.mock; -public class MarkwonConfigurationTest { - - @Test - public void testNewBuilder() { - final MarkwonConfiguration configuration = MarkwonConfiguration - .builder(null) - .theme(mock(MarkwonTheme.class)) - .asyncDrawableLoader(mock(AsyncDrawable.Loader.class)) - .syntaxHighlight(mock(SyntaxHighlight.class)) - .linkResolver(mock(LinkSpan.Resolver.class)) - .urlProcessor(mock(UrlProcessor.class)) - .imageSizeResolver(mock(ImageSizeResolver.class)) - .factory(mock(SpannableFactory.class)) - .softBreakAddsNewLine(true) - .htmlParser(mock(MarkwonHtmlParser.class)) - .htmlRenderer(mock(MarkwonHtmlRenderer.class)) - .htmlAllowNonClosedTags(true) - .build(); - - final MarkwonConfiguration newConfiguration = configuration - .newBuilder(null) - .build(); - - assertEquals(configuration.theme(), newConfiguration.theme()); - assertEquals(configuration.asyncDrawableLoader(), newConfiguration.asyncDrawableLoader()); - assertEquals(configuration.syntaxHighlight(), newConfiguration.syntaxHighlight()); - assertEquals(configuration.linkResolver(), newConfiguration.linkResolver()); - assertEquals(configuration.urlProcessor(), newConfiguration.urlProcessor()); - assertEquals(configuration.imageSizeResolver(), newConfiguration.imageSizeResolver()); - assertEquals(configuration.factory(), newConfiguration.factory()); - assertEquals(configuration.softBreakAddsNewLine(), newConfiguration.softBreakAddsNewLine()); - assertEquals(configuration.htmlParser(), newConfiguration.htmlParser()); - assertEquals(configuration.htmlRenderer(), newConfiguration.htmlRenderer()); - assertEquals(configuration.htmlAllowNonClosedTags(), newConfiguration.htmlAllowNonClosedTags()); - } -} diff --git a/markwon/src/test/java/ru/noties/markwon/renderer/html2/CssInlineStyleParserTest.java b/markwon/src/test/java/ru/noties/markwon/renderer/html2/CssInlineStyleParserTest.java deleted file mode 100644 index 4ba3fffb..00000000 --- a/markwon/src/test/java/ru/noties/markwon/renderer/html2/CssInlineStyleParserTest.java +++ /dev/null @@ -1,239 +0,0 @@ -package ru.noties.markwon.renderer.html2; - -import android.support.annotation.NonNull; - -import org.junit.Before; -import org.junit.Test; -import org.junit.runner.RunWith; -import org.robolectric.RobolectricTestRunner; -import org.robolectric.annotation.Config; - -import java.util.HashMap; -import java.util.List; -import java.util.Map; - -import ix.Ix; -import ix.IxFunction; -import ru.noties.markwon.test.TestUtils; - -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertNotNull; -import static ru.noties.markwon.test.TestUtils.with; - -@RunWith(RobolectricTestRunner.class) -@Config(manifest = Config.NONE) -public class CssInlineStyleParserTest { - - private CssInlineStyleParser.Impl impl; - - @Before - public void before() { - impl = new CssInlineStyleParser.Impl(); - } - - @Test - public void simple_single_pair() { - - final String input = "key: value;"; - - final List list = listProperties(input); - - assertEquals(1, list.size()); - - with(list.get(0), new TestUtils.Action() { - @Override - public void apply(@NonNull CssProperty cssProperty) { - assertEquals("key", cssProperty.key()); - assertEquals("value", cssProperty.value()); - } - }); - } - - @Test - public void simple_two_pairs() { - - final String input = "key1: value1; key2: value2;"; - - final List list = listProperties(input); - - assertEquals(2, list.size()); - - with(list.get(0), new TestUtils.Action() { - @Override - public void apply(@NonNull CssProperty cssProperty) { - assertEquals("key1", cssProperty.key()); - assertEquals("value1", cssProperty.value()); - } - }); - - with(list.get(1), new TestUtils.Action() { - @Override - public void apply(@NonNull CssProperty cssProperty) { - assertEquals("key2", cssProperty.key()); - assertEquals("value2", cssProperty.value()); - } - }); - } - - @Test - public void one_pair_eof() { - - final String input = "key: value"; - final List list = listProperties(input); - assertEquals(1, list.size()); - - with(list.get(0), new TestUtils.Action() { - @Override - public void apply(@NonNull CssProperty cssProperty) { - assertEquals("key", cssProperty.key()); - assertEquals("value", cssProperty.value()); - } - }); - } - - @Test - public void one_pair_eof_whitespaces() { - - final String input = "key: value \n\n\t"; - final List list = listProperties(input); - assertEquals(1, list.size()); - - with(list.get(0), new TestUtils.Action() { - @Override - public void apply(@NonNull CssProperty cssProperty) { - assertEquals("key", cssProperty.key()); - assertEquals("value", cssProperty.value()); - } - }); - } - - @Test - public void white_spaces() { - - final String input = "\n\n\n\t \t key1 \n\n\n\t : \n\n\n\n \t value1 \n\n\n\n ; \n key2\n : \n value2 \n ; "; - final List list = listProperties(input); - assertEquals(2, list.size()); - - with(list.get(0), new TestUtils.Action() { - @Override - public void apply(@NonNull CssProperty cssProperty) { - assertEquals("key1", cssProperty.key()); - assertEquals("value1", cssProperty.value()); - } - }); - - with(list.get(1), new TestUtils.Action() { - @Override - public void apply(@NonNull CssProperty cssProperty) { - assertEquals("key2", cssProperty.key()); - assertEquals("value2", cssProperty.value()); - } - }); - } - - @Test - public void list_of_keys() { - - final String input = "key1 key2 key3 key4"; - final List list = listProperties(input); - - assertEquals(0, list.size()); - } - - @Test - public void list_of_keys_and_value() { - - final String input = "key1 key2 key3 key4: value4"; - final List list = listProperties(input); - assertEquals(1, list.size()); - - with(list.get(0), new TestUtils.Action() { - @Override - public void apply(@NonNull CssProperty cssProperty) { - assertEquals("key4", cssProperty.key()); - assertEquals("value4", cssProperty.value()); - } - }); - } - - @Test - public void list_of_keys_separated_by_semi_colon() { - - final String input = "key1;key2;key3;key4;"; - final List list = listProperties(input); - assertEquals(0, list.size()); - } - - @Test - public void key_value_with_invalid_between() { - - final String input = "key1: value1; key2 key3: value3;"; - final List list = listProperties(input); - - assertEquals(2, list.size()); - - with(list.get(0), new TestUtils.Action() { - @Override - public void apply(@NonNull CssProperty cssProperty) { - assertEquals("key1", cssProperty.key()); - assertEquals("value1", cssProperty.value()); - } - }); - - with(list.get(1), new TestUtils.Action() { - @Override - public void apply(@NonNull CssProperty cssProperty) { - assertEquals("key3", cssProperty.key()); - assertEquals("value3", cssProperty.value()); - } - }); - } - - @Test - public void css_functions() { - - final Map map = new HashMap() {{ - put("attr", "\" (\" attr(href) \")\""); - put("calc", "calc(100% - 100px)"); - put("cubic-bezier", "cubic-bezier(0.1, 0.7, 1.0, 0.1)"); - put("hsl", "hsl(120,100%,50%)"); - put("hsla", "hsla(120,100%,50%,0.3)"); - put("linear-gradient", "linear-gradient(red, yellow, blue)"); - put("radial-gradient", "radial-gradient(red, green, blue)"); - put("repeating-linear-gradient", "repeating-linear-gradient(red, yellow 10%, green 20%)"); - put("repeating-radial-gradient", "repeating-radial-gradient(red, yellow 10%, green 15%)"); - put("rgb", "rgb(255,0,0)"); - put("rgba", "rgba(255,0,0,0.3)"); - put("var", "var(--some-variable)"); - put("url", "url(\"url.gif\")"); - }}; - - final StringBuilder builder = new StringBuilder(); - for (Map.Entry entry: map.entrySet()) { - builder.append(entry.getKey()) - .append(':') - .append(entry.getValue()) - .append(';'); - } - - for (CssProperty cssProperty: impl.parse(builder.toString())) { - final String value = map.remove(cssProperty.key()); - assertNotNull(cssProperty.key(), value); - assertEquals(cssProperty.key(), value, cssProperty.value()); - } - - assertEquals(0, map.size()); - } - - @NonNull - private List listProperties(@NonNull String input) { - return Ix.from(impl.parse(input)) - .map(new IxFunction() { - @Override - public CssProperty apply(CssProperty cssProperty) { - return cssProperty.mutate(); - } - }) - .toList(); - } -} \ No newline at end of file diff --git a/markwon/src/test/java/ru/noties/markwon/renderer/html2/tag/ImageSizeParserImplTest.java b/markwon/src/test/java/ru/noties/markwon/renderer/html2/tag/ImageSizeParserImplTest.java deleted file mode 100644 index f7ccca03..00000000 --- a/markwon/src/test/java/ru/noties/markwon/renderer/html2/tag/ImageSizeParserImplTest.java +++ /dev/null @@ -1,186 +0,0 @@ -package ru.noties.markwon.renderer.html2.tag; - -import android.support.annotation.NonNull; -import android.support.annotation.Nullable; - -import org.junit.Before; -import org.junit.Test; -import org.junit.runner.RunWith; -import org.robolectric.RobolectricTestRunner; -import org.robolectric.annotation.Config; - -import java.util.Collections; -import java.util.HashMap; -import java.util.Map; - -import ru.noties.markwon.image.ImageSize; -import ru.noties.markwon.renderer.html2.CssInlineStyleParser; - -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertNotNull; -import static org.junit.Assert.assertNull; - -@RunWith(RobolectricTestRunner.class) -@Config(manifest = Config.NONE) -public class ImageSizeParserImplTest { - - private static final float DELTA = 1e-7F; - - private ImageSizeParserImpl impl; - - @Before - public void before() { - impl = new ImageSizeParserImpl(CssInlineStyleParser.create()); - } - - @Test - public void nothing() { - assertNull(impl.parse(Collections.emptyMap())); - } - - @Test - public void width_height_from_style() { - - final String style = "width: 123; height: 321"; - - assertImageSize( - new ImageSize(dimension(123, null), dimension(321, null)), - impl.parse(Collections.singletonMap("style", style)) - ); - } - - @Test - public void style_has_higher_priority_width() { - - // if property is found in styles, do not lookup raw attribute - final Map attributes = new HashMap() {{ - put("style", "width: 43"); - put("width", "991"); - }}; - - assertImageSize( - new ImageSize(dimension(43, null), null), - impl.parse(attributes) - ); - } - - @Test - public void style_has_higher_priority_height() { - - // if property is found in styles, do not lookup raw attribute - final Map attributes = new HashMap() {{ - put("style", "height: 177"); - put("height", "8"); - }}; - - assertImageSize( - new ImageSize(null, dimension(177, null)), - impl.parse(attributes) - ); - } - - @Test - public void width_style_height_attributes() { - - final Map attributes = new HashMap() {{ - put("style", "width: 99"); - put("height", "7"); - }}; - - assertImageSize( - new ImageSize(dimension(99, null), dimension(7, null)), - impl.parse(attributes) - ); - } - - @Test - public void height_style_width_attributes() { - - final Map attributes = new HashMap() {{ - put("style", "height: 15"); - put("width", "88"); - }}; - - assertImageSize( - new ImageSize(dimension(88, null), dimension(15, null)), - impl.parse(attributes) - ); - } - - @Test - public void non_empty_styles_width_height_attributes() { - - final Map attributes = new HashMap() {{ - put("style", "key1: value1; width0: 123; height0: 99"); - put("width", "40"); - put("height", "77"); - }}; - - assertImageSize( - new ImageSize(dimension(40, null), dimension(77, null)), - impl.parse(attributes) - ); - } - - @Test - public void dimension_units() { - - final Map map = new HashMap() {{ - put("100", dimension(100, null)); - put("100%", dimension(100, "%")); - put("1%", dimension(1, "%")); - put("0.2em", dimension(0.2F, "em")); - put("155px", dimension(155, "px")); - put("67blah", dimension(67, "blah")); - put("-1", dimension(-1, null)); - put("-0.01pt", dimension(-0.01F, "pt")); - }}; - - for (Map.Entry entry : map.entrySet()) { - assertDimension(entry.getKey(), entry.getValue(), impl.dimension(entry.getKey())); - } - } - - @Test - public void bad_dimension() { - - final String[] dimensions = { - "calc(5px + 10rem)", - "whataver6", - "165 165", - "!@#$%^&*(%" - }; - - for (String dimension: dimensions) { - assertNull(dimension, impl.dimension(dimension)); - } - } - - private static void assertImageSize(@Nullable ImageSize expected, @Nullable ImageSize actual) { - if (expected == null) { - assertNull(actual); - } else { - assertNotNull(actual); - assertDimension("width", expected.width, actual.width); - assertDimension("height", expected.height, actual.height); - } - } - - private static void assertDimension( - @NonNull String name, - @Nullable ImageSize.Dimension expected, - @Nullable ImageSize.Dimension actual) { - if (expected == null) { - assertNull(name, actual); - } else { - assertNotNull(name, actual); - assertEquals(name, expected.value, actual.value, DELTA); - assertEquals(name, expected.unit, actual.unit); - } - } - - @NonNull - private static ImageSize.Dimension dimension(float value, @Nullable String unit) { - return new ImageSize.Dimension(value, unit); - } -} \ No newline at end of file diff --git a/markwon/src/test/java/ru/noties/markwon/renderer/visitor/SpannableMarkdownVisitorTest.java b/markwon/src/test/java/ru/noties/markwon/renderer/visitor/SpannableMarkdownVisitorTest.java index 6d77b130..07890959 100644 --- a/markwon/src/test/java/ru/noties/markwon/renderer/visitor/SpannableMarkdownVisitorTest.java +++ b/markwon/src/test/java/ru/noties/markwon/renderer/visitor/SpannableMarkdownVisitorTest.java @@ -1,25 +1,23 @@ package ru.noties.markwon.renderer.visitor; +import android.content.Context; import android.support.annotation.NonNull; import android.text.SpannableStringBuilder; -import org.commonmark.node.Node; import org.junit.Test; import org.junit.runner.RunWith; import org.robolectric.ParameterizedRobolectricTestRunner; +import org.robolectric.RuntimeEnvironment; import org.robolectric.annotation.Config; import java.util.Arrays; import java.util.Collection; -import ru.noties.markwon.LinkResolverDef; +import ru.noties.markwon.AbstractMarkwonPlugin; import ru.noties.markwon.Markwon; import ru.noties.markwon.MarkwonConfiguration; -import ru.noties.markwon.SpannableBuilder; -import ru.noties.markwon.SpannableFactory; -import ru.noties.markwon.html.api.MarkwonHtmlParser; -import ru.noties.markwon.renderer.SpannableMarkdownVisitor; -import ru.noties.markwon.spans.MarkwonTheme; +import ru.noties.markwon.core.CorePlugin; +import ru.noties.markwon.image.ImagesPlugin; import static org.junit.Assert.assertEquals; import static org.mockito.Mockito.mock; @@ -43,14 +41,11 @@ public class SpannableMarkdownVisitorTest { public void test() { final TestData data = TestDataReader.readTest(file); + + final Markwon markwon = markwon(data.config()); - final MarkwonConfiguration configuration = configuration(data.config()); - final SpannableBuilder builder = new SpannableBuilder(); - final SpannableMarkdownVisitor visitor = new SpannableMarkdownVisitor(configuration, builder); - final Node node = Markwon.createParser().parse(data.input()); - node.accept(visitor); - - final SpannableStringBuilder stringBuilder = builder.spannableStringBuilder(); + // okay we must thing about it... casting? + final SpannableStringBuilder stringBuilder = (SpannableStringBuilder) markwon.toMarkdown(data.input()); final TestValidator validator = TestValidator.create(file); @@ -71,25 +66,18 @@ public class SpannableMarkdownVisitorTest { assertEquals(Arrays.toString(spans), validator.processedSpanNodesCount(), length); } - @SuppressWarnings("ConstantConditions") + @NonNull - private MarkwonConfiguration configuration(@NonNull TestConfig config) { - - final SpannableFactory factory = new TestFactory(config.hasOption(TestConfig.USE_PARAGRAPHS)); - final MarkwonHtmlParser htmlParser = config.hasOption(TestConfig.USE_HTML) - ? null - : MarkwonHtmlParser.noOp(); - - final boolean softBreakAddsNewLine = config.hasOption(TestConfig.SOFT_BREAK_ADDS_NEW_LINE); - final boolean htmlAllowNonClosedTags = config.hasOption(TestConfig.HTML_ALLOW_NON_CLOSED_TAGS); - - return MarkwonConfiguration.builder(null) - .theme(mock(MarkwonTheme.class)) - .linkResolver(mock(LinkResolverDef.class)) - .htmlParser(htmlParser) - .factory(factory) - .softBreakAddsNewLine(softBreakAddsNewLine) - .htmlAllowNonClosedTags(htmlAllowNonClosedTags) + private Markwon markwon(@NonNull final TestConfig config) { + return Markwon.builder(RuntimeEnvironment.application) + .use(CorePlugin.create(config.hasOption(TestConfig.SOFT_BREAK_ADDS_NEW_LINE))) + .use(ImagesPlugin.create(mock(Context.class))) + .use(new AbstractMarkwonPlugin() { + @Override + public void configureConfiguration(@NonNull MarkwonConfiguration.Builder builder) { + builder.factory(new TestFactory(config.hasOption(TestConfig.USE_PARAGRAPHS))); + } + }) .build(); } } \ No newline at end of file diff --git a/markwon/src/test/java/ru/noties/markwon/renderer/visitor/TestConfig.java b/markwon/src/test/java/ru/noties/markwon/renderer/visitor/TestConfig.java index 61fc29a5..367f8a43 100644 --- a/markwon/src/test/java/ru/noties/markwon/renderer/visitor/TestConfig.java +++ b/markwon/src/test/java/ru/noties/markwon/renderer/visitor/TestConfig.java @@ -7,9 +7,9 @@ import java.util.Map; class TestConfig { static final String USE_PARAGRAPHS = "use-paragraphs"; - static final String USE_HTML = "use-html"; +// static final String USE_HTML = "use-html"; static final String SOFT_BREAK_ADDS_NEW_LINE = "soft-break-adds-new-line"; - static final String HTML_ALLOW_NON_CLOSED_TAGS = "html-allow-non-closed-tags"; +// static final String HTML_ALLOW_NON_CLOSED_TAGS = "html-allow-non-closed-tags"; private final Map map; diff --git a/markwon/src/test/java/ru/noties/markwon/renderer/visitor/TestDataReader.java b/markwon/src/test/java/ru/noties/markwon/renderer/visitor/TestDataReader.java index 7f04566b..0aa5e998 100644 --- a/markwon/src/test/java/ru/noties/markwon/renderer/visitor/TestDataReader.java +++ b/markwon/src/test/java/ru/noties/markwon/renderer/visitor/TestDataReader.java @@ -30,7 +30,6 @@ import java.util.Set; import ix.Ix; import ix.IxFunction; import ix.IxPredicate; -import ru.noties.markwon.table.TableRowSpan; import static ru.noties.markwon.renderer.visitor.TestSpan.BLOCK_QUOTE; import static ru.noties.markwon.renderer.visitor.TestSpan.BULLET_LIST; @@ -42,14 +41,8 @@ import static ru.noties.markwon.renderer.visitor.TestSpan.IMAGE; import static ru.noties.markwon.renderer.visitor.TestSpan.LINK; import static ru.noties.markwon.renderer.visitor.TestSpan.ORDERED_LIST; import static ru.noties.markwon.renderer.visitor.TestSpan.PARAGRAPH; -import static ru.noties.markwon.renderer.visitor.TestSpan.STRIKE_THROUGH; import static ru.noties.markwon.renderer.visitor.TestSpan.STRONG_EMPHASIS; -import static ru.noties.markwon.renderer.visitor.TestSpan.SUB_SCRIPT; -import static ru.noties.markwon.renderer.visitor.TestSpan.SUPER_SCRIPT; -import static ru.noties.markwon.renderer.visitor.TestSpan.TABLE_ROW; -import static ru.noties.markwon.renderer.visitor.TestSpan.TASK_LIST; import static ru.noties.markwon.renderer.visitor.TestSpan.THEMATIC_BREAK; -import static ru.noties.markwon.renderer.visitor.TestSpan.UNDERLINE; abstract class TestDataReader { @@ -103,7 +96,7 @@ abstract class TestDataReader { static class Reader { private static final String TEXT = "text"; - private static final String CELLS = "cells"; +// private static final String CELLS = "cells"; private static final Set TAGS; @@ -118,15 +111,9 @@ abstract class TestDataReader { BULLET_LIST, THEMATIC_BREAK, HEADING, - STRIKE_THROUGH, - TASK_LIST, - TABLE_ROW, PARAGRAPH, IMAGE, LINK, - SUPER_SCRIPT, - SUB_SCRIPT, - UNDERLINE, HEADING + "1", HEADING + "2", HEADING + "3", @@ -251,24 +238,27 @@ abstract class TestDataReader { if (valueElement.isJsonNull()) { value = null; } else { - // another special case: table cell - // this is not so good - if (CELLS.equals(key)) { - final JsonArray cells = valueElement.getAsJsonArray(); - final int length = cells.size(); - final List list = new ArrayList<>(length); - for (int k = 0; k < length; k++) { - final JsonObject cell = cells.get(k).getAsJsonObject(); - list.add(new TableRowSpan.Cell( - cell.get("alignment").getAsInt(), - cell.get("text").getAsString() - )); - } - value = list.toString(); - } else { - value = valueElement.getAsString(); - } + value = valueElement.getAsString(); } +// else { +// // another special case: table cell +// // this is not so good +// if (CELLS.equals(key)) { +// final JsonArray cells = valueElement.getAsJsonArray(); +// final int length = cells.size(); +// final List list = new ArrayList<>(length); +// for (int k = 0; k < length; k++) { +// final JsonObject cell = cells.get(k).getAsJsonObject(); +// list.add(new TableRowSpan.Cell( +// cell.get("alignment").getAsInt(), +// cell.get("text").getAsString() +// )); +// } +// value = list.toString(); +// } else { +// value = valueElement.getAsString(); +// } +// } attributes.put(key, value); } } diff --git a/markwon/src/test/java/ru/noties/markwon/renderer/visitor/TestFactory.java b/markwon/src/test/java/ru/noties/markwon/renderer/visitor/TestFactory.java index b8cd7c8d..d2bf8d4e 100644 --- a/markwon/src/test/java/ru/noties/markwon/renderer/visitor/TestFactory.java +++ b/markwon/src/test/java/ru/noties/markwon/renderer/visitor/TestFactory.java @@ -5,16 +5,14 @@ import android.support.annotation.Nullable; import java.util.Collections; import java.util.HashMap; -import java.util.List; import java.util.Map; import ru.noties.markwon.SpannableFactory; +import ru.noties.markwon.core.MarkwonTheme; +import ru.noties.markwon.core.spans.LinkSpan; +import ru.noties.markwon.image.AsyncDrawableLoader; import ru.noties.markwon.image.ImageSize; import ru.noties.markwon.image.ImageSizeResolver; -import ru.noties.markwon.image.AsyncDrawable; -import ru.noties.markwon.spans.LinkSpan; -import ru.noties.markwon.spans.MarkwonTheme; -import ru.noties.markwon.table.TableRowSpan; import static ru.noties.markwon.renderer.visitor.TestSpan.BLOCK_QUOTE; import static ru.noties.markwon.renderer.visitor.TestSpan.BULLET_LIST; @@ -26,14 +24,8 @@ import static ru.noties.markwon.renderer.visitor.TestSpan.IMAGE; import static ru.noties.markwon.renderer.visitor.TestSpan.LINK; import static ru.noties.markwon.renderer.visitor.TestSpan.ORDERED_LIST; import static ru.noties.markwon.renderer.visitor.TestSpan.PARAGRAPH; -import static ru.noties.markwon.renderer.visitor.TestSpan.STRIKE_THROUGH; import static ru.noties.markwon.renderer.visitor.TestSpan.STRONG_EMPHASIS; -import static ru.noties.markwon.renderer.visitor.TestSpan.SUB_SCRIPT; -import static ru.noties.markwon.renderer.visitor.TestSpan.SUPER_SCRIPT; -import static ru.noties.markwon.renderer.visitor.TestSpan.TABLE_ROW; -import static ru.noties.markwon.renderer.visitor.TestSpan.TASK_LIST; import static ru.noties.markwon.renderer.visitor.TestSpan.THEMATIC_BREAK; -import static ru.noties.markwon.renderer.visitor.TestSpan.UNDERLINE; class TestFactory implements SpannableFactory { @@ -94,31 +86,6 @@ class TestFactory implements SpannableFactory { return new TestSpan(HEADING + level); } - @Nullable - @Override - public Object strikethrough() { - return new TestSpan(STRIKE_THROUGH); - } - - @Nullable - @Override - public Object taskListItem(@NonNull MarkwonTheme theme, int blockIndent, boolean isDone) { - return new TestSpan(TASK_LIST, map( - Pair.of("blockIdent", blockIndent), - Pair.of("done", isDone) - )); - } - - @Nullable - @Override - public Object tableRow(@NonNull MarkwonTheme theme, @NonNull List cells, boolean isHeader, boolean isOdd) { - return new TestSpan(TABLE_ROW, map( - Pair.of("cells", cells), - Pair.of("header", isHeader), - Pair.of("odd", isOdd) - )); - } - @Nullable @Override public Object paragraph(boolean inTightList) { @@ -129,7 +96,7 @@ class TestFactory implements SpannableFactory { @Nullable @Override - public Object image(@NonNull MarkwonTheme theme, @NonNull String destination, @NonNull AsyncDrawable.Loader loader, @NonNull ImageSizeResolver imageSizeResolver, @Nullable ImageSize imageSize, boolean replacementTextIsLink) { + public Object image(@NonNull MarkwonTheme theme, @NonNull String destination, @NonNull AsyncDrawableLoader loader, @NonNull ImageSizeResolver imageSizeResolver, @Nullable ImageSize imageSize, boolean replacementTextIsLink) { return new TestSpan(IMAGE, map( Pair.of("src", destination), Pair.of("imageSize", imageSize), @@ -143,24 +110,6 @@ class TestFactory implements SpannableFactory { return new TestSpan(LINK, map("href", destination)); } - @Nullable - @Override - public Object superScript(@NonNull MarkwonTheme theme) { - return new TestSpan(SUPER_SCRIPT); - } - - @Nullable - @Override - public Object subScript(@NonNull MarkwonTheme theme) { - return new TestSpan(SUB_SCRIPT); - } - - @Nullable - @Override - public Object underline() { - return new TestSpan(UNDERLINE); - } - @NonNull private static Map map(@NonNull String key, @Nullable Object value) { return Collections.singletonMap(key, String.valueOf(value)); diff --git a/markwon/src/test/java/ru/noties/markwon/renderer/visitor/TestSpan.java b/markwon/src/test/java/ru/noties/markwon/renderer/visitor/TestSpan.java index f4c8d6ba..1740ba3d 100644 --- a/markwon/src/test/java/ru/noties/markwon/renderer/visitor/TestSpan.java +++ b/markwon/src/test/java/ru/noties/markwon/renderer/visitor/TestSpan.java @@ -16,15 +16,15 @@ class TestSpan { static final String BULLET_LIST = "ul"; static final String THEMATIC_BREAK = "hr"; static final String HEADING = "h"; - static final String STRIKE_THROUGH = "s"; - static final String TASK_LIST = "task-list"; - static final String TABLE_ROW = "tr"; +// static final String STRIKE_THROUGH = "s"; +// static final String TASK_LIST = "task-list"; +// static final String TABLE_ROW = "tr"; static final String PARAGRAPH = "p"; static final String IMAGE = "img"; static final String LINK = "a"; - static final String SUPER_SCRIPT = "sup"; - static final String SUB_SCRIPT = "sub"; - static final String UNDERLINE = "u"; +// static final String SUPER_SCRIPT = "sup"; +// static final String SUB_SCRIPT = "sub"; +// static final String UNDERLINE = "u"; private final String name; diff --git a/markwon/src/test/java/ru/noties/markwon/renderer/SyntaxHighlightTest.java b/markwon/src/test/java/ru/noties/markwon/syntax/SyntaxHighlightTest.java similarity index 76% rename from markwon/src/test/java/ru/noties/markwon/renderer/SyntaxHighlightTest.java rename to markwon/src/test/java/ru/noties/markwon/syntax/SyntaxHighlightTest.java index ecca6e91..ea43bf36 100644 --- a/markwon/src/test/java/ru/noties/markwon/renderer/SyntaxHighlightTest.java +++ b/markwon/src/test/java/ru/noties/markwon/syntax/SyntaxHighlightTest.java @@ -1,4 +1,4 @@ -package ru.noties.markwon.renderer; +package ru.noties.markwon.syntax; import android.content.Context; import android.os.Build; @@ -8,16 +8,23 @@ import android.text.SpannableStringBuilder; import android.text.Spanned; import org.commonmark.node.FencedCodeBlock; +import org.commonmark.node.Node; import org.junit.Test; import org.junit.runner.RunWith; import org.robolectric.RobolectricTestRunner; import org.robolectric.annotation.Config; +import java.util.HashMap; +import java.util.Map; + +import ru.noties.markwon.AbstractMarkwonVisitorImpl; import ru.noties.markwon.MarkwonConfiguration; +import ru.noties.markwon.MarkwonVisitor; import ru.noties.markwon.SpannableBuilder; import ru.noties.markwon.SpannableFactory; -import ru.noties.markwon.SyntaxHighlight; -import ru.noties.markwon.spans.MarkwonTheme; +import ru.noties.markwon.core.MarkwonTheme; +import ru.noties.markwon.core.visitor.CodeBlockNodeVisitor; +import ru.noties.markwon.image.AsyncDrawableLoader; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertTrue; @@ -32,6 +39,8 @@ import static org.mockito.Mockito.when; Build.VERSION_CODES.M, Build.VERSION_CODES.O }) +// although it is called SyntaxHighlightTest all it does is check that spans are in the correct order +// and syntax highlight is the primary user of this functionality public class SyntaxHighlightTest { // codeSpan must be before actual highlight spans (true reverse of builder) @@ -68,10 +77,16 @@ public class SyntaxHighlightTest { final MarkwonConfiguration configuration = MarkwonConfiguration.builder(mock(Context.class)) .syntaxHighlight(highlight) .factory(factory) - .theme(mock(MarkwonTheme.class)) - .build(); + .build(mock(MarkwonTheme.class), mock(AsyncDrawableLoader.class)); - final SpannableBuilder builder = new SpannableBuilder(); + final Map, MarkwonVisitor.NodeVisitor> visitorMap = new HashMap<>(1); + visitorMap.put(FencedCodeBlock.class, new CodeBlockNodeVisitor.Fenced()); + + final MarkwonVisitor visitor = new AbstractMarkwonVisitorImpl( + configuration, + visitorMap); + + final SpannableBuilder builder = visitor.builder(); append(builder, "# Header 1\n", new Object()); append(builder, "## Header 2\n", new Object()); @@ -79,11 +94,15 @@ public class SyntaxHighlightTest { final int start = builder.length(); - final SpannableMarkdownVisitor visitor = new SpannableMarkdownVisitor(configuration, builder); final FencedCodeBlock fencedCodeBlock = new FencedCodeBlock(); fencedCodeBlock.setLiteral("{code}"); - visitor.visit(fencedCodeBlock); + CodeBlockNodeVisitor.visitCodeBlock( + visitor, + null, + "{code}", + fencedCodeBlock + ); final int end = builder.length(); diff --git a/markwon/src/test/java/ru/noties/markwon/UrlProcessorAndroidAssetsTest.java b/markwon/src/test/java/ru/noties/markwon/urlprocessor/UrlProcessorAndroidAssetsTest.java similarity index 86% rename from markwon/src/test/java/ru/noties/markwon/UrlProcessorAndroidAssetsTest.java rename to markwon/src/test/java/ru/noties/markwon/urlprocessor/UrlProcessorAndroidAssetsTest.java index ecf31fd2..7a5cbf1a 100644 --- a/markwon/src/test/java/ru/noties/markwon/UrlProcessorAndroidAssetsTest.java +++ b/markwon/src/test/java/ru/noties/markwon/urlprocessor/UrlProcessorAndroidAssetsTest.java @@ -1,4 +1,4 @@ -package ru.noties.markwon; +package ru.noties.markwon.urlprocessor; import org.junit.Before; import org.junit.Test; @@ -6,8 +6,10 @@ import org.junit.runner.RunWith; import org.robolectric.RobolectricTestRunner; import org.robolectric.annotation.Config; +import ru.noties.markwon.urlprocessor.UrlProcessorAndroidAssets; + import static org.junit.Assert.assertEquals; -import static ru.noties.markwon.UrlProcessorAndroidAssets.BASE; +import static ru.noties.markwon.urlprocessor.UrlProcessorAndroidAssets.BASE; @RunWith(RobolectricTestRunner.class) @Config(manifest = Config.NONE) diff --git a/markwon/src/test/java/ru/noties/markwon/UrlProcessorRelativeToAbsoluteTest.java b/markwon/src/test/java/ru/noties/markwon/urlprocessor/UrlProcessorRelativeToAbsoluteTest.java similarity index 95% rename from markwon/src/test/java/ru/noties/markwon/UrlProcessorRelativeToAbsoluteTest.java rename to markwon/src/test/java/ru/noties/markwon/urlprocessor/UrlProcessorRelativeToAbsoluteTest.java index 05956f3b..a8bf2d01 100644 --- a/markwon/src/test/java/ru/noties/markwon/UrlProcessorRelativeToAbsoluteTest.java +++ b/markwon/src/test/java/ru/noties/markwon/urlprocessor/UrlProcessorRelativeToAbsoluteTest.java @@ -1,10 +1,12 @@ -package ru.noties.markwon; +package ru.noties.markwon.urlprocessor; import org.junit.Test; import org.junit.runner.RunWith; import org.robolectric.RobolectricTestRunner; import org.robolectric.annotation.Config; +import ru.noties.markwon.urlprocessor.UrlProcessorRelativeToAbsolute; + import static org.junit.Assert.*; @RunWith(RobolectricTestRunner.class) diff --git a/markwon/src/test/resources/tests/deeply-nested.yaml b/markwon/src/test/resources/tests/deeply-nested.yaml index 2f40ddf3..4ed4da52 100644 --- a/markwon/src/test/resources/tests/deeply-nested.yaml +++ b/markwon/src/test/resources/tests/deeply-nested.yaml @@ -1,15 +1,12 @@ input: |- - **bold *bold italic ~~bold italic strike `bold italic strike code` bold italic strike~~ bold italic* bold** normal + **bold *bold italic `bold italic code` bold italic* bold** normal output: - b: - "bold " - i: - "bold italic " - - s: - - "bold italic strike " - - code: "bold italic strike code" - - " bold italic strike" + - code: "bold italic code" - " bold italic" - " bold" - " normal" \ No newline at end of file diff --git a/markwon/src/test/resources/tests/html-allow-non-closed-tags.yaml b/markwon/src/test/resources/tests/html-allow-non-closed-tags.yaml deleted file mode 100644 index f0f219b0..00000000 --- a/markwon/src/test/resources/tests/html-allow-non-closed-tags.yaml +++ /dev/null @@ -1,18 +0,0 @@ -input: |- - italic - bold italic - underline bold italic - strike underline bold italic - -config: - use-html: true - html-allow-non-closed-tags: true - -output: - - i: - - "italic " - - b: - - "bold italic " - - u: - - "underline bold italic " - - s: "strike underline bold italic" \ No newline at end of file diff --git a/markwon/src/test/resources/tests/html-non-closed-ignore.yaml b/markwon/src/test/resources/tests/html-non-closed-ignore.yaml deleted file mode 100644 index 0c97e34e..00000000 --- a/markwon/src/test/resources/tests/html-non-closed-ignore.yaml +++ /dev/null @@ -1,12 +0,0 @@ -input: |- - no italic here - bold yeah - no underline - -config: - use-html: true - -output: - - "no italic here " - - b: "bold yeah" - - " no underline" \ No newline at end of file diff --git a/markwon/src/test/resources/tests/html.yaml b/markwon/src/test/resources/tests/html.yaml deleted file mode 100644 index d952ce96..00000000 --- a/markwon/src/test/resources/tests/html.yaml +++ /dev/null @@ -1,105 +0,0 @@ -input: |- -

html

-

emphasis

- iemcitedfn -

strong-emphasis

- bstrong -

super-script

- sup -

sub-script

- sub -

underline

- uins -

strike

- sdel -

link

- a -

unordered-list

-
  • ul1
  • ul2
-

ordered-list

-
  1. ol1
  2. ol2
-

image

- img -

blockquote

-
blockquote
-

3

-

4

-
5
-
6
- -config: - use-html: true - -output: - - h1: "html" - - "\n" - - h2: "emphasis" - - "\n" - - i: "i" - - i: "em" - - i: "cite" - - i: "dfn" - - "\n" - - h2: "strong-emphasis" - - "\n" - - b: "b" - - b: "strong" - - "\n" - - h2: "super-script" - - "\n" - - sup: "sup" - - "\n" - - h2: "sub-script" - - "\n" - - sub: "sub" - - "\n" - - h2: "underline" - - "\n" - - u: "u" - - u: "ins" - - "\n" - - h2: "strike" - - "\n" - - s: "s" - - s: "del" - - "\n" - - h2: "link" - - "\n" - - a: "a" - href: "a://href" - - "\n" - - h2: "unordered-list" - - "\n" - - ul: "ul1" - level: 0 - - "\n" - - ul: "ul2" - level: 0 - - "\n" - - h2: "ordered-list" - - "\n" - - ol: "ol1" - start: 1 - - "\n" - - ol: "ol2" - start: 2 - - "\n" - - h2: "image" - - "\n" - - img: "img" - src: "img://src" - imageSize: null - replacementTextIsLink: false - - "\n" - - h2: "blockquote" - - "\n" - - blockquote: "blockquote" - - "\n" - - h3: "3" - - "\n" - - h4: "4" - - "\n" - - h5: "5" - - "\n" - - h6: "6" - diff --git a/markwon/src/test/resources/tests/second.yaml b/markwon/src/test/resources/tests/second.yaml index bd088dc2..e34e0270 100644 --- a/markwon/src/test/resources/tests/second.yaml +++ b/markwon/src/test/resources/tests/second.yaml @@ -1,6 +1,5 @@ input: |- First **line** is *always* - ~~strike~~ down > Some quote here! @@ -18,9 +17,7 @@ output: - b: "line" - text: " is " - i: "always" - - text: " " - - s: "strike" - - text: " down\n\n" + - text: "\n\n" - blockquote: "Some quote here!" - text: "\n\n" - h1: "Header 1" diff --git a/markwon/src/test/resources/tests/single-s.yaml b/markwon/src/test/resources/tests/single-s.yaml deleted file mode 100644 index 3a1c12cc..00000000 --- a/markwon/src/test/resources/tests/single-s.yaml +++ /dev/null @@ -1,4 +0,0 @@ -input: "~~strike~~" - -output: - - s: "strike" \ No newline at end of file diff --git a/markwon/src/test/resources/tests/single-sub.yaml b/markwon/src/test/resources/tests/single-sub.yaml deleted file mode 100644 index bfeb5367..00000000 --- a/markwon/src/test/resources/tests/single-sub.yaml +++ /dev/null @@ -1,7 +0,0 @@ -input: "sub" - -config: - use-html: true - -output: - - sub: "sub" \ No newline at end of file diff --git a/markwon/src/test/resources/tests/single-sup.yaml b/markwon/src/test/resources/tests/single-sup.yaml deleted file mode 100644 index 8b21ad61..00000000 --- a/markwon/src/test/resources/tests/single-sup.yaml +++ /dev/null @@ -1,7 +0,0 @@ -input: "sup" - -config: - use-html: true - -output: - - sup: "sup" \ No newline at end of file diff --git a/markwon/src/test/resources/tests/single-task-list.yaml b/markwon/src/test/resources/tests/single-task-list.yaml deleted file mode 100644 index cbb15186..00000000 --- a/markwon/src/test/resources/tests/single-task-list.yaml +++ /dev/null @@ -1,6 +0,0 @@ -input: "- [ ] task-list" - -output: - - task-list: "task-list" - blockIdent: 1 - done: false \ No newline at end of file diff --git a/markwon/src/test/resources/tests/single-tr.yaml b/markwon/src/test/resources/tests/single-tr.yaml deleted file mode 100644 index 6ca92909..00000000 --- a/markwon/src/test/resources/tests/single-tr.yaml +++ /dev/null @@ -1,13 +0,0 @@ -input: "col1|col2|col3\n---|---|---|" - -output: - - tr: "\u00a0" - header: true - odd: false - cells: - - alignment: 0 - text: "col1" - - alignment: 0 - text: "col2" - - alignment: 0 - text: "col3" \ No newline at end of file diff --git a/markwon/src/test/resources/tests/single-u.yaml b/markwon/src/test/resources/tests/single-u.yaml deleted file mode 100644 index 1bd135d1..00000000 --- a/markwon/src/test/resources/tests/single-u.yaml +++ /dev/null @@ -1,7 +0,0 @@ -input: "underline" - -config: - use-html: true - -output: - - u: "underline" \ No newline at end of file diff --git a/markwon/src/test/resources/tests/table.yaml b/markwon/src/test/resources/tests/table.yaml deleted file mode 100644 index 96d8236f..00000000 --- a/markwon/src/test/resources/tests/table.yaml +++ /dev/null @@ -1,51 +0,0 @@ -input: |- - head1|head2|head3 - ---|:---:|---: - row1-col1|row1-col2|row1-col3 - row2-col1|row2-col2|row2-col3 - row3-col1|row3-col2|row3-col3 - -output: - - tr: "\u00a0" - header: true - odd: false - cells: - - alignment: 0 - text: "head1" - - alignment: 1 - text: "head2" - - alignment: 2 - text: "head3" - - text: "\n" - - tr: "\u00a0" - header: false - odd: false - cells: - - alignment: 0 - text: "row1-col1" - - alignment: 1 - text: "row1-col2" - - alignment: 2 - text: "row1-col3" - - text: "\n" - - tr: "\u00a0" - header: false - odd: true - cells: - - alignment: 0 - text: "row2-col1" - - alignment: 1 - text: "row2-col2" - - alignment: 2 - text: "row2-col3" - - text: "\n" - - tr: "\u00a0" - header: false - odd: false - cells: - - alignment: 0 - text: "row3-col1" - - alignment: 1 - text: "row3-col2" - - alignment: 2 - text: "row3-col3" \ No newline at end of file diff --git a/sample-custom-extension/src/main/java/ru/noties/markwon/sample/extension/MainActivity.java b/sample-custom-extension/src/main/java/ru/noties/markwon/sample/extension/MainActivity.java index 6477f5d0..29ad6c51 100644 --- a/sample-custom-extension/src/main/java/ru/noties/markwon/sample/extension/MainActivity.java +++ b/sample-custom-extension/src/main/java/ru/noties/markwon/sample/extension/MainActivity.java @@ -8,7 +8,7 @@ import android.widget.TextView; import ru.noties.markwon.AbstractMarkwonPlugin; import ru.noties.markwon.Markwon; -import ru.noties.markwon.spans.MarkwonTheme; +import ru.noties.markwon.core.MarkwonTheme; public class MainActivity extends Activity { From 448a6203999a0a8f7b573e7b2aeb65f1cf403b92 Mon Sep 17 00:00:00 2001 From: Dimitry Ivanov Date: Mon, 17 Dec 2018 14:40:23 +0300 Subject: [PATCH 037/103] Add TestSpan module --- markwon-test-span/build.gradle | 24 ++++ .../src/main/AndroidManifest.xml | 1 + .../java/ru/noties/markwon/test/TestSpan.java | 124 +++++++++++++++++ .../noties/markwon/test/TestSpanDocument.java | 60 ++++++++ .../markwon/test/TestSpanEnumerator.java | 34 +++++ .../noties/markwon/test/TestSpanMatcher.java | 131 ++++++++++++++++++ .../ru/noties/markwon/test/TestSpanSpan.java | 60 ++++++++ .../ru/noties/markwon/test/TestSpanText.java | 47 +++++++ settings.gradle | 1 + 9 files changed, 482 insertions(+) create mode 100644 markwon-test-span/build.gradle create mode 100644 markwon-test-span/src/main/AndroidManifest.xml create mode 100644 markwon-test-span/src/main/java/ru/noties/markwon/test/TestSpan.java create mode 100644 markwon-test-span/src/main/java/ru/noties/markwon/test/TestSpanDocument.java create mode 100644 markwon-test-span/src/main/java/ru/noties/markwon/test/TestSpanEnumerator.java create mode 100644 markwon-test-span/src/main/java/ru/noties/markwon/test/TestSpanMatcher.java create mode 100644 markwon-test-span/src/main/java/ru/noties/markwon/test/TestSpanSpan.java create mode 100644 markwon-test-span/src/main/java/ru/noties/markwon/test/TestSpanText.java diff --git a/markwon-test-span/build.gradle b/markwon-test-span/build.gradle new file mode 100644 index 00000000..cba39010 --- /dev/null +++ b/markwon-test-span/build.gradle @@ -0,0 +1,24 @@ +apply plugin: 'com.android.library' + +android { + + compileSdkVersion config['compile-sdk'] + buildToolsVersion config['build-tools'] + + defaultConfig { + minSdkVersion config['min-sdk'] + targetSdkVersion config['target-sdk'] + versionCode 1 + versionName version + } +} + +dependencies { + + api deps['support-annotations'] + + deps['test'].with { + api it['junit'] + api it['ix-java'] + } +} diff --git a/markwon-test-span/src/main/AndroidManifest.xml b/markwon-test-span/src/main/AndroidManifest.xml new file mode 100644 index 00000000..c2e8a267 --- /dev/null +++ b/markwon-test-span/src/main/AndroidManifest.xml @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/markwon-test-span/src/main/java/ru/noties/markwon/test/TestSpan.java b/markwon-test-span/src/main/java/ru/noties/markwon/test/TestSpan.java new file mode 100644 index 00000000..bc2bd524 --- /dev/null +++ b/markwon-test-span/src/main/java/ru/noties/markwon/test/TestSpan.java @@ -0,0 +1,124 @@ +package ru.noties.markwon.test; + +import android.support.annotation.NonNull; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +/** + * Utility class to validate spannable content + * + * @since 3.0.0 + */ +public abstract class TestSpan { + + @NonNull + public static TestSpan.Document document(TestSpan... children) { + return new TestSpanDocument(children(children)); + } + + @NonNull + public static TestSpan.Span span(@NonNull String name, TestSpan... children) { + return span(name, Collections.emptyMap(), children); + } + + @NonNull + public static TestSpan.Span span(@NonNull String name, @NonNull Map arguments, TestSpan... children) { + return new TestSpanSpan(name, children(children), arguments); + } + + @NonNull + public static TestSpan.Text text(@NonNull String literal) { + return new TestSpanText(literal); + } + + @NonNull + public static List children(TestSpan... children) { + final int length = children.length; + final List list; + if (length == 0) { + list = Collections.emptyList(); + } else if (length == 1) { + list = Collections.singletonList(children[0]); + } else { + final List spans = new ArrayList<>(length); + Collections.addAll(spans, children); + list = Collections.unmodifiableList(spans); + } + return list; + } + + @NonNull + public static Map args(Object... args) { + + final int length = args.length; + if (length == 0) { + return Collections.emptyMap(); + } + + // validate that length is even (k=v) + if ((length % 2) != 0) { + throw new IllegalStateException("Supplied key-values array must contain " + + "even number of arguments"); + } + + final Map map = new HashMap<>(length / 2 + 1); + + String key; + Object value; + + for (int i = 0; i < length; i += 2) { + // possible class-cast exception + key = (String) args[i]; + value = args[i + 1]; + map.put(key, value); + } + + return Collections.unmodifiableMap(map); + } + + + @NonNull + public abstract List children(); + + @Override + public abstract int hashCode(); + + @Override + public abstract boolean equals(Object o); + + + public static abstract class Document extends TestSpan { + + @NonNull + public abstract String wholeText(); + } + + public static abstract class Text extends TestSpan { + + @NonNull + public abstract String literal(); + + public abstract int length(); + } + + public static abstract class Span extends TestSpan { + + @NonNull + public abstract String name(); + + @NonNull + public abstract Map arguments(); + + @NonNull + @Override + public abstract List children(); + } + + // package-private constructor + TestSpan() { + } +} diff --git a/markwon-test-span/src/main/java/ru/noties/markwon/test/TestSpanDocument.java b/markwon-test-span/src/main/java/ru/noties/markwon/test/TestSpanDocument.java new file mode 100644 index 00000000..ef876311 --- /dev/null +++ b/markwon-test-span/src/main/java/ru/noties/markwon/test/TestSpanDocument.java @@ -0,0 +1,60 @@ +package ru.noties.markwon.test; + +import android.support.annotation.NonNull; + +import java.util.List; + +class TestSpanDocument extends TestSpan.Document { + + private static void fillWholeText(@NonNull StringBuilder builder, @NonNull TestSpan span) { + if (span instanceof Text) { + builder.append(((Text) span).literal()); + } else if (span instanceof Span) { + for (TestSpan child : span.children()) { + fillWholeText(builder, child); + } + } else { + throw new IllegalStateException("Unexpected state. Found unexpected TestSpan " + + "object of type `" + span.getClass().getName() + "`"); + } + } + + private final List children; + + TestSpanDocument(@NonNull List children) { + this.children = children; + } + + @NonNull + @Override + public List children() { + return children; + } + + @NonNull + @Override + public String wholeText() { + final StringBuilder builder = new StringBuilder(); + + for (TestSpan child : children) { + fillWholeText(builder, child); + } + + return builder.toString(); + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + + TestSpanDocument that = (TestSpanDocument) o; + + return children.equals(that.children); + } + + @Override + public int hashCode() { + return children.hashCode(); + } +} diff --git a/markwon-test-span/src/main/java/ru/noties/markwon/test/TestSpanEnumerator.java b/markwon-test-span/src/main/java/ru/noties/markwon/test/TestSpanEnumerator.java new file mode 100644 index 00000000..1c2172db --- /dev/null +++ b/markwon-test-span/src/main/java/ru/noties/markwon/test/TestSpanEnumerator.java @@ -0,0 +1,34 @@ +package ru.noties.markwon.test; + +import android.support.annotation.NonNull; + +public class TestSpanEnumerator { + + public interface Listener { + void onNext(int start, int end, @NonNull TestSpan span); + } + + public void enumerate(@NonNull TestSpan.Document document, @NonNull Listener listener) { + visit(0, document, listener); + } + + private int visit(int start, @NonNull TestSpan span, @NonNull Listener listener) { + + if (span instanceof TestSpan.Text) { + final int end = start + ((TestSpan.Text) span).length(); + listener.onNext(start, end, span); + return end; + } + + // yeah, we will need end... and from recursive call also -> children can have text inside + int s = start; + + for (TestSpan child : span.children()) { + s = visit(s, child, listener); + } + + listener.onNext(start, s, span); + + return s; + } +} diff --git a/markwon-test-span/src/main/java/ru/noties/markwon/test/TestSpanMatcher.java b/markwon-test-span/src/main/java/ru/noties/markwon/test/TestSpanMatcher.java new file mode 100644 index 00000000..e9bf190b --- /dev/null +++ b/markwon-test-span/src/main/java/ru/noties/markwon/test/TestSpanMatcher.java @@ -0,0 +1,131 @@ +package ru.noties.markwon.test; + +import android.support.annotation.NonNull; +import android.text.Spanned; + +import junit.framework.Assert; +import junit.framework.ComparisonFailure; + +import java.util.Locale; +import java.util.concurrent.atomic.AtomicInteger; + +import ix.Ix; +import ix.IxPredicate; + +public abstract class TestSpanMatcher { + + public static void matches(@NonNull final Spanned spanned, @NonNull TestSpan.Document document) { + + // assert number for spans + // assert raw text + + final TestSpanEnumerator enumerator = new TestSpanEnumerator(); + + // keep track of total spans encountered + final AtomicInteger counter = new AtomicInteger(); + + enumerator.enumerate(document, new TestSpanEnumerator.Listener() { + @Override + public void onNext(final int start, final int end, @NonNull TestSpan span) { + if (span instanceof TestSpan.Document) { + + TestSpanMatcher.documentMatches(spanned, (TestSpan.Document) span); + } else if (span instanceof TestSpan.Span) { + + // increment span count so after enumeration we match total number of spans + counter.incrementAndGet(); + + TestSpanMatcher.spanMatches(spanned, start, end, (TestSpan.Span) span); + + } else if (span instanceof TestSpan.Text) { + TestSpanMatcher.textMatches(spanned, start, end, (TestSpan.Text) span); + } else { + // in case we add a new type + throw new IllegalStateException("Unexpected type of a TestSpan: `" + + span.getClass().getName() + "`, " + span); + } + } + }); + + final Object[] spans = spanned.getSpans(0, spanned.length(), Object.class); + Assert.assertEquals("Total spans count", counter.get(), spans.length); + } + + public static void documentMatches( + @NonNull Spanned spanned, + @NonNull TestSpan.Document document) { + + // match full text + + final String expected = document.wholeText(); + final String actual = spanned.toString(); + + if (!expected.equals(actual)) { + throw new ComparisonFailure( + "Document text mismatch", + expected, + actual); + } + } + + public static void spanMatches( + @NonNull final Spanned spanned, + final int start, + final int end, + @NonNull TestSpan.Span expected) { + + // when queried multiple spans can be returned (for example if one span + // wraps another one. so [0 1 [2 3] 4 5] where [] represents start/end of + // a span of same type, when queried for spans at 2-3 position, both will be returned + final TestSpan.Span actual = Ix.fromArray(spanned.getSpans(start, end, expected.getClass())) + .filter(new IxPredicate() { + @Override + public boolean test(TestSpan.Span span) { + return start == spanned.getSpanStart(span) + && end == spanned.getSpanEnd(span); + } + }) + .first(null); + + if (!expected.equals(actual)) { + + final String expectedSpan = expected.arguments().isEmpty() + ? expected.name() + : expected.name() + ": " + expected.arguments(); + + final String actualSpan; + if (actual == null) { + actualSpan = "null"; + } else { + actualSpan = actual.arguments().isEmpty() + ? actual.name() + : actual.name() + ": " + actual.arguments(); + } + + throw new AssertionError( + String.format(Locale.US, "Expected span{%s} at {start: %d, end: %d}, found: %s", + expectedSpan, start, end, actualSpan)); + } + } + + public static void textMatches( + @NonNull Spanned spanned, + int start, + int end, + @NonNull TestSpan.Text text) { + + final String expected = text.literal(); + final String actual = spanned.subSequence(start, end).toString(); + + if (!expected.equals(actual)) { + throw new ComparisonFailure( + String.format(Locale.US, "Text mismatch at {start: %d, end: %d}", start, end), + expected, + actual + ); + } + } + + private TestSpanMatcher() { + } +} diff --git a/markwon-test-span/src/main/java/ru/noties/markwon/test/TestSpanSpan.java b/markwon-test-span/src/main/java/ru/noties/markwon/test/TestSpanSpan.java new file mode 100644 index 00000000..5dc60acd --- /dev/null +++ b/markwon-test-span/src/main/java/ru/noties/markwon/test/TestSpanSpan.java @@ -0,0 +1,60 @@ +package ru.noties.markwon.test; + +import android.support.annotation.NonNull; + +import java.util.List; +import java.util.Map; + +class TestSpanSpan extends TestSpan.Span { + + private final String name; + private final List children; + private final Map arguments; + + public TestSpanSpan( + @NonNull String name, + @NonNull List children, + @NonNull Map arguments) { + this.name = name; + this.children = children; + this.arguments = arguments; + } + + @NonNull + @Override + public String name() { + return name; + } + + @NonNull + @Override + public Map arguments() { + return arguments; + } + + @NonNull + @Override + public List children() { + return children; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + + TestSpanSpan that = (TestSpanSpan) o; + + if (!name.equals(that.name)) return false; + if (!children.equals(that.children)) return false; + return arguments.equals(that.arguments); + } + + @Override + public int hashCode() { + int result = name.hashCode(); + result = 31 * result + children.hashCode(); + result = 31 * result + arguments.hashCode(); + return result; + } +} diff --git a/markwon-test-span/src/main/java/ru/noties/markwon/test/TestSpanText.java b/markwon-test-span/src/main/java/ru/noties/markwon/test/TestSpanText.java new file mode 100644 index 00000000..8a34a410 --- /dev/null +++ b/markwon-test-span/src/main/java/ru/noties/markwon/test/TestSpanText.java @@ -0,0 +1,47 @@ +package ru.noties.markwon.test; + +import android.support.annotation.NonNull; + +import java.util.Collections; +import java.util.List; + +class TestSpanText extends TestSpan.Text { + + private final String literal; + + TestSpanText(@NonNull String literal) { + this.literal = literal; + } + + @NonNull + @Override + public String literal() { + return literal; + } + + @Override + public int length() { + return literal.length(); + } + + @NonNull + @Override + public List children() { + return Collections.emptyList(); + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + + TestSpanText that = (TestSpanText) o; + + return literal.equals(that.literal); + } + + @Override + public int hashCode() { + return literal.hashCode(); + } +} diff --git a/settings.gradle b/settings.gradle index 10d8564b..d5f56c45 100644 --- a/settings.gradle +++ b/settings.gradle @@ -10,5 +10,6 @@ include ':app', ':markwon-syntax-highlight', ':markwon-html', ':markwon-view', + ':markwon-test-span', ':sample-custom-extension', ':sample-latex-math' From 414a8763f2f7e7d8b8bd2df6e90f1e355d625b6a Mon Sep 17 00:00:00 2001 From: Dimitry Ivanov Date: Mon, 17 Dec 2018 16:18:20 +0300 Subject: [PATCH 038/103] Redefine test format --- .../java/ru/noties/markwon/test/TestSpan.java | 1 + .../noties/markwon/test/TestSpanMatcher.java | 8 +- .../ru/noties/markwon/test/TestSpanSpan.java | 2 - .../ru/noties/markwon/test/TestSpanTest.java | 70 +++++++++++++++ markwon-test-util/build.gradle | 13 +++ .../java/ru/noties/markwon/test/TestUtil.java | 32 +++++++ markwon/build.gradle | 8 +- .../java/ru/noties/markwon/core/CoreTest.java | 60 +++++++++++++ .../noties/markwon/core/suite/.editorconfig | 4 + .../markwon/core/suite/BaseSuiteTest.java | 44 ++++++++++ .../noties/markwon/core/suite/BoldItalic.java | 33 +++++++ .../noties/markwon/core/suite/CodeBlocks.java | 51 +++++++++++ .../markwon/core/suite/DeeplyNested.java | 44 ++++++++++ .../ru/noties/markwon/core/suite/First.java | 46 ++++++++++ .../markwon/core/suite/TestFactory.java | 88 +++++++++++++++++++ .../visitor/BlockQuoteNodeVisitorTest.java | 18 ++++ .../src/test/resources/tests/bold-italic.md | 1 + .../src/test/resources/tests/bold-italic.yaml | 5 -- .../src/test/resources/tests/code-blocks.md | 9 ++ .../src/test/resources/tests/code-blocks.yaml | 17 ---- .../src/test/resources/tests/deeply-nested.md | 1 + .../test/resources/tests/deeply-nested.yaml | 12 --- markwon/src/test/resources/tests/first.md | 2 + markwon/src/test/resources/tests/first.yaml | 23 ----- settings.gradle | 1 + 25 files changed, 530 insertions(+), 63 deletions(-) create mode 100644 markwon-test-span/src/test/java/ru/noties/markwon/test/TestSpanTest.java create mode 100644 markwon-test-util/build.gradle create mode 100644 markwon-test-util/src/main/java/ru/noties/markwon/test/TestUtil.java create mode 100644 markwon/src/test/java/ru/noties/markwon/core/CoreTest.java create mode 100644 markwon/src/test/java/ru/noties/markwon/core/suite/.editorconfig create mode 100644 markwon/src/test/java/ru/noties/markwon/core/suite/BaseSuiteTest.java create mode 100644 markwon/src/test/java/ru/noties/markwon/core/suite/BoldItalic.java create mode 100644 markwon/src/test/java/ru/noties/markwon/core/suite/CodeBlocks.java create mode 100644 markwon/src/test/java/ru/noties/markwon/core/suite/DeeplyNested.java create mode 100644 markwon/src/test/java/ru/noties/markwon/core/suite/First.java create mode 100644 markwon/src/test/java/ru/noties/markwon/core/suite/TestFactory.java create mode 100644 markwon/src/test/java/ru/noties/markwon/core/visitor/BlockQuoteNodeVisitorTest.java create mode 100644 markwon/src/test/resources/tests/bold-italic.md delete mode 100644 markwon/src/test/resources/tests/bold-italic.yaml create mode 100644 markwon/src/test/resources/tests/code-blocks.md delete mode 100644 markwon/src/test/resources/tests/code-blocks.yaml create mode 100644 markwon/src/test/resources/tests/deeply-nested.md delete mode 100644 markwon/src/test/resources/tests/deeply-nested.yaml create mode 100644 markwon/src/test/resources/tests/first.md delete mode 100644 markwon/src/test/resources/tests/first.yaml diff --git a/markwon-test-span/src/main/java/ru/noties/markwon/test/TestSpan.java b/markwon-test-span/src/main/java/ru/noties/markwon/test/TestSpan.java index bc2bd524..f979d633 100644 --- a/markwon-test-span/src/main/java/ru/noties/markwon/test/TestSpan.java +++ b/markwon-test-span/src/main/java/ru/noties/markwon/test/TestSpan.java @@ -105,6 +105,7 @@ public abstract class TestSpan { public abstract int length(); } + // important: children should not be included in equals... public static abstract class Span extends TestSpan { @NonNull diff --git a/markwon-test-span/src/main/java/ru/noties/markwon/test/TestSpanMatcher.java b/markwon-test-span/src/main/java/ru/noties/markwon/test/TestSpanMatcher.java index e9bf190b..1eb2a1bc 100644 --- a/markwon-test-span/src/main/java/ru/noties/markwon/test/TestSpanMatcher.java +++ b/markwon-test-span/src/main/java/ru/noties/markwon/test/TestSpanMatcher.java @@ -72,16 +72,18 @@ public abstract class TestSpanMatcher { @NonNull final Spanned spanned, final int start, final int end, - @NonNull TestSpan.Span expected) { + @NonNull final TestSpan.Span expected) { // when queried multiple spans can be returned (for example if one span // wraps another one. so [0 1 [2 3] 4 5] where [] represents start/end of // a span of same type, when queried for spans at 2-3 position, both will be returned - final TestSpan.Span actual = Ix.fromArray(spanned.getSpans(start, end, expected.getClass())) + final TestSpan.Span actual = Ix.fromArray(spanned.getSpans(start, end, Object.class)) + .cast(TestSpan.Span.class) .filter(new IxPredicate() { @Override public boolean test(TestSpan.Span span) { - return start == spanned.getSpanStart(span) + return expected.name().equals(span.name()) + && start == spanned.getSpanStart(span) && end == spanned.getSpanEnd(span); } }) diff --git a/markwon-test-span/src/main/java/ru/noties/markwon/test/TestSpanSpan.java b/markwon-test-span/src/main/java/ru/noties/markwon/test/TestSpanSpan.java index 5dc60acd..d9669cb2 100644 --- a/markwon-test-span/src/main/java/ru/noties/markwon/test/TestSpanSpan.java +++ b/markwon-test-span/src/main/java/ru/noties/markwon/test/TestSpanSpan.java @@ -46,14 +46,12 @@ class TestSpanSpan extends TestSpan.Span { TestSpanSpan that = (TestSpanSpan) o; if (!name.equals(that.name)) return false; - if (!children.equals(that.children)) return false; return arguments.equals(that.arguments); } @Override public int hashCode() { int result = name.hashCode(); - result = 31 * result + children.hashCode(); result = 31 * result + arguments.hashCode(); return result; } diff --git a/markwon-test-span/src/test/java/ru/noties/markwon/test/TestSpanTest.java b/markwon-test-span/src/test/java/ru/noties/markwon/test/TestSpanTest.java new file mode 100644 index 00000000..5e178409 --- /dev/null +++ b/markwon-test-span/src/test/java/ru/noties/markwon/test/TestSpanTest.java @@ -0,0 +1,70 @@ +package ru.noties.markwon.test; + +import org.junit.Test; + +import java.util.Map; + +import ru.noties.markwon.test.TestSpan.Document; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; +import static ru.noties.markwon.test.TestSpan.args; +import static ru.noties.markwon.test.TestSpan.document; +import static ru.noties.markwon.test.TestSpan.span; +import static ru.noties.markwon.test.TestSpan.text; + +public class TestSpanTest { + + @Test + public void args_not_event_throws() { + try { + args("key"); + fail(); + } catch (IllegalStateException e) { + assertTrue(e.getMessage(), e.getMessage().contains("Supplied key-values array must contain ")); + } + } + + @Test + public void args_key_not_string_throws() { + try { + args("key", 1, 2, 3); + fail(); + } catch (ClassCastException e) { + assertTrue(true); + } + } + + @Test + public void args_correct() { + + final Map args = args("key1", true, "key2", 4); + + assertEquals(2, args.size()); + assertEquals(true, args.get("key1")); + assertEquals(4, args.get("key2")); + } + + @Test + public void empty_document() { + final Document document = document(); + assertEquals(0, document.children().size()); + assertEquals("", document.wholeText()); + } + + @Test + public void document_single_text_child() { + final Document document = document(text("Text")); + assertEquals(1, document.children().size()); + assertEquals("Text", document.wholeText()); + } + + @Test + public void document_single_span_child() { + final Document document = document(span("span", text("TextInSpan"))); + assertEquals(1, document.children().size()); + assertTrue(document.children().get(0) instanceof TestSpan.Span); + assertEquals("TextInSpan", document.wholeText()); + } +} diff --git a/markwon-test-util/build.gradle b/markwon-test-util/build.gradle new file mode 100644 index 00000000..3909ab27 --- /dev/null +++ b/markwon-test-util/build.gradle @@ -0,0 +1,13 @@ +apply plugin: 'java-library' + +sourceCompatibility = 1.7 +targetCompatibility = 1.7 + +dependencies { + + api deps['support-annotations'] + + deps['test'].with { + implementation it['commons-io'] + } +} \ No newline at end of file diff --git a/markwon-test-util/src/main/java/ru/noties/markwon/test/TestUtil.java b/markwon-test-util/src/main/java/ru/noties/markwon/test/TestUtil.java new file mode 100644 index 00000000..2ce00e1f --- /dev/null +++ b/markwon-test-util/src/main/java/ru/noties/markwon/test/TestUtil.java @@ -0,0 +1,32 @@ +package ru.noties.markwon.test; + +import android.support.annotation.NonNull; + +import org.apache.commons.io.IOUtils; + +import java.io.IOException; +import java.nio.charset.StandardCharsets; + +public abstract class TestUtil { + + @NonNull + public static String read(@NonNull String path) { + try { + return IOUtils.resourceToString(path, StandardCharsets.UTF_8); + } catch (IOException e) { + throw new RuntimeException(e); + } + } + + @NonNull + public static String read(@NonNull Object who, @NonNull String path) { + try { + return IOUtils.resourceToString(path, StandardCharsets.UTF_8, who.getClass().getClassLoader()); + } catch (IOException e) { + throw new RuntimeException(e); + } + } + + private TestUtil() { + } +} diff --git a/markwon/build.gradle b/markwon/build.gradle index e1d9db87..604519a2 100644 --- a/markwon/build.gradle +++ b/markwon/build.gradle @@ -21,14 +21,20 @@ dependencies { } deps['test'].with { + + testImplementation project(':markwon-test-span') + testImplementation project(':markwon-test-util') + testImplementation it['junit'] testImplementation it['robolectric'] + testImplementation it['mockito'] + + // to remove after migration testImplementation it['ix-java'] testImplementation it['jackson-yaml'] testImplementation it['jackson-databind'] testImplementation it['gson'] testImplementation it['commons-io'] - testImplementation it['mockito'] } } diff --git a/markwon/src/test/java/ru/noties/markwon/core/CoreTest.java b/markwon/src/test/java/ru/noties/markwon/core/CoreTest.java new file mode 100644 index 00000000..7fc110ff --- /dev/null +++ b/markwon/src/test/java/ru/noties/markwon/core/CoreTest.java @@ -0,0 +1,60 @@ +package ru.noties.markwon.core; + +import android.support.annotation.NonNull; +import android.support.annotation.Nullable; +import android.text.Spanned; + +import org.junit.Test; +import org.junit.runner.RunWith; +import org.robolectric.RobolectricTestRunner; +import org.robolectric.RuntimeEnvironment; +import org.robolectric.annotation.Config; + +import ru.noties.markwon.AbstractMarkwonPlugin; +import ru.noties.markwon.Markwon; +import ru.noties.markwon.MarkwonConfiguration; +import ru.noties.markwon.SpannableFactoryDef; +import ru.noties.markwon.test.TestSpan; +import ru.noties.markwon.test.TestSpanMatcher; + +import static ru.noties.markwon.test.TestSpan.document; +import static ru.noties.markwon.test.TestSpan.span; +import static ru.noties.markwon.test.TestSpan.text; + +@RunWith(RobolectricTestRunner.class) +@Config(manifest = Config.NONE) +public class CoreTest { + + @Test + public void bold_italic() { + + final String input = "**_bold italic_**"; + final TestSpan.Document document = document( + span("bold", + span("italic", text("bold italic")))); + + final Spanned spanned = (Spanned) Markwon.builder(RuntimeEnvironment.application) + .use(CorePlugin.create()) + .use(new AbstractMarkwonPlugin() { + @Override + public void configureConfiguration(@NonNull MarkwonConfiguration.Builder builder) { + builder.factory(new SpannableFactoryDef() { + + @Override + public Object strongEmphasis() { + return span("bold"); + } + + @Override + public Object emphasis() { + return span("italic"); + } + }); + } + }) + .build() + .toMarkdown(input); + + TestSpanMatcher.matches(spanned, document); + } +} diff --git a/markwon/src/test/java/ru/noties/markwon/core/suite/.editorconfig b/markwon/src/test/java/ru/noties/markwon/core/suite/.editorconfig new file mode 100644 index 00000000..be598039 --- /dev/null +++ b/markwon/src/test/java/ru/noties/markwon/core/suite/.editorconfig @@ -0,0 +1,4 @@ +# 2 space indentation +[*.java] +indent_style = space +indent_size = 2 \ No newline at end of file diff --git a/markwon/src/test/java/ru/noties/markwon/core/suite/BaseSuiteTest.java b/markwon/src/test/java/ru/noties/markwon/core/suite/BaseSuiteTest.java new file mode 100644 index 00000000..6fec8d8d --- /dev/null +++ b/markwon/src/test/java/ru/noties/markwon/core/suite/BaseSuiteTest.java @@ -0,0 +1,44 @@ +package ru.noties.markwon.core.suite; + +import android.support.annotation.NonNull; +import android.text.Spanned; + +import org.robolectric.RuntimeEnvironment; + +import ru.noties.markwon.AbstractMarkwonPlugin; +import ru.noties.markwon.Markwon; +import ru.noties.markwon.MarkwonConfiguration; +import ru.noties.markwon.core.CorePlugin; +import ru.noties.markwon.test.TestSpan; +import ru.noties.markwon.test.TestSpanMatcher; +import ru.noties.markwon.test.TestUtil; + +abstract class BaseSuiteTest { + + void matches(@NonNull String input, @NonNull TestSpan.Document document) { + final Spanned spanned = (Spanned) markwon().toMarkdown(input); + TestSpanMatcher.matches(spanned, document); + } + + void matchInput(@NonNull String name, @NonNull TestSpan.Document document) { + matches(read(name), document); + } + + @NonNull + String read(@NonNull String name) { + return TestUtil.read(this, "tests/" + name); + } + + @NonNull + Markwon markwon() { + return Markwon.builder(RuntimeEnvironment.application) + .use(CorePlugin.create()) + .use(new AbstractMarkwonPlugin() { + @Override + public void configureConfiguration(@NonNull MarkwonConfiguration.Builder builder) { + builder.factory(new TestFactory()); + } + }) + .build(); + } +} diff --git a/markwon/src/test/java/ru/noties/markwon/core/suite/BoldItalic.java b/markwon/src/test/java/ru/noties/markwon/core/suite/BoldItalic.java new file mode 100644 index 00000000..c0ad4bd1 --- /dev/null +++ b/markwon/src/test/java/ru/noties/markwon/core/suite/BoldItalic.java @@ -0,0 +1,33 @@ +package ru.noties.markwon.core.suite; + +import org.junit.Test; +import org.junit.runner.RunWith; +import org.robolectric.RobolectricTestRunner; +import org.robolectric.annotation.Config; + +import ru.noties.markwon.test.TestSpan; + +import static ru.noties.markwon.core.suite.TestFactory.BOLD; +import static ru.noties.markwon.core.suite.TestFactory.ITALIC; +import static ru.noties.markwon.test.TestSpan.document; +import static ru.noties.markwon.test.TestSpan.span; +import static ru.noties.markwon.test.TestSpan.text; + +@RunWith(RobolectricTestRunner.class) +@Config(manifest = Config.NONE) +public class BoldItalic extends BaseSuiteTest { + + /* + **_bold italic_** + */ + + @Test + public void test() { + + final TestSpan.Document document = document( + span(BOLD, + span(ITALIC, text("bold italic")))); + + matchInput("bold-italic.md", document); + } +} diff --git a/markwon/src/test/java/ru/noties/markwon/core/suite/CodeBlocks.java b/markwon/src/test/java/ru/noties/markwon/core/suite/CodeBlocks.java new file mode 100644 index 00000000..6427f0ec --- /dev/null +++ b/markwon/src/test/java/ru/noties/markwon/core/suite/CodeBlocks.java @@ -0,0 +1,51 @@ +package ru.noties.markwon.core.suite; + +import org.junit.Test; +import org.junit.runner.RunWith; +import org.robolectric.RobolectricTestRunner; +import org.robolectric.annotation.Config; + +import ru.noties.markwon.test.TestSpan; + +import static ru.noties.markwon.core.suite.TestFactory.CODE; +import static ru.noties.markwon.test.TestSpan.args; +import static ru.noties.markwon.test.TestSpan.document; +import static ru.noties.markwon.test.TestSpan.span; +import static ru.noties.markwon.test.TestSpan.text; + +@RunWith(RobolectricTestRunner.class) +@Config(manifest = Config.NONE) +public class CodeBlocks extends BaseSuiteTest { + + /* + ```java + final String s = null; + ``` + ```html + + ``` + ``` + nothing here + ``` + */ + + @Test + public void test() { + + final TestSpan.Document document = document( + span(CODE, + args("multiline", true), + text("\u00a0\nfinal String s = null;\n\u00a0")), + text("\n\n"), + span(CODE, + args("multiline", true), + text("\u00a0\n\n\u00a0")), + text("\n\n"), + span(CODE, + args("multiline", true), + text("\u00a0\nnothing here\n\u00a0")) + ); + + matchInput("code-blocks.md", document); + } +} diff --git a/markwon/src/test/java/ru/noties/markwon/core/suite/DeeplyNested.java b/markwon/src/test/java/ru/noties/markwon/core/suite/DeeplyNested.java new file mode 100644 index 00000000..16e5b2e6 --- /dev/null +++ b/markwon/src/test/java/ru/noties/markwon/core/suite/DeeplyNested.java @@ -0,0 +1,44 @@ +package ru.noties.markwon.core.suite; + +import org.junit.Test; +import org.junit.runner.RunWith; +import org.robolectric.RobolectricTestRunner; +import org.robolectric.annotation.Config; + +import ru.noties.markwon.test.TestSpan.Document; + +import static ru.noties.markwon.core.suite.TestFactory.BOLD; +import static ru.noties.markwon.core.suite.TestFactory.CODE; +import static ru.noties.markwon.core.suite.TestFactory.ITALIC; +import static ru.noties.markwon.test.TestSpan.args; +import static ru.noties.markwon.test.TestSpan.document; +import static ru.noties.markwon.test.TestSpan.span; +import static ru.noties.markwon.test.TestSpan.text; + +@RunWith(RobolectricTestRunner.class) +@Config(manifest = Config.NONE) +public class DeeplyNested extends BaseSuiteTest { + + /* + **bold *bold italic `bold italic code` bold italic* bold** normal + */ + + @Test + public void test() { + + final Document document = document( + span(BOLD, + text("bold "), + span(ITALIC, + text("bold italic "), + span(CODE, + args("multiline", false), + text("\u00a0bold italic code\u00a0")), + text(" bold italic")), + text(" bold")), + text(" normal") + ); + + matchInput("deeply-nested.md", document); + } +} diff --git a/markwon/src/test/java/ru/noties/markwon/core/suite/First.java b/markwon/src/test/java/ru/noties/markwon/core/suite/First.java new file mode 100644 index 00000000..9aaf05ed --- /dev/null +++ b/markwon/src/test/java/ru/noties/markwon/core/suite/First.java @@ -0,0 +1,46 @@ +package ru.noties.markwon.core.suite; + +import org.junit.Test; +import org.junit.runner.RunWith; +import org.robolectric.RobolectricTestRunner; +import org.robolectric.annotation.Config; + +import ru.noties.markwon.test.TestSpan.Document; + +import static ru.noties.markwon.core.suite.TestFactory.BOLD; +import static ru.noties.markwon.core.suite.TestFactory.ITALIC; +import static ru.noties.markwon.core.suite.TestFactory.LINK; +import static ru.noties.markwon.test.TestSpan.args; +import static ru.noties.markwon.test.TestSpan.document; +import static ru.noties.markwon.test.TestSpan.span; +import static ru.noties.markwon.test.TestSpan.text; + +@RunWith(RobolectricTestRunner.class) +@Config(manifest = Config.NONE) +public class First extends BaseSuiteTest { + + /* + Here is some [link](https://my.href) + **bold _bold italic_ bold** normal + */ + + @Test + public void test() { + + final Document document = document( + text("Here is some "), + span(LINK, + args("href", "https://my.href"), + text("link")), + text(" "), + span(BOLD, + text("bold "), + span(ITALIC, + text("bold italic")), + text(" bold")), + text(" normal") + ); + + matchInput("first.md", document); + } +} diff --git a/markwon/src/test/java/ru/noties/markwon/core/suite/TestFactory.java b/markwon/src/test/java/ru/noties/markwon/core/suite/TestFactory.java new file mode 100644 index 00000000..222e2092 --- /dev/null +++ b/markwon/src/test/java/ru/noties/markwon/core/suite/TestFactory.java @@ -0,0 +1,88 @@ +package ru.noties.markwon.core.suite; + +import android.support.annotation.NonNull; +import android.support.annotation.Nullable; + +import ru.noties.markwon.SpannableFactory; +import ru.noties.markwon.core.MarkwonTheme; +import ru.noties.markwon.core.spans.LinkSpan; +import ru.noties.markwon.image.AsyncDrawableLoader; +import ru.noties.markwon.image.ImageSize; +import ru.noties.markwon.image.ImageSizeResolver; + +import static ru.noties.markwon.test.TestSpan.args; +import static ru.noties.markwon.test.TestSpan.span; + +class TestFactory implements SpannableFactory { + + static final String BOLD = "bold"; + static final String ITALIC = "italic"; + static final String CODE = "code"; + static final String LINK = "link"; + + @Nullable + @Override + public Object strongEmphasis() { + return span(BOLD); + } + + @Nullable + @Override + public Object emphasis() { + return span(ITALIC); + } + + @Nullable + @Override + public Object blockQuote(@NonNull MarkwonTheme theme) { + return null; + } + + @Nullable + @Override + public Object code(@NonNull MarkwonTheme theme, boolean multiline) { + return span(CODE, args("multiline", multiline)); + } + + @Nullable + @Override + public Object orderedListItem(@NonNull MarkwonTheme theme, int startNumber) { + return null; + } + + @Nullable + @Override + public Object bulletListItem(@NonNull MarkwonTheme theme, int level) { + return null; + } + + @Nullable + @Override + public Object thematicBreak(@NonNull MarkwonTheme theme) { + return null; + } + + @Nullable + @Override + public Object heading(@NonNull MarkwonTheme theme, int level) { + return null; + } + + @Nullable + @Override + public Object paragraph(boolean inTightList) { + return null; + } + + @Nullable + @Override + public Object image(@NonNull MarkwonTheme theme, @NonNull String destination, @NonNull AsyncDrawableLoader loader, @NonNull ImageSizeResolver imageSizeResolver, @Nullable ImageSize imageSize, boolean replacementTextIsLink) { + return null; + } + + @Nullable + @Override + public Object link(@NonNull MarkwonTheme theme, @NonNull String destination, @NonNull LinkSpan.Resolver resolver) { + return span(LINK, args("href", destination)); + } +} diff --git a/markwon/src/test/java/ru/noties/markwon/core/visitor/BlockQuoteNodeVisitorTest.java b/markwon/src/test/java/ru/noties/markwon/core/visitor/BlockQuoteNodeVisitorTest.java new file mode 100644 index 00000000..f83173ce --- /dev/null +++ b/markwon/src/test/java/ru/noties/markwon/core/visitor/BlockQuoteNodeVisitorTest.java @@ -0,0 +1,18 @@ +package ru.noties.markwon.core.visitor; + +import org.junit.Test; +import org.junit.runner.RunWith; +import org.robolectric.RobolectricTestRunner; +import org.robolectric.annotation.Config; + +import static org.junit.Assert.*; + +@RunWith(RobolectricTestRunner.class) +@Config(manifest = Config.NONE) +public class BlockQuoteNodeVisitorTest { + + @Test + public void test() { + fail(); + } +} \ No newline at end of file diff --git a/markwon/src/test/resources/tests/bold-italic.md b/markwon/src/test/resources/tests/bold-italic.md new file mode 100644 index 00000000..757cfe8c --- /dev/null +++ b/markwon/src/test/resources/tests/bold-italic.md @@ -0,0 +1 @@ +**_bold italic_** \ No newline at end of file diff --git a/markwon/src/test/resources/tests/bold-italic.yaml b/markwon/src/test/resources/tests/bold-italic.yaml deleted file mode 100644 index d7d24682..00000000 --- a/markwon/src/test/resources/tests/bold-italic.yaml +++ /dev/null @@ -1,5 +0,0 @@ -input: "**_bold italic_**" - -output: - - b: - - i: "bold italic" \ No newline at end of file diff --git a/markwon/src/test/resources/tests/code-blocks.md b/markwon/src/test/resources/tests/code-blocks.md new file mode 100644 index 00000000..3b24ed05 --- /dev/null +++ b/markwon/src/test/resources/tests/code-blocks.md @@ -0,0 +1,9 @@ +```java +final String s = null; +``` +```html + +``` +``` +nothing here +``` \ No newline at end of file diff --git a/markwon/src/test/resources/tests/code-blocks.yaml b/markwon/src/test/resources/tests/code-blocks.yaml deleted file mode 100644 index 53b9bd50..00000000 --- a/markwon/src/test/resources/tests/code-blocks.yaml +++ /dev/null @@ -1,17 +0,0 @@ -input: |- - ```java - final String s = null; - ``` - ```html - - ``` - ``` - nothing here - ``` - -output: - - code-block: "final String s = null;" - - "\n\n" - - code-block: "" - - "\n\n" - - code-block: "nothing here" \ No newline at end of file diff --git a/markwon/src/test/resources/tests/deeply-nested.md b/markwon/src/test/resources/tests/deeply-nested.md new file mode 100644 index 00000000..3f8f18ab --- /dev/null +++ b/markwon/src/test/resources/tests/deeply-nested.md @@ -0,0 +1 @@ +**bold *bold italic `bold italic code` bold italic* bold** normal \ No newline at end of file diff --git a/markwon/src/test/resources/tests/deeply-nested.yaml b/markwon/src/test/resources/tests/deeply-nested.yaml deleted file mode 100644 index 4ed4da52..00000000 --- a/markwon/src/test/resources/tests/deeply-nested.yaml +++ /dev/null @@ -1,12 +0,0 @@ -input: |- - **bold *bold italic `bold italic code` bold italic* bold** normal - -output: - - b: - - "bold " - - i: - - "bold italic " - - code: "bold italic code" - - " bold italic" - - " bold" - - " normal" \ No newline at end of file diff --git a/markwon/src/test/resources/tests/first.md b/markwon/src/test/resources/tests/first.md new file mode 100644 index 00000000..6f075385 --- /dev/null +++ b/markwon/src/test/resources/tests/first.md @@ -0,0 +1,2 @@ +Here is some [link](https://my.href) +**bold _bold italic_ bold** normal \ No newline at end of file diff --git a/markwon/src/test/resources/tests/first.yaml b/markwon/src/test/resources/tests/first.yaml deleted file mode 100644 index d52d670d..00000000 --- a/markwon/src/test/resources/tests/first.yaml +++ /dev/null @@ -1,23 +0,0 @@ -description: Defining test case format - -input: |- - Here is some [link](https://my.href) - **bold _bold italic_ bold** normal - -config: - use-paragraphs: false - use-html: false - soft-break-adds-new-line: false - html-allow-non-closed-tags: false - -output: - - "Here is some " - - a: "link" - href: "https://my.href" - - " " - - b: - - "bold " - - i: "bold italic" #equals to: `- i: - text: "bold italic"` - - " bold" - - " normal" - diff --git a/settings.gradle b/settings.gradle index d5f56c45..ede0753a 100644 --- a/settings.gradle +++ b/settings.gradle @@ -11,5 +11,6 @@ include ':app', ':markwon-html', ':markwon-view', ':markwon-test-span', + ':markwon-test-util', ':sample-custom-extension', ':sample-latex-math' From 7c473b3d37cef429e3f46882a4f42b86ee2bf237 Mon Sep 17 00:00:00 2001 From: Dimitry Ivanov Date: Mon, 17 Dec 2018 16:19:31 +0300 Subject: [PATCH 039/103] Change markwon render return type to Spanned --- markwon/src/main/java/ru/noties/markwon/Markwon.java | 5 +++-- markwon/src/main/java/ru/noties/markwon/MarkwonImpl.java | 7 ++++--- 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/markwon/src/main/java/ru/noties/markwon/Markwon.java b/markwon/src/main/java/ru/noties/markwon/Markwon.java index b9fe2dad..6214fc16 100644 --- a/markwon/src/main/java/ru/noties/markwon/Markwon.java +++ b/markwon/src/main/java/ru/noties/markwon/Markwon.java @@ -2,6 +2,7 @@ package ru.noties.markwon; import android.content.Context; import android.support.annotation.NonNull; +import android.text.Spanned; import android.widget.TextView; import org.commonmark.node.Node; @@ -17,11 +18,11 @@ public abstract class Markwon { public abstract Node parse(@NonNull String input); @NonNull - public abstract CharSequence render(@NonNull Node node); + public abstract Spanned render(@NonNull Node node); // parse + render @NonNull - public abstract CharSequence toMarkdown(@NonNull String input); + public abstract Spanned toMarkdown(@NonNull String input); public abstract void setMarkdown(@NonNull TextView textView, @NonNull String markdown); diff --git a/markwon/src/main/java/ru/noties/markwon/MarkwonImpl.java b/markwon/src/main/java/ru/noties/markwon/MarkwonImpl.java index fe6747c7..2114f050 100644 --- a/markwon/src/main/java/ru/noties/markwon/MarkwonImpl.java +++ b/markwon/src/main/java/ru/noties/markwon/MarkwonImpl.java @@ -1,6 +1,7 @@ package ru.noties.markwon; import android.support.annotation.NonNull; +import android.text.Spanned; import android.widget.TextView; import org.commonmark.node.Node; @@ -39,7 +40,7 @@ class MarkwonImpl extends Markwon { @NonNull @Override - public CharSequence render(@NonNull Node node) { + public Spanned render(@NonNull Node node) { for (MarkwonPlugin plugin : plugins) { plugin.beforeRender(node); @@ -51,12 +52,12 @@ class MarkwonImpl extends Markwon { plugin.afterRender(node, visitor); } - return visitor.builder().text(); + return visitor.builder().spannableStringBuilder(); } @NonNull @Override - public CharSequence toMarkdown(@NonNull String input) { + public Spanned toMarkdown(@NonNull String input) { return render(parse(input)); } From 1245712ef6e8ccfee3040d369aa0670a80333ba6 Mon Sep 17 00:00:00 2001 From: Dimitry Ivanov Date: Mon, 17 Dec 2018 17:10:58 +0300 Subject: [PATCH 040/103] Migrating tests to new format --- .../markwon/core/suite/BaseSuiteTest.java | 16 +-- .../markwon/core/suite/BlockquoteTest.java | 48 ++++++++ .../{BoldItalic.java => BoldItalicTest.java} | 2 +- .../suite/{CodeBlocks.java => CodeTest.java} | 28 ++++- ...eeplyNested.java => DeeplyNestedTest.java} | 2 +- .../core/suite/{First.java => FirstTest.java} | 2 +- .../markwon/core/suite/NoParagraphsTest.java | 33 ++++++ .../markwon/core/suite/OrderedListTest.java | 108 ++++++++++++++++++ .../markwon/core/suite/ParagraphTest.java | 43 +++++++ .../noties/markwon/core/suite/SecondTest.java | 63 ++++++++++ .../markwon/core/suite/TestFactory.java | 20 +++- .../resources/tests/nested-blockquotes.md | 3 + .../resources/tests/nested-blockquotes.yaml | 12 -- .../src/test/resources/tests/no-paragraphs.md | 3 + .../test/resources/tests/no-paragraphs.yaml | 12 -- .../src/test/resources/tests/ol-2-spaces.md | 3 + .../src/test/resources/tests/ol-2-spaces.yaml | 16 --- .../test/resources/tests/ol-starts-with-5.md | 3 + .../resources/tests/ol-starts-with-5.yaml | 14 --- markwon/src/test/resources/tests/ol.md | 3 + markwon/src/test/resources/tests/ol.yaml | 14 --- markwon/src/test/resources/tests/paragraph.md | 3 + .../src/test/resources/tests/paragraph.yaml | 12 -- markwon/src/test/resources/tests/second.md | 12 ++ markwon/src/test/resources/tests/second.yaml | 29 ----- .../test/resources/tests/single-blockquote.md | 1 + .../resources/tests/single-blockquote.yaml | 4 - .../test/resources/tests/single-code-block.md | 3 + .../resources/tests/single-code-block.yaml | 7 -- .../src/test/resources/tests/single-code.md | 1 + .../src/test/resources/tests/single-code.yaml | 4 - markwon/src/test/resources/tests/single-ol.md | 1 + .../src/test/resources/tests/single-ol.yaml | 5 - 33 files changed, 382 insertions(+), 148 deletions(-) create mode 100644 markwon/src/test/java/ru/noties/markwon/core/suite/BlockquoteTest.java rename markwon/src/test/java/ru/noties/markwon/core/suite/{BoldItalic.java => BoldItalicTest.java} (93%) rename markwon/src/test/java/ru/noties/markwon/core/suite/{CodeBlocks.java => CodeTest.java} (64%) rename markwon/src/test/java/ru/noties/markwon/core/suite/{DeeplyNested.java => DeeplyNestedTest.java} (95%) rename markwon/src/test/java/ru/noties/markwon/core/suite/{First.java => FirstTest.java} (96%) create mode 100644 markwon/src/test/java/ru/noties/markwon/core/suite/NoParagraphsTest.java create mode 100644 markwon/src/test/java/ru/noties/markwon/core/suite/OrderedListTest.java create mode 100644 markwon/src/test/java/ru/noties/markwon/core/suite/ParagraphTest.java create mode 100644 markwon/src/test/java/ru/noties/markwon/core/suite/SecondTest.java create mode 100644 markwon/src/test/resources/tests/nested-blockquotes.md delete mode 100644 markwon/src/test/resources/tests/nested-blockquotes.yaml create mode 100644 markwon/src/test/resources/tests/no-paragraphs.md delete mode 100644 markwon/src/test/resources/tests/no-paragraphs.yaml create mode 100644 markwon/src/test/resources/tests/ol-2-spaces.md delete mode 100644 markwon/src/test/resources/tests/ol-2-spaces.yaml create mode 100644 markwon/src/test/resources/tests/ol-starts-with-5.md delete mode 100644 markwon/src/test/resources/tests/ol-starts-with-5.yaml create mode 100644 markwon/src/test/resources/tests/ol.md delete mode 100644 markwon/src/test/resources/tests/ol.yaml create mode 100644 markwon/src/test/resources/tests/paragraph.md delete mode 100644 markwon/src/test/resources/tests/paragraph.yaml create mode 100644 markwon/src/test/resources/tests/second.md delete mode 100644 markwon/src/test/resources/tests/second.yaml create mode 100644 markwon/src/test/resources/tests/single-blockquote.md delete mode 100644 markwon/src/test/resources/tests/single-blockquote.yaml create mode 100644 markwon/src/test/resources/tests/single-code-block.md delete mode 100644 markwon/src/test/resources/tests/single-code-block.yaml create mode 100644 markwon/src/test/resources/tests/single-code.md delete mode 100644 markwon/src/test/resources/tests/single-code.yaml create mode 100644 markwon/src/test/resources/tests/single-ol.md delete mode 100644 markwon/src/test/resources/tests/single-ol.yaml diff --git a/markwon/src/test/java/ru/noties/markwon/core/suite/BaseSuiteTest.java b/markwon/src/test/java/ru/noties/markwon/core/suite/BaseSuiteTest.java index 6fec8d8d..f2604a67 100644 --- a/markwon/src/test/java/ru/noties/markwon/core/suite/BaseSuiteTest.java +++ b/markwon/src/test/java/ru/noties/markwon/core/suite/BaseSuiteTest.java @@ -15,17 +15,13 @@ import ru.noties.markwon.test.TestUtil; abstract class BaseSuiteTest { - void matches(@NonNull String input, @NonNull TestSpan.Document document) { - final Spanned spanned = (Spanned) markwon().toMarkdown(input); + void matchInput(@NonNull String name, @NonNull TestSpan.Document document) { + final Spanned spanned = markwon().toMarkdown(read(name)); TestSpanMatcher.matches(spanned, document); } - void matchInput(@NonNull String name, @NonNull TestSpan.Document document) { - matches(read(name), document); - } - @NonNull - String read(@NonNull String name) { + private String read(@NonNull String name) { return TestUtil.read(this, "tests/" + name); } @@ -36,9 +32,13 @@ abstract class BaseSuiteTest { .use(new AbstractMarkwonPlugin() { @Override public void configureConfiguration(@NonNull MarkwonConfiguration.Builder builder) { - builder.factory(new TestFactory()); + builder.factory(new TestFactory(useParagraphs())); } }) .build(); } + + boolean useParagraphs() { + return false; + } } diff --git a/markwon/src/test/java/ru/noties/markwon/core/suite/BlockquoteTest.java b/markwon/src/test/java/ru/noties/markwon/core/suite/BlockquoteTest.java new file mode 100644 index 00000000..d5d0a8cb --- /dev/null +++ b/markwon/src/test/java/ru/noties/markwon/core/suite/BlockquoteTest.java @@ -0,0 +1,48 @@ +package ru.noties.markwon.core.suite; + +import org.junit.Test; +import org.junit.runner.RunWith; +import org.robolectric.RobolectricTestRunner; +import org.robolectric.annotation.Config; + +import ru.noties.markwon.test.TestSpan.Document; + +import static ru.noties.markwon.core.suite.TestFactory.BLOCK_QUOTE; +import static ru.noties.markwon.test.TestSpan.document; +import static ru.noties.markwon.test.TestSpan.span; +import static ru.noties.markwon.test.TestSpan.text; + +@RunWith(RobolectricTestRunner.class) +@Config(manifest = Config.NONE) +public class BlockquoteTest extends BaseSuiteTest { + + /* + > First + > > Second + > > > Third + */ + + @Test + public void nested() { + + final Document document = document( + span(BLOCK_QUOTE, + text("First\n\n"), + span(BLOCK_QUOTE, + text("Second\n\n"), + span(BLOCK_QUOTE, + text("Third")))) + ); + + matchInput("nested-blockquotes.md", document); + } + + @Test + public void single() { + + final Document document = document( + span(BLOCK_QUOTE, text("blockquote"))); + + matchInput("single-blockquote.md", document); + } +} diff --git a/markwon/src/test/java/ru/noties/markwon/core/suite/BoldItalic.java b/markwon/src/test/java/ru/noties/markwon/core/suite/BoldItalicTest.java similarity index 93% rename from markwon/src/test/java/ru/noties/markwon/core/suite/BoldItalic.java rename to markwon/src/test/java/ru/noties/markwon/core/suite/BoldItalicTest.java index c0ad4bd1..0fa65ddd 100644 --- a/markwon/src/test/java/ru/noties/markwon/core/suite/BoldItalic.java +++ b/markwon/src/test/java/ru/noties/markwon/core/suite/BoldItalicTest.java @@ -15,7 +15,7 @@ import static ru.noties.markwon.test.TestSpan.text; @RunWith(RobolectricTestRunner.class) @Config(manifest = Config.NONE) -public class BoldItalic extends BaseSuiteTest { +public class BoldItalicTest extends BaseSuiteTest { /* **_bold italic_** diff --git a/markwon/src/test/java/ru/noties/markwon/core/suite/CodeBlocks.java b/markwon/src/test/java/ru/noties/markwon/core/suite/CodeTest.java similarity index 64% rename from markwon/src/test/java/ru/noties/markwon/core/suite/CodeBlocks.java rename to markwon/src/test/java/ru/noties/markwon/core/suite/CodeTest.java index 6427f0ec..1331b7b8 100644 --- a/markwon/src/test/java/ru/noties/markwon/core/suite/CodeBlocks.java +++ b/markwon/src/test/java/ru/noties/markwon/core/suite/CodeTest.java @@ -5,7 +5,7 @@ import org.junit.runner.RunWith; import org.robolectric.RobolectricTestRunner; import org.robolectric.annotation.Config; -import ru.noties.markwon.test.TestSpan; +import ru.noties.markwon.test.TestSpan.Document; import static ru.noties.markwon.core.suite.TestFactory.CODE; import static ru.noties.markwon.test.TestSpan.args; @@ -15,7 +15,7 @@ import static ru.noties.markwon.test.TestSpan.text; @RunWith(RobolectricTestRunner.class) @Config(manifest = Config.NONE) -public class CodeBlocks extends BaseSuiteTest { +public class CodeTest extends BaseSuiteTest { /* ```java @@ -30,9 +30,9 @@ public class CodeBlocks extends BaseSuiteTest { */ @Test - public void test() { + public void multiple_blocks() { - final TestSpan.Document document = document( + final Document document = document( span(CODE, args("multiline", true), text("\u00a0\nfinal String s = null;\n\u00a0")), @@ -48,4 +48,24 @@ public class CodeBlocks extends BaseSuiteTest { matchInput("code-blocks.md", document); } + + @Test + public void single() { + + final Document document = document( + span(CODE, args("multiline", false), text("\u00a0code\u00a0")) + ); + + matchInput("single-code.md", document); + } + + @Test + public void single_block() { + + final Document document = document( + span(CODE, args("multiline", true), text("\u00a0\ncode block\n\u00a0")) + ); + + matchInput("single-code-block.md", document); + } } diff --git a/markwon/src/test/java/ru/noties/markwon/core/suite/DeeplyNested.java b/markwon/src/test/java/ru/noties/markwon/core/suite/DeeplyNestedTest.java similarity index 95% rename from markwon/src/test/java/ru/noties/markwon/core/suite/DeeplyNested.java rename to markwon/src/test/java/ru/noties/markwon/core/suite/DeeplyNestedTest.java index 16e5b2e6..49606f90 100644 --- a/markwon/src/test/java/ru/noties/markwon/core/suite/DeeplyNested.java +++ b/markwon/src/test/java/ru/noties/markwon/core/suite/DeeplyNestedTest.java @@ -17,7 +17,7 @@ import static ru.noties.markwon.test.TestSpan.text; @RunWith(RobolectricTestRunner.class) @Config(manifest = Config.NONE) -public class DeeplyNested extends BaseSuiteTest { +public class DeeplyNestedTest extends BaseSuiteTest { /* **bold *bold italic `bold italic code` bold italic* bold** normal diff --git a/markwon/src/test/java/ru/noties/markwon/core/suite/First.java b/markwon/src/test/java/ru/noties/markwon/core/suite/FirstTest.java similarity index 96% rename from markwon/src/test/java/ru/noties/markwon/core/suite/First.java rename to markwon/src/test/java/ru/noties/markwon/core/suite/FirstTest.java index 9aaf05ed..340b5fe9 100644 --- a/markwon/src/test/java/ru/noties/markwon/core/suite/First.java +++ b/markwon/src/test/java/ru/noties/markwon/core/suite/FirstTest.java @@ -17,7 +17,7 @@ import static ru.noties.markwon.test.TestSpan.text; @RunWith(RobolectricTestRunner.class) @Config(manifest = Config.NONE) -public class First extends BaseSuiteTest { +public class FirstTest extends BaseSuiteTest { /* Here is some [link](https://my.href) diff --git a/markwon/src/test/java/ru/noties/markwon/core/suite/NoParagraphsTest.java b/markwon/src/test/java/ru/noties/markwon/core/suite/NoParagraphsTest.java new file mode 100644 index 00000000..32ea306b --- /dev/null +++ b/markwon/src/test/java/ru/noties/markwon/core/suite/NoParagraphsTest.java @@ -0,0 +1,33 @@ +package ru.noties.markwon.core.suite; + +import org.junit.Test; +import org.junit.runner.RunWith; +import org.robolectric.RobolectricTestRunner; +import org.robolectric.annotation.Config; + +import ru.noties.markwon.test.TestSpan.Document; + +import static ru.noties.markwon.test.TestSpan.document; +import static ru.noties.markwon.test.TestSpan.text; + +@RunWith(RobolectricTestRunner.class) +@Config(manifest = Config.NONE) +public class NoParagraphsTest extends BaseSuiteTest { + /* + This could be a paragraph + + But it is not and this one is not also + */ + + @Test + public void test() { + + final Document document = document( + text("This could be a paragraph"), + text("\n\n"), + text("But it is not and this one is not also") + ); + + matchInput("no-paragraphs.md", document); + } +} diff --git a/markwon/src/test/java/ru/noties/markwon/core/suite/OrderedListTest.java b/markwon/src/test/java/ru/noties/markwon/core/suite/OrderedListTest.java new file mode 100644 index 00000000..9a674b3f --- /dev/null +++ b/markwon/src/test/java/ru/noties/markwon/core/suite/OrderedListTest.java @@ -0,0 +1,108 @@ +package ru.noties.markwon.core.suite; + +import org.junit.Test; +import org.junit.runner.RunWith; +import org.robolectric.RobolectricTestRunner; +import org.robolectric.annotation.Config; + +import ru.noties.markwon.test.TestSpan.Document; + +import static ru.noties.markwon.core.suite.TestFactory.ORDERED_LIST; +import static ru.noties.markwon.test.TestSpan.args; +import static ru.noties.markwon.test.TestSpan.document; +import static ru.noties.markwon.test.TestSpan.span; +import static ru.noties.markwon.test.TestSpan.text; + +@RunWith(RobolectricTestRunner.class) +@Config(manifest = Config.NONE) +public class OrderedListTest extends BaseSuiteTest { + + /* + 1. First + 1. Second + 1. Third + */ + + @Test + public void nested() { + + // wanted to use 1,2,3 as start numbers, but anything but `1` won't be treated as sub-list + + final Document document = document( + span(ORDERED_LIST, + args("start", 1), + text("First\n"), + span(ORDERED_LIST, + args("start", 1), + text("Second\n"), + span(ORDERED_LIST, + args("start", 1), + text("Third")))) + ); + + matchInput("ol.md", document); + } + + /* + 1. First + 2. Second + 3. Third + */ + @Test + public void two_spaces() { + // just a regular flat-list (no sub-lists) + + final Document document = document( + span(ORDERED_LIST, + args("start", 1), + text("First")), + text("\n"), + span(ORDERED_LIST, + args("start", 2), + text("Second")), + text("\n"), + span(ORDERED_LIST, + args("start", 3), + text("Third")) + ); + + matchInput("ol-2-spaces.md", document); + } + + /* + 5. Five + 6. Six + 7. Seven + */ + @Test + public void starts_with_5() { + + final Document document = document( + span(ORDERED_LIST, + args("start", 5), + text("Five")), + text("\n"), + span(ORDERED_LIST, + args("start", 6), + text("Six")), + text("\n"), + span(ORDERED_LIST, + args("start", 7), + text("Seven")) + ); + + matchInput("ol-starts-with-5.md", document); + } + + @Test + public void single() { + + final Document document = document( + span(ORDERED_LIST, + args("start", 1), + text("ol")) + ); + + matchInput("single-ol.md", document); + } +} diff --git a/markwon/src/test/java/ru/noties/markwon/core/suite/ParagraphTest.java b/markwon/src/test/java/ru/noties/markwon/core/suite/ParagraphTest.java new file mode 100644 index 00000000..79b678c5 --- /dev/null +++ b/markwon/src/test/java/ru/noties/markwon/core/suite/ParagraphTest.java @@ -0,0 +1,43 @@ +package ru.noties.markwon.core.suite; + +import org.junit.Test; +import org.junit.runner.RunWith; +import org.robolectric.RobolectricTestRunner; +import org.robolectric.annotation.Config; + +import ru.noties.markwon.test.TestSpan.Document; + +import static ru.noties.markwon.core.suite.TestFactory.PARAGRAPH; +import static ru.noties.markwon.test.TestSpan.document; +import static ru.noties.markwon.test.TestSpan.span; +import static ru.noties.markwon.test.TestSpan.text; + +@RunWith(RobolectricTestRunner.class) +@Config(manifest = Config.NONE) +public class ParagraphTest extends BaseSuiteTest { + + /* + So, this is a paragraph + + And this one is another + */ + + @Test + public void test() { + + final Document document = document( + span(PARAGRAPH, + text("So, this is a paragraph")), + text("\n\n"), + span(PARAGRAPH, + text("And this one is another")) + ); + + matchInput("paragraph.md", document); + } + + @Override + boolean useParagraphs() { + return true; + } +} diff --git a/markwon/src/test/java/ru/noties/markwon/core/suite/SecondTest.java b/markwon/src/test/java/ru/noties/markwon/core/suite/SecondTest.java new file mode 100644 index 00000000..65f238ed --- /dev/null +++ b/markwon/src/test/java/ru/noties/markwon/core/suite/SecondTest.java @@ -0,0 +1,63 @@ +package ru.noties.markwon.core.suite; + +import org.junit.Test; +import org.junit.runner.RunWith; +import org.robolectric.RobolectricTestRunner; +import org.robolectric.annotation.Config; + +import ru.noties.markwon.test.TestSpan.Document; + +import static ru.noties.markwon.core.suite.TestFactory.BLOCK_QUOTE; +import static ru.noties.markwon.core.suite.TestFactory.BOLD; +import static ru.noties.markwon.core.suite.TestFactory.CODE; +import static ru.noties.markwon.core.suite.TestFactory.HEADING; +import static ru.noties.markwon.core.suite.TestFactory.ITALIC; +import static ru.noties.markwon.test.TestSpan.args; +import static ru.noties.markwon.test.TestSpan.document; +import static ru.noties.markwon.test.TestSpan.span; +import static ru.noties.markwon.test.TestSpan.text; + +@RunWith(RobolectricTestRunner.class) +@Config(manifest = Config.NONE) +public class SecondTest extends BaseSuiteTest { + + /* +First **line** is *always* + +> Some quote here! + +# Header 1 +## Header 2 + +and `some code` and more: + +```java +the code in multiline +``` + */ + + @Test + public void test() { + + final Document document = document( + text("First "), + span(BOLD, text("line")), + text(" is "), + span(ITALIC, text("always")), + text("\n\n"), + span(BLOCK_QUOTE, text("Some quote here!")), + text("\n\n"), + span(HEADING, args("level", 1), text("Header 1")), + text("\n\n"), + span(HEADING, args("level", 2), text("Header 2")), + text("\n\n"), + text("and "), + span(CODE, args("multiline", false), text("\u00a0some code\u00a0")), + text(" and more:"), + text("\n\n"), + span(CODE, args("multiline", true), text("\u00a0\nthe code in multiline\n\u00a0")) + ); + + matchInput("second.md", document); + } +} diff --git a/markwon/src/test/java/ru/noties/markwon/core/suite/TestFactory.java b/markwon/src/test/java/ru/noties/markwon/core/suite/TestFactory.java index 222e2092..76e4dc03 100644 --- a/markwon/src/test/java/ru/noties/markwon/core/suite/TestFactory.java +++ b/markwon/src/test/java/ru/noties/markwon/core/suite/TestFactory.java @@ -19,6 +19,16 @@ class TestFactory implements SpannableFactory { static final String ITALIC = "italic"; static final String CODE = "code"; static final String LINK = "link"; + static final String BLOCK_QUOTE = "blockquote"; + static final String PARAGRAPH = "paragraph"; + static final String ORDERED_LIST = "ordered-list"; + static final String HEADING = "heading"; + + private final boolean useParagraphs; + + TestFactory(boolean useParagraphs) { + this.useParagraphs = useParagraphs; + } @Nullable @Override @@ -35,7 +45,7 @@ class TestFactory implements SpannableFactory { @Nullable @Override public Object blockQuote(@NonNull MarkwonTheme theme) { - return null; + return span(BLOCK_QUOTE); } @Nullable @@ -47,7 +57,7 @@ class TestFactory implements SpannableFactory { @Nullable @Override public Object orderedListItem(@NonNull MarkwonTheme theme, int startNumber) { - return null; + return span(ORDERED_LIST, args("start", startNumber)); } @Nullable @@ -65,13 +75,15 @@ class TestFactory implements SpannableFactory { @Nullable @Override public Object heading(@NonNull MarkwonTheme theme, int level) { - return null; + return span(HEADING, args("level", level)); } @Nullable @Override public Object paragraph(boolean inTightList) { - return null; + return useParagraphs + ? span(PARAGRAPH) + : null; } @Nullable diff --git a/markwon/src/test/resources/tests/nested-blockquotes.md b/markwon/src/test/resources/tests/nested-blockquotes.md new file mode 100644 index 00000000..08b25416 --- /dev/null +++ b/markwon/src/test/resources/tests/nested-blockquotes.md @@ -0,0 +1,3 @@ +> First +> > Second +> > > Third \ No newline at end of file diff --git a/markwon/src/test/resources/tests/nested-blockquotes.yaml b/markwon/src/test/resources/tests/nested-blockquotes.yaml deleted file mode 100644 index fbdb04b9..00000000 --- a/markwon/src/test/resources/tests/nested-blockquotes.yaml +++ /dev/null @@ -1,12 +0,0 @@ -input: |- - > First - > > Second - > > > Third - -output: - - blockquote: - - "First\n\n" - - blockquote: - - "Second\n\n" - - blockquote: - - "Third" \ No newline at end of file diff --git a/markwon/src/test/resources/tests/no-paragraphs.md b/markwon/src/test/resources/tests/no-paragraphs.md new file mode 100644 index 00000000..75fa2e29 --- /dev/null +++ b/markwon/src/test/resources/tests/no-paragraphs.md @@ -0,0 +1,3 @@ +This could be a paragraph + +But it is not and this one is not also \ No newline at end of file diff --git a/markwon/src/test/resources/tests/no-paragraphs.yaml b/markwon/src/test/resources/tests/no-paragraphs.yaml deleted file mode 100644 index 048f3ef4..00000000 --- a/markwon/src/test/resources/tests/no-paragraphs.yaml +++ /dev/null @@ -1,12 +0,0 @@ -input: |- - This could be a paragraph - - But it is not and this one is not also - -config: - use-paragraphs: false - -output: - - text: "This could be a paragraph" - - text: "\n\n" - - text: "But it is not and this one is not also" \ No newline at end of file diff --git a/markwon/src/test/resources/tests/ol-2-spaces.md b/markwon/src/test/resources/tests/ol-2-spaces.md new file mode 100644 index 00000000..08087952 --- /dev/null +++ b/markwon/src/test/resources/tests/ol-2-spaces.md @@ -0,0 +1,3 @@ +1. First + 2. Second + 3. Third \ No newline at end of file diff --git a/markwon/src/test/resources/tests/ol-2-spaces.yaml b/markwon/src/test/resources/tests/ol-2-spaces.yaml deleted file mode 100644 index 0ffd7adb..00000000 --- a/markwon/src/test/resources/tests/ol-2-spaces.yaml +++ /dev/null @@ -1,16 +0,0 @@ -description: "Will be rendered as simple flat list" - -input: |- - 1. First - 2. Second - 3. Third - -output: - - ol: "First" - start: 1 - - text: "\n" - - ol: "Second" - start: 2 - - text: "\n" - - ol: "Third" - start: 3 \ No newline at end of file diff --git a/markwon/src/test/resources/tests/ol-starts-with-5.md b/markwon/src/test/resources/tests/ol-starts-with-5.md new file mode 100644 index 00000000..384dba30 --- /dev/null +++ b/markwon/src/test/resources/tests/ol-starts-with-5.md @@ -0,0 +1,3 @@ +5. Five +6. Six +7. Seven \ No newline at end of file diff --git a/markwon/src/test/resources/tests/ol-starts-with-5.yaml b/markwon/src/test/resources/tests/ol-starts-with-5.yaml deleted file mode 100644 index eaabc7bb..00000000 --- a/markwon/src/test/resources/tests/ol-starts-with-5.yaml +++ /dev/null @@ -1,14 +0,0 @@ -input: |- - 5. Five - 6. Six - 7. Seven - -output: - - ol: "Five" - start: 5 - - text: "\n" - - ol: "Six" - start: 6 - - text: "\n" - - ol: "Seven" - start: 7 \ No newline at end of file diff --git a/markwon/src/test/resources/tests/ol.md b/markwon/src/test/resources/tests/ol.md new file mode 100644 index 00000000..d4e5b9db --- /dev/null +++ b/markwon/src/test/resources/tests/ol.md @@ -0,0 +1,3 @@ +1. First + 1. Second + 1. Third \ No newline at end of file diff --git a/markwon/src/test/resources/tests/ol.yaml b/markwon/src/test/resources/tests/ol.yaml deleted file mode 100644 index 246b84ba..00000000 --- a/markwon/src/test/resources/tests/ol.yaml +++ /dev/null @@ -1,14 +0,0 @@ -input: |- - 1. First - 1. Second - 1. Third - -output: - - ol: - - text: "First\n" - - ol: - - text: "Second\n" - - ol: "Third" - start: 1 - start: 1 - start: 1 \ No newline at end of file diff --git a/markwon/src/test/resources/tests/paragraph.md b/markwon/src/test/resources/tests/paragraph.md new file mode 100644 index 00000000..98fce9a0 --- /dev/null +++ b/markwon/src/test/resources/tests/paragraph.md @@ -0,0 +1,3 @@ +So, this is a paragraph + +And this one is another \ No newline at end of file diff --git a/markwon/src/test/resources/tests/paragraph.yaml b/markwon/src/test/resources/tests/paragraph.yaml deleted file mode 100644 index 862bbc06..00000000 --- a/markwon/src/test/resources/tests/paragraph.yaml +++ /dev/null @@ -1,12 +0,0 @@ -input: |- - So, this is a paragraph - - And this one is another - -config: - use-paragraphs: true - -output: - - p: "So, this is a paragraph" - - text: "\n\n" - - p: "And this one is another" \ No newline at end of file diff --git a/markwon/src/test/resources/tests/second.md b/markwon/src/test/resources/tests/second.md new file mode 100644 index 00000000..44410d75 --- /dev/null +++ b/markwon/src/test/resources/tests/second.md @@ -0,0 +1,12 @@ +First **line** is *always* + +> Some quote here! + +# Header 1 +## Header 2 + +and `some code` and more: + +```java +the code in multiline +``` \ No newline at end of file diff --git a/markwon/src/test/resources/tests/second.yaml b/markwon/src/test/resources/tests/second.yaml deleted file mode 100644 index e34e0270..00000000 --- a/markwon/src/test/resources/tests/second.yaml +++ /dev/null @@ -1,29 +0,0 @@ -input: |- - First **line** is *always* - - > Some quote here! - - # Header 1 - ## Header 2 - - and `some code` and more: - - ```java - the code in multiline - ``` - -output: - - text: "First " - - b: "line" - - text: " is " - - i: "always" - - text: "\n\n" - - blockquote: "Some quote here!" - - text: "\n\n" - - h1: "Header 1" - - text: "\n\n" - - h2: "Header 2" - - text: "\n\nand " - - code: "some code" - - text: " and more:\n\n" - - code-block: "the code in multiline" diff --git a/markwon/src/test/resources/tests/single-blockquote.md b/markwon/src/test/resources/tests/single-blockquote.md new file mode 100644 index 00000000..32ce4098 --- /dev/null +++ b/markwon/src/test/resources/tests/single-blockquote.md @@ -0,0 +1 @@ +> blockquote \ No newline at end of file diff --git a/markwon/src/test/resources/tests/single-blockquote.yaml b/markwon/src/test/resources/tests/single-blockquote.yaml deleted file mode 100644 index 3c8d818e..00000000 --- a/markwon/src/test/resources/tests/single-blockquote.yaml +++ /dev/null @@ -1,4 +0,0 @@ -input: "> blockquote" - -output: - - blockquote: "blockquote" \ No newline at end of file diff --git a/markwon/src/test/resources/tests/single-code-block.md b/markwon/src/test/resources/tests/single-code-block.md new file mode 100644 index 00000000..fc057a09 --- /dev/null +++ b/markwon/src/test/resources/tests/single-code-block.md @@ -0,0 +1,3 @@ +``` +code block +``` \ No newline at end of file diff --git a/markwon/src/test/resources/tests/single-code-block.yaml b/markwon/src/test/resources/tests/single-code-block.yaml deleted file mode 100644 index d44d9818..00000000 --- a/markwon/src/test/resources/tests/single-code-block.yaml +++ /dev/null @@ -1,7 +0,0 @@ -input: |- - ``` - code block - ``` - -output: - - code-block: "code block" \ No newline at end of file diff --git a/markwon/src/test/resources/tests/single-code.md b/markwon/src/test/resources/tests/single-code.md new file mode 100644 index 00000000..d3a84179 --- /dev/null +++ b/markwon/src/test/resources/tests/single-code.md @@ -0,0 +1 @@ +`code` \ No newline at end of file diff --git a/markwon/src/test/resources/tests/single-code.yaml b/markwon/src/test/resources/tests/single-code.yaml deleted file mode 100644 index e82d1123..00000000 --- a/markwon/src/test/resources/tests/single-code.yaml +++ /dev/null @@ -1,4 +0,0 @@ -input: "`code`" - -output: - - code: "code" \ No newline at end of file diff --git a/markwon/src/test/resources/tests/single-ol.md b/markwon/src/test/resources/tests/single-ol.md new file mode 100644 index 00000000..61a41e63 --- /dev/null +++ b/markwon/src/test/resources/tests/single-ol.md @@ -0,0 +1 @@ +1. ol \ No newline at end of file diff --git a/markwon/src/test/resources/tests/single-ol.yaml b/markwon/src/test/resources/tests/single-ol.yaml deleted file mode 100644 index a2046cb1..00000000 --- a/markwon/src/test/resources/tests/single-ol.yaml +++ /dev/null @@ -1,5 +0,0 @@ -input: "1. ol" - -output: - - ol: "ol" - start: 1 \ No newline at end of file From 066b634beea607bcdeb4cda417e29dfe60c30bf8 Mon Sep 17 00:00:00 2001 From: Dimitry Ivanov Date: Mon, 17 Dec 2018 18:25:48 +0300 Subject: [PATCH 041/103] Migrated existing tests to new format --- .../noties/markwon/test/TestSpanMatcher.java | 3 +- .../markwon/core/suite/BaseSuiteTest.java | 11 ++- .../markwon/core/suite/BlockquoteTest.java | 2 +- .../markwon/core/suite/BoldItalicTest.java | 6 +- .../noties/markwon/core/suite/CodeTest.java | 2 +- .../markwon/core/suite/EmphasisTest.java | 27 ++++++ .../markwon/core/suite/HeadingTest.java | 40 +++++++++ .../noties/markwon/core/suite/LinkTest.java | 29 +++++++ .../markwon/core/suite/OrderedListTest.java | 2 +- .../core/suite/SoftBreakAddsNewLineTest.java | 39 +++++++++ .../markwon/core/suite/SoftBreakTest.java | 33 ++++++++ .../core/suite/StrongEmphasisTest.java | 29 +++++++ .../markwon/core/suite/TestFactory.java | 6 +- .../markwon/core/suite/ThematicBreakTest.java | 30 +++++++ .../markwon/core/suite/UnOrderedListTest.java | 84 +++++++++++++++++++ .../src/test/resources/tests/bold-italic.md | 1 - .../src/test/resources/tests/single-a.yaml | 5 -- .../src/test/resources/tests/single-b.yaml | 4 - .../test/resources/tests/single-blockquote.md | 1 - .../src/test/resources/tests/single-code.md | 1 - .../src/test/resources/tests/single-h1.yaml | 4 - .../src/test/resources/tests/single-h2.yaml | 4 - .../src/test/resources/tests/single-h3.yaml | 4 - .../src/test/resources/tests/single-h4.yaml | 4 - .../src/test/resources/tests/single-h5.yaml | 4 - .../src/test/resources/tests/single-h6.yaml | 4 - .../src/test/resources/tests/single-hr.yaml | 7 -- .../src/test/resources/tests/single-i.yaml | 4 - markwon/src/test/resources/tests/single-ol.md | 1 - .../src/test/resources/tests/single-ul.yaml | 5 -- .../tests/soft-break-adds-new-line.md | 3 + .../tests/soft-break-adds-new-line.yaml | 10 --- .../src/test/resources/tests/soft-break.md | 3 + .../src/test/resources/tests/soft-break.yaml | 7 -- markwon/src/test/resources/tests/ul-levels.md | 3 + .../src/test/resources/tests/ul-levels.yaml | 20 ----- markwon/src/test/resources/tests/ul.md | 3 + markwon/src/test/resources/tests/ul.yaml | 14 ---- 38 files changed, 343 insertions(+), 116 deletions(-) create mode 100644 markwon/src/test/java/ru/noties/markwon/core/suite/EmphasisTest.java create mode 100644 markwon/src/test/java/ru/noties/markwon/core/suite/HeadingTest.java create mode 100644 markwon/src/test/java/ru/noties/markwon/core/suite/LinkTest.java create mode 100644 markwon/src/test/java/ru/noties/markwon/core/suite/SoftBreakAddsNewLineTest.java create mode 100644 markwon/src/test/java/ru/noties/markwon/core/suite/SoftBreakTest.java create mode 100644 markwon/src/test/java/ru/noties/markwon/core/suite/StrongEmphasisTest.java create mode 100644 markwon/src/test/java/ru/noties/markwon/core/suite/ThematicBreakTest.java create mode 100644 markwon/src/test/java/ru/noties/markwon/core/suite/UnOrderedListTest.java delete mode 100644 markwon/src/test/resources/tests/bold-italic.md delete mode 100644 markwon/src/test/resources/tests/single-a.yaml delete mode 100644 markwon/src/test/resources/tests/single-b.yaml delete mode 100644 markwon/src/test/resources/tests/single-blockquote.md delete mode 100644 markwon/src/test/resources/tests/single-code.md delete mode 100644 markwon/src/test/resources/tests/single-h1.yaml delete mode 100644 markwon/src/test/resources/tests/single-h2.yaml delete mode 100644 markwon/src/test/resources/tests/single-h3.yaml delete mode 100644 markwon/src/test/resources/tests/single-h4.yaml delete mode 100644 markwon/src/test/resources/tests/single-h5.yaml delete mode 100644 markwon/src/test/resources/tests/single-h6.yaml delete mode 100644 markwon/src/test/resources/tests/single-hr.yaml delete mode 100644 markwon/src/test/resources/tests/single-i.yaml delete mode 100644 markwon/src/test/resources/tests/single-ol.md delete mode 100644 markwon/src/test/resources/tests/single-ul.yaml create mode 100644 markwon/src/test/resources/tests/soft-break-adds-new-line.md delete mode 100644 markwon/src/test/resources/tests/soft-break-adds-new-line.yaml create mode 100644 markwon/src/test/resources/tests/soft-break.md delete mode 100644 markwon/src/test/resources/tests/soft-break.yaml create mode 100644 markwon/src/test/resources/tests/ul-levels.md delete mode 100644 markwon/src/test/resources/tests/ul-levels.yaml create mode 100644 markwon/src/test/resources/tests/ul.md delete mode 100644 markwon/src/test/resources/tests/ul.yaml diff --git a/markwon-test-span/src/main/java/ru/noties/markwon/test/TestSpanMatcher.java b/markwon-test-span/src/main/java/ru/noties/markwon/test/TestSpanMatcher.java index 1eb2a1bc..df014266 100644 --- a/markwon-test-span/src/main/java/ru/noties/markwon/test/TestSpanMatcher.java +++ b/markwon-test-span/src/main/java/ru/noties/markwon/test/TestSpanMatcher.java @@ -84,7 +84,8 @@ public abstract class TestSpanMatcher { public boolean test(TestSpan.Span span) { return expected.name().equals(span.name()) && start == spanned.getSpanStart(span) - && end == spanned.getSpanEnd(span); + && end == spanned.getSpanEnd(span) + && expected.arguments().equals(span.arguments()); } }) .first(null); diff --git a/markwon/src/test/java/ru/noties/markwon/core/suite/BaseSuiteTest.java b/markwon/src/test/java/ru/noties/markwon/core/suite/BaseSuiteTest.java index f2604a67..73c05bdf 100644 --- a/markwon/src/test/java/ru/noties/markwon/core/suite/BaseSuiteTest.java +++ b/markwon/src/test/java/ru/noties/markwon/core/suite/BaseSuiteTest.java @@ -15,6 +15,11 @@ import ru.noties.markwon.test.TestUtil; abstract class BaseSuiteTest { + void match(@NonNull String markdown, @NonNull TestSpan.Document document) { + final Spanned spanned = markwon().toMarkdown(markdown); + TestSpanMatcher.matches(spanned, document); + } + void matchInput(@NonNull String name, @NonNull TestSpan.Document document) { final Spanned spanned = markwon().toMarkdown(read(name)); TestSpanMatcher.matches(spanned, document); @@ -28,7 +33,7 @@ abstract class BaseSuiteTest { @NonNull Markwon markwon() { return Markwon.builder(RuntimeEnvironment.application) - .use(CorePlugin.create()) + .use(CorePlugin.create(softBreakAddsNewLine())) .use(new AbstractMarkwonPlugin() { @Override public void configureConfiguration(@NonNull MarkwonConfiguration.Builder builder) { @@ -41,4 +46,8 @@ abstract class BaseSuiteTest { boolean useParagraphs() { return false; } + + boolean softBreakAddsNewLine() { + return false; + } } diff --git a/markwon/src/test/java/ru/noties/markwon/core/suite/BlockquoteTest.java b/markwon/src/test/java/ru/noties/markwon/core/suite/BlockquoteTest.java index d5d0a8cb..02eb2674 100644 --- a/markwon/src/test/java/ru/noties/markwon/core/suite/BlockquoteTest.java +++ b/markwon/src/test/java/ru/noties/markwon/core/suite/BlockquoteTest.java @@ -43,6 +43,6 @@ public class BlockquoteTest extends BaseSuiteTest { final Document document = document( span(BLOCK_QUOTE, text("blockquote"))); - matchInput("single-blockquote.md", document); + match("> blockquote", document); } } diff --git a/markwon/src/test/java/ru/noties/markwon/core/suite/BoldItalicTest.java b/markwon/src/test/java/ru/noties/markwon/core/suite/BoldItalicTest.java index 0fa65ddd..67cb1a0d 100644 --- a/markwon/src/test/java/ru/noties/markwon/core/suite/BoldItalicTest.java +++ b/markwon/src/test/java/ru/noties/markwon/core/suite/BoldItalicTest.java @@ -17,10 +17,6 @@ import static ru.noties.markwon.test.TestSpan.text; @Config(manifest = Config.NONE) public class BoldItalicTest extends BaseSuiteTest { - /* - **_bold italic_** - */ - @Test public void test() { @@ -28,6 +24,6 @@ public class BoldItalicTest extends BaseSuiteTest { span(BOLD, span(ITALIC, text("bold italic")))); - matchInput("bold-italic.md", document); + match("**_bold italic_**", document); } } diff --git a/markwon/src/test/java/ru/noties/markwon/core/suite/CodeTest.java b/markwon/src/test/java/ru/noties/markwon/core/suite/CodeTest.java index 1331b7b8..6e5d0c53 100644 --- a/markwon/src/test/java/ru/noties/markwon/core/suite/CodeTest.java +++ b/markwon/src/test/java/ru/noties/markwon/core/suite/CodeTest.java @@ -56,7 +56,7 @@ public class CodeTest extends BaseSuiteTest { span(CODE, args("multiline", false), text("\u00a0code\u00a0")) ); - matchInput("single-code.md", document); + match("`code`", document); } @Test diff --git a/markwon/src/test/java/ru/noties/markwon/core/suite/EmphasisTest.java b/markwon/src/test/java/ru/noties/markwon/core/suite/EmphasisTest.java new file mode 100644 index 00000000..0e1b1d1c --- /dev/null +++ b/markwon/src/test/java/ru/noties/markwon/core/suite/EmphasisTest.java @@ -0,0 +1,27 @@ +package ru.noties.markwon.core.suite; + +import org.junit.Test; +import org.junit.runner.RunWith; +import org.robolectric.RobolectricTestRunner; +import org.robolectric.annotation.Config; + +import ru.noties.markwon.test.TestSpan.Document; + +import static ru.noties.markwon.core.suite.TestFactory.ITALIC; +import static ru.noties.markwon.test.TestSpan.document; +import static ru.noties.markwon.test.TestSpan.span; +import static ru.noties.markwon.test.TestSpan.text; + +@RunWith(RobolectricTestRunner.class) +@Config(manifest = Config.NONE) +public class EmphasisTest extends BaseSuiteTest { + + @Test + public void single() { + + final Document document = document(span(ITALIC, text("italic"))); + + match("*italic*", document); + match("_italic_", document); + } +} diff --git a/markwon/src/test/java/ru/noties/markwon/core/suite/HeadingTest.java b/markwon/src/test/java/ru/noties/markwon/core/suite/HeadingTest.java new file mode 100644 index 00000000..efc1a090 --- /dev/null +++ b/markwon/src/test/java/ru/noties/markwon/core/suite/HeadingTest.java @@ -0,0 +1,40 @@ +package ru.noties.markwon.core.suite; + +import org.junit.Test; +import org.junit.runner.RunWith; +import org.robolectric.RobolectricTestRunner; +import org.robolectric.annotation.Config; + +import ru.noties.markwon.test.TestSpan.Document; + +import static ru.noties.markwon.core.suite.TestFactory.HEADING; +import static ru.noties.markwon.test.TestSpan.args; +import static ru.noties.markwon.test.TestSpan.document; +import static ru.noties.markwon.test.TestSpan.span; +import static ru.noties.markwon.test.TestSpan.text; + +@RunWith(RobolectricTestRunner.class) +@Config(manifest = Config.NONE) +public class HeadingTest extends BaseSuiteTest { + + @Test + public void single_headings() { + + final int[] levels = {1, 2, 3, 4, 5, 6}; + + for (int level : levels) { + + final Document document = document( + span(HEADING, args("level", level), text("head" + level)) + ); + + final StringBuilder builder = new StringBuilder(); + for (int i = 0; i < level; i++) { + builder.append('#'); + } + builder.append(" head").append(level); + + match(builder.toString(), document); + } + } +} diff --git a/markwon/src/test/java/ru/noties/markwon/core/suite/LinkTest.java b/markwon/src/test/java/ru/noties/markwon/core/suite/LinkTest.java new file mode 100644 index 00000000..b691db9c --- /dev/null +++ b/markwon/src/test/java/ru/noties/markwon/core/suite/LinkTest.java @@ -0,0 +1,29 @@ +package ru.noties.markwon.core.suite; + +import org.junit.Test; +import org.junit.runner.RunWith; +import org.robolectric.RobolectricTestRunner; +import org.robolectric.annotation.Config; + +import ru.noties.markwon.test.TestSpan.Document; + +import static ru.noties.markwon.core.suite.TestFactory.LINK; +import static ru.noties.markwon.test.TestSpan.args; +import static ru.noties.markwon.test.TestSpan.document; +import static ru.noties.markwon.test.TestSpan.span; +import static ru.noties.markwon.test.TestSpan.text; + +@RunWith(RobolectricTestRunner.class) +@Config(manifest = Config.NONE) +public class LinkTest extends BaseSuiteTest { + + @Test + public void single() { + + final Document document = document( + span(LINK, args("href", "#href"), text("link")) + ); + + match("[link](#href)", document); + } +} diff --git a/markwon/src/test/java/ru/noties/markwon/core/suite/OrderedListTest.java b/markwon/src/test/java/ru/noties/markwon/core/suite/OrderedListTest.java index 9a674b3f..31fed7d8 100644 --- a/markwon/src/test/java/ru/noties/markwon/core/suite/OrderedListTest.java +++ b/markwon/src/test/java/ru/noties/markwon/core/suite/OrderedListTest.java @@ -103,6 +103,6 @@ public class OrderedListTest extends BaseSuiteTest { text("ol")) ); - matchInput("single-ol.md", document); + match("1. ol", document); } } diff --git a/markwon/src/test/java/ru/noties/markwon/core/suite/SoftBreakAddsNewLineTest.java b/markwon/src/test/java/ru/noties/markwon/core/suite/SoftBreakAddsNewLineTest.java new file mode 100644 index 00000000..4658dc7d --- /dev/null +++ b/markwon/src/test/java/ru/noties/markwon/core/suite/SoftBreakAddsNewLineTest.java @@ -0,0 +1,39 @@ +package ru.noties.markwon.core.suite; + +import org.junit.Test; +import org.junit.runner.RunWith; +import org.robolectric.RobolectricTestRunner; +import org.robolectric.annotation.Config; + +import ru.noties.markwon.test.TestSpan.Document; + +import static ru.noties.markwon.test.TestSpan.document; +import static ru.noties.markwon.test.TestSpan.text; + +@RunWith(RobolectricTestRunner.class) +@Config(manifest = Config.NONE) +public class SoftBreakAddsNewLineTest extends BaseSuiteTest { + + /* +hello there! +this one is on the next line +hard break to the full extend + */ + + @Test + public void test() { + + final Document document = document( + text("hello there!\n"), + text("this one is on the next line\n"), + text("hard break to the full extend") + ); + + matchInput("soft-break-adds-new-line.md", document); + } + + @Override + boolean softBreakAddsNewLine() { + return true; + } +} diff --git a/markwon/src/test/java/ru/noties/markwon/core/suite/SoftBreakTest.java b/markwon/src/test/java/ru/noties/markwon/core/suite/SoftBreakTest.java new file mode 100644 index 00000000..25991e4e --- /dev/null +++ b/markwon/src/test/java/ru/noties/markwon/core/suite/SoftBreakTest.java @@ -0,0 +1,33 @@ +package ru.noties.markwon.core.suite; + +import org.junit.Test; +import org.junit.runner.RunWith; +import org.robolectric.RobolectricTestRunner; +import org.robolectric.annotation.Config; + +import ru.noties.markwon.test.TestSpan.Document; + +import static ru.noties.markwon.test.TestSpan.document; +import static ru.noties.markwon.test.TestSpan.text; + +@RunWith(RobolectricTestRunner.class) +@Config(manifest = Config.NONE) +public class SoftBreakTest extends BaseSuiteTest { + + @Test + public void test() { + + final Document document = document( + text("First line "), + text("same line but with space between "), + text("this is also the first line") + ); + + matchInput("soft-break.md", document); + } + + @Override + boolean softBreakAddsNewLine() { + return false; + } +} diff --git a/markwon/src/test/java/ru/noties/markwon/core/suite/StrongEmphasisTest.java b/markwon/src/test/java/ru/noties/markwon/core/suite/StrongEmphasisTest.java new file mode 100644 index 00000000..12d9358b --- /dev/null +++ b/markwon/src/test/java/ru/noties/markwon/core/suite/StrongEmphasisTest.java @@ -0,0 +1,29 @@ +package ru.noties.markwon.core.suite; + +import org.junit.Test; +import org.junit.runner.RunWith; +import org.robolectric.RobolectricTestRunner; +import org.robolectric.annotation.Config; + +import ru.noties.markwon.test.TestSpan.Document; + +import static ru.noties.markwon.core.suite.TestFactory.BOLD; +import static ru.noties.markwon.test.TestSpan.document; +import static ru.noties.markwon.test.TestSpan.span; +import static ru.noties.markwon.test.TestSpan.text; + +@RunWith(RobolectricTestRunner.class) +@Config(manifest = Config.NONE) +public class StrongEmphasisTest extends BaseSuiteTest { + + @Test + public void single() { + + final Document document = document( + span(BOLD, text("bold")) + ); + + match("**bold**", document); + match("__bold__", document); + } +} diff --git a/markwon/src/test/java/ru/noties/markwon/core/suite/TestFactory.java b/markwon/src/test/java/ru/noties/markwon/core/suite/TestFactory.java index 76e4dc03..47b63fda 100644 --- a/markwon/src/test/java/ru/noties/markwon/core/suite/TestFactory.java +++ b/markwon/src/test/java/ru/noties/markwon/core/suite/TestFactory.java @@ -22,7 +22,9 @@ class TestFactory implements SpannableFactory { static final String BLOCK_QUOTE = "blockquote"; static final String PARAGRAPH = "paragraph"; static final String ORDERED_LIST = "ordered-list"; + static final String UN_ORDERED_LIST = "un-ordered-list"; static final String HEADING = "heading"; + static final String THEMATIC_BREAK = "thematic-break"; private final boolean useParagraphs; @@ -63,13 +65,13 @@ class TestFactory implements SpannableFactory { @Nullable @Override public Object bulletListItem(@NonNull MarkwonTheme theme, int level) { - return null; + return span(UN_ORDERED_LIST, args("level", level)); } @Nullable @Override public Object thematicBreak(@NonNull MarkwonTheme theme) { - return null; + return span(THEMATIC_BREAK); } @Nullable diff --git a/markwon/src/test/java/ru/noties/markwon/core/suite/ThematicBreakTest.java b/markwon/src/test/java/ru/noties/markwon/core/suite/ThematicBreakTest.java new file mode 100644 index 00000000..a5005a98 --- /dev/null +++ b/markwon/src/test/java/ru/noties/markwon/core/suite/ThematicBreakTest.java @@ -0,0 +1,30 @@ +package ru.noties.markwon.core.suite; + +import org.junit.Test; +import org.junit.runner.RunWith; +import org.robolectric.RobolectricTestRunner; +import org.robolectric.annotation.Config; + +import ru.noties.markwon.test.TestSpan.Document; + +import static ru.noties.markwon.core.suite.TestFactory.THEMATIC_BREAK; +import static ru.noties.markwon.test.TestSpan.document; +import static ru.noties.markwon.test.TestSpan.span; +import static ru.noties.markwon.test.TestSpan.text; + +@RunWith(RobolectricTestRunner.class) +@Config(manifest = Config.NONE) +public class ThematicBreakTest extends BaseSuiteTest { + + @Test + public void single() { + + final Document document = document( + span(THEMATIC_BREAK, text("\u00a0")) + ); + + match("---", document); + match("----", document); + match("***", document); + } +} diff --git a/markwon/src/test/java/ru/noties/markwon/core/suite/UnOrderedListTest.java b/markwon/src/test/java/ru/noties/markwon/core/suite/UnOrderedListTest.java new file mode 100644 index 00000000..b5aa346e --- /dev/null +++ b/markwon/src/test/java/ru/noties/markwon/core/suite/UnOrderedListTest.java @@ -0,0 +1,84 @@ +package ru.noties.markwon.core.suite; + +import org.junit.Test; +import org.junit.runner.RunWith; +import org.robolectric.RobolectricTestRunner; +import org.robolectric.annotation.Config; + +import ru.noties.markwon.test.TestSpan.Document; + +import static ru.noties.markwon.core.suite.TestFactory.UN_ORDERED_LIST; +import static ru.noties.markwon.test.TestSpan.args; +import static ru.noties.markwon.test.TestSpan.document; +import static ru.noties.markwon.test.TestSpan.span; +import static ru.noties.markwon.test.TestSpan.text; + +@RunWith(RobolectricTestRunner.class) +@Config(manifest = Config.NONE) +public class UnOrderedListTest extends BaseSuiteTest { + + @Test + public void single() { + + final Document document = document( + span(UN_ORDERED_LIST, args("level", 0), text("ul")) + ); + + match("* ul", document); + } + + @Test + public void test() { + /* + * First + * Second + * Third + */ + + final Document document = document( + span(UN_ORDERED_LIST, + args("level", 0), + text("First\n"), + span(UN_ORDERED_LIST, + args("level", 1), + text("Second\n"), + span(UN_ORDERED_LIST, + args("level", 2), + text("Third")))) + ); + + matchInput("ul.md", document); + } + + @Test + public void levels() { + + /* + * First + * * Second + * * * Third + */ + + final Document document = document( + span(UN_ORDERED_LIST, + args("level", 0), + text("First")), + text("\n"), + span(UN_ORDERED_LIST, + args("level", 0), + span(UN_ORDERED_LIST, + args("level", 1), + text("Second"))), + text("\n"), + span(UN_ORDERED_LIST, + args("level", 0), + span(UN_ORDERED_LIST, + args("level", 1), + span(UN_ORDERED_LIST, + args("level", 2), + text("Third")))) + ); + + matchInput("ul-levels.md", document); + } +} diff --git a/markwon/src/test/resources/tests/bold-italic.md b/markwon/src/test/resources/tests/bold-italic.md deleted file mode 100644 index 757cfe8c..00000000 --- a/markwon/src/test/resources/tests/bold-italic.md +++ /dev/null @@ -1 +0,0 @@ -**_bold italic_** \ No newline at end of file diff --git a/markwon/src/test/resources/tests/single-a.yaml b/markwon/src/test/resources/tests/single-a.yaml deleted file mode 100644 index 82b1b134..00000000 --- a/markwon/src/test/resources/tests/single-a.yaml +++ /dev/null @@ -1,5 +0,0 @@ -input: "[link](#href)" - -output: - - a: "link" - href: "#href" \ No newline at end of file diff --git a/markwon/src/test/resources/tests/single-b.yaml b/markwon/src/test/resources/tests/single-b.yaml deleted file mode 100644 index a107424d..00000000 --- a/markwon/src/test/resources/tests/single-b.yaml +++ /dev/null @@ -1,4 +0,0 @@ -input: "**bold**" - -output: - - b: "bold" \ No newline at end of file diff --git a/markwon/src/test/resources/tests/single-blockquote.md b/markwon/src/test/resources/tests/single-blockquote.md deleted file mode 100644 index 32ce4098..00000000 --- a/markwon/src/test/resources/tests/single-blockquote.md +++ /dev/null @@ -1 +0,0 @@ -> blockquote \ No newline at end of file diff --git a/markwon/src/test/resources/tests/single-code.md b/markwon/src/test/resources/tests/single-code.md deleted file mode 100644 index d3a84179..00000000 --- a/markwon/src/test/resources/tests/single-code.md +++ /dev/null @@ -1 +0,0 @@ -`code` \ No newline at end of file diff --git a/markwon/src/test/resources/tests/single-h1.yaml b/markwon/src/test/resources/tests/single-h1.yaml deleted file mode 100644 index 613ae0c5..00000000 --- a/markwon/src/test/resources/tests/single-h1.yaml +++ /dev/null @@ -1,4 +0,0 @@ -input: "# head1" - -output: - - h1: "head1" \ No newline at end of file diff --git a/markwon/src/test/resources/tests/single-h2.yaml b/markwon/src/test/resources/tests/single-h2.yaml deleted file mode 100644 index 09489697..00000000 --- a/markwon/src/test/resources/tests/single-h2.yaml +++ /dev/null @@ -1,4 +0,0 @@ -input: "## head2" - -output: - - h2: "head2" \ No newline at end of file diff --git a/markwon/src/test/resources/tests/single-h3.yaml b/markwon/src/test/resources/tests/single-h3.yaml deleted file mode 100644 index 8f2d99a0..00000000 --- a/markwon/src/test/resources/tests/single-h3.yaml +++ /dev/null @@ -1,4 +0,0 @@ -input: "### head3" - -output: - - h3: "head3" \ No newline at end of file diff --git a/markwon/src/test/resources/tests/single-h4.yaml b/markwon/src/test/resources/tests/single-h4.yaml deleted file mode 100644 index b65a2b73..00000000 --- a/markwon/src/test/resources/tests/single-h4.yaml +++ /dev/null @@ -1,4 +0,0 @@ -input: "#### head4" - -output: - - h4: "head4" \ No newline at end of file diff --git a/markwon/src/test/resources/tests/single-h5.yaml b/markwon/src/test/resources/tests/single-h5.yaml deleted file mode 100644 index 44a3d078..00000000 --- a/markwon/src/test/resources/tests/single-h5.yaml +++ /dev/null @@ -1,4 +0,0 @@ -input: "##### head5" - -output: - - h5: "head5" \ No newline at end of file diff --git a/markwon/src/test/resources/tests/single-h6.yaml b/markwon/src/test/resources/tests/single-h6.yaml deleted file mode 100644 index f040ecaf..00000000 --- a/markwon/src/test/resources/tests/single-h6.yaml +++ /dev/null @@ -1,4 +0,0 @@ -input: "###### head6" - -output: - - h6: "head6" \ No newline at end of file diff --git a/markwon/src/test/resources/tests/single-hr.yaml b/markwon/src/test/resources/tests/single-hr.yaml deleted file mode 100644 index 86bb106a..00000000 --- a/markwon/src/test/resources/tests/single-hr.yaml +++ /dev/null @@ -1,7 +0,0 @@ -# it is failing as we are still removing white spaces manually -# this will be fixed when different logic for new lines will be introduced - -input: "---" - -output: - - hr: "\u00a0" \ No newline at end of file diff --git a/markwon/src/test/resources/tests/single-i.yaml b/markwon/src/test/resources/tests/single-i.yaml deleted file mode 100644 index 334e923c..00000000 --- a/markwon/src/test/resources/tests/single-i.yaml +++ /dev/null @@ -1,4 +0,0 @@ -input: "*italic*" - -output: - - i: "italic" \ No newline at end of file diff --git a/markwon/src/test/resources/tests/single-ol.md b/markwon/src/test/resources/tests/single-ol.md deleted file mode 100644 index 61a41e63..00000000 --- a/markwon/src/test/resources/tests/single-ol.md +++ /dev/null @@ -1 +0,0 @@ -1. ol \ No newline at end of file diff --git a/markwon/src/test/resources/tests/single-ul.yaml b/markwon/src/test/resources/tests/single-ul.yaml deleted file mode 100644 index d5fc6647..00000000 --- a/markwon/src/test/resources/tests/single-ul.yaml +++ /dev/null @@ -1,5 +0,0 @@ -input: "* ul" - -output: - - ul: "ul" - level: 0 \ No newline at end of file diff --git a/markwon/src/test/resources/tests/soft-break-adds-new-line.md b/markwon/src/test/resources/tests/soft-break-adds-new-line.md new file mode 100644 index 00000000..5d81a53e --- /dev/null +++ b/markwon/src/test/resources/tests/soft-break-adds-new-line.md @@ -0,0 +1,3 @@ +hello there! +this one is on the next line +hard break to the full extend \ No newline at end of file diff --git a/markwon/src/test/resources/tests/soft-break-adds-new-line.yaml b/markwon/src/test/resources/tests/soft-break-adds-new-line.yaml deleted file mode 100644 index 4fbb7d0a..00000000 --- a/markwon/src/test/resources/tests/soft-break-adds-new-line.yaml +++ /dev/null @@ -1,10 +0,0 @@ -input: |- - hello there! - this one is on the next line - hard break to the full extend - -config: - soft-break-adds-new-line: true - -output: - - text: "hello there!\nthis one is on the next line\nhard break to the full extend" \ No newline at end of file diff --git a/markwon/src/test/resources/tests/soft-break.md b/markwon/src/test/resources/tests/soft-break.md new file mode 100644 index 00000000..8e768a71 --- /dev/null +++ b/markwon/src/test/resources/tests/soft-break.md @@ -0,0 +1,3 @@ +First line +same line but with space between +this is also the first line \ No newline at end of file diff --git a/markwon/src/test/resources/tests/soft-break.yaml b/markwon/src/test/resources/tests/soft-break.yaml deleted file mode 100644 index 4406a5c7..00000000 --- a/markwon/src/test/resources/tests/soft-break.yaml +++ /dev/null @@ -1,7 +0,0 @@ -input: |- - First line - same line but with space between - this is also the first line - -output: - - text: "First line same line but with space between this is also the first line" \ No newline at end of file diff --git a/markwon/src/test/resources/tests/ul-levels.md b/markwon/src/test/resources/tests/ul-levels.md new file mode 100644 index 00000000..767d3e7c --- /dev/null +++ b/markwon/src/test/resources/tests/ul-levels.md @@ -0,0 +1,3 @@ +* First +* * Second +* * * Third \ No newline at end of file diff --git a/markwon/src/test/resources/tests/ul-levels.yaml b/markwon/src/test/resources/tests/ul-levels.yaml deleted file mode 100644 index a7a06c96..00000000 --- a/markwon/src/test/resources/tests/ul-levels.yaml +++ /dev/null @@ -1,20 +0,0 @@ -input: |- - * First - * * Second - * * * Third - -output: - - ul: "First" - level: 0 - - text: "\n" - - ul: - - ul: "Second" - level: 1 - level: 0 - - text: "\n" - - ul: - - ul: - - ul: "Third" - level: 2 - level: 1 - level: 0 \ No newline at end of file diff --git a/markwon/src/test/resources/tests/ul.md b/markwon/src/test/resources/tests/ul.md new file mode 100644 index 00000000..0dd60b3e --- /dev/null +++ b/markwon/src/test/resources/tests/ul.md @@ -0,0 +1,3 @@ +* First + * Second + * Third \ No newline at end of file diff --git a/markwon/src/test/resources/tests/ul.yaml b/markwon/src/test/resources/tests/ul.yaml deleted file mode 100644 index 171145da..00000000 --- a/markwon/src/test/resources/tests/ul.yaml +++ /dev/null @@ -1,14 +0,0 @@ -input: |- - * First - * Second - * Third - -output: - - ul: - - text: "First\n" - - ul: - - text: "Second\n" - - ul: "Third" - level: 2 - level: 1 - level: 0 \ No newline at end of file From 69f9d0ebb8121bb3a9107300df366230b1441773 Mon Sep 17 00:00:00 2001 From: Dimitry Ivanov Date: Mon, 17 Dec 2018 18:42:42 +0300 Subject: [PATCH 042/103] Moved image span from factory to image-plugin --- .../markwon/gif/GifAwareSpannableFactory.java | 6 +-- .../noties/markwon/AbstractMarkwonPlugin.java | 2 +- .../ru/noties/markwon/MarkwonBuilderImpl.java | 2 +- .../noties/markwon/MarkwonConfiguration.java | 12 +++--- .../java/ru/noties/markwon/MarkwonPlugin.java | 2 +- .../ru/noties/markwon/MarkwonVisitor.java | 3 +- .../ru/noties/markwon/MarkwonVisitorImpl.java | 5 ++- .../MarkwonSpannableFactory.java} | 17 +-------- .../MarkwonSpannableFactoryDef.java} | 30 ++------------- .../ru/noties/markwon/core/MarkwonTheme.java | 37 ------------------- .../ru/noties/markwon/image/ImagesPlugin.java | 26 ++++++++++++- .../java/ru/noties/markwon/core/CoreTest.java | 4 +- .../markwon/core/suite/TestFactory.java | 13 +------ .../markwon/renderer/visitor/TestFactory.java | 4 +- .../markwon/syntax/SyntaxHighlightTest.java | 4 +- 15 files changed, 55 insertions(+), 112 deletions(-) rename markwon/src/main/java/ru/noties/markwon/{SpannableFactory.java => core/MarkwonSpannableFactory.java} (64%) rename markwon/src/main/java/ru/noties/markwon/{SpannableFactoryDef.java => core/MarkwonSpannableFactoryDef.java} (67%) diff --git a/app/src/main/java/ru/noties/markwon/gif/GifAwareSpannableFactory.java b/app/src/main/java/ru/noties/markwon/gif/GifAwareSpannableFactory.java index 3cd805b3..8d9e14fb 100644 --- a/app/src/main/java/ru/noties/markwon/gif/GifAwareSpannableFactory.java +++ b/app/src/main/java/ru/noties/markwon/gif/GifAwareSpannableFactory.java @@ -3,14 +3,14 @@ package ru.noties.markwon.gif; import android.support.annotation.NonNull; import android.support.annotation.Nullable; -import ru.noties.markwon.SpannableFactoryDef; +import ru.noties.markwon.core.MarkwonTheme; +import ru.noties.markwon.core.MarkwonSpannableFactoryDef; import ru.noties.markwon.image.AsyncDrawableLoader; import ru.noties.markwon.image.ImageSize; import ru.noties.markwon.image.ImageSizeResolver; import ru.noties.markwon.core.spans.AsyncDrawableSpan; -import ru.noties.markwon.core.MarkwonTheme; -public class GifAwareSpannableFactory extends SpannableFactoryDef { +public class GifAwareSpannableFactory extends MarkwonSpannableFactoryDef { private final GifPlaceholder gifPlaceholder; diff --git a/markwon/src/main/java/ru/noties/markwon/AbstractMarkwonPlugin.java b/markwon/src/main/java/ru/noties/markwon/AbstractMarkwonPlugin.java index a873add3..9820f18f 100644 --- a/markwon/src/main/java/ru/noties/markwon/AbstractMarkwonPlugin.java +++ b/markwon/src/main/java/ru/noties/markwon/AbstractMarkwonPlugin.java @@ -6,8 +6,8 @@ import android.widget.TextView; import org.commonmark.node.Node; import org.commonmark.parser.Parser; -import ru.noties.markwon.image.AsyncDrawableLoader; import ru.noties.markwon.core.MarkwonTheme; +import ru.noties.markwon.image.AsyncDrawableLoader; public abstract class AbstractMarkwonPlugin implements MarkwonPlugin { @Override diff --git a/markwon/src/main/java/ru/noties/markwon/MarkwonBuilderImpl.java b/markwon/src/main/java/ru/noties/markwon/MarkwonBuilderImpl.java index 0e830e00..641f9691 100644 --- a/markwon/src/main/java/ru/noties/markwon/MarkwonBuilderImpl.java +++ b/markwon/src/main/java/ru/noties/markwon/MarkwonBuilderImpl.java @@ -10,8 +10,8 @@ import java.util.ArrayList; import java.util.Collections; import java.util.List; -import ru.noties.markwon.image.AsyncDrawableLoader; import ru.noties.markwon.core.MarkwonTheme; +import ru.noties.markwon.image.AsyncDrawableLoader; class MarkwonBuilderImpl implements Markwon.Builder { diff --git a/markwon/src/main/java/ru/noties/markwon/MarkwonConfiguration.java b/markwon/src/main/java/ru/noties/markwon/MarkwonConfiguration.java index 97f77a69..b55c1df5 100644 --- a/markwon/src/main/java/ru/noties/markwon/MarkwonConfiguration.java +++ b/markwon/src/main/java/ru/noties/markwon/MarkwonConfiguration.java @@ -4,6 +4,8 @@ import android.content.Context; import android.support.annotation.NonNull; import ru.noties.markwon.core.MarkwonTheme; +import ru.noties.markwon.core.MarkwonSpannableFactory; +import ru.noties.markwon.core.MarkwonSpannableFactoryDef; import ru.noties.markwon.core.spans.LinkSpan; import ru.noties.markwon.image.AsyncDrawableLoader; import ru.noties.markwon.image.ImageSizeResolver; @@ -37,7 +39,7 @@ public class MarkwonConfiguration { private final LinkSpan.Resolver linkResolver; private final UrlProcessor urlProcessor; private final ImageSizeResolver imageSizeResolver; - private final SpannableFactory factory; // @since 1.1.0 + private final MarkwonSpannableFactory factory; // @since 1.1.0 private MarkwonConfiguration(@NonNull Builder builder) { this.theme = builder.theme; @@ -80,7 +82,7 @@ public class MarkwonConfiguration { } @NonNull - public SpannableFactory factory() { + public MarkwonSpannableFactory factory() { return factory; } @@ -95,7 +97,7 @@ public class MarkwonConfiguration { private LinkSpan.Resolver linkResolver; private UrlProcessor urlProcessor; private ImageSizeResolver imageSizeResolver; - private SpannableFactory factory; // @since 1.1.0 + private MarkwonSpannableFactory factory; // @since 1.1.0 Builder(@NonNull Context context) { this.context = context; @@ -132,7 +134,7 @@ public class MarkwonConfiguration { * @since 1.1.0 */ @NonNull - public Builder factory(@NonNull SpannableFactory factory) { + public Builder factory(@NonNull MarkwonSpannableFactory factory) { this.factory = factory; return this; } @@ -161,7 +163,7 @@ public class MarkwonConfiguration { // @since 1.1.0 if (factory == null) { - factory = SpannableFactoryDef.create(); + factory = MarkwonSpannableFactoryDef.create(); } return new MarkwonConfiguration(this); diff --git a/markwon/src/main/java/ru/noties/markwon/MarkwonPlugin.java b/markwon/src/main/java/ru/noties/markwon/MarkwonPlugin.java index 0cd959f0..fc0cf4ca 100644 --- a/markwon/src/main/java/ru/noties/markwon/MarkwonPlugin.java +++ b/markwon/src/main/java/ru/noties/markwon/MarkwonPlugin.java @@ -6,8 +6,8 @@ import android.widget.TextView; import org.commonmark.node.Node; import org.commonmark.parser.Parser; -import ru.noties.markwon.image.AsyncDrawableLoader; import ru.noties.markwon.core.MarkwonTheme; +import ru.noties.markwon.image.AsyncDrawableLoader; public interface MarkwonPlugin { diff --git a/markwon/src/main/java/ru/noties/markwon/MarkwonVisitor.java b/markwon/src/main/java/ru/noties/markwon/MarkwonVisitor.java index becbbbb4..88ae1b5d 100644 --- a/markwon/src/main/java/ru/noties/markwon/MarkwonVisitor.java +++ b/markwon/src/main/java/ru/noties/markwon/MarkwonVisitor.java @@ -7,6 +7,7 @@ import org.commonmark.node.Node; import org.commonmark.node.Visitor; import ru.noties.markwon.core.MarkwonTheme; +import ru.noties.markwon.core.MarkwonSpannableFactory; public interface MarkwonVisitor extends Visitor { @@ -30,7 +31,7 @@ public interface MarkwonVisitor extends Visitor { MarkwonTheme theme(); @NonNull - SpannableFactory factory(); + MarkwonSpannableFactory factory(); @NonNull SpannableBuilder builder(); diff --git a/markwon/src/main/java/ru/noties/markwon/MarkwonVisitorImpl.java b/markwon/src/main/java/ru/noties/markwon/MarkwonVisitorImpl.java index 60e8af80..6982b74a 100644 --- a/markwon/src/main/java/ru/noties/markwon/MarkwonVisitorImpl.java +++ b/markwon/src/main/java/ru/noties/markwon/MarkwonVisitorImpl.java @@ -32,6 +32,7 @@ import java.util.HashMap; import java.util.Map; import ru.noties.markwon.core.MarkwonTheme; +import ru.noties.markwon.core.MarkwonSpannableFactory; class MarkwonVisitorImpl implements MarkwonVisitor { @@ -39,7 +40,7 @@ class MarkwonVisitorImpl implements MarkwonVisitor { private final MarkwonConfiguration configuration; private final MarkwonTheme theme; - private final SpannableFactory factory; + private final MarkwonSpannableFactory factory; private final SpannableBuilder builder = new SpannableBuilder(); @@ -186,7 +187,7 @@ class MarkwonVisitorImpl implements MarkwonVisitor { @NonNull @Override - public SpannableFactory factory() { + public MarkwonSpannableFactory factory() { return factory; } diff --git a/markwon/src/main/java/ru/noties/markwon/SpannableFactory.java b/markwon/src/main/java/ru/noties/markwon/core/MarkwonSpannableFactory.java similarity index 64% rename from markwon/src/main/java/ru/noties/markwon/SpannableFactory.java rename to markwon/src/main/java/ru/noties/markwon/core/MarkwonSpannableFactory.java index c1b06a57..ccdb8a4c 100644 --- a/markwon/src/main/java/ru/noties/markwon/SpannableFactory.java +++ b/markwon/src/main/java/ru/noties/markwon/core/MarkwonSpannableFactory.java @@ -1,20 +1,16 @@ -package ru.noties.markwon; +package ru.noties.markwon.core; import android.support.annotation.NonNull; import android.support.annotation.Nullable; -import ru.noties.markwon.image.AsyncDrawableLoader; -import ru.noties.markwon.image.ImageSize; -import ru.noties.markwon.image.ImageSizeResolver; import ru.noties.markwon.core.spans.LinkSpan; -import ru.noties.markwon.core.MarkwonTheme; /** * Each method can return null or a Span object or an array of spans * * @since 1.1.0 */ -public interface SpannableFactory { +public interface MarkwonSpannableFactory { @Nullable Object strongEmphasis(); @@ -46,15 +42,6 @@ public interface SpannableFactory { @Nullable Object paragraph(boolean inTightList); - @Nullable - Object image( - @NonNull MarkwonTheme theme, - @NonNull String destination, - @NonNull AsyncDrawableLoader loader, - @NonNull ImageSizeResolver imageSizeResolver, - @Nullable ImageSize imageSize, - boolean replacementTextIsLink); - @Nullable Object link( @NonNull MarkwonTheme theme, diff --git a/markwon/src/main/java/ru/noties/markwon/SpannableFactoryDef.java b/markwon/src/main/java/ru/noties/markwon/core/MarkwonSpannableFactoryDef.java similarity index 67% rename from markwon/src/main/java/ru/noties/markwon/SpannableFactoryDef.java rename to markwon/src/main/java/ru/noties/markwon/core/MarkwonSpannableFactoryDef.java index ca3b02cf..8b0a6461 100644 --- a/markwon/src/main/java/ru/noties/markwon/SpannableFactoryDef.java +++ b/markwon/src/main/java/ru/noties/markwon/core/MarkwonSpannableFactoryDef.java @@ -1,20 +1,14 @@ -package ru.noties.markwon; +package ru.noties.markwon.core; import android.support.annotation.NonNull; import android.support.annotation.Nullable; -import ru.noties.markwon.image.AsyncDrawable; -import ru.noties.markwon.image.AsyncDrawableLoader; -import ru.noties.markwon.image.ImageSize; -import ru.noties.markwon.image.ImageSizeResolver; -import ru.noties.markwon.core.spans.AsyncDrawableSpan; import ru.noties.markwon.core.spans.BlockQuoteSpan; import ru.noties.markwon.core.spans.BulletListItemSpan; import ru.noties.markwon.core.spans.CodeSpan; import ru.noties.markwon.core.spans.EmphasisSpan; import ru.noties.markwon.core.spans.HeadingSpan; import ru.noties.markwon.core.spans.LinkSpan; -import ru.noties.markwon.core.MarkwonTheme; import ru.noties.markwon.core.spans.OrderedListItemSpan; import ru.noties.markwon.core.spans.StrongEmphasisSpan; import ru.noties.markwon.core.spans.ThematicBreakSpan; @@ -22,11 +16,11 @@ import ru.noties.markwon.core.spans.ThematicBreakSpan; /** * @since 1.1.0 */ -public class SpannableFactoryDef implements SpannableFactory { +public class MarkwonSpannableFactoryDef implements MarkwonSpannableFactory { @NonNull - public static SpannableFactoryDef create() { - return new SpannableFactoryDef(); + public static MarkwonSpannableFactoryDef create() { + return new MarkwonSpannableFactoryDef(); } @Nullable @@ -87,22 +81,6 @@ public class SpannableFactoryDef implements SpannableFactory { return null; } - @Nullable - @Override - public Object image(@NonNull MarkwonTheme theme, @NonNull String destination, @NonNull AsyncDrawableLoader loader, @NonNull ImageSizeResolver imageSizeResolver, @Nullable ImageSize imageSize, boolean replacementTextIsLink) { - return new AsyncDrawableSpan( - theme, - new AsyncDrawable( - destination, - loader, - imageSizeResolver, - imageSize - ), - AsyncDrawableSpan.ALIGN_BOTTOM, - replacementTextIsLink - ); - } - @Nullable @Override public Object link(@NonNull MarkwonTheme theme, @NonNull String destination, @NonNull LinkSpan.Resolver resolver) { diff --git a/markwon/src/main/java/ru/noties/markwon/core/MarkwonTheme.java b/markwon/src/main/java/ru/noties/markwon/core/MarkwonTheme.java index 681c6048..347a2321 100644 --- a/markwon/src/main/java/ru/noties/markwon/core/MarkwonTheme.java +++ b/markwon/src/main/java/ru/noties/markwon/core/MarkwonTheme.java @@ -4,7 +4,6 @@ import android.content.Context; import android.graphics.Paint; import android.graphics.Typeface; import android.support.annotation.ColorInt; -import android.support.annotation.FloatRange; import android.support.annotation.IntRange; import android.support.annotation.NonNull; import android.support.annotation.Px; @@ -96,8 +95,6 @@ public class MarkwonTheme { 2.F, 1.5F, 1.17F, 1.F, .83F, .67F, }; - protected static final float SCRIPT_DEF_TEXT_SIZE_RATIO = .75F; - protected static final int THEMATIC_BREAK_DEF_ALPHA = 25; protected final int linkColor; @@ -158,9 +155,6 @@ public class MarkwonTheme { // @since 1.1.0 protected final float[] headingTextSizeMultipliers; - // by default `SCRIPT_DEF_TEXT_SIZE_RATIO` - protected final float scriptTextSizeRatio; - // by default textColor with `THEMATIC_BREAK_DEF_ALPHA` applied alpha protected final int thematicBreakColor; @@ -186,7 +180,6 @@ public class MarkwonTheme { this.headingBreakColor = builder.headingBreakColor; this.headingTypeface = builder.headingTypeface; this.headingTextSizeMultipliers = builder.headingTextSizeMultipliers; - this.scriptTextSizeRatio = builder.scriptTextSizeRatio; this.thematicBreakColor = builder.thematicBreakColor; this.thematicBreakHeight = builder.thematicBreakHeight; } @@ -369,28 +362,6 @@ public class MarkwonTheme { } } - public void applySuperScriptStyle(@NonNull TextPaint paint) { - final float ratio; - if (Float.compare(scriptTextSizeRatio, .0F) == 0) { - ratio = SCRIPT_DEF_TEXT_SIZE_RATIO; - } else { - ratio = scriptTextSizeRatio; - } - paint.setTextSize(paint.getTextSize() * ratio); - paint.baselineShift += (int) (paint.ascent() / 2); - } - - public void applySubScriptStyle(@NonNull TextPaint paint) { - final float ratio; - if (Float.compare(scriptTextSizeRatio, .0F) == 0) { - ratio = SCRIPT_DEF_TEXT_SIZE_RATIO; - } else { - ratio = scriptTextSizeRatio; - } - paint.setTextSize(paint.getTextSize() * ratio); - paint.baselineShift -= (int) (paint.ascent() / 2); - } - public void applyThematicBreakStyle(@NonNull Paint paint) { final int color; if (thematicBreakColor != 0) { @@ -428,7 +399,6 @@ public class MarkwonTheme { private int headingBreakColor; private Typeface headingTypeface; private float[] headingTextSizeMultipliers; - private float scriptTextSizeRatio; private int thematicBreakColor; private int thematicBreakHeight = -1; @@ -454,7 +424,6 @@ public class MarkwonTheme { this.headingBreakColor = theme.headingBreakColor; this.headingTypeface = theme.headingTypeface; this.headingTextSizeMultipliers = theme.headingTextSizeMultipliers; - this.scriptTextSizeRatio = theme.scriptTextSizeRatio; this.thematicBreakColor = theme.thematicBreakColor; this.thematicBreakHeight = theme.thematicBreakHeight; } @@ -586,12 +555,6 @@ public class MarkwonTheme { return this; } - @NonNull - public Builder scriptTextSizeRatio(@FloatRange(from = .0F, to = Float.MAX_VALUE) float scriptTextSizeRatio) { - this.scriptTextSizeRatio = scriptTextSizeRatio; - return this; - } - @NonNull public Builder thematicBreakColor(@ColorInt int thematicBreakColor) { this.thematicBreakColor = thematicBreakColor; diff --git a/markwon/src/main/java/ru/noties/markwon/image/ImagesPlugin.java b/markwon/src/main/java/ru/noties/markwon/image/ImagesPlugin.java index 5f43ab54..2be7763f 100644 --- a/markwon/src/main/java/ru/noties/markwon/image/ImagesPlugin.java +++ b/markwon/src/main/java/ru/noties/markwon/image/ImagesPlugin.java @@ -2,6 +2,7 @@ package ru.noties.markwon.image; import android.content.Context; import android.support.annotation.NonNull; +import android.support.annotation.Nullable; import android.widget.TextView; import org.commonmark.node.Image; @@ -13,6 +14,8 @@ import java.util.Arrays; import ru.noties.markwon.AbstractMarkwonPlugin; import ru.noties.markwon.MarkwonConfiguration; import ru.noties.markwon.MarkwonVisitor; +import ru.noties.markwon.core.MarkwonTheme; +import ru.noties.markwon.core.spans.AsyncDrawableSpan; import ru.noties.markwon.image.data.DataUriSchemeHandler; import ru.noties.markwon.image.file.FileSchemeHandler; import ru.noties.markwon.image.network.NetworkSchemeHandler; @@ -78,12 +81,11 @@ public class ImagesPlugin extends AbstractMarkwonPlugin { .urlProcessor() .process(image.getDestination()); - final Object spans = visitor.factory().image( + final Object spans = imageSpan( visitor.theme(), destination, configuration.asyncDrawableLoader(), configuration.imageSizeResolver(), - null, link); visitor.setSpans(length, spans); @@ -100,4 +102,24 @@ public class ImagesPlugin extends AbstractMarkwonPlugin { public void afterSetText(@NonNull TextView textView) { AsyncDrawableScheduler.schedule(textView); } + + @Nullable + protected Object imageSpan( + @NonNull MarkwonTheme theme, + @NonNull String destination, + @NonNull AsyncDrawableLoader loader, + @NonNull ImageSizeResolver imageSizeResolver, + boolean replacementTextIsLink) { + return new AsyncDrawableSpan( + theme, + new AsyncDrawable( + destination, + loader, + imageSizeResolver, + null + ), + AsyncDrawableSpan.ALIGN_BOTTOM, + replacementTextIsLink + ); + } } diff --git a/markwon/src/test/java/ru/noties/markwon/core/CoreTest.java b/markwon/src/test/java/ru/noties/markwon/core/CoreTest.java index 7fc110ff..e417267a 100644 --- a/markwon/src/test/java/ru/noties/markwon/core/CoreTest.java +++ b/markwon/src/test/java/ru/noties/markwon/core/CoreTest.java @@ -1,7 +1,6 @@ package ru.noties.markwon.core; import android.support.annotation.NonNull; -import android.support.annotation.Nullable; import android.text.Spanned; import org.junit.Test; @@ -13,7 +12,6 @@ import org.robolectric.annotation.Config; import ru.noties.markwon.AbstractMarkwonPlugin; import ru.noties.markwon.Markwon; import ru.noties.markwon.MarkwonConfiguration; -import ru.noties.markwon.SpannableFactoryDef; import ru.noties.markwon.test.TestSpan; import ru.noties.markwon.test.TestSpanMatcher; @@ -38,7 +36,7 @@ public class CoreTest { .use(new AbstractMarkwonPlugin() { @Override public void configureConfiguration(@NonNull MarkwonConfiguration.Builder builder) { - builder.factory(new SpannableFactoryDef() { + builder.factory(new MarkwonSpannableFactoryDef() { @Override public Object strongEmphasis() { diff --git a/markwon/src/test/java/ru/noties/markwon/core/suite/TestFactory.java b/markwon/src/test/java/ru/noties/markwon/core/suite/TestFactory.java index 47b63fda..693c5cb5 100644 --- a/markwon/src/test/java/ru/noties/markwon/core/suite/TestFactory.java +++ b/markwon/src/test/java/ru/noties/markwon/core/suite/TestFactory.java @@ -3,17 +3,14 @@ package ru.noties.markwon.core.suite; import android.support.annotation.NonNull; import android.support.annotation.Nullable; -import ru.noties.markwon.SpannableFactory; +import ru.noties.markwon.core.MarkwonSpannableFactory; import ru.noties.markwon.core.MarkwonTheme; import ru.noties.markwon.core.spans.LinkSpan; -import ru.noties.markwon.image.AsyncDrawableLoader; -import ru.noties.markwon.image.ImageSize; -import ru.noties.markwon.image.ImageSizeResolver; import static ru.noties.markwon.test.TestSpan.args; import static ru.noties.markwon.test.TestSpan.span; -class TestFactory implements SpannableFactory { +class TestFactory implements MarkwonSpannableFactory { static final String BOLD = "bold"; static final String ITALIC = "italic"; @@ -88,12 +85,6 @@ class TestFactory implements SpannableFactory { : null; } - @Nullable - @Override - public Object image(@NonNull MarkwonTheme theme, @NonNull String destination, @NonNull AsyncDrawableLoader loader, @NonNull ImageSizeResolver imageSizeResolver, @Nullable ImageSize imageSize, boolean replacementTextIsLink) { - return null; - } - @Nullable @Override public Object link(@NonNull MarkwonTheme theme, @NonNull String destination, @NonNull LinkSpan.Resolver resolver) { diff --git a/markwon/src/test/java/ru/noties/markwon/renderer/visitor/TestFactory.java b/markwon/src/test/java/ru/noties/markwon/renderer/visitor/TestFactory.java index d2bf8d4e..2171c1c1 100644 --- a/markwon/src/test/java/ru/noties/markwon/renderer/visitor/TestFactory.java +++ b/markwon/src/test/java/ru/noties/markwon/renderer/visitor/TestFactory.java @@ -7,8 +7,8 @@ import java.util.Collections; import java.util.HashMap; import java.util.Map; -import ru.noties.markwon.SpannableFactory; import ru.noties.markwon.core.MarkwonTheme; +import ru.noties.markwon.core.MarkwonSpannableFactory; import ru.noties.markwon.core.spans.LinkSpan; import ru.noties.markwon.image.AsyncDrawableLoader; import ru.noties.markwon.image.ImageSize; @@ -27,7 +27,7 @@ import static ru.noties.markwon.renderer.visitor.TestSpan.PARAGRAPH; import static ru.noties.markwon.renderer.visitor.TestSpan.STRONG_EMPHASIS; import static ru.noties.markwon.renderer.visitor.TestSpan.THEMATIC_BREAK; -class TestFactory implements SpannableFactory { +class TestFactory implements MarkwonSpannableFactory { private final boolean useParagraphs; diff --git a/markwon/src/test/java/ru/noties/markwon/syntax/SyntaxHighlightTest.java b/markwon/src/test/java/ru/noties/markwon/syntax/SyntaxHighlightTest.java index ea43bf36..2f2d99b7 100644 --- a/markwon/src/test/java/ru/noties/markwon/syntax/SyntaxHighlightTest.java +++ b/markwon/src/test/java/ru/noties/markwon/syntax/SyntaxHighlightTest.java @@ -21,8 +21,8 @@ import ru.noties.markwon.AbstractMarkwonVisitorImpl; import ru.noties.markwon.MarkwonConfiguration; import ru.noties.markwon.MarkwonVisitor; import ru.noties.markwon.SpannableBuilder; -import ru.noties.markwon.SpannableFactory; import ru.noties.markwon.core.MarkwonTheme; +import ru.noties.markwon.core.MarkwonSpannableFactory; import ru.noties.markwon.core.visitor.CodeBlockNodeVisitor; import ru.noties.markwon.image.AsyncDrawableLoader; @@ -71,7 +71,7 @@ public class SyntaxHighlightTest { } }; - final SpannableFactory factory = mock(SpannableFactory.class); + final MarkwonSpannableFactory factory = mock(MarkwonSpannableFactory.class); when(factory.code(any(MarkwonTheme.class), anyBoolean())).thenReturn(codeSpan); final MarkwonConfiguration configuration = MarkwonConfiguration.builder(mock(Context.class)) From 9dd3d4a94d03a659563b661552a43628d6ca9da6 Mon Sep 17 00:00:00 2001 From: Dimitry Ivanov Date: Sat, 22 Dec 2018 15:38:29 +0300 Subject: [PATCH 043/103] RenderProps and SpanFactories --- .../ru/noties/markwon/MarkdownRenderer.java | 22 +- .../ru/noties/markwon/gif/GifAwarePlugin.java | 31 +- .../markwon/gif/GifAwareSpannableFactory.java | 37 -- .../ru/noties/markwon/gif/GifProcessor.java | 2 +- .../markwon/ext/tasklist/TaskListPlugin.java | 24 +- .../markwon/ext/tasklist/TaskListProps.java | 16 + .../markwon/ext/tasklist/TaskListSpan.java | 12 - .../ext/tasklist/TaskListSpanFactory.java | 29 ++ .../noties/markwon/html/tag/ImageHandler.java | 18 +- .../noties/markwon/AbstractMarkwonPlugin.java | 10 + .../main/java/ru/noties/markwon/Markwon.java | 11 +- .../ru/noties/markwon/MarkwonBuilderImpl.java | 36 +- .../noties/markwon/MarkwonConfiguration.java | 56 +-- .../java/ru/noties/markwon/MarkwonPlugin.java | 8 + .../noties/markwon/MarkwonSpansFactory.java | 34 ++ .../markwon/MarkwonSpansFactoryImpl.java | 74 ++++ .../ru/noties/markwon/MarkwonVisitor.java | 21 +- .../ru/noties/markwon/MarkwonVisitorImpl.java | 44 +-- .../src/main/java/ru/noties/markwon/Prop.java | 84 +++++ .../java/ru/noties/markwon/RenderProps.java | 20 + .../ru/noties/markwon/RenderPropsImpl.java | 51 +++ .../java/ru/noties/markwon/SpanFactory.java | 15 + .../ru/noties/markwon/core/CorePlugin.java | 338 ++++++++++++++--- .../ru/noties/markwon/core/CoreProps.java | 26 ++ .../markwon/core/MarkwonSpannableFactory.java | 1 + .../core/MarkwonSpannableFactoryDef.java | 1 + ...sitor.java => SimpleBlockNodeVisitor.java} | 11 +- .../core/factory/BlockQuoteSpanFactory.java | 17 + .../core/factory/CodeBlockSpanFactory.java | 17 + .../markwon/core/factory/CodeSpanFactory.java | 17 + .../core/factory/EmphasisSpanFactory.java | 17 + .../core/factory/HeadingSpanFactory.java | 21 ++ .../markwon/core/factory/LinkSpanFactory.java | 22 ++ .../core/factory/ListItemSpanFactory.java | 43 +++ .../factory/StrongEmphasisSpanFactory.java | 17 + .../factory/ThematicBreakSpanFactory.java | 17 + .../core/visitor/BlockQuoteNodeVisitor.java | 25 -- .../core/visitor/CodeBlockNodeVisitor.java | 56 --- .../markwon/core/visitor/CodeNodeVisitor.java | 24 -- .../core/visitor/EmphasisNodeVisitor.java | 16 - .../visitor/HardLineBreakNodeVisitor.java | 14 - .../core/visitor/HeadingNodeVisitor.java | 24 -- .../markwon/core/visitor/LinkNodeVisitor.java | 19 - .../core/visitor/ListItemNodeVisitor.java | 54 --- .../core/visitor/ParagraphNodeVisitor.java | 44 --- .../visitor/SoftLineBreakNodeVisitor.java | 25 -- .../visitor/StrongEmphasisNodeVisitor.java | 16 - .../markwon/core/visitor/TextNodeVisitor.java | 14 - .../visitor/ThematicBreakNodeVisitor.java | 27 -- .../markwon/image/AsyncDrawableScheduler.java | 1 - .../spans => image}/AsyncDrawableSpan.java | 15 +- .../ru/noties/markwon/image/ImageProps.java | 20 + .../markwon/image/ImageSpanFactory.java | 26 ++ .../ru/noties/markwon/image/ImagesPlugin.java | 49 +-- .../java/ru/noties/markwon/core/CoreTest.java | 4 +- .../markwon/core/suite/BaseSuiteTest.java | 4 +- .../ru/noties/markwon/image/ImageTest.java | 49 +++ .../visitor/SpannableMarkdownVisitorTest.java | 83 ----- .../markwon/renderer/visitor/TestConfig.java | 31 -- .../markwon/renderer/visitor/TestData.java | 45 --- .../renderer/visitor/TestDataReader.java | 341 ------------------ .../markwon/renderer/visitor/TestFactory.java | 142 -------- .../markwon/renderer/visitor/TestNode.java | 140 ------- .../markwon/renderer/visitor/TestSpan.java | 59 --- .../renderer/visitor/TestValidator.java | 199 ---------- .../markwon/syntax/SyntaxHighlightTest.java | 1 - .../src/test/resources/tests/single-img.yaml | 7 - .../markwon/sample/extension/IconPlugin.java | 1 + .../sample/extension/MainActivity.java | 4 +- .../sample/jlatexmath/MainActivity.java | 6 +- 70 files changed, 1144 insertions(+), 1661 deletions(-) delete mode 100644 app/src/main/java/ru/noties/markwon/gif/GifAwareSpannableFactory.java create mode 100644 markwon-ext-tasklist/src/main/java/ru/noties/markwon/ext/tasklist/TaskListProps.java create mode 100644 markwon-ext-tasklist/src/main/java/ru/noties/markwon/ext/tasklist/TaskListSpanFactory.java create mode 100644 markwon/src/main/java/ru/noties/markwon/MarkwonSpansFactory.java create mode 100644 markwon/src/main/java/ru/noties/markwon/MarkwonSpansFactoryImpl.java create mode 100644 markwon/src/main/java/ru/noties/markwon/Prop.java create mode 100644 markwon/src/main/java/ru/noties/markwon/RenderProps.java create mode 100644 markwon/src/main/java/ru/noties/markwon/RenderPropsImpl.java create mode 100644 markwon/src/main/java/ru/noties/markwon/SpanFactory.java create mode 100644 markwon/src/main/java/ru/noties/markwon/core/CoreProps.java rename markwon/src/main/java/ru/noties/markwon/core/{visitor/ListBlockNodeVisitor.java => SimpleBlockNodeVisitor.java} (52%) create mode 100644 markwon/src/main/java/ru/noties/markwon/core/factory/BlockQuoteSpanFactory.java create mode 100644 markwon/src/main/java/ru/noties/markwon/core/factory/CodeBlockSpanFactory.java create mode 100644 markwon/src/main/java/ru/noties/markwon/core/factory/CodeSpanFactory.java create mode 100644 markwon/src/main/java/ru/noties/markwon/core/factory/EmphasisSpanFactory.java create mode 100644 markwon/src/main/java/ru/noties/markwon/core/factory/HeadingSpanFactory.java create mode 100644 markwon/src/main/java/ru/noties/markwon/core/factory/LinkSpanFactory.java create mode 100644 markwon/src/main/java/ru/noties/markwon/core/factory/ListItemSpanFactory.java create mode 100644 markwon/src/main/java/ru/noties/markwon/core/factory/StrongEmphasisSpanFactory.java create mode 100644 markwon/src/main/java/ru/noties/markwon/core/factory/ThematicBreakSpanFactory.java delete mode 100644 markwon/src/main/java/ru/noties/markwon/core/visitor/BlockQuoteNodeVisitor.java delete mode 100644 markwon/src/main/java/ru/noties/markwon/core/visitor/CodeBlockNodeVisitor.java delete mode 100644 markwon/src/main/java/ru/noties/markwon/core/visitor/CodeNodeVisitor.java delete mode 100644 markwon/src/main/java/ru/noties/markwon/core/visitor/EmphasisNodeVisitor.java delete mode 100644 markwon/src/main/java/ru/noties/markwon/core/visitor/HardLineBreakNodeVisitor.java delete mode 100644 markwon/src/main/java/ru/noties/markwon/core/visitor/HeadingNodeVisitor.java delete mode 100644 markwon/src/main/java/ru/noties/markwon/core/visitor/LinkNodeVisitor.java delete mode 100644 markwon/src/main/java/ru/noties/markwon/core/visitor/ListItemNodeVisitor.java delete mode 100644 markwon/src/main/java/ru/noties/markwon/core/visitor/ParagraphNodeVisitor.java delete mode 100644 markwon/src/main/java/ru/noties/markwon/core/visitor/SoftLineBreakNodeVisitor.java delete mode 100644 markwon/src/main/java/ru/noties/markwon/core/visitor/StrongEmphasisNodeVisitor.java delete mode 100644 markwon/src/main/java/ru/noties/markwon/core/visitor/TextNodeVisitor.java delete mode 100644 markwon/src/main/java/ru/noties/markwon/core/visitor/ThematicBreakNodeVisitor.java rename markwon/src/main/java/ru/noties/markwon/{core/spans => image}/AsyncDrawableSpan.java (91%) create mode 100644 markwon/src/main/java/ru/noties/markwon/image/ImageProps.java create mode 100644 markwon/src/main/java/ru/noties/markwon/image/ImageSpanFactory.java create mode 100644 markwon/src/test/java/ru/noties/markwon/image/ImageTest.java delete mode 100644 markwon/src/test/java/ru/noties/markwon/renderer/visitor/SpannableMarkdownVisitorTest.java delete mode 100644 markwon/src/test/java/ru/noties/markwon/renderer/visitor/TestConfig.java delete mode 100644 markwon/src/test/java/ru/noties/markwon/renderer/visitor/TestData.java delete mode 100644 markwon/src/test/java/ru/noties/markwon/renderer/visitor/TestDataReader.java delete mode 100644 markwon/src/test/java/ru/noties/markwon/renderer/visitor/TestFactory.java delete mode 100644 markwon/src/test/java/ru/noties/markwon/renderer/visitor/TestNode.java delete mode 100644 markwon/src/test/java/ru/noties/markwon/renderer/visitor/TestSpan.java delete mode 100644 markwon/src/test/java/ru/noties/markwon/renderer/visitor/TestValidator.java delete mode 100644 markwon/src/test/resources/tests/single-img.yaml diff --git a/app/src/main/java/ru/noties/markwon/MarkdownRenderer.java b/app/src/main/java/ru/noties/markwon/MarkdownRenderer.java index df1c0b5d..5c8658e7 100644 --- a/app/src/main/java/ru/noties/markwon/MarkdownRenderer.java +++ b/app/src/main/java/ru/noties/markwon/MarkdownRenderer.java @@ -85,17 +85,17 @@ public class MarkdownRenderer { : prism4JThemeDarkula; final Markwon markwon = Markwon.builder(context) - .use(CorePlugin.create()) - .use(ImagesPlugin.createWithAssets(context)) - .use(SvgPlugin.create(context.getResources())) - .use(GifPlugin.create(false)) - .use(SyntaxHighlightPlugin.create(prism4j, prism4jTheme)) - .use(GifAwarePlugin.create(context)) - .use(TablePlugin.create(context)) - .use(TaskListPlugin.create(context)) - .use(StrikethroughPlugin.create()) - .use(HtmlPlugin.create()) - .use(new AbstractMarkwonPlugin() { + .usePlugin(CorePlugin.create()) + .usePlugin(ImagesPlugin.createWithAssets(context)) + .usePlugin(SvgPlugin.create(context.getResources())) + .usePlugin(GifPlugin.create(false)) + .usePlugin(SyntaxHighlightPlugin.create(prism4j, prism4jTheme)) + .usePlugin(GifAwarePlugin.create(context)) + .usePlugin(TablePlugin.create(context)) + .usePlugin(TaskListPlugin.create(context)) + .usePlugin(StrikethroughPlugin.create()) + .usePlugin(HtmlPlugin.create()) + .usePlugin(new AbstractMarkwonPlugin() { @Override public void configureConfiguration(@NonNull MarkwonConfiguration.Builder builder) { builder.urlProcessor(urlProcessor); diff --git a/app/src/main/java/ru/noties/markwon/gif/GifAwarePlugin.java b/app/src/main/java/ru/noties/markwon/gif/GifAwarePlugin.java index 6bc6682b..ab69ceb7 100644 --- a/app/src/main/java/ru/noties/markwon/gif/GifAwarePlugin.java +++ b/app/src/main/java/ru/noties/markwon/gif/GifAwarePlugin.java @@ -4,9 +4,16 @@ import android.content.Context; import android.support.annotation.NonNull; import android.widget.TextView; +import org.commonmark.node.Image; + import ru.noties.markwon.AbstractMarkwonPlugin; import ru.noties.markwon.MarkwonConfiguration; +import ru.noties.markwon.MarkwonSpansFactory; import ru.noties.markwon.R; +import ru.noties.markwon.RenderProps; +import ru.noties.markwon.SpanFactory; +import ru.noties.markwon.image.AsyncDrawableSpan; +import ru.noties.markwon.image.ImageProps; public class GifAwarePlugin extends AbstractMarkwonPlugin { @@ -18,18 +25,36 @@ public class GifAwarePlugin extends AbstractMarkwonPlugin { private final Context context; private final GifProcessor processor; - public GifAwarePlugin(@NonNull Context context) { + GifAwarePlugin(@NonNull Context context) { this.context = context; this.processor = GifProcessor.create(); } @Override - public void configureConfiguration(@NonNull MarkwonConfiguration.Builder builder) { + public void configureSpansFactory(@NonNull MarkwonSpansFactory.Builder builder) { + final GifPlaceholder gifPlaceholder = new GifPlaceholder( context.getResources().getDrawable(R.drawable.ic_play_circle_filled_18dp_white), 0x20000000 ); - builder.factory(new GifAwareSpannableFactory(gifPlaceholder)); + + builder.setFactory(Image.class, new SpanFactory() { + @Override + public Object getSpans(@NonNull MarkwonConfiguration configuration, @NonNull RenderProps context) { + return new AsyncDrawableSpan( + configuration.theme(), + new GifAwareAsyncDrawable( + gifPlaceholder, + ImageProps.DESTINATION.require(context), + configuration.asyncDrawableLoader(), + configuration.imageSizeResolver(), + ImageProps.IMAGE_SIZE.get(context) + ), + AsyncDrawableSpan.ALIGN_BOTTOM, + ImageProps.REPLACEMENT_TEXT_IS_LINK.get(context, false) + ); + } + }); } @Override diff --git a/app/src/main/java/ru/noties/markwon/gif/GifAwareSpannableFactory.java b/app/src/main/java/ru/noties/markwon/gif/GifAwareSpannableFactory.java deleted file mode 100644 index 8d9e14fb..00000000 --- a/app/src/main/java/ru/noties/markwon/gif/GifAwareSpannableFactory.java +++ /dev/null @@ -1,37 +0,0 @@ -package ru.noties.markwon.gif; - -import android.support.annotation.NonNull; -import android.support.annotation.Nullable; - -import ru.noties.markwon.core.MarkwonTheme; -import ru.noties.markwon.core.MarkwonSpannableFactoryDef; -import ru.noties.markwon.image.AsyncDrawableLoader; -import ru.noties.markwon.image.ImageSize; -import ru.noties.markwon.image.ImageSizeResolver; -import ru.noties.markwon.core.spans.AsyncDrawableSpan; - -public class GifAwareSpannableFactory extends MarkwonSpannableFactoryDef { - - private final GifPlaceholder gifPlaceholder; - - public GifAwareSpannableFactory(@NonNull GifPlaceholder gifPlaceholder) { - this.gifPlaceholder = gifPlaceholder; - } - - @Nullable - @Override - public Object image(@NonNull MarkwonTheme theme, @NonNull String destination, @NonNull AsyncDrawableLoader loader, @NonNull ImageSizeResolver imageSizeResolver, @Nullable ImageSize imageSize, boolean replacementTextIsLink) { - return new AsyncDrawableSpan( - theme, - new GifAwareAsyncDrawable( - gifPlaceholder, - destination, - loader, - imageSizeResolver, - imageSize - ), - AsyncDrawableSpan.ALIGN_BOTTOM, - replacementTextIsLink - ); - } -} diff --git a/app/src/main/java/ru/noties/markwon/gif/GifProcessor.java b/app/src/main/java/ru/noties/markwon/gif/GifProcessor.java index 6a297f8b..fcec2e17 100644 --- a/app/src/main/java/ru/noties/markwon/gif/GifProcessor.java +++ b/app/src/main/java/ru/noties/markwon/gif/GifProcessor.java @@ -9,7 +9,7 @@ import android.view.View; import android.widget.TextView; import pl.droidsonroids.gif.GifDrawable; -import ru.noties.markwon.core.spans.AsyncDrawableSpan; +import ru.noties.markwon.image.AsyncDrawableSpan; public abstract class GifProcessor { diff --git a/markwon-ext-tasklist/src/main/java/ru/noties/markwon/ext/tasklist/TaskListPlugin.java b/markwon-ext-tasklist/src/main/java/ru/noties/markwon/ext/tasklist/TaskListPlugin.java index 31a61a1b..5a787dc9 100644 --- a/markwon-ext-tasklist/src/main/java/ru/noties/markwon/ext/tasklist/TaskListPlugin.java +++ b/markwon-ext-tasklist/src/main/java/ru/noties/markwon/ext/tasklist/TaskListPlugin.java @@ -12,8 +12,13 @@ import org.commonmark.node.Node; import org.commonmark.parser.Parser; import ru.noties.markwon.AbstractMarkwonPlugin; +import ru.noties.markwon.MarkwonSpansFactory; import ru.noties.markwon.MarkwonVisitor; +import ru.noties.markwon.RenderProps; +/** + * @since 3.0.0 + */ public class TaskListPlugin extends AbstractMarkwonPlugin { /** @@ -62,6 +67,11 @@ public class TaskListPlugin extends AbstractMarkwonPlugin { builder.customBlockParserFactory(new TaskListBlockParser.Factory()); } + @Override + public void configureSpansFactory(@NonNull MarkwonSpansFactory.Builder builder) { + builder.setFactory(TaskListItem.class, new TaskListSpanFactory(drawable)); + } + @Override public void configureVisitor(@NonNull MarkwonVisitor.Builder builder) { builder @@ -86,11 +96,13 @@ public class TaskListPlugin extends AbstractMarkwonPlugin { final int length = visitor.length(); visitor.visitChildren(taskListItem); - visitor.setSpans(length, new TaskListSpan( - visitor.theme(), - drawable, - indent(taskListItem) + taskListItem.indent(), - taskListItem.done())); + + final RenderProps context = visitor.renderProps(); + + TaskListProps.BLOCK_INDENT.set(context, indent(taskListItem) + taskListItem.indent()); + TaskListProps.DONE.set(context, taskListItem.done()); + + visitor.setSpansForNode(taskListItem, length); if (visitor.hasNext(taskListItem)) { visitor.ensureNewLine(); @@ -99,7 +111,7 @@ public class TaskListPlugin extends AbstractMarkwonPlugin { }); } - private static int resolve(Context context, @AttrRes int attr) { + private static int resolve(@NonNull Context context, @AttrRes int attr) { final TypedValue typedValue = new TypedValue(); final int attrs[] = new int[]{attr}; final TypedArray typedArray = context.obtainStyledAttributes(typedValue.data, attrs); diff --git a/markwon-ext-tasklist/src/main/java/ru/noties/markwon/ext/tasklist/TaskListProps.java b/markwon-ext-tasklist/src/main/java/ru/noties/markwon/ext/tasklist/TaskListProps.java new file mode 100644 index 00000000..7a0b6ca7 --- /dev/null +++ b/markwon-ext-tasklist/src/main/java/ru/noties/markwon/ext/tasklist/TaskListProps.java @@ -0,0 +1,16 @@ +package ru.noties.markwon.ext.tasklist; + +import ru.noties.markwon.Prop; + +/** + * @since 3.0.0 + */ +public abstract class TaskListProps { + + public static final Prop BLOCK_INDENT = Prop.of("task-list-block-indent"); + + public static final Prop DONE = Prop.of("task-list-done"); + + private TaskListProps() { + } +} diff --git a/markwon-ext-tasklist/src/main/java/ru/noties/markwon/ext/tasklist/TaskListSpan.java b/markwon-ext-tasklist/src/main/java/ru/noties/markwon/ext/tasklist/TaskListSpan.java index b339360f..b851f382 100644 --- a/markwon-ext-tasklist/src/main/java/ru/noties/markwon/ext/tasklist/TaskListSpan.java +++ b/markwon-ext-tasklist/src/main/java/ru/noties/markwon/ext/tasklist/TaskListSpan.java @@ -26,13 +26,6 @@ public class TaskListSpan implements LeadingMarginSpan { // @since 2.0.1 field is NOT final (to allow mutation) private boolean isDone; - @Deprecated - public TaskListSpan(@NonNull MarkwonTheme theme, int blockIndent, boolean isDone) { - this.theme = theme; - this.drawable = null; - this.blockIndent = blockIndent; - this.isDone = isDone; - } public TaskListSpan(@NonNull MarkwonTheme theme, @NonNull Drawable drawable, int blockIndent, boolean isDone) { this.theme = theme; @@ -71,11 +64,6 @@ public class TaskListSpan implements LeadingMarginSpan { return; } -// final Drawable drawable = theme.getTaskListDrawable(); -// if (drawable == null) { -// return; -// } - final int save = c.save(); try { diff --git a/markwon-ext-tasklist/src/main/java/ru/noties/markwon/ext/tasklist/TaskListSpanFactory.java b/markwon-ext-tasklist/src/main/java/ru/noties/markwon/ext/tasklist/TaskListSpanFactory.java new file mode 100644 index 00000000..b0f4fb20 --- /dev/null +++ b/markwon-ext-tasklist/src/main/java/ru/noties/markwon/ext/tasklist/TaskListSpanFactory.java @@ -0,0 +1,29 @@ +package ru.noties.markwon.ext.tasklist; + +import android.graphics.drawable.Drawable; +import android.support.annotation.NonNull; +import android.support.annotation.Nullable; + +import ru.noties.markwon.MarkwonConfiguration; +import ru.noties.markwon.RenderProps; +import ru.noties.markwon.SpanFactory; + +public class TaskListSpanFactory implements SpanFactory { + + private final Drawable drawable; + + public TaskListSpanFactory(@NonNull Drawable drawable) { + this.drawable = drawable; + } + + @Nullable + @Override + public Object getSpans(@NonNull MarkwonConfiguration configuration, @NonNull RenderProps context) { + return new TaskListSpan( + configuration.theme(), + drawable, + TaskListProps.BLOCK_INDENT.get(context, 0), + TaskListProps.DONE.get(context, false) + ); + } +} diff --git a/markwon-html/src/main/java/ru/noties/markwon/html/tag/ImageHandler.java b/markwon-html/src/main/java/ru/noties/markwon/html/tag/ImageHandler.java index 2faf4826..8554a645 100644 --- a/markwon-html/src/main/java/ru/noties/markwon/html/tag/ImageHandler.java +++ b/markwon-html/src/main/java/ru/noties/markwon/html/tag/ImageHandler.java @@ -47,13 +47,15 @@ public class ImageHandler extends SimpleTagHandler { // but we can look and see if we are inside a LinkSpan (will have to extend TagHandler // to obtain an instance SpannableBuilder for inspection) - return configuration.factory().image( - configuration.theme(), - destination, - configuration.asyncDrawableLoader(), - configuration.imageSizeResolver(), - imageSizeParser.parse(tag.attributes()), - false - ); + return null; + +// return configuration.factory().image( +// configuration.theme(), +// destination, +// configuration.asyncDrawableLoader(), +// configuration.imageSizeResolver(), +// imageSizeParser.parse(tag.attributes()), +// false +// ); } } diff --git a/markwon/src/main/java/ru/noties/markwon/AbstractMarkwonPlugin.java b/markwon/src/main/java/ru/noties/markwon/AbstractMarkwonPlugin.java index 9820f18f..28767521 100644 --- a/markwon/src/main/java/ru/noties/markwon/AbstractMarkwonPlugin.java +++ b/markwon/src/main/java/ru/noties/markwon/AbstractMarkwonPlugin.java @@ -35,6 +35,16 @@ public abstract class AbstractMarkwonPlugin implements MarkwonPlugin { } + @Override + public void configureSpansFactory(@NonNull MarkwonSpansFactory.Builder builder) { + + } + + @Override + public void configureRenderProps(@NonNull RenderProps renderProps) { + + } + @NonNull @Override public String processMarkdown(@NonNull String markdown) { diff --git a/markwon/src/main/java/ru/noties/markwon/Markwon.java b/markwon/src/main/java/ru/noties/markwon/Markwon.java index 6214fc16..6c4e4bea 100644 --- a/markwon/src/main/java/ru/noties/markwon/Markwon.java +++ b/markwon/src/main/java/ru/noties/markwon/Markwon.java @@ -9,6 +9,9 @@ import org.commonmark.node.Node; public abstract class Markwon { + /** + * @since 3.0.0 + */ @NonNull public static Builder builder(@NonNull Context context) { return new MarkwonBuilderImpl(context); @@ -28,6 +31,9 @@ public abstract class Markwon { public abstract void setParsedMarkdown(@NonNull TextView textView, @NonNull CharSequence markdown); + /** + * @since 3.0.0 + */ public interface Builder { /** @@ -40,7 +46,10 @@ public abstract class Markwon { Builder bufferType(@NonNull TextView.BufferType bufferType); @NonNull - Builder use(@NonNull MarkwonPlugin plugin); + Builder usePlugin(@NonNull MarkwonPlugin plugin); + + @NonNull + Builder usePlugins(@NonNull Iterable plugins); @NonNull Markwon build(); diff --git a/markwon/src/main/java/ru/noties/markwon/MarkwonBuilderImpl.java b/markwon/src/main/java/ru/noties/markwon/MarkwonBuilderImpl.java index 641f9691..6098155a 100644 --- a/markwon/src/main/java/ru/noties/markwon/MarkwonBuilderImpl.java +++ b/markwon/src/main/java/ru/noties/markwon/MarkwonBuilderImpl.java @@ -8,11 +8,15 @@ import org.commonmark.parser.Parser; import java.util.ArrayList; import java.util.Collections; +import java.util.Iterator; import java.util.List; import ru.noties.markwon.core.MarkwonTheme; import ru.noties.markwon.image.AsyncDrawableLoader; +/** + * @since 3.0.0 + */ class MarkwonBuilderImpl implements Markwon.Builder { private final Context context; @@ -33,11 +37,30 @@ class MarkwonBuilderImpl implements Markwon.Builder { @NonNull @Override - public Markwon.Builder use(@NonNull MarkwonPlugin plugin) { + public Markwon.Builder usePlugin(@NonNull MarkwonPlugin plugin) { plugins.add(plugin); return this; } + @NonNull + @Override + public Markwon.Builder usePlugins(@NonNull Iterable plugins) { + + final Iterator iterator = plugins.iterator(); + + MarkwonPlugin plugin; + + while (iterator.hasNext()) { + plugin = iterator.next(); + if (plugin == null) { + throw new NullPointerException(); + } + this.plugins.add(plugin); + } + + return this; + } + @NonNull @Override public Markwon build() { @@ -45,8 +68,10 @@ class MarkwonBuilderImpl implements Markwon.Builder { final Parser.Builder parserBuilder = new Parser.Builder(); final MarkwonTheme.Builder themeBuilder = MarkwonTheme.builderWithDefaults(context); final AsyncDrawableLoader.Builder asyncDrawableLoaderBuilder = new AsyncDrawableLoader.Builder(); - final MarkwonConfiguration.Builder configurationBuilder = new MarkwonConfiguration.Builder(context); + final MarkwonConfiguration.Builder configurationBuilder = new MarkwonConfiguration.Builder(); final MarkwonVisitor.Builder visitorBuilder = new MarkwonVisitorImpl.BuilderImpl(); + final MarkwonSpansFactory.Builder spanFactoryBuilder = new MarkwonSpansFactoryImpl.BuilderImpl(); + final RenderProps renderProps = new RenderPropsImpl(); for (MarkwonPlugin plugin : plugins) { plugin.configureParser(parserBuilder); @@ -54,16 +79,19 @@ class MarkwonBuilderImpl implements Markwon.Builder { plugin.configureImages(asyncDrawableLoaderBuilder); plugin.configureConfiguration(configurationBuilder); plugin.configureVisitor(visitorBuilder); + plugin.configureSpansFactory(spanFactoryBuilder); + plugin.configureRenderProps(renderProps); } final MarkwonConfiguration configuration = configurationBuilder.build( themeBuilder.build(), - asyncDrawableLoaderBuilder.build()); + asyncDrawableLoaderBuilder.build(), + spanFactoryBuilder.build()); return new MarkwonImpl( bufferType, parserBuilder.build(), - visitorBuilder.build(configuration), + visitorBuilder.build(configuration, renderProps), Collections.unmodifiableList(plugins) ); } diff --git a/markwon/src/main/java/ru/noties/markwon/MarkwonConfiguration.java b/markwon/src/main/java/ru/noties/markwon/MarkwonConfiguration.java index b55c1df5..b5266c1d 100644 --- a/markwon/src/main/java/ru/noties/markwon/MarkwonConfiguration.java +++ b/markwon/src/main/java/ru/noties/markwon/MarkwonConfiguration.java @@ -1,11 +1,8 @@ package ru.noties.markwon; -import android.content.Context; import android.support.annotation.NonNull; import ru.noties.markwon.core.MarkwonTheme; -import ru.noties.markwon.core.MarkwonSpannableFactory; -import ru.noties.markwon.core.MarkwonSpannableFactoryDef; import ru.noties.markwon.core.spans.LinkSpan; import ru.noties.markwon.image.AsyncDrawableLoader; import ru.noties.markwon.image.ImageSizeResolver; @@ -21,16 +18,9 @@ import ru.noties.markwon.urlprocessor.UrlProcessorNoOp; @SuppressWarnings("WeakerAccess") public class MarkwonConfiguration { - // creates default configuration @NonNull - @Deprecated - public static MarkwonConfiguration create(@NonNull Context context) { - return new Builder(context).build(MarkwonTheme.create(context), AsyncDrawableLoader.noOp()); - } - - @NonNull - public static Builder builder(@NonNull Context context) { - return new Builder(context); + public static Builder builder() { + return new Builder(); } private final MarkwonTheme theme; @@ -39,7 +29,9 @@ public class MarkwonConfiguration { private final LinkSpan.Resolver linkResolver; private final UrlProcessor urlProcessor; private final ImageSizeResolver imageSizeResolver; - private final MarkwonSpannableFactory factory; // @since 1.1.0 + + // @since 3.0.0 + private final MarkwonSpansFactory spansFactory; private MarkwonConfiguration(@NonNull Builder builder) { this.theme = builder.theme; @@ -48,7 +40,7 @@ public class MarkwonConfiguration { this.linkResolver = builder.linkResolver; this.urlProcessor = builder.urlProcessor; this.imageSizeResolver = builder.imageSizeResolver; - this.factory = builder.factory; + this.spansFactory = builder.spansFactory; } @NonNull @@ -81,26 +73,26 @@ public class MarkwonConfiguration { return imageSizeResolver; } + /** + * @since 3.0.0 + */ @NonNull - public MarkwonSpannableFactory factory() { - return factory; + public MarkwonSpansFactory spansFactory() { + return spansFactory; } - @SuppressWarnings("unused") + @SuppressWarnings({"unused", "UnusedReturnValue"}) public static class Builder { - private final Context context; - private MarkwonTheme theme; private AsyncDrawableLoader asyncDrawableLoader; private SyntaxHighlight syntaxHighlight; private LinkSpan.Resolver linkResolver; private UrlProcessor urlProcessor; private ImageSizeResolver imageSizeResolver; - private MarkwonSpannableFactory factory; // @since 1.1.0 + private MarkwonSpansFactory spansFactory; - Builder(@NonNull Context context) { - this.context = context; + Builder() { } @NonNull @@ -130,20 +122,15 @@ public class MarkwonConfiguration { return this; } - /** - * @since 1.1.0 - */ @NonNull - public Builder factory(@NonNull MarkwonSpannableFactory factory) { - this.factory = factory; - return this; - } - - @NonNull - public MarkwonConfiguration build(@NonNull MarkwonTheme theme, @NonNull AsyncDrawableLoader asyncDrawableLoader) { + public MarkwonConfiguration build( + @NonNull MarkwonTheme theme, + @NonNull AsyncDrawableLoader asyncDrawableLoader, + @NonNull MarkwonSpansFactory spansFactory) { this.theme = theme; this.asyncDrawableLoader = asyncDrawableLoader; + this.spansFactory = spansFactory; if (syntaxHighlight == null) { syntaxHighlight = new SyntaxHighlightNoOp(); @@ -161,11 +148,6 @@ public class MarkwonConfiguration { imageSizeResolver = new ImageSizeResolverDef(); } - // @since 1.1.0 - if (factory == null) { - factory = MarkwonSpannableFactoryDef.create(); - } - return new MarkwonConfiguration(this); } } diff --git a/markwon/src/main/java/ru/noties/markwon/MarkwonPlugin.java b/markwon/src/main/java/ru/noties/markwon/MarkwonPlugin.java index fc0cf4ca..910b044e 100644 --- a/markwon/src/main/java/ru/noties/markwon/MarkwonPlugin.java +++ b/markwon/src/main/java/ru/noties/markwon/MarkwonPlugin.java @@ -9,6 +9,9 @@ import org.commonmark.parser.Parser; import ru.noties.markwon.core.MarkwonTheme; import ru.noties.markwon.image.AsyncDrawableLoader; +/** + * @since 3.0.0 + */ public interface MarkwonPlugin { void configureParser(@NonNull Parser.Builder builder); @@ -21,6 +24,11 @@ public interface MarkwonPlugin { void configureVisitor(@NonNull MarkwonVisitor.Builder builder); + void configureSpansFactory(@NonNull MarkwonSpansFactory.Builder builder); + + // can be used to configure own properties and use between plugins + void configureRenderProps(@NonNull RenderProps renderProps); + @NonNull String processMarkdown(@NonNull String markdown); diff --git a/markwon/src/main/java/ru/noties/markwon/MarkwonSpansFactory.java b/markwon/src/main/java/ru/noties/markwon/MarkwonSpansFactory.java new file mode 100644 index 00000000..748ebd57 --- /dev/null +++ b/markwon/src/main/java/ru/noties/markwon/MarkwonSpansFactory.java @@ -0,0 +1,34 @@ +package ru.noties.markwon; + +import android.support.annotation.NonNull; +import android.support.annotation.Nullable; + +import org.commonmark.node.Node; + +/** + * @since 3.0.0 + */ +public interface MarkwonSpansFactory { + + @Nullable + F get(@NonNull Class node); + + @Nullable + F get(@NonNull N node); + + @NonNull + F require(@NonNull Class node); + + @NonNull + F require(@NonNull N node); + + + interface Builder { + + @NonNull + Builder setFactory(@NonNull Class node, @NonNull F factory); + + @NonNull + MarkwonSpansFactory build(); + } +} diff --git a/markwon/src/main/java/ru/noties/markwon/MarkwonSpansFactoryImpl.java b/markwon/src/main/java/ru/noties/markwon/MarkwonSpansFactoryImpl.java new file mode 100644 index 00000000..7bbc8ca8 --- /dev/null +++ b/markwon/src/main/java/ru/noties/markwon/MarkwonSpansFactoryImpl.java @@ -0,0 +1,74 @@ +package ru.noties.markwon; + +import android.support.annotation.NonNull; +import android.support.annotation.Nullable; + +import org.commonmark.node.Node; + +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; + +/** + * @since 3.0.0 + */ +class MarkwonSpansFactoryImpl implements MarkwonSpansFactory { + + private final Map, SpanFactory> factories; + + private MarkwonSpansFactoryImpl(@NonNull Map, SpanFactory> factories) { + this.factories = factories; + } + + @Nullable + @Override + public F get(@NonNull Class node) { + //noinspection unchecked + return (F) factories.get(node); + } + + @Nullable + @Override + public F get(@NonNull N node) { + return get(node.getClass()); + } + + @NonNull + @Override + public F require(@NonNull Class node) { + final F f = get(node); + if (f == null) { + throw new NullPointerException(); + } + return f; + } + + @NonNull + @Override + public F require(@NonNull N node) { + final F f = get(node); + if (f == null) { + throw new NullPointerException(); + } + return f; + } + + static class BuilderImpl implements Builder { + + private final Map, SpanFactory> factories = + new HashMap<>(3); + + @NonNull + @Override + public Builder setFactory(@NonNull Class node, @NonNull F factory) { + factories.put(node, factory); + return this; + } + + @NonNull + @Override + public MarkwonSpansFactory build() { + return new MarkwonSpansFactoryImpl(Collections.unmodifiableMap(factories)); + } + } +} diff --git a/markwon/src/main/java/ru/noties/markwon/MarkwonVisitor.java b/markwon/src/main/java/ru/noties/markwon/MarkwonVisitor.java index 88ae1b5d..f5e67c1f 100644 --- a/markwon/src/main/java/ru/noties/markwon/MarkwonVisitor.java +++ b/markwon/src/main/java/ru/noties/markwon/MarkwonVisitor.java @@ -6,9 +6,9 @@ import android.support.annotation.Nullable; import org.commonmark.node.Node; import org.commonmark.node.Visitor; -import ru.noties.markwon.core.MarkwonTheme; -import ru.noties.markwon.core.MarkwonSpannableFactory; - +/** + * @since 3.0.0 + */ public interface MarkwonVisitor extends Visitor { interface NodeVisitor { @@ -21,17 +21,14 @@ public interface MarkwonVisitor extends Visitor { Builder on(@NonNull Class node, @Nullable NodeVisitor nodeVisitor); @NonNull - MarkwonVisitor build(@NonNull MarkwonConfiguration configuration); + MarkwonVisitor build(@NonNull MarkwonConfiguration configuration, @NonNull RenderProps renderProps); } @NonNull MarkwonConfiguration configuration(); @NonNull - MarkwonTheme theme(); - - @NonNull - MarkwonSpannableFactory factory(); + RenderProps renderProps(); @NonNull SpannableBuilder builder(); @@ -48,6 +45,10 @@ public interface MarkwonVisitor extends Visitor { void setSpans(int start, @Nullable Object spans); - @Nullable - NodeVisitor nodeVisitor(@NonNull Class node); + // will automatically obtain SpanFactory instance and use it, it no SpanFactory is registered, + // will throw, if not desired use setSpansForNodeOptional + void setSpansForNode(@NonNull N node, int start); + + // does not throw if there is no SpanFactory registered for this node + void setSpansForNodeOptional(@NonNull N node, int start); } diff --git a/markwon/src/main/java/ru/noties/markwon/MarkwonVisitorImpl.java b/markwon/src/main/java/ru/noties/markwon/MarkwonVisitorImpl.java index 6982b74a..0421f9e3 100644 --- a/markwon/src/main/java/ru/noties/markwon/MarkwonVisitorImpl.java +++ b/markwon/src/main/java/ru/noties/markwon/MarkwonVisitorImpl.java @@ -31,25 +31,25 @@ import java.util.Collections; import java.util.HashMap; import java.util.Map; -import ru.noties.markwon.core.MarkwonTheme; -import ru.noties.markwon.core.MarkwonSpannableFactory; - +/** + * @since 3.0.0 + */ class MarkwonVisitorImpl implements MarkwonVisitor { private final Map, NodeVisitor> nodes; private final MarkwonConfiguration configuration; - private final MarkwonTheme theme; - private final MarkwonSpannableFactory factory; + + private final RenderProps renderProps; private final SpannableBuilder builder = new SpannableBuilder(); MarkwonVisitorImpl( @NonNull MarkwonConfiguration configuration, + @NonNull RenderProps renderProps, @NonNull Map, NodeVisitor> nodes) { this.configuration = configuration; - this.theme = configuration.theme(); - this.factory = configuration.factory(); + this.renderProps = renderProps; this.nodes = nodes; } @@ -181,14 +181,8 @@ class MarkwonVisitorImpl implements MarkwonVisitor { @NonNull @Override - public MarkwonTheme theme() { - return theme; - } - - @NonNull - @Override - public MarkwonSpannableFactory factory() { - return factory; + public RenderProps renderProps() { + return renderProps; } @NonNull @@ -237,17 +231,22 @@ class MarkwonVisitorImpl implements MarkwonVisitor { SpannableBuilder.setSpans(builder, spans, start, builder.length()); } - @Nullable @Override - public NodeVisitor nodeVisitor(@NonNull Class node) { - //noinspection unchecked - return (NodeVisitor) nodes.get(node); + public void setSpansForNode(@NonNull N node, int start) { + setSpans(start, configuration.spansFactory().require(node).getSpans(configuration, renderProps)); + } + + @Override + public void setSpansForNodeOptional(@NonNull N node, int start) { + final SpanFactory factory = configuration.spansFactory().get(node); + if (factory != null) { + setSpans(start, factory.getSpans(configuration, renderProps)); + } } static class BuilderImpl implements Builder { - private final Map, NodeVisitor> nodes = - new HashMap<>(3); + private final Map, NodeVisitor> nodes = new HashMap<>(); @NonNull @Override @@ -264,9 +263,10 @@ class MarkwonVisitorImpl implements MarkwonVisitor { @NonNull @Override - public MarkwonVisitor build(@NonNull MarkwonConfiguration configuration) { + public MarkwonVisitor build(@NonNull MarkwonConfiguration configuration, @NonNull RenderProps renderProps) { return new MarkwonVisitorImpl( configuration, + renderProps, Collections.unmodifiableMap(nodes)); } } diff --git a/markwon/src/main/java/ru/noties/markwon/Prop.java b/markwon/src/main/java/ru/noties/markwon/Prop.java new file mode 100644 index 00000000..a930288f --- /dev/null +++ b/markwon/src/main/java/ru/noties/markwon/Prop.java @@ -0,0 +1,84 @@ +package ru.noties.markwon; + +import android.support.annotation.NonNull; +import android.support.annotation.Nullable; + +/** + * Class to hold data in {@link RenderProps} + * + * @param represents the type that this instance holds + * @since 3.0.0 + */ +public final class Prop { + + @SuppressWarnings("unused") + @NonNull + public static Prop of(@NonNull Class type, @NonNull String name) { + return new Prop<>(name); + } + + @NonNull + public static Prop of(@NonNull String name) { + return new Prop<>(name); + } + + private final String name; + + private Prop(@NonNull String name) { + this.name = name; + } + + @NonNull + public String name() { + return name; + } + + @Nullable + public T get(@NonNull RenderProps context) { + return context.get(this); + } + + @NonNull + public T get(@NonNull RenderProps context, @NonNull T defValue) { + return context.get(this, defValue); + } + + @NonNull + public T require(@NonNull RenderProps context) { + final T t = get(context); + if (t == null) { + throw new NullPointerException(); + } + return t; + } + + public void set(@NonNull RenderProps context, @Nullable T value) { + context.set(this, value); + } + + public void clear(@NonNull RenderProps context) { + context.clear(this); + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + + Prop prop = (Prop) o; + + return name.equals(prop.name); + } + + @Override + public int hashCode() { + return name.hashCode(); + } + + @Override + public String toString() { + return "Prop{" + + "name='" + name + '\'' + + '}'; + } +} diff --git a/markwon/src/main/java/ru/noties/markwon/RenderProps.java b/markwon/src/main/java/ru/noties/markwon/RenderProps.java new file mode 100644 index 00000000..b939acc4 --- /dev/null +++ b/markwon/src/main/java/ru/noties/markwon/RenderProps.java @@ -0,0 +1,20 @@ +package ru.noties.markwon; + +import android.support.annotation.NonNull; +import android.support.annotation.Nullable; + +/** + * @since 3.0.0 + */ +public interface RenderProps { + + @Nullable + T get(@NonNull Prop prop); + + @NonNull + T get(@NonNull Prop prop, @NonNull T defValue); + + void set(@NonNull Prop prop, @Nullable T value); + + void clear(@NonNull Prop prop); +} diff --git a/markwon/src/main/java/ru/noties/markwon/RenderPropsImpl.java b/markwon/src/main/java/ru/noties/markwon/RenderPropsImpl.java new file mode 100644 index 00000000..81605ee9 --- /dev/null +++ b/markwon/src/main/java/ru/noties/markwon/RenderPropsImpl.java @@ -0,0 +1,51 @@ +package ru.noties.markwon; + +import android.support.annotation.NonNull; +import android.support.annotation.Nullable; + +import java.util.HashMap; +import java.util.Map; + +/** + * @since 3.0.0 + */ +public class RenderPropsImpl implements RenderProps { + + private final Map values = new HashMap<>(3); + + @Nullable + @Override + public T get(@NonNull Prop prop) { + //noinspection unchecked + return (T) values.get(prop); + } + + @NonNull + @Override + public T get(@NonNull Prop prop, @NonNull T defValue) { + Object value = values.get(prop); + if (value != null) { + //noinspection unchecked + return (T) value; + } + return defValue; + } + + @Override + public void set(@NonNull Prop prop, @Nullable T value) { + if (value == null) { + values.remove(prop); + } else { + values.put(prop, value); + } + } + + @Override + public void clear(@NonNull Prop prop) { + values.remove(prop); + } + + public void clearAll() { + values.clear(); + } +} diff --git a/markwon/src/main/java/ru/noties/markwon/SpanFactory.java b/markwon/src/main/java/ru/noties/markwon/SpanFactory.java new file mode 100644 index 00000000..62780776 --- /dev/null +++ b/markwon/src/main/java/ru/noties/markwon/SpanFactory.java @@ -0,0 +1,15 @@ +package ru.noties.markwon; + +import android.support.annotation.NonNull; +import android.support.annotation.Nullable; + +/** + * @since 3.0.0 + */ +public interface SpanFactory { + + @Nullable + Object getSpans( + @NonNull MarkwonConfiguration configuration, + @NonNull RenderProps context); +} diff --git a/markwon/src/main/java/ru/noties/markwon/core/CorePlugin.java b/markwon/src/main/java/ru/noties/markwon/core/CorePlugin.java index c6e6c1bb..4d1ff29b 100644 --- a/markwon/src/main/java/ru/noties/markwon/core/CorePlugin.java +++ b/markwon/src/main/java/ru/noties/markwon/core/CorePlugin.java @@ -1,6 +1,7 @@ package ru.noties.markwon.core; import android.support.annotation.NonNull; +import android.support.annotation.Nullable; import android.widget.TextView; import org.commonmark.node.BlockQuote; @@ -12,7 +13,9 @@ import org.commonmark.node.HardLineBreak; import org.commonmark.node.Heading; import org.commonmark.node.IndentedCodeBlock; import org.commonmark.node.Link; +import org.commonmark.node.ListBlock; import org.commonmark.node.ListItem; +import org.commonmark.node.Node; import org.commonmark.node.OrderedList; import org.commonmark.node.Paragraph; import org.commonmark.node.SoftLineBreak; @@ -21,23 +24,23 @@ import org.commonmark.node.Text; import org.commonmark.node.ThematicBreak; import ru.noties.markwon.AbstractMarkwonPlugin; +import ru.noties.markwon.MarkwonConfiguration; +import ru.noties.markwon.MarkwonSpansFactory; import ru.noties.markwon.MarkwonVisitor; -import ru.noties.markwon.core.visitor.BlockQuoteNodeVisitor; -import ru.noties.markwon.core.visitor.CodeBlockNodeVisitor; -import ru.noties.markwon.core.visitor.CodeNodeVisitor; -import ru.noties.markwon.core.visitor.EmphasisNodeVisitor; -import ru.noties.markwon.core.visitor.HardLineBreakNodeVisitor; -import ru.noties.markwon.core.visitor.HeadingNodeVisitor; -import ru.noties.markwon.core.visitor.LinkNodeVisitor; -import ru.noties.markwon.core.visitor.ListBlockNodeVisitor; -import ru.noties.markwon.core.visitor.ListItemNodeVisitor; -import ru.noties.markwon.core.visitor.ParagraphNodeVisitor; -import ru.noties.markwon.core.visitor.SoftLineBreakNodeVisitor; -import ru.noties.markwon.core.visitor.StrongEmphasisNodeVisitor; -import ru.noties.markwon.core.visitor.TextNodeVisitor; -import ru.noties.markwon.core.visitor.ThematicBreakNodeVisitor; +import ru.noties.markwon.core.factory.BlockQuoteSpanFactory; +import ru.noties.markwon.core.factory.CodeBlockSpanFactory; +import ru.noties.markwon.core.factory.CodeSpanFactory; +import ru.noties.markwon.core.factory.EmphasisSpanFactory; +import ru.noties.markwon.core.factory.HeadingSpanFactory; +import ru.noties.markwon.core.factory.LinkSpanFactory; +import ru.noties.markwon.core.factory.ListItemSpanFactory; +import ru.noties.markwon.core.factory.StrongEmphasisSpanFactory; +import ru.noties.markwon.core.factory.ThematicBreakSpanFactory; import ru.noties.markwon.core.spans.OrderedListItemSpan; +/** + * @since 3.0.0 + */ public class CorePlugin extends AbstractMarkwonPlugin { @NonNull @@ -50,8 +53,12 @@ public class CorePlugin extends AbstractMarkwonPlugin { return new CorePlugin(softBreakAddsNewLine); } + // todo: can we make it configurable somewhere else? + // even possibility of options that require creating factory method for each configuration... meh private final boolean softBreakAddsNewLine; + // todo: test that visitors are registered for all expected nodes + protected CorePlugin(boolean softBreakAddsNewLine) { this.softBreakAddsNewLine = softBreakAddsNewLine; } @@ -70,78 +77,315 @@ public class CorePlugin extends AbstractMarkwonPlugin { listItem(builder); thematicBreak(builder); heading(builder); - softLineBreak(builder); + softLineBreak(builder, softBreakAddsNewLine); hardLineBreak(builder); paragraph(builder); link(builder); } + @Override + public void configureSpansFactory(@NonNull MarkwonSpansFactory.Builder builder) { + + // reuse this one for both code-blocks (indent & fenced) + final CodeBlockSpanFactory codeBlockSpanFactory = new CodeBlockSpanFactory(); + + builder + .setFactory(StrongEmphasis.class, new StrongEmphasisSpanFactory()) + .setFactory(Emphasis.class, new EmphasisSpanFactory()) + .setFactory(BlockQuote.class, new BlockQuoteSpanFactory()) + .setFactory(Code.class, new CodeSpanFactory()) + .setFactory(FencedCodeBlock.class, codeBlockSpanFactory) + .setFactory(IndentedCodeBlock.class, codeBlockSpanFactory) + .setFactory(ListItem.class, new ListItemSpanFactory()) + .setFactory(Heading.class, new HeadingSpanFactory()) + .setFactory(Link.class, new LinkSpanFactory()) + .setFactory(ThematicBreak.class, new ThematicBreakSpanFactory()); + } + @Override public void beforeSetText(@NonNull TextView textView, @NonNull CharSequence markdown) { OrderedListItemSpan.measure(textView, markdown); } - protected void text(@NonNull MarkwonVisitor.Builder builder) { - builder.on(Text.class, new TextNodeVisitor()); + private static void text(@NonNull MarkwonVisitor.Builder builder) { + builder.on(Text.class, new MarkwonVisitor.NodeVisitor() { + @Override + public void visit(@NonNull MarkwonVisitor visitor, @NonNull Text text) { + visitor.builder().append(text.getLiteral()); + } + }); } - protected void strongEmphasis(@NonNull MarkwonVisitor.Builder builder) { - builder.on(StrongEmphasis.class, new StrongEmphasisNodeVisitor()); + private static void strongEmphasis(@NonNull MarkwonVisitor.Builder builder) { + builder.on(StrongEmphasis.class, new MarkwonVisitor.NodeVisitor() { + @Override + public void visit(@NonNull MarkwonVisitor visitor, @NonNull StrongEmphasis strongEmphasis) { + final int length = visitor.length(); + visitor.visitChildren(strongEmphasis); + visitor.setSpansForNode(strongEmphasis, length); + } + }); } - protected void emphasis(@NonNull MarkwonVisitor.Builder builder) { - builder.on(Emphasis.class, new EmphasisNodeVisitor()); + private static void emphasis(@NonNull MarkwonVisitor.Builder builder) { + builder.on(Emphasis.class, new MarkwonVisitor.NodeVisitor() { + @Override + public void visit(@NonNull MarkwonVisitor visitor, @NonNull Emphasis emphasis) { + final int length = visitor.length(); + visitor.visitChildren(emphasis); + visitor.setSpansForNode(emphasis, length); + } + }); } - protected void blockQuote(@NonNull MarkwonVisitor.Builder builder) { - builder.on(BlockQuote.class, new BlockQuoteNodeVisitor()); + private static void blockQuote(@NonNull MarkwonVisitor.Builder builder) { + builder.on(BlockQuote.class, new MarkwonVisitor.NodeVisitor
() { + @Override + public void visit(@NonNull MarkwonVisitor visitor, @NonNull BlockQuote blockQuote) { + final int length = visitor.length(); + visitor.visitChildren(blockQuote); + visitor.setSpansForNode(blockQuote, length); + } + }); } - protected void code(@NonNull MarkwonVisitor.Builder builder) { - builder.on(Code.class, new CodeNodeVisitor()); + private static void code(@NonNull MarkwonVisitor.Builder builder) { + builder.on(Code.class, new MarkwonVisitor.NodeVisitor() { + @Override + public void visit(@NonNull MarkwonVisitor visitor, @NonNull Code code) { + + final int length = visitor.length(); + + // NB, in order to provide a _padding_ feeling code is wrapped inside two unbreakable spaces + // unfortunately we cannot use this for multiline code as we cannot control where a new line break will be inserted + visitor.builder() + .append('\u00a0') + .append(code.getLiteral()) + .append('\u00a0'); + + visitor.setSpansForNode(code, length); + } + }); } - protected void fencedCodeBlock(@NonNull MarkwonVisitor.Builder builder) { - builder.on(FencedCodeBlock.class, new CodeBlockNodeVisitor.Fenced()); + private static void fencedCodeBlock(@NonNull MarkwonVisitor.Builder builder) { + builder.on(FencedCodeBlock.class, new MarkwonVisitor.NodeVisitor() { + @Override + public void visit(@NonNull MarkwonVisitor visitor, @NonNull FencedCodeBlock fencedCodeBlock) { + visitCodeBlock(visitor, fencedCodeBlock.getInfo(), fencedCodeBlock.getLiteral(), fencedCodeBlock); + } + }); } - protected void indentedCodeBlock(@NonNull MarkwonVisitor.Builder builder) { - builder.on(IndentedCodeBlock.class, new CodeBlockNodeVisitor.Indented()); + private static void indentedCodeBlock(@NonNull MarkwonVisitor.Builder builder) { + builder.on(IndentedCodeBlock.class, new MarkwonVisitor.NodeVisitor() { + @Override + public void visit(@NonNull MarkwonVisitor visitor, @NonNull IndentedCodeBlock indentedCodeBlock) { + visitCodeBlock(visitor, null, indentedCodeBlock.getLiteral(), indentedCodeBlock); + } + }); } - protected void bulletList(@NonNull MarkwonVisitor.Builder builder) { - builder.on(BulletList.class, new ListBlockNodeVisitor()); + private static void visitCodeBlock( + @NonNull MarkwonVisitor visitor, + @Nullable String info, + @NonNull String code, + @NonNull Node node) { + + visitor.ensureNewLine(); + + final int length = visitor.length(); + + visitor.builder() + .append('\u00a0').append('\n') + .append(visitor.configuration().syntaxHighlight().highlight(info, code)); + + visitor.ensureNewLine(); + + visitor.builder().append('\u00a0'); + + visitor.setSpansForNode(node, length); + + if (visitor.hasNext(node)) { + visitor.ensureNewLine(); + visitor.forceNewLine(); + } } - protected void orderedList(@NonNull MarkwonVisitor.Builder builder) { - builder.on(OrderedList.class, new ListBlockNodeVisitor()); + private static void bulletList(@NonNull MarkwonVisitor.Builder builder) { + builder.on(BulletList.class, new SimpleBlockNodeVisitor()); } - protected void listItem(@NonNull MarkwonVisitor.Builder builder) { - builder.on(ListItem.class, new ListItemNodeVisitor()); + private static void orderedList(@NonNull MarkwonVisitor.Builder builder) { + builder.on(OrderedList.class, new SimpleBlockNodeVisitor()); } - protected void thematicBreak(@NonNull MarkwonVisitor.Builder builder) { - builder.on(ThematicBreak.class, new ThematicBreakNodeVisitor()); + private static void listItem(@NonNull MarkwonVisitor.Builder builder) { + builder.on(ListItem.class, new MarkwonVisitor.NodeVisitor() { + @Override + public void visit(@NonNull MarkwonVisitor visitor, @NonNull ListItem listItem) { + + final int length = visitor.length(); + + final Node parent = listItem.getParent(); + if (parent instanceof OrderedList) { + + final int start = ((OrderedList) parent).getStartNumber(); + + CoreProps.LIST_ITEM_TYPE.set(visitor.renderProps(), CoreProps.ListItemType.ORDERED); + CoreProps.ORDERED_LIST_ITEM_NUMBER.set(visitor.renderProps(), start); + + // after we have visited the children increment start number + final OrderedList orderedList = (OrderedList) parent; + orderedList.setStartNumber(orderedList.getStartNumber() + 1); + + } else { + CoreProps.LIST_ITEM_TYPE.set(visitor.renderProps(), CoreProps.ListItemType.BULLET); + CoreProps.BULLET_LIST_ITEM_LEVEL.set(visitor.renderProps(), listLevel(listItem)); + } + + visitor.visitChildren(listItem); + visitor.setSpansForNode(listItem, length); + + if (visitor.hasNext(listItem)) { + visitor.ensureNewLine(); + } + } + }); } - protected void heading(@NonNull MarkwonVisitor.Builder builder) { - builder.on(Heading.class, new HeadingNodeVisitor()); + private static int listLevel(@NonNull Node node) { + int level = 0; + Node parent = node.getParent(); + while (parent != null) { + if (parent instanceof ListItem) { + level += 1; + } + parent = parent.getParent(); + } + return level; } - protected void softLineBreak(@NonNull MarkwonVisitor.Builder builder) { - builder.on(SoftLineBreak.class, new SoftLineBreakNodeVisitor(softBreakAddsNewLine)); + private static void thematicBreak(@NonNull MarkwonVisitor.Builder builder) { + builder.on(ThematicBreak.class, new MarkwonVisitor.NodeVisitor() { + @Override + public void visit(@NonNull MarkwonVisitor visitor, @NonNull ThematicBreak thematicBreak) { + + visitor.ensureNewLine(); + + final int length = visitor.length(); + + // without space it won't render + visitor.builder().append('\u00a0'); + + visitor.setSpansForNode(thematicBreak, length); + + if (visitor.hasNext(thematicBreak)) { + visitor.ensureNewLine(); + visitor.forceNewLine(); + } + } + }); } - protected void hardLineBreak(@NonNull MarkwonVisitor.Builder builder) { - builder.on(HardLineBreak.class, new HardLineBreakNodeVisitor()); + private static void heading(@NonNull MarkwonVisitor.Builder builder) { + builder.on(Heading.class, new MarkwonVisitor.NodeVisitor() { + @Override + public void visit(@NonNull MarkwonVisitor visitor, @NonNull Heading heading) { + + visitor.ensureNewLine(); + + final int length = visitor.length(); + visitor.visitChildren(heading); + + CoreProps.HEADING_LEVEL.set(visitor.renderProps(), heading.getLevel()); + + visitor.setSpansForNode(heading, length); + + if (visitor.hasNext(heading)) { + visitor.ensureNewLine(); + visitor.forceNewLine(); + } + } + }); } - protected void paragraph(@NonNull MarkwonVisitor.Builder builder) { - builder.on(Paragraph.class, new ParagraphNodeVisitor()); + private static void softLineBreak(@NonNull MarkwonVisitor.Builder builder, final boolean softBreakAddsNewLine) { + builder.on(SoftLineBreak.class, new MarkwonVisitor.NodeVisitor() { + @Override + public void visit(@NonNull MarkwonVisitor visitor, @NonNull SoftLineBreak softLineBreak) { + if (softBreakAddsNewLine) { + visitor.ensureNewLine(); + } else { + visitor.builder().append(' '); + } + } + }); } - protected void link(@NonNull MarkwonVisitor.Builder builder) { - builder.on(Link.class, new LinkNodeVisitor()); + private static void hardLineBreak(@NonNull MarkwonVisitor.Builder builder) { + builder.on(HardLineBreak.class, new MarkwonVisitor.NodeVisitor() { + @Override + public void visit(@NonNull MarkwonVisitor visitor, @NonNull HardLineBreak hardLineBreak) { + visitor.ensureNewLine(); + } + }); + } + + private static void paragraph(@NonNull MarkwonVisitor.Builder builder) { + builder.on(Paragraph.class, new MarkwonVisitor.NodeVisitor() { + @Override + public void visit(@NonNull MarkwonVisitor visitor, @NonNull Paragraph paragraph) { + + final boolean inTightList = isInTightList(paragraph); + + if (!inTightList) { + visitor.ensureNewLine(); + } + + final int length = visitor.length(); + visitor.visitChildren(paragraph); + + CoreProps.PARAGRAPH_IS_IN_TIGHT_LIST.set(visitor.renderProps(), inTightList); + + // @since 1.1.1 apply paragraph span + visitor.setSpansForNodeOptional(paragraph, length); + + if (!inTightList && visitor.hasNext(paragraph)) { + visitor.ensureNewLine(); + visitor.forceNewLine(); + } + } + }); + } + + private static boolean isInTightList(@NonNull Paragraph paragraph) { + final Node parent = paragraph.getParent(); + if (parent != null) { + final Node gramps = parent.getParent(); + if (gramps instanceof ListBlock) { + ListBlock list = (ListBlock) gramps; + return list.isTight(); + } + } + return false; + } + + private static void link(@NonNull MarkwonVisitor.Builder builder) { + builder.on(Link.class, new MarkwonVisitor.NodeVisitor() { + @Override + public void visit(@NonNull MarkwonVisitor visitor, @NonNull Link link) { + + final int length = visitor.length(); + visitor.visitChildren(link); + + final MarkwonConfiguration configuration = visitor.configuration(); + final String destination = configuration.urlProcessor().process(link.getDestination()); + + CoreProps.LINK_DESTINATION.set(visitor.renderProps(), destination); + + visitor.setSpansForNode(link, length); + } + }); } } diff --git a/markwon/src/main/java/ru/noties/markwon/core/CoreProps.java b/markwon/src/main/java/ru/noties/markwon/core/CoreProps.java new file mode 100644 index 00000000..158c5802 --- /dev/null +++ b/markwon/src/main/java/ru/noties/markwon/core/CoreProps.java @@ -0,0 +1,26 @@ +package ru.noties.markwon.core; + +import ru.noties.markwon.Prop; + +public abstract class CoreProps { + + public static final Prop LIST_ITEM_TYPE = Prop.of("list-item-type"); + + public static final Prop BULLET_LIST_ITEM_LEVEL = Prop.of("bullet-list-item-level"); + + public static final Prop ORDERED_LIST_ITEM_NUMBER = Prop.of("ordered-list-item-number"); + + public static final Prop HEADING_LEVEL = Prop.of("heading-level"); + + public static final Prop LINK_DESTINATION = Prop.of("link-destination"); + + public static final Prop PARAGRAPH_IS_IN_TIGHT_LIST = Prop.of("paragraph-is-in-tight-list"); + + public enum ListItemType { + BULLET, + ORDERED + } + + private CoreProps() { + } +} diff --git a/markwon/src/main/java/ru/noties/markwon/core/MarkwonSpannableFactory.java b/markwon/src/main/java/ru/noties/markwon/core/MarkwonSpannableFactory.java index ccdb8a4c..1e440591 100644 --- a/markwon/src/main/java/ru/noties/markwon/core/MarkwonSpannableFactory.java +++ b/markwon/src/main/java/ru/noties/markwon/core/MarkwonSpannableFactory.java @@ -10,6 +10,7 @@ import ru.noties.markwon.core.spans.LinkSpan; * * @since 1.1.0 */ +@Deprecated public interface MarkwonSpannableFactory { @Nullable diff --git a/markwon/src/main/java/ru/noties/markwon/core/MarkwonSpannableFactoryDef.java b/markwon/src/main/java/ru/noties/markwon/core/MarkwonSpannableFactoryDef.java index 8b0a6461..5765c7b4 100644 --- a/markwon/src/main/java/ru/noties/markwon/core/MarkwonSpannableFactoryDef.java +++ b/markwon/src/main/java/ru/noties/markwon/core/MarkwonSpannableFactoryDef.java @@ -16,6 +16,7 @@ import ru.noties.markwon.core.spans.ThematicBreakSpan; /** * @since 1.1.0 */ +@Deprecated public class MarkwonSpannableFactoryDef implements MarkwonSpannableFactory { @NonNull diff --git a/markwon/src/main/java/ru/noties/markwon/core/visitor/ListBlockNodeVisitor.java b/markwon/src/main/java/ru/noties/markwon/core/SimpleBlockNodeVisitor.java similarity index 52% rename from markwon/src/main/java/ru/noties/markwon/core/visitor/ListBlockNodeVisitor.java rename to markwon/src/main/java/ru/noties/markwon/core/SimpleBlockNodeVisitor.java index f06d324b..1cc2fa02 100644 --- a/markwon/src/main/java/ru/noties/markwon/core/visitor/ListBlockNodeVisitor.java +++ b/markwon/src/main/java/ru/noties/markwon/core/SimpleBlockNodeVisitor.java @@ -1,4 +1,4 @@ -package ru.noties.markwon.core.visitor; +package ru.noties.markwon.core; import android.support.annotation.NonNull; @@ -6,7 +6,14 @@ import org.commonmark.node.Node; import ru.noties.markwon.MarkwonVisitor; -public class ListBlockNodeVisitor implements MarkwonVisitor.NodeVisitor { +/** + * A {@link ru.noties.markwon.MarkwonVisitor.NodeVisitor} that ensures that a markdown + * block starts with a new line, all children are visited and if further content available + * ensures a new line after self. Does not render any spans + * + * @since 3.0.0 + */ +public class SimpleBlockNodeVisitor implements MarkwonVisitor.NodeVisitor { @Override public void visit(@NonNull MarkwonVisitor visitor, @NonNull Node node) { diff --git a/markwon/src/main/java/ru/noties/markwon/core/factory/BlockQuoteSpanFactory.java b/markwon/src/main/java/ru/noties/markwon/core/factory/BlockQuoteSpanFactory.java new file mode 100644 index 00000000..437c0028 --- /dev/null +++ b/markwon/src/main/java/ru/noties/markwon/core/factory/BlockQuoteSpanFactory.java @@ -0,0 +1,17 @@ +package ru.noties.markwon.core.factory; + +import android.support.annotation.NonNull; +import android.support.annotation.Nullable; + +import ru.noties.markwon.MarkwonConfiguration; +import ru.noties.markwon.RenderProps; +import ru.noties.markwon.SpanFactory; +import ru.noties.markwon.core.spans.BlockQuoteSpan; + +public class BlockQuoteSpanFactory implements SpanFactory { + @Nullable + @Override + public Object getSpans(@NonNull MarkwonConfiguration configuration, @NonNull RenderProps context) { + return new BlockQuoteSpan(configuration.theme()); + } +} diff --git a/markwon/src/main/java/ru/noties/markwon/core/factory/CodeBlockSpanFactory.java b/markwon/src/main/java/ru/noties/markwon/core/factory/CodeBlockSpanFactory.java new file mode 100644 index 00000000..472bf620 --- /dev/null +++ b/markwon/src/main/java/ru/noties/markwon/core/factory/CodeBlockSpanFactory.java @@ -0,0 +1,17 @@ +package ru.noties.markwon.core.factory; + +import android.support.annotation.NonNull; +import android.support.annotation.Nullable; + +import ru.noties.markwon.MarkwonConfiguration; +import ru.noties.markwon.RenderProps; +import ru.noties.markwon.SpanFactory; +import ru.noties.markwon.core.spans.CodeSpan; + +public class CodeBlockSpanFactory implements SpanFactory { + @Nullable + @Override + public Object getSpans(@NonNull MarkwonConfiguration configuration, @NonNull RenderProps context) { + return new CodeSpan(configuration.theme(), true); + } +} diff --git a/markwon/src/main/java/ru/noties/markwon/core/factory/CodeSpanFactory.java b/markwon/src/main/java/ru/noties/markwon/core/factory/CodeSpanFactory.java new file mode 100644 index 00000000..6850ff44 --- /dev/null +++ b/markwon/src/main/java/ru/noties/markwon/core/factory/CodeSpanFactory.java @@ -0,0 +1,17 @@ +package ru.noties.markwon.core.factory; + +import android.support.annotation.NonNull; +import android.support.annotation.Nullable; + +import ru.noties.markwon.MarkwonConfiguration; +import ru.noties.markwon.RenderProps; +import ru.noties.markwon.SpanFactory; +import ru.noties.markwon.core.spans.CodeSpan; + +public class CodeSpanFactory implements SpanFactory { + @Nullable + @Override + public Object getSpans(@NonNull MarkwonConfiguration configuration, @NonNull RenderProps context) { + return new CodeSpan(configuration.theme(), false); + } +} diff --git a/markwon/src/main/java/ru/noties/markwon/core/factory/EmphasisSpanFactory.java b/markwon/src/main/java/ru/noties/markwon/core/factory/EmphasisSpanFactory.java new file mode 100644 index 00000000..eb49a906 --- /dev/null +++ b/markwon/src/main/java/ru/noties/markwon/core/factory/EmphasisSpanFactory.java @@ -0,0 +1,17 @@ +package ru.noties.markwon.core.factory; + +import android.support.annotation.NonNull; +import android.support.annotation.Nullable; + +import ru.noties.markwon.MarkwonConfiguration; +import ru.noties.markwon.RenderProps; +import ru.noties.markwon.SpanFactory; +import ru.noties.markwon.core.spans.EmphasisSpan; + +public class EmphasisSpanFactory implements SpanFactory { + @Nullable + @Override + public Object getSpans(@NonNull MarkwonConfiguration configuration, @NonNull RenderProps context) { + return new EmphasisSpan(); + } +} diff --git a/markwon/src/main/java/ru/noties/markwon/core/factory/HeadingSpanFactory.java b/markwon/src/main/java/ru/noties/markwon/core/factory/HeadingSpanFactory.java new file mode 100644 index 00000000..e0b1b517 --- /dev/null +++ b/markwon/src/main/java/ru/noties/markwon/core/factory/HeadingSpanFactory.java @@ -0,0 +1,21 @@ +package ru.noties.markwon.core.factory; + +import android.support.annotation.NonNull; +import android.support.annotation.Nullable; + +import ru.noties.markwon.MarkwonConfiguration; +import ru.noties.markwon.RenderProps; +import ru.noties.markwon.SpanFactory; +import ru.noties.markwon.core.CoreProps; +import ru.noties.markwon.core.spans.HeadingSpan; + +public class HeadingSpanFactory implements SpanFactory { + @Nullable + @Override + public Object getSpans(@NonNull MarkwonConfiguration configuration, @NonNull RenderProps context) { + return new HeadingSpan( + configuration.theme(), + CoreProps.HEADING_LEVEL.require(context) + ); + } +} diff --git a/markwon/src/main/java/ru/noties/markwon/core/factory/LinkSpanFactory.java b/markwon/src/main/java/ru/noties/markwon/core/factory/LinkSpanFactory.java new file mode 100644 index 00000000..e0a39dd2 --- /dev/null +++ b/markwon/src/main/java/ru/noties/markwon/core/factory/LinkSpanFactory.java @@ -0,0 +1,22 @@ +package ru.noties.markwon.core.factory; + +import android.support.annotation.NonNull; +import android.support.annotation.Nullable; + +import ru.noties.markwon.MarkwonConfiguration; +import ru.noties.markwon.RenderProps; +import ru.noties.markwon.SpanFactory; +import ru.noties.markwon.core.CoreProps; +import ru.noties.markwon.core.spans.LinkSpan; + +public class LinkSpanFactory implements SpanFactory { + @Nullable + @Override + public Object getSpans(@NonNull MarkwonConfiguration configuration, @NonNull RenderProps context) { + return new LinkSpan( + configuration.theme(), + CoreProps.LINK_DESTINATION.require(context), + configuration.linkResolver() + ); + } +} diff --git a/markwon/src/main/java/ru/noties/markwon/core/factory/ListItemSpanFactory.java b/markwon/src/main/java/ru/noties/markwon/core/factory/ListItemSpanFactory.java new file mode 100644 index 00000000..87bd4e4e --- /dev/null +++ b/markwon/src/main/java/ru/noties/markwon/core/factory/ListItemSpanFactory.java @@ -0,0 +1,43 @@ +package ru.noties.markwon.core.factory; + +import android.support.annotation.NonNull; +import android.support.annotation.Nullable; + +import ru.noties.markwon.MarkwonConfiguration; +import ru.noties.markwon.RenderProps; +import ru.noties.markwon.SpanFactory; +import ru.noties.markwon.core.CoreProps; +import ru.noties.markwon.core.spans.BulletListItemSpan; +import ru.noties.markwon.core.spans.OrderedListItemSpan; + +public class ListItemSpanFactory implements SpanFactory { + + @Nullable + @Override + public Object getSpans(@NonNull MarkwonConfiguration configuration, @NonNull RenderProps context) { + + // type of list item + // bullet : level + // ordered: number + final Object spans; + + if (CoreProps.ListItemType.BULLET == CoreProps.LIST_ITEM_TYPE.require(context)) { + spans = new BulletListItemSpan( + configuration.theme(), + CoreProps.BULLET_LIST_ITEM_LEVEL.require(context) + ); + } else { + + // todo| in order to provide real RTL experience there must be a way to provide this string + final String number = String.valueOf(CoreProps.ORDERED_LIST_ITEM_NUMBER.require(context)) + + "." + '\u00a0'; + + spans = new OrderedListItemSpan( + configuration.theme(), + number + ); + } + + return spans; + } +} diff --git a/markwon/src/main/java/ru/noties/markwon/core/factory/StrongEmphasisSpanFactory.java b/markwon/src/main/java/ru/noties/markwon/core/factory/StrongEmphasisSpanFactory.java new file mode 100644 index 00000000..e1905e05 --- /dev/null +++ b/markwon/src/main/java/ru/noties/markwon/core/factory/StrongEmphasisSpanFactory.java @@ -0,0 +1,17 @@ +package ru.noties.markwon.core.factory; + +import android.support.annotation.NonNull; +import android.support.annotation.Nullable; + +import ru.noties.markwon.MarkwonConfiguration; +import ru.noties.markwon.RenderProps; +import ru.noties.markwon.SpanFactory; +import ru.noties.markwon.core.spans.StrongEmphasisSpan; + +public class StrongEmphasisSpanFactory implements SpanFactory { + @Nullable + @Override + public Object getSpans(@NonNull MarkwonConfiguration configuration, @NonNull RenderProps context) { + return new StrongEmphasisSpan(); + } +} diff --git a/markwon/src/main/java/ru/noties/markwon/core/factory/ThematicBreakSpanFactory.java b/markwon/src/main/java/ru/noties/markwon/core/factory/ThematicBreakSpanFactory.java new file mode 100644 index 00000000..82c1dea8 --- /dev/null +++ b/markwon/src/main/java/ru/noties/markwon/core/factory/ThematicBreakSpanFactory.java @@ -0,0 +1,17 @@ +package ru.noties.markwon.core.factory; + +import android.support.annotation.NonNull; +import android.support.annotation.Nullable; + +import ru.noties.markwon.MarkwonConfiguration; +import ru.noties.markwon.RenderProps; +import ru.noties.markwon.SpanFactory; +import ru.noties.markwon.core.spans.ThematicBreakSpan; + +public class ThematicBreakSpanFactory implements SpanFactory { + @Nullable + @Override + public Object getSpans(@NonNull MarkwonConfiguration configuration, @NonNull RenderProps context) { + return new ThematicBreakSpan(configuration.theme()); + } +} diff --git a/markwon/src/main/java/ru/noties/markwon/core/visitor/BlockQuoteNodeVisitor.java b/markwon/src/main/java/ru/noties/markwon/core/visitor/BlockQuoteNodeVisitor.java deleted file mode 100644 index 1c236c42..00000000 --- a/markwon/src/main/java/ru/noties/markwon/core/visitor/BlockQuoteNodeVisitor.java +++ /dev/null @@ -1,25 +0,0 @@ -package ru.noties.markwon.core.visitor; - -import android.support.annotation.NonNull; - -import org.commonmark.node.BlockQuote; - -import ru.noties.markwon.MarkwonVisitor; - -public class BlockQuoteNodeVisitor implements MarkwonVisitor.NodeVisitor
{ - @Override - public void visit(@NonNull MarkwonVisitor visitor, @NonNull BlockQuote blockQuote) { - - visitor.ensureNewLine(); - - final int length = visitor.length(); - - visitor.visitChildren(blockQuote); - visitor.setSpans(length, visitor.factory().blockQuote(visitor.theme())); - - if (visitor.hasNext(blockQuote)) { - visitor.ensureNewLine(); - visitor.forceNewLine(); - } - } -} diff --git a/markwon/src/main/java/ru/noties/markwon/core/visitor/CodeBlockNodeVisitor.java b/markwon/src/main/java/ru/noties/markwon/core/visitor/CodeBlockNodeVisitor.java deleted file mode 100644 index 491da0e8..00000000 --- a/markwon/src/main/java/ru/noties/markwon/core/visitor/CodeBlockNodeVisitor.java +++ /dev/null @@ -1,56 +0,0 @@ -package ru.noties.markwon.core.visitor; - -import android.support.annotation.NonNull; -import android.support.annotation.Nullable; - -import org.commonmark.node.FencedCodeBlock; -import org.commonmark.node.IndentedCodeBlock; -import org.commonmark.node.Node; - -import ru.noties.markwon.MarkwonVisitor; - -public abstract class CodeBlockNodeVisitor { - - public static class Fenced implements MarkwonVisitor.NodeVisitor { - - @Override - public void visit(@NonNull MarkwonVisitor visitor, @NonNull FencedCodeBlock fencedCodeBlock) { - visitCodeBlock(visitor, fencedCodeBlock.getInfo(), fencedCodeBlock.getLiteral(), fencedCodeBlock); - } - } - - public static class Indented implements MarkwonVisitor.NodeVisitor { - - @Override - public void visit(@NonNull MarkwonVisitor visitor, @NonNull IndentedCodeBlock indentedCodeBlock) { - visitCodeBlock(visitor, null, indentedCodeBlock.getLiteral(), indentedCodeBlock); - } - } - - - public static void visitCodeBlock( - @NonNull MarkwonVisitor visitor, - @Nullable String info, - @NonNull String code, - @NonNull Node node) { - - visitor.ensureNewLine(); - - final int length = visitor.length(); - - visitor.builder() - .append('\u00a0').append('\n') - .append(visitor.configuration().syntaxHighlight().highlight(info, code)); - - visitor.ensureNewLine(); - - visitor.builder().append('\u00a0'); - - visitor.setSpans(length, visitor.factory().code(visitor.theme(), true)); - - if (visitor.hasNext(node)) { - visitor.ensureNewLine(); - visitor.forceNewLine(); - } - } -} diff --git a/markwon/src/main/java/ru/noties/markwon/core/visitor/CodeNodeVisitor.java b/markwon/src/main/java/ru/noties/markwon/core/visitor/CodeNodeVisitor.java deleted file mode 100644 index fb893839..00000000 --- a/markwon/src/main/java/ru/noties/markwon/core/visitor/CodeNodeVisitor.java +++ /dev/null @@ -1,24 +0,0 @@ -package ru.noties.markwon.core.visitor; - -import android.support.annotation.NonNull; - -import org.commonmark.node.Code; - -import ru.noties.markwon.MarkwonVisitor; - -public class CodeNodeVisitor implements MarkwonVisitor.NodeVisitor { - @Override - public void visit(@NonNull MarkwonVisitor visitor, @NonNull Code code) { - - final int length = visitor.length(); - - // NB, in order to provide a _padding_ feeling code is wrapped inside two unbreakable spaces - // unfortunately we cannot use this for multiline code as we cannot control where a new line break will be inserted - visitor.builder() - .append('\u00a0') - .append(code.getLiteral()) - .append('\u00a0'); - - visitor.setSpans(length, visitor.factory().code(visitor.theme(), false)); - } -} diff --git a/markwon/src/main/java/ru/noties/markwon/core/visitor/EmphasisNodeVisitor.java b/markwon/src/main/java/ru/noties/markwon/core/visitor/EmphasisNodeVisitor.java deleted file mode 100644 index f4010b32..00000000 --- a/markwon/src/main/java/ru/noties/markwon/core/visitor/EmphasisNodeVisitor.java +++ /dev/null @@ -1,16 +0,0 @@ -package ru.noties.markwon.core.visitor; - -import android.support.annotation.NonNull; - -import org.commonmark.node.Emphasis; - -import ru.noties.markwon.MarkwonVisitor; - -public class EmphasisNodeVisitor implements MarkwonVisitor.NodeVisitor { - @Override - public void visit(@NonNull MarkwonVisitor visitor, @NonNull Emphasis emphasis) { - final int length = visitor.length(); - visitor.visitChildren(emphasis); - visitor.setSpans(length, visitor.factory().emphasis()); - } -} diff --git a/markwon/src/main/java/ru/noties/markwon/core/visitor/HardLineBreakNodeVisitor.java b/markwon/src/main/java/ru/noties/markwon/core/visitor/HardLineBreakNodeVisitor.java deleted file mode 100644 index 809638b7..00000000 --- a/markwon/src/main/java/ru/noties/markwon/core/visitor/HardLineBreakNodeVisitor.java +++ /dev/null @@ -1,14 +0,0 @@ -package ru.noties.markwon.core.visitor; - -import android.support.annotation.NonNull; - -import org.commonmark.node.HardLineBreak; - -import ru.noties.markwon.MarkwonVisitor; - -public class HardLineBreakNodeVisitor implements MarkwonVisitor.NodeVisitor { - @Override - public void visit(@NonNull MarkwonVisitor visitor, @NonNull HardLineBreak hardLineBreak) { - visitor.ensureNewLine(); - } -} diff --git a/markwon/src/main/java/ru/noties/markwon/core/visitor/HeadingNodeVisitor.java b/markwon/src/main/java/ru/noties/markwon/core/visitor/HeadingNodeVisitor.java deleted file mode 100644 index c82d51fe..00000000 --- a/markwon/src/main/java/ru/noties/markwon/core/visitor/HeadingNodeVisitor.java +++ /dev/null @@ -1,24 +0,0 @@ -package ru.noties.markwon.core.visitor; - -import android.support.annotation.NonNull; - -import org.commonmark.node.Heading; - -import ru.noties.markwon.MarkwonVisitor; - -public class HeadingNodeVisitor implements MarkwonVisitor.NodeVisitor { - @Override - public void visit(@NonNull MarkwonVisitor visitor, @NonNull Heading heading) { - - visitor.ensureNewLine(); - - final int length = visitor.length(); - visitor.visitChildren(heading); - visitor.setSpans(length, visitor.factory().heading(visitor.theme(), heading.getLevel())); - - if (visitor.hasNext(heading)) { - visitor.ensureNewLine(); - visitor.forceNewLine(); - } - } -} diff --git a/markwon/src/main/java/ru/noties/markwon/core/visitor/LinkNodeVisitor.java b/markwon/src/main/java/ru/noties/markwon/core/visitor/LinkNodeVisitor.java deleted file mode 100644 index cc4dfd6d..00000000 --- a/markwon/src/main/java/ru/noties/markwon/core/visitor/LinkNodeVisitor.java +++ /dev/null @@ -1,19 +0,0 @@ -package ru.noties.markwon.core.visitor; - -import android.support.annotation.NonNull; - -import org.commonmark.node.Link; - -import ru.noties.markwon.MarkwonConfiguration; -import ru.noties.markwon.MarkwonVisitor; - -public class LinkNodeVisitor implements MarkwonVisitor.NodeVisitor { - @Override - public void visit(@NonNull MarkwonVisitor visitor, @NonNull Link link) { - final int length = visitor.length(); - visitor.visitChildren(link); - final MarkwonConfiguration configuration = visitor.configuration(); - final String destination = configuration.urlProcessor().process(link.getDestination()); - visitor.setSpans(length, visitor.factory().link(visitor.theme(), destination, configuration.linkResolver())); - } -} diff --git a/markwon/src/main/java/ru/noties/markwon/core/visitor/ListItemNodeVisitor.java b/markwon/src/main/java/ru/noties/markwon/core/visitor/ListItemNodeVisitor.java deleted file mode 100644 index f362c3ba..00000000 --- a/markwon/src/main/java/ru/noties/markwon/core/visitor/ListItemNodeVisitor.java +++ /dev/null @@ -1,54 +0,0 @@ -package ru.noties.markwon.core.visitor; - -import android.support.annotation.NonNull; - -import org.commonmark.node.ListItem; -import org.commonmark.node.Node; -import org.commonmark.node.OrderedList; - -import ru.noties.markwon.MarkwonVisitor; - -public class ListItemNodeVisitor implements MarkwonVisitor.NodeVisitor { - - @Override - public void visit(@NonNull MarkwonVisitor visitor, @NonNull ListItem listItem) { - - final int length = visitor.length(); - - final Node parent = listItem.getParent(); - if (parent instanceof OrderedList) { - - final int start = ((OrderedList) parent).getStartNumber(); - - visitor.visitChildren(listItem); - visitor.setSpans(length, visitor.factory().orderedListItem(visitor.theme(), start)); - - - // after we have visited the children increment start number - final OrderedList orderedList = (OrderedList) parent; - orderedList.setStartNumber(orderedList.getStartNumber() + 1); - - } else { - - visitor.visitChildren(listItem); - visitor.setSpans(length, visitor.factory().bulletListItem(visitor.theme(), listLevel(listItem))); - - } - - if (visitor.hasNext(listItem)) { - visitor.ensureNewLine(); - } - } - - private static int listLevel(@NonNull Node node) { - int level = 0; - Node parent = node.getParent(); - while (parent != null) { - if (parent instanceof ListItem) { - level += 1; - } - parent = parent.getParent(); - } - return level; - } -} diff --git a/markwon/src/main/java/ru/noties/markwon/core/visitor/ParagraphNodeVisitor.java b/markwon/src/main/java/ru/noties/markwon/core/visitor/ParagraphNodeVisitor.java deleted file mode 100644 index b945c501..00000000 --- a/markwon/src/main/java/ru/noties/markwon/core/visitor/ParagraphNodeVisitor.java +++ /dev/null @@ -1,44 +0,0 @@ -package ru.noties.markwon.core.visitor; - -import android.support.annotation.NonNull; - -import org.commonmark.node.ListBlock; -import org.commonmark.node.Node; -import org.commonmark.node.Paragraph; - -import ru.noties.markwon.MarkwonVisitor; - -public class ParagraphNodeVisitor implements MarkwonVisitor.NodeVisitor { - @Override - public void visit(@NonNull MarkwonVisitor visitor, @NonNull Paragraph paragraph) { - - final boolean inTightList = isInTightList(paragraph); - - if (!inTightList) { - visitor.ensureNewLine(); - } - - final int length = visitor.length(); - visitor.visitChildren(paragraph); - - // @since 1.1.1 apply paragraph span - visitor.setSpans(length, visitor.factory().paragraph(inTightList)); - - if (!inTightList && visitor.hasNext(paragraph)) { - visitor.ensureNewLine(); - visitor.forceNewLine(); - } - } - - private static boolean isInTightList(@NonNull Paragraph paragraph) { - final Node parent = paragraph.getParent(); - if (parent != null) { - final Node gramps = parent.getParent(); - if (gramps instanceof ListBlock) { - ListBlock list = (ListBlock) gramps; - return list.isTight(); - } - } - return false; - } -} diff --git a/markwon/src/main/java/ru/noties/markwon/core/visitor/SoftLineBreakNodeVisitor.java b/markwon/src/main/java/ru/noties/markwon/core/visitor/SoftLineBreakNodeVisitor.java deleted file mode 100644 index 79880bb2..00000000 --- a/markwon/src/main/java/ru/noties/markwon/core/visitor/SoftLineBreakNodeVisitor.java +++ /dev/null @@ -1,25 +0,0 @@ -package ru.noties.markwon.core.visitor; - -import android.support.annotation.NonNull; - -import org.commonmark.node.SoftLineBreak; - -import ru.noties.markwon.MarkwonVisitor; - -public class SoftLineBreakNodeVisitor implements MarkwonVisitor.NodeVisitor { - - private final boolean softBreakAddsNewLine; - - public SoftLineBreakNodeVisitor(boolean softBreakAddsNewLine) { - this.softBreakAddsNewLine = softBreakAddsNewLine; - } - - @Override - public void visit(@NonNull MarkwonVisitor visitor, @NonNull SoftLineBreak softLineBreak) { - if (softBreakAddsNewLine) { - visitor.ensureNewLine(); - } else { - visitor.builder().append(' '); - } - } -} diff --git a/markwon/src/main/java/ru/noties/markwon/core/visitor/StrongEmphasisNodeVisitor.java b/markwon/src/main/java/ru/noties/markwon/core/visitor/StrongEmphasisNodeVisitor.java deleted file mode 100644 index ae3c701d..00000000 --- a/markwon/src/main/java/ru/noties/markwon/core/visitor/StrongEmphasisNodeVisitor.java +++ /dev/null @@ -1,16 +0,0 @@ -package ru.noties.markwon.core.visitor; - -import android.support.annotation.NonNull; - -import org.commonmark.node.StrongEmphasis; - -import ru.noties.markwon.MarkwonVisitor; - -public class StrongEmphasisNodeVisitor implements MarkwonVisitor.NodeVisitor { - @Override - public void visit(@NonNull MarkwonVisitor visitor, @NonNull StrongEmphasis strongEmphasis) { - final int length = visitor.length(); - visitor.visitChildren(strongEmphasis); - visitor.setSpans(length, visitor.factory().strongEmphasis()); - } -} diff --git a/markwon/src/main/java/ru/noties/markwon/core/visitor/TextNodeVisitor.java b/markwon/src/main/java/ru/noties/markwon/core/visitor/TextNodeVisitor.java deleted file mode 100644 index 17ebfbb0..00000000 --- a/markwon/src/main/java/ru/noties/markwon/core/visitor/TextNodeVisitor.java +++ /dev/null @@ -1,14 +0,0 @@ -package ru.noties.markwon.core.visitor; - -import android.support.annotation.NonNull; - -import org.commonmark.node.Text; - -import ru.noties.markwon.MarkwonVisitor; - -public class TextNodeVisitor implements MarkwonVisitor.NodeVisitor { - @Override - public void visit(@NonNull MarkwonVisitor visitor, @NonNull Text text) { - visitor.builder().append(text.getLiteral()); - } -} diff --git a/markwon/src/main/java/ru/noties/markwon/core/visitor/ThematicBreakNodeVisitor.java b/markwon/src/main/java/ru/noties/markwon/core/visitor/ThematicBreakNodeVisitor.java deleted file mode 100644 index 2bc2886a..00000000 --- a/markwon/src/main/java/ru/noties/markwon/core/visitor/ThematicBreakNodeVisitor.java +++ /dev/null @@ -1,27 +0,0 @@ -package ru.noties.markwon.core.visitor; - -import android.support.annotation.NonNull; - -import org.commonmark.node.ThematicBreak; - -import ru.noties.markwon.MarkwonVisitor; - -public class ThematicBreakNodeVisitor implements MarkwonVisitor.NodeVisitor { - @Override - public void visit(@NonNull MarkwonVisitor visitor, @NonNull ThematicBreak thematicBreak) { - - visitor.ensureNewLine(); - - final int length = visitor.length(); - - // without space it won't render - visitor.builder().append('\u00a0'); - - visitor.setSpans(length, visitor.factory().thematicBreak(visitor.theme())); - - if (visitor.hasNext(thematicBreak)) { - visitor.ensureNewLine(); - visitor.forceNewLine(); - } - } -} diff --git a/markwon/src/main/java/ru/noties/markwon/image/AsyncDrawableScheduler.java b/markwon/src/main/java/ru/noties/markwon/image/AsyncDrawableScheduler.java index 27348a76..0093abed 100644 --- a/markwon/src/main/java/ru/noties/markwon/image/AsyncDrawableScheduler.java +++ b/markwon/src/main/java/ru/noties/markwon/image/AsyncDrawableScheduler.java @@ -15,7 +15,6 @@ import java.util.Collections; import java.util.List; import ru.noties.markwon.renderer.R; -import ru.noties.markwon.core.spans.AsyncDrawableSpan; public abstract class AsyncDrawableScheduler { diff --git a/markwon/src/main/java/ru/noties/markwon/core/spans/AsyncDrawableSpan.java b/markwon/src/main/java/ru/noties/markwon/image/AsyncDrawableSpan.java similarity index 91% rename from markwon/src/main/java/ru/noties/markwon/core/spans/AsyncDrawableSpan.java rename to markwon/src/main/java/ru/noties/markwon/image/AsyncDrawableSpan.java index b06b2348..a268ef19 100644 --- a/markwon/src/main/java/ru/noties/markwon/core/spans/AsyncDrawableSpan.java +++ b/markwon/src/main/java/ru/noties/markwon/image/AsyncDrawableSpan.java @@ -1,4 +1,4 @@ -package ru.noties.markwon.core.spans; +package ru.noties.markwon.image; import android.graphics.Canvas; import android.graphics.Paint; @@ -13,7 +13,6 @@ import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import ru.noties.markwon.core.MarkwonTheme; -import ru.noties.markwon.image.AsyncDrawable; @SuppressWarnings("WeakerAccess") public class AsyncDrawableSpan extends ReplacementSpan { @@ -32,17 +31,6 @@ public class AsyncDrawableSpan extends ReplacementSpan { private final int alignment; private final boolean replacementTextIsLink; -// public AsyncDrawableSpan(@NonNull MarkwonTheme theme, @NonNull AsyncDrawable drawable) { -// this(theme, drawable, ALIGN_BOTTOM); -// } - -// public AsyncDrawableSpan( -// @NonNull MarkwonTheme theme, -// @NonNull AsyncDrawable drawable, -// @Alignment int alignment) { -// this(theme, drawable, alignment, false); -// } - public AsyncDrawableSpan( @NonNull MarkwonTheme theme, @NonNull AsyncDrawable drawable, @@ -150,6 +138,7 @@ public class AsyncDrawableSpan extends ReplacementSpan { } } + @NonNull public AsyncDrawable getDrawable() { return drawable; } diff --git a/markwon/src/main/java/ru/noties/markwon/image/ImageProps.java b/markwon/src/main/java/ru/noties/markwon/image/ImageProps.java new file mode 100644 index 00000000..7bb9bfd1 --- /dev/null +++ b/markwon/src/main/java/ru/noties/markwon/image/ImageProps.java @@ -0,0 +1,20 @@ +package ru.noties.markwon.image; + +import ru.noties.markwon.Prop; + +/** + * @since 3.0.0 + */ +public abstract class ImageProps { + + public static final Prop DESTINATION = Prop.of("image-destination"); + + public static final Prop REPLACEMENT_TEXT_IS_LINK = + Prop.of("image-replacement-text-is-link"); + + public static final Prop IMAGE_SIZE = Prop.of("image-size"); + + + private ImageProps() { + } +} diff --git a/markwon/src/main/java/ru/noties/markwon/image/ImageSpanFactory.java b/markwon/src/main/java/ru/noties/markwon/image/ImageSpanFactory.java new file mode 100644 index 00000000..ba73d047 --- /dev/null +++ b/markwon/src/main/java/ru/noties/markwon/image/ImageSpanFactory.java @@ -0,0 +1,26 @@ +package ru.noties.markwon.image; + +import android.support.annotation.NonNull; +import android.support.annotation.Nullable; + +import ru.noties.markwon.MarkwonConfiguration; +import ru.noties.markwon.RenderProps; +import ru.noties.markwon.SpanFactory; + +public class ImageSpanFactory implements SpanFactory { + @Nullable + @Override + public Object getSpans(@NonNull MarkwonConfiguration configuration, @NonNull RenderProps context) { + return new AsyncDrawableSpan( + configuration.theme(), + new AsyncDrawable( + ImageProps.DESTINATION.require(context), + configuration.asyncDrawableLoader(), + configuration.imageSizeResolver(), + ImageProps.IMAGE_SIZE.get(context) + ), + AsyncDrawableSpan.ALIGN_BOTTOM, + ImageProps.REPLACEMENT_TEXT_IS_LINK.get(context, false) + ); + } +} diff --git a/markwon/src/main/java/ru/noties/markwon/image/ImagesPlugin.java b/markwon/src/main/java/ru/noties/markwon/image/ImagesPlugin.java index 2be7763f..7e91f288 100644 --- a/markwon/src/main/java/ru/noties/markwon/image/ImagesPlugin.java +++ b/markwon/src/main/java/ru/noties/markwon/image/ImagesPlugin.java @@ -2,7 +2,6 @@ package ru.noties.markwon.image; import android.content.Context; import android.support.annotation.NonNull; -import android.support.annotation.Nullable; import android.widget.TextView; import org.commonmark.node.Image; @@ -13,9 +12,9 @@ import java.util.Arrays; import ru.noties.markwon.AbstractMarkwonPlugin; import ru.noties.markwon.MarkwonConfiguration; +import ru.noties.markwon.MarkwonSpansFactory; import ru.noties.markwon.MarkwonVisitor; -import ru.noties.markwon.core.MarkwonTheme; -import ru.noties.markwon.core.spans.AsyncDrawableSpan; +import ru.noties.markwon.RenderProps; import ru.noties.markwon.image.data.DataUriSchemeHandler; import ru.noties.markwon.image.file.FileSchemeHandler; import ru.noties.markwon.image.network.NetworkSchemeHandler; @@ -35,7 +34,7 @@ public class ImagesPlugin extends AbstractMarkwonPlugin { private final Context context; private final boolean useAssets; - private ImagesPlugin(Context context, boolean useAssets) { + protected ImagesPlugin(Context context, boolean useAssets) { this.context = context; this.useAssets = useAssets; } @@ -58,6 +57,11 @@ public class ImagesPlugin extends AbstractMarkwonPlugin { .defaultMediaDecoder(ImageMediaDecoder.create(context.getResources())); } + @Override + public void configureSpansFactory(@NonNull MarkwonSpansFactory.Builder builder) { + builder.setFactory(Image.class, new ImageSpanFactory()); + } + @Override public void configureVisitor(@NonNull MarkwonVisitor.Builder builder) { builder.on(Image.class, new MarkwonVisitor.NodeVisitor() { @@ -77,18 +81,21 @@ public class ImagesPlugin extends AbstractMarkwonPlugin { final Node parent = image.getParent(); final boolean link = parent instanceof Link; + final String destination = configuration .urlProcessor() .process(image.getDestination()); - final Object spans = imageSpan( - visitor.theme(), - destination, - configuration.asyncDrawableLoader(), - configuration.imageSizeResolver(), - link); + final RenderProps context = visitor.renderProps(); - visitor.setSpans(length, spans); + // apply image properties + // Please note that we explicitly set IMAGE_SIZE to null as we do not clear + // properties after we applied span (we could though) + ImageProps.DESTINATION.set(context, destination); + ImageProps.REPLACEMENT_TEXT_IS_LINK.set(context, link); + ImageProps.IMAGE_SIZE.set(context, null); + + visitor.setSpansForNode(image, length); } }); } @@ -102,24 +109,4 @@ public class ImagesPlugin extends AbstractMarkwonPlugin { public void afterSetText(@NonNull TextView textView) { AsyncDrawableScheduler.schedule(textView); } - - @Nullable - protected Object imageSpan( - @NonNull MarkwonTheme theme, - @NonNull String destination, - @NonNull AsyncDrawableLoader loader, - @NonNull ImageSizeResolver imageSizeResolver, - boolean replacementTextIsLink) { - return new AsyncDrawableSpan( - theme, - new AsyncDrawable( - destination, - loader, - imageSizeResolver, - null - ), - AsyncDrawableSpan.ALIGN_BOTTOM, - replacementTextIsLink - ); - } } diff --git a/markwon/src/test/java/ru/noties/markwon/core/CoreTest.java b/markwon/src/test/java/ru/noties/markwon/core/CoreTest.java index e417267a..17ee51db 100644 --- a/markwon/src/test/java/ru/noties/markwon/core/CoreTest.java +++ b/markwon/src/test/java/ru/noties/markwon/core/CoreTest.java @@ -32,8 +32,8 @@ public class CoreTest { span("italic", text("bold italic")))); final Spanned spanned = (Spanned) Markwon.builder(RuntimeEnvironment.application) - .use(CorePlugin.create()) - .use(new AbstractMarkwonPlugin() { + .usePlugin(CorePlugin.create()) + .usePlugin(new AbstractMarkwonPlugin() { @Override public void configureConfiguration(@NonNull MarkwonConfiguration.Builder builder) { builder.factory(new MarkwonSpannableFactoryDef() { diff --git a/markwon/src/test/java/ru/noties/markwon/core/suite/BaseSuiteTest.java b/markwon/src/test/java/ru/noties/markwon/core/suite/BaseSuiteTest.java index 73c05bdf..65555352 100644 --- a/markwon/src/test/java/ru/noties/markwon/core/suite/BaseSuiteTest.java +++ b/markwon/src/test/java/ru/noties/markwon/core/suite/BaseSuiteTest.java @@ -33,8 +33,8 @@ abstract class BaseSuiteTest { @NonNull Markwon markwon() { return Markwon.builder(RuntimeEnvironment.application) - .use(CorePlugin.create(softBreakAddsNewLine())) - .use(new AbstractMarkwonPlugin() { + .usePlugin(CorePlugin.create(softBreakAddsNewLine())) + .usePlugin(new AbstractMarkwonPlugin() { @Override public void configureConfiguration(@NonNull MarkwonConfiguration.Builder builder) { builder.factory(new TestFactory(useParagraphs())); diff --git a/markwon/src/test/java/ru/noties/markwon/image/ImageTest.java b/markwon/src/test/java/ru/noties/markwon/image/ImageTest.java new file mode 100644 index 00000000..48cd3227 --- /dev/null +++ b/markwon/src/test/java/ru/noties/markwon/image/ImageTest.java @@ -0,0 +1,49 @@ +package ru.noties.markwon.image; + +import android.content.Context; +import android.support.annotation.NonNull; + +import org.junit.Test; +import org.junit.runner.RunWith; +import org.robolectric.RobolectricTestRunner; +import org.robolectric.RuntimeEnvironment; +import org.robolectric.annotation.Config; + +import ru.noties.markwon.Markwon; +import ru.noties.markwon.core.CorePlugin; +import ru.noties.markwon.core.MarkwonTheme; +import ru.noties.markwon.test.TestSpan.Document; +import ru.noties.markwon.test.TestSpanMatcher; + +import static ru.noties.markwon.test.TestSpan.args; +import static ru.noties.markwon.test.TestSpan.document; +import static ru.noties.markwon.test.TestSpan.span; +import static ru.noties.markwon.test.TestSpan.text; + +@RunWith(RobolectricTestRunner.class) +@Config(manifest = Config.NONE) +public class ImageTest { + + @Test + public void test() { + + final String markdown = "![alt](#href)"; + + final Context context = RuntimeEnvironment.application; + final Markwon markwon = Markwon.builder(context) + .usePlugin(CorePlugin.create()) + .usePlugin(new ImagesPlugin(context, false) { + @Override + protected Object imageSpan(@NonNull MarkwonTheme theme, @NonNull String destination, @NonNull AsyncDrawableLoader loader, @NonNull ImageSizeResolver imageSizeResolver, boolean replacementTextIsLink) { + return span("image", args("href", destination)); + } + }) + .build(); + + final Document document = document( + span("image", args("href", "#href"), text("alt")) + ); + + TestSpanMatcher.matches(markwon.toMarkdown(markdown), document); + } +} diff --git a/markwon/src/test/java/ru/noties/markwon/renderer/visitor/SpannableMarkdownVisitorTest.java b/markwon/src/test/java/ru/noties/markwon/renderer/visitor/SpannableMarkdownVisitorTest.java deleted file mode 100644 index 07890959..00000000 --- a/markwon/src/test/java/ru/noties/markwon/renderer/visitor/SpannableMarkdownVisitorTest.java +++ /dev/null @@ -1,83 +0,0 @@ -package ru.noties.markwon.renderer.visitor; - -import android.content.Context; -import android.support.annotation.NonNull; -import android.text.SpannableStringBuilder; - -import org.junit.Test; -import org.junit.runner.RunWith; -import org.robolectric.ParameterizedRobolectricTestRunner; -import org.robolectric.RuntimeEnvironment; -import org.robolectric.annotation.Config; - -import java.util.Arrays; -import java.util.Collection; - -import ru.noties.markwon.AbstractMarkwonPlugin; -import ru.noties.markwon.Markwon; -import ru.noties.markwon.MarkwonConfiguration; -import ru.noties.markwon.core.CorePlugin; -import ru.noties.markwon.image.ImagesPlugin; - -import static org.junit.Assert.assertEquals; -import static org.mockito.Mockito.mock; - -@RunWith(ParameterizedRobolectricTestRunner.class) -@Config(manifest = Config.NONE) -public class SpannableMarkdownVisitorTest { - - @ParameterizedRobolectricTestRunner.Parameters(name = "{0}") - public static Collection parameters() { - return TestDataReader.testFiles(); - } - - private final String file; - - public SpannableMarkdownVisitorTest(@NonNull String file) { - this.file = file; - } - - @Test - public void test() { - - final TestData data = TestDataReader.readTest(file); - - final Markwon markwon = markwon(data.config()); - - // okay we must thing about it... casting? - final SpannableStringBuilder stringBuilder = (SpannableStringBuilder) markwon.toMarkdown(data.input()); - - final TestValidator validator = TestValidator.create(file); - - int index = 0; - - for (TestNode testNode : data.output()) { - index = validator.validate(stringBuilder, index, testNode); - } - - // assert that the whole thing is processed - assertEquals("`" + stringBuilder + "`", stringBuilder.length(), index); - - final Object[] spans = stringBuilder.getSpans(0, stringBuilder.length(), Object.class); - final int length = spans != null - ? spans.length - : 0; - - assertEquals(Arrays.toString(spans), validator.processedSpanNodesCount(), length); - } - - - @NonNull - private Markwon markwon(@NonNull final TestConfig config) { - return Markwon.builder(RuntimeEnvironment.application) - .use(CorePlugin.create(config.hasOption(TestConfig.SOFT_BREAK_ADDS_NEW_LINE))) - .use(ImagesPlugin.create(mock(Context.class))) - .use(new AbstractMarkwonPlugin() { - @Override - public void configureConfiguration(@NonNull MarkwonConfiguration.Builder builder) { - builder.factory(new TestFactory(config.hasOption(TestConfig.USE_PARAGRAPHS))); - } - }) - .build(); - } -} \ No newline at end of file diff --git a/markwon/src/test/java/ru/noties/markwon/renderer/visitor/TestConfig.java b/markwon/src/test/java/ru/noties/markwon/renderer/visitor/TestConfig.java deleted file mode 100644 index 367f8a43..00000000 --- a/markwon/src/test/java/ru/noties/markwon/renderer/visitor/TestConfig.java +++ /dev/null @@ -1,31 +0,0 @@ -package ru.noties.markwon.renderer.visitor; - -import android.support.annotation.NonNull; - -import java.util.Map; - -class TestConfig { - - static final String USE_PARAGRAPHS = "use-paragraphs"; -// static final String USE_HTML = "use-html"; - static final String SOFT_BREAK_ADDS_NEW_LINE = "soft-break-adds-new-line"; -// static final String HTML_ALLOW_NON_CLOSED_TAGS = "html-allow-non-closed-tags"; - - private final Map map; - - TestConfig(@NonNull Map map) { - this.map = map; - } - - boolean hasOption(@NonNull String option) { - final Boolean value = map.get(option); - return value != null && value; - } - - @Override - public String toString() { - return "TestConfig{" + - "map=" + map + - '}'; - } -} diff --git a/markwon/src/test/java/ru/noties/markwon/renderer/visitor/TestData.java b/markwon/src/test/java/ru/noties/markwon/renderer/visitor/TestData.java deleted file mode 100644 index 67807202..00000000 --- a/markwon/src/test/java/ru/noties/markwon/renderer/visitor/TestData.java +++ /dev/null @@ -1,45 +0,0 @@ -package ru.noties.markwon.renderer.visitor; - -import android.support.annotation.NonNull; -import android.support.annotation.Nullable; - -import java.util.List; - -class TestData { - - private final String description; - private final String input; - private final TestConfig config; - private final List output; - - TestData( - @Nullable String description, - @NonNull String input, - @NonNull TestConfig config, - @NonNull List output) { - this.description = description; - this.input = input; - this.config = config; - this.output = output; - } - - @Nullable - public String description() { - return description; - } - - @NonNull - public String input() { - return input; - } - - @NonNull - public TestConfig config() { - return config; - } - - @NonNull - public List output() { - return output; - } -} diff --git a/markwon/src/test/java/ru/noties/markwon/renderer/visitor/TestDataReader.java b/markwon/src/test/java/ru/noties/markwon/renderer/visitor/TestDataReader.java deleted file mode 100644 index 0aa5e998..00000000 --- a/markwon/src/test/java/ru/noties/markwon/renderer/visitor/TestDataReader.java +++ /dev/null @@ -1,341 +0,0 @@ -package ru.noties.markwon.renderer.visitor; - -import android.support.annotation.NonNull; -import android.support.annotation.Nullable; -import android.text.TextUtils; - -import com.fasterxml.jackson.databind.ObjectMapper; -import com.fasterxml.jackson.dataformat.yaml.YAMLFactory; -import com.google.gson.Gson; -import com.google.gson.JsonArray; -import com.google.gson.JsonElement; -import com.google.gson.JsonObject; -import com.google.gson.JsonPrimitive; - -import org.apache.commons.io.IOUtils; - -import java.io.IOException; -import java.io.InputStream; -import java.nio.charset.StandardCharsets; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Collection; -import java.util.Collections; -import java.util.HashMap; -import java.util.HashSet; -import java.util.List; -import java.util.Map; -import java.util.Set; - -import ix.Ix; -import ix.IxFunction; -import ix.IxPredicate; - -import static ru.noties.markwon.renderer.visitor.TestSpan.BLOCK_QUOTE; -import static ru.noties.markwon.renderer.visitor.TestSpan.BULLET_LIST; -import static ru.noties.markwon.renderer.visitor.TestSpan.CODE; -import static ru.noties.markwon.renderer.visitor.TestSpan.CODE_BLOCK; -import static ru.noties.markwon.renderer.visitor.TestSpan.EMPHASIS; -import static ru.noties.markwon.renderer.visitor.TestSpan.HEADING; -import static ru.noties.markwon.renderer.visitor.TestSpan.IMAGE; -import static ru.noties.markwon.renderer.visitor.TestSpan.LINK; -import static ru.noties.markwon.renderer.visitor.TestSpan.ORDERED_LIST; -import static ru.noties.markwon.renderer.visitor.TestSpan.PARAGRAPH; -import static ru.noties.markwon.renderer.visitor.TestSpan.STRONG_EMPHASIS; -import static ru.noties.markwon.renderer.visitor.TestSpan.THEMATIC_BREAK; - -abstract class TestDataReader { - - private static final String FOLDER = "tests/"; - - @NonNull - static Collection testFiles() { - - final InputStream in = TestDataReader.class.getClassLoader().getResourceAsStream(FOLDER); - if (in == null) { - throw new RuntimeException("Cannot access test cases folder"); - } - - try { - //noinspection unchecked - return (Collection) Ix.from(IOUtils.readLines(in, StandardCharsets.UTF_8)) - .filter(new IxPredicate() { - @Override - public boolean test(String s) { - return s.endsWith(".yaml"); - } - }) - .map(new IxFunction() { - @Override - public String apply(String s) { - return FOLDER + s; - } - }) - .map(new IxFunction() { - @Override - public Object[] apply(String s) { - return new Object[]{ - s - }; - } - }) - .toList(); - } catch (IOException e) { - throw new RuntimeException(e); - } - } - - @NonNull - static TestData readTest(@NonNull String file) { - return new Reader(file).read(); - } - - private TestDataReader() { - } - - static class Reader { - - private static final String TEXT = "text"; -// private static final String CELLS = "cells"; - - private static final Set TAGS; - - static { - TAGS = new HashSet<>(Arrays.asList( - STRONG_EMPHASIS, - EMPHASIS, - BLOCK_QUOTE, - CODE, - CODE_BLOCK, - ORDERED_LIST, - BULLET_LIST, - THEMATIC_BREAK, - HEADING, - PARAGRAPH, - IMAGE, - LINK, - HEADING + "1", - HEADING + "2", - HEADING + "3", - HEADING + "4", - HEADING + "5", - HEADING + "6", - TEXT - )); - } - - private final String file; - - Reader(@NonNull String file) { - this.file = file; - } - - @NonNull - TestData read() { - return testData(jsonObject()); - } - - @NonNull - private JsonObject jsonObject() { - try { - final String input = IOUtils.resourceToString(file, StandardCharsets.UTF_8, TestDataReader.class.getClassLoader()); - final ObjectMapper objectMapper = new ObjectMapper(new YAMLFactory()); - final Object object = objectMapper.readValue(input, Object.class); - final ObjectMapper jsonWriter = new ObjectMapper(); - final String json = jsonWriter.writeValueAsString(object); - return new Gson().fromJson(json, JsonObject.class); - } catch (Throwable t) { - throw new RuntimeException(t); - } - } - - @NonNull - private TestData testData(@NonNull JsonObject jsonObject) { - - final String description; - { - final JsonElement element = jsonObject.get("description"); - if (element != null - && element.isJsonPrimitive()) { - description = element.getAsString(); - } else { - description = null; - } - } - - final String input = jsonObject.get("input").getAsString(); - if (TextUtils.isEmpty(input)) { - throw new RuntimeException(String.format("Test case file `%s` is missing " + - "input parameter", file)); - } - - final TestConfig testConfig = testConfig(jsonObject.get("config")); - - final List testNodes = testNodes(jsonObject.get("output").getAsJsonArray()); - if (testNodes.size() == 0) { - throw new RuntimeException(String.format("Test case file `%s` has no " + - "output specified", file)); - } - - return new TestData( - description, - input, - testConfig, - testNodes - ); - } - - @NonNull - private List testNodes(@NonNull JsonArray array) { - return testNodes(null, array); - } - - @NonNull - private List testNodes(@Nullable TestNode parent, @NonNull JsonArray array) { - - // an item in array is a JsonObject - - // it can be "b": "bold" -> means Span(name="b", children=[Text(bold)] - // or b: - // - text: "bold" -> which is the same as above - - // it can additionally contain "attrs" key which is the attributes - // b: - // - text: "bold" - // href: "my-href" - - final int size = array.size(); - - final List testNodes = new ArrayList<>(size); - - for (int i = 0; i < size; i++) { - - // if element is a string (or a json primitive) let's just add a text node - // right away, this way we will not have to provide text with `text: "my-text"` - // (we still can though) - final JsonElement jsonElement = array.get(i); - if (jsonElement.isJsonPrimitive()) { - testNodes.add(new TestNode.Text(parent, jsonElement.getAsString())); - continue; - } - - final JsonObject object = jsonElement.getAsJsonObject(); - - String name = null; - Map attributes = new HashMap<>(0); - - for (String key : object.keySet()) { - if (TAGS.contains(key)) { - if (name == null) { - name = key; - } else { - throw new RuntimeException("Unexpected key in object: " + object); - } - } else { - // fill attribute map with it - final String value; - final JsonElement valueElement = object.get(key); - if (valueElement.isJsonNull()) { - value = null; - } else { - value = valueElement.getAsString(); - } -// else { -// // another special case: table cell -// // this is not so good -// if (CELLS.equals(key)) { -// final JsonArray cells = valueElement.getAsJsonArray(); -// final int length = cells.size(); -// final List list = new ArrayList<>(length); -// for (int k = 0; k < length; k++) { -// final JsonObject cell = cells.get(k).getAsJsonObject(); -// list.add(new TableRowSpan.Cell( -// cell.get("alignment").getAsInt(), -// cell.get("text").getAsString() -// )); -// } -// value = list.toString(); -// } else { -// value = valueElement.getAsString(); -// } -// } - attributes.put(key, value); - } - } - - if (name == null) { - throw new RuntimeException("Object is missing tag name: " + object); - } - - final JsonElement element = object.get(name); - - if (TEXT.equals(name)) { - testNodes.add(new TestNode.Text(parent, element.getAsString())); - } else { - - final List children = new ArrayList<>(1); - final TestNode.Span span = new TestNode.Span(parent, name, children, attributes); - - // if it's primitive string -> just append text node - if (element.isJsonPrimitive()) { - children.add(new TestNode.Text(span, element.getAsString())); - } else if (element.isJsonArray()) { - children.addAll(testNodes(span, element.getAsJsonArray())); - } else { - throw new RuntimeException("Unexpected element: " + object); - } - - testNodes.add(span); - } - } - - return testNodes; - } - - @NonNull - private TestConfig testConfig(@Nullable JsonElement element) { - - final JsonObject object = element != null && element.isJsonObject() - ? element.getAsJsonObject() - : null; - - final Map map; - - if (object != null) { - - map = new HashMap<>(object.size()); - - for (String key : object.keySet()) { - - final JsonElement value = object.get(key); - - if (value.isJsonPrimitive()) { - - final JsonPrimitive jsonPrimitive = value.getAsJsonPrimitive(); - - Boolean b = null; - - if (jsonPrimitive.isBoolean()) { - b = jsonPrimitive.getAsBoolean(); - } else if (jsonPrimitive.isString()) { - final String s = jsonPrimitive.getAsString(); - if ("true".equalsIgnoreCase(s)) { - b = Boolean.TRUE; - } else if ("false".equalsIgnoreCase(s)) { - b = Boolean.FALSE; - } - } - - if (b != null) { - map.put(key, b); - } - } - } - } else { - map = Collections.emptyMap(); - } - - return new TestConfig(map); - } - } -} diff --git a/markwon/src/test/java/ru/noties/markwon/renderer/visitor/TestFactory.java b/markwon/src/test/java/ru/noties/markwon/renderer/visitor/TestFactory.java deleted file mode 100644 index 2171c1c1..00000000 --- a/markwon/src/test/java/ru/noties/markwon/renderer/visitor/TestFactory.java +++ /dev/null @@ -1,142 +0,0 @@ -package ru.noties.markwon.renderer.visitor; - -import android.support.annotation.NonNull; -import android.support.annotation.Nullable; - -import java.util.Collections; -import java.util.HashMap; -import java.util.Map; - -import ru.noties.markwon.core.MarkwonTheme; -import ru.noties.markwon.core.MarkwonSpannableFactory; -import ru.noties.markwon.core.spans.LinkSpan; -import ru.noties.markwon.image.AsyncDrawableLoader; -import ru.noties.markwon.image.ImageSize; -import ru.noties.markwon.image.ImageSizeResolver; - -import static ru.noties.markwon.renderer.visitor.TestSpan.BLOCK_QUOTE; -import static ru.noties.markwon.renderer.visitor.TestSpan.BULLET_LIST; -import static ru.noties.markwon.renderer.visitor.TestSpan.CODE; -import static ru.noties.markwon.renderer.visitor.TestSpan.CODE_BLOCK; -import static ru.noties.markwon.renderer.visitor.TestSpan.EMPHASIS; -import static ru.noties.markwon.renderer.visitor.TestSpan.HEADING; -import static ru.noties.markwon.renderer.visitor.TestSpan.IMAGE; -import static ru.noties.markwon.renderer.visitor.TestSpan.LINK; -import static ru.noties.markwon.renderer.visitor.TestSpan.ORDERED_LIST; -import static ru.noties.markwon.renderer.visitor.TestSpan.PARAGRAPH; -import static ru.noties.markwon.renderer.visitor.TestSpan.STRONG_EMPHASIS; -import static ru.noties.markwon.renderer.visitor.TestSpan.THEMATIC_BREAK; - -class TestFactory implements MarkwonSpannableFactory { - - private final boolean useParagraphs; - - TestFactory(boolean useParagraphs) { - this.useParagraphs = useParagraphs; - } - - @Nullable - @Override - public Object strongEmphasis() { - return new TestSpan(STRONG_EMPHASIS); - } - - @Nullable - @Override - public Object emphasis() { - return new TestSpan(EMPHASIS); - } - - @Nullable - @Override - public Object blockQuote(@NonNull MarkwonTheme theme) { - return new TestSpan(BLOCK_QUOTE); - } - - @Nullable - @Override - public Object code(@NonNull MarkwonTheme theme, boolean multiline) { - final String name = multiline - ? CODE_BLOCK - : CODE; - return new TestSpan(name); - } - - @Nullable - @Override - public Object orderedListItem(@NonNull MarkwonTheme theme, int startNumber) { - return new TestSpan(ORDERED_LIST, map("start", startNumber)); - } - - @Nullable - @Override - public Object bulletListItem(@NonNull MarkwonTheme theme, int level) { - return new TestSpan(BULLET_LIST, map("level", level)); - } - - @Nullable - @Override - public Object thematicBreak(@NonNull MarkwonTheme theme) { - return new TestSpan(THEMATIC_BREAK); - } - - @Nullable - @Override - public Object heading(@NonNull MarkwonTheme theme, int level) { - return new TestSpan(HEADING + level); - } - - @Nullable - @Override - public Object paragraph(boolean inTightList) { - return !useParagraphs - ? null - : new TestSpan(PARAGRAPH); - } - - @Nullable - @Override - public Object image(@NonNull MarkwonTheme theme, @NonNull String destination, @NonNull AsyncDrawableLoader loader, @NonNull ImageSizeResolver imageSizeResolver, @Nullable ImageSize imageSize, boolean replacementTextIsLink) { - return new TestSpan(IMAGE, map( - Pair.of("src", destination), - Pair.of("imageSize", imageSize), - Pair.of("replacementTextIsLink", replacementTextIsLink) - )); - } - - @Nullable - @Override - public Object link(@NonNull MarkwonTheme theme, @NonNull String destination, @NonNull LinkSpan.Resolver resolver) { - return new TestSpan(LINK, map("href", destination)); - } - - @NonNull - private static Map map(@NonNull String key, @Nullable Object value) { - return Collections.singletonMap(key, String.valueOf(value)); - } - - private static class Pair { - - static Pair of(@NonNull String key, @Nullable Object value) { - return new Pair(key, value); - } - - final String key; - final Object value; - - Pair(@NonNull String key, @Nullable Object value) { - this.key = key; - this.value = value; - } - } - - @NonNull - private static Map map(Pair... pairs) { - final int length = pairs.length; - final Map map = new HashMap<>(length); - for (Pair pair : pairs) { - map.put(pair.key, pair.value == null ? null : String.valueOf(pair.value)); - } - return map; - } -} diff --git a/markwon/src/test/java/ru/noties/markwon/renderer/visitor/TestNode.java b/markwon/src/test/java/ru/noties/markwon/renderer/visitor/TestNode.java deleted file mode 100644 index 9124fd59..00000000 --- a/markwon/src/test/java/ru/noties/markwon/renderer/visitor/TestNode.java +++ /dev/null @@ -1,140 +0,0 @@ -package ru.noties.markwon.renderer.visitor; - -import android.support.annotation.NonNull; -import android.support.annotation.Nullable; - -import java.util.List; -import java.util.Map; - -abstract class TestNode { - - private final TestNode parent; - - TestNode(@Nullable TestNode parent) { - this.parent = parent; - } - - @Nullable - public TestNode parent() { - return parent; - } - - abstract boolean isText(); - - abstract boolean isSpan(); - - @NonNull - abstract Text getAsText(); - - @NonNull - abstract Span getAsSpan(); - - - static class Text extends TestNode { - - private final String text; - - Text(@Nullable TestNode parent, @NonNull String text) { - super(parent); - this.text = text; - } - - @NonNull - public String text() { - return text; - } - - @Override - boolean isText() { - return true; - } - - @Override - boolean isSpan() { - return false; - } - - @NonNull - @Override - Text getAsText() { - return this; - } - - @NonNull - @Override - Span getAsSpan() { - throw new ClassCastException(); - } - - @Override - public String toString() { - return "Text{" + - "text='" + text + '\'' + - '}'; - } - } - - static class Span extends TestNode { - - private final String name; - private final List children; - private final Map attributes; - - Span( - @Nullable TestNode parent, - @NonNull String name, - @NonNull List children, - @NonNull Map attributes) { - super(parent); - this.name = name; - this.children = children; - this.attributes = attributes; - } - - @NonNull - public String name() { - return name; - } - - @NonNull - public List children() { - return children; - } - - @NonNull - public Map attributes() { - return attributes; - } - - @Override - boolean isText() { - return false; - } - - @Override - boolean isSpan() { - return true; - } - - @NonNull - @Override - Text getAsText() { - throw new ClassCastException(); - } - - @NonNull - @Override - Span getAsSpan() { - return this; - } - - @Override - public String toString() { - return "Span{" + - "name='" + name + '\'' + - ", children=" + children + - ", attributes=" + attributes + - '}'; - } - } -} diff --git a/markwon/src/test/java/ru/noties/markwon/renderer/visitor/TestSpan.java b/markwon/src/test/java/ru/noties/markwon/renderer/visitor/TestSpan.java deleted file mode 100644 index 1740ba3d..00000000 --- a/markwon/src/test/java/ru/noties/markwon/renderer/visitor/TestSpan.java +++ /dev/null @@ -1,59 +0,0 @@ -package ru.noties.markwon.renderer.visitor; - -import android.support.annotation.NonNull; - -import java.util.Collections; -import java.util.Map; - -class TestSpan { - - static final String STRONG_EMPHASIS = "b"; - static final String EMPHASIS = "i"; - static final String BLOCK_QUOTE = "blockquote"; - static final String CODE = "code"; - static final String CODE_BLOCK = "code-block"; - static final String ORDERED_LIST = "ol"; - static final String BULLET_LIST = "ul"; - static final String THEMATIC_BREAK = "hr"; - static final String HEADING = "h"; -// static final String STRIKE_THROUGH = "s"; -// static final String TASK_LIST = "task-list"; -// static final String TABLE_ROW = "tr"; - static final String PARAGRAPH = "p"; - static final String IMAGE = "img"; - static final String LINK = "a"; -// static final String SUPER_SCRIPT = "sup"; -// static final String SUB_SCRIPT = "sub"; -// static final String UNDERLINE = "u"; - - - private final String name; - private final Map attributes; - - TestSpan(@NonNull String name) { - this(name, Collections.emptyMap()); - } - - TestSpan(@NonNull String name, @NonNull Map attributes) { - this.name = name; - this.attributes = attributes; - } - - @NonNull - public String name() { - return name; - } - - @NonNull - public Map attributes() { - return attributes; - } - - @Override - public String toString() { - return "TestSpan{" + - "name='" + name + '\'' + - ", attributes=" + attributes + - '}'; - } -} diff --git a/markwon/src/test/java/ru/noties/markwon/renderer/visitor/TestValidator.java b/markwon/src/test/java/ru/noties/markwon/renderer/visitor/TestValidator.java deleted file mode 100644 index 59c000fe..00000000 --- a/markwon/src/test/java/ru/noties/markwon/renderer/visitor/TestValidator.java +++ /dev/null @@ -1,199 +0,0 @@ -package ru.noties.markwon.renderer.visitor; - -import android.support.annotation.NonNull; -import android.support.annotation.Nullable; -import android.text.SpannableStringBuilder; -import android.text.Spanned; - -import java.util.Map; - -import ix.Ix; -import ix.IxPredicate; - -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertNotNull; -import static org.junit.Assert.assertTrue; - -abstract class TestValidator { - - abstract int validate( - @NonNull SpannableStringBuilder builder, - int index, - @NonNull TestNode node); - - abstract int processedSpanNodesCount(); - - - @NonNull - static TestValidator create(@NonNull String id) { - return new Impl(id); - } - - static class Impl extends TestValidator { - - private final String id; - - private int processedCount; - - Impl(@NonNull String id) { - this.id = id; - } - - @Override - int validate( - @NonNull final SpannableStringBuilder builder, - final int index, - @NonNull TestNode node) { - - if (node.isText()) { - - final String text; - { - final String content = node.getAsText().text(); - - // code is a special case as we wrap it around non-breakable spaces - final TestNode parent = node.parent(); - if (parent != null) { - final TestNode.Span span = parent.getAsSpan(); - if (TestSpan.CODE.equals(span.name())) { - text = "\u00a0" + content + "\u00a0"; - } else if (TestSpan.CODE_BLOCK.equals(span.name())) { - text = "\u00a0\n" + content + "\n\u00a0"; - } else { - text = content; - } - } else { - text = content; - } - } - - assertEquals( - String.format("text: %s, position: {%d-%d}", text, index, index + text.length()), - text, - builder.subSequence(index, index + text.length()).toString()); - - return index + text.length(); - } - - final TestNode.Span span = node.getAsSpan(); - processedCount += 1; - - int out = index; - - for (TestNode child : span.children()) { - out = validate(builder, out, child); - } - - final int end = out; - - // we can possibly have parent spans here, should filter them - final Object[] spans = builder.getSpans(index, out, Object.class); - - // expected span{name, attributes} at position{start-end}, with text: `%s`, spans: [] - - - assertTrue( - message(span, index, end, builder, spans), - spans != null - ); - - final TestSpan testSpan = Ix.fromArray(spans) - .filter(new IxPredicate() { - @Override - public boolean test(Object o) { - return o instanceof TestSpan; - } - }) - .cast(TestSpan.class) - .filter(new IxPredicate() { - @Override - public boolean test(TestSpan testSpan) { - - // in case of nested spans with the same name (lists) - // we also must validate attributes - // and thus we are moving most of assertions to this filter method - return span.name().equals(testSpan.name()) - && index == builder.getSpanStart(testSpan) - && end == builder.getSpanEnd(testSpan) - && mapEquals(span.attributes(), testSpan.attributes()); - } - }) - .first(null); - - assertNotNull( - message(span, index, end, builder, spans), - testSpan - ); - - return out; - } - - @Override - int processedSpanNodesCount() { - return processedCount; - } - - private static boolean mapEquals( - @NonNull Map expected, - @NonNull Map actual) { - - if (expected.size() != actual.size()) { - return false; - } - - boolean result = true; - - for (Map.Entry entry : expected.entrySet()) { - if (!actual.containsKey(entry.getKey()) - || !equals(entry.getValue(), actual.get(entry.getKey()))) { - result = false; - break; - } - } - - return result; - } - - private static boolean equals(@Nullable Object o1, @Nullable Object o2) { - return o1 != null - ? o1.equals(o2) - : o2 == null; - } - - @NonNull - private static String message( - @NonNull TestNode.Span span, - int start, - int end, - @NonNull Spanned text, - @Nullable Object[] spans) { - final String spansText; - if (spans == null - || spans.length == 0) { - spansText = "[]"; - } else { - final StringBuilder builder = new StringBuilder(); - for (Object o : spans) { - final TestSpan testSpan = (TestSpan) o; - if (builder.length() > 0) { - builder.append(", "); - } - - builder - .append("{name: '").append(testSpan.name()).append('\'') - .append(", position{").append(start).append(", ").append(end).append('}'); - - if (testSpan.attributes().size() > 0) { - builder.append(", attributes: ").append(testSpan.attributes()); - } - - builder.append('}'); - } - spansText = builder.toString(); - } - return String.format("Expected span: %s at position{%d-%d} with text `%s`, spans: %s", - span, start, end, text.subSequence(start, end), spansText - ); - } - } -} diff --git a/markwon/src/test/java/ru/noties/markwon/syntax/SyntaxHighlightTest.java b/markwon/src/test/java/ru/noties/markwon/syntax/SyntaxHighlightTest.java index 2f2d99b7..c15426d2 100644 --- a/markwon/src/test/java/ru/noties/markwon/syntax/SyntaxHighlightTest.java +++ b/markwon/src/test/java/ru/noties/markwon/syntax/SyntaxHighlightTest.java @@ -23,7 +23,6 @@ import ru.noties.markwon.MarkwonVisitor; import ru.noties.markwon.SpannableBuilder; import ru.noties.markwon.core.MarkwonTheme; import ru.noties.markwon.core.MarkwonSpannableFactory; -import ru.noties.markwon.core.visitor.CodeBlockNodeVisitor; import ru.noties.markwon.image.AsyncDrawableLoader; import static org.junit.Assert.assertEquals; diff --git a/markwon/src/test/resources/tests/single-img.yaml b/markwon/src/test/resources/tests/single-img.yaml deleted file mode 100644 index 9c55a2f6..00000000 --- a/markwon/src/test/resources/tests/single-img.yaml +++ /dev/null @@ -1,7 +0,0 @@ -input: "![image](#href)" - -output: - - img: "image" - src: "#href" - imageSize: null - replacementTextIsLink: false \ No newline at end of file diff --git a/sample-custom-extension/src/main/java/ru/noties/markwon/sample/extension/IconPlugin.java b/sample-custom-extension/src/main/java/ru/noties/markwon/sample/extension/IconPlugin.java index eb821000..e03f8a86 100644 --- a/sample-custom-extension/src/main/java/ru/noties/markwon/sample/extension/IconPlugin.java +++ b/sample-custom-extension/src/main/java/ru/noties/markwon/sample/extension/IconPlugin.java @@ -24,6 +24,7 @@ public class IconPlugin extends AbstractMarkwonPlugin { @Override public void configureParser(@NonNull Parser.Builder builder) { builder.customDelimiterProcessor(IconProcessor.create()); + builder.postProcessor() } @Override diff --git a/sample-custom-extension/src/main/java/ru/noties/markwon/sample/extension/MainActivity.java b/sample-custom-extension/src/main/java/ru/noties/markwon/sample/extension/MainActivity.java index 29ad6c51..4fa0bac7 100644 --- a/sample-custom-extension/src/main/java/ru/noties/markwon/sample/extension/MainActivity.java +++ b/sample-custom-extension/src/main/java/ru/noties/markwon/sample/extension/MainActivity.java @@ -21,8 +21,8 @@ public class MainActivity extends Activity { final TextView textView = findViewById(R.id.text_view); final Markwon markwon = Markwon.builder(this) - .use(IconPlugin.create(IconSpanProvider.create(this, 0))) - .use(new AbstractMarkwonPlugin() { + .usePlugin(IconPlugin.create(IconSpanProvider.create(this, 0))) + .usePlugin(new AbstractMarkwonPlugin() { @Override public void configureTheme(@NonNull MarkwonTheme.Builder builder) { final float[] textSizeMultipliers = new float[]{3f, 2f, 1.5f, 1f, .5f, .25f}; diff --git a/sample-latex-math/src/main/java/ru/noties/markwon/sample/jlatexmath/MainActivity.java b/sample-latex-math/src/main/java/ru/noties/markwon/sample/jlatexmath/MainActivity.java index 01fb2718..7eeb8006 100644 --- a/sample-latex-math/src/main/java/ru/noties/markwon/sample/jlatexmath/MainActivity.java +++ b/sample-latex-math/src/main/java/ru/noties/markwon/sample/jlatexmath/MainActivity.java @@ -46,11 +46,11 @@ public class MainActivity extends Activity { + latex + "$$\n\n something like **this**"; final Markwon markwon = Markwon.builder(this) - .use(CorePlugin.create()) + .usePlugin(CorePlugin.create()) // strictly speaking this one is not required as long as JLatexMathPlugin schedules // drawables on it's own - .use(ImagesPlugin.create(this)) - .use(JLatexMathPlugin.create(config)) + .usePlugin(ImagesPlugin.create(this)) + .usePlugin(JLatexMathPlugin.create(config)) .build(); markwon.setMarkdown(textView, markdown); From 107246c573a4dbe24f24a8c77e8a5c02a5753f0a Mon Sep 17 00:00:00 2001 From: Dimitry Ivanov Date: Sat, 22 Dec 2018 16:20:39 +0300 Subject: [PATCH 044/103] Update HTML plugin to reflect latest API changes --- .../strikethrough/StrikethroughPlugin.java | 16 +++++++- markwon-html/build.gradle | 6 +++ .../ru/noties/markwon/html/HtmlPlugin.java | 3 +- .../markwon/html/MarkwonHtmlRenderer.java | 14 +------ .../markwon/html/MarkwonHtmlRendererImpl.java | 12 +++--- .../markwon/html/MarkwonHtmlRendererNoOp.java | 21 ---------- .../ru/noties/markwon/html/TagHandler.java | 13 +++---- .../markwon/html/tag/BlockquoteHandler.java | 25 +++++++----- .../markwon/html/tag/EmphasisHandler.java | 15 ++++++- .../markwon/html/tag/HeadingHandler.java | 20 +++++++++- .../noties/markwon/html/tag/ImageHandler.java | 31 +++++++++------ .../noties/markwon/html/tag/LinkHandler.java | 21 +++++++--- .../noties/markwon/html/tag/ListHandler.java | 39 ++++++++++++------- .../markwon/html/tag/SimpleTagHandler.java | 13 +++++-- .../markwon/html/tag/StrikeHandler.java | 38 +++++++++++++++--- .../html/tag/StrongEmphasisHandler.java | 15 ++++++- .../markwon/html/tag/SubScriptHandler.java | 3 +- .../markwon/html/tag/SuperScriptHandler.java | 3 +- .../markwon/html/tag/UnderlineHandler.java | 9 ++--- 19 files changed, 203 insertions(+), 114 deletions(-) delete mode 100644 markwon-html/src/main/java/ru/noties/markwon/html/MarkwonHtmlRendererNoOp.java diff --git a/markwon-ext-strikethrough/src/main/java/ru/noties/markwon/ext/strikethrough/StrikethroughPlugin.java b/markwon-ext-strikethrough/src/main/java/ru/noties/markwon/ext/strikethrough/StrikethroughPlugin.java index 69c55890..eb832e7e 100644 --- a/markwon-ext-strikethrough/src/main/java/ru/noties/markwon/ext/strikethrough/StrikethroughPlugin.java +++ b/markwon-ext-strikethrough/src/main/java/ru/noties/markwon/ext/strikethrough/StrikethroughPlugin.java @@ -10,7 +10,11 @@ import org.commonmark.parser.Parser; import java.util.Collections; import ru.noties.markwon.AbstractMarkwonPlugin; +import ru.noties.markwon.MarkwonConfiguration; +import ru.noties.markwon.MarkwonSpansFactory; import ru.noties.markwon.MarkwonVisitor; +import ru.noties.markwon.RenderProps; +import ru.noties.markwon.SpanFactory; public class StrikethroughPlugin extends AbstractMarkwonPlugin { @@ -24,6 +28,16 @@ public class StrikethroughPlugin extends AbstractMarkwonPlugin { builder.extensions(Collections.singleton(StrikethroughExtension.create())); } + @Override + public void configureSpansFactory(@NonNull MarkwonSpansFactory.Builder builder) { + builder.setFactory(Strikethrough.class, new SpanFactory() { + @Override + public Object getSpans(@NonNull MarkwonConfiguration configuration, @NonNull RenderProps context) { + return new StrikethroughSpan(); + } + }); + } + @Override public void configureVisitor(@NonNull MarkwonVisitor.Builder builder) { builder.on(Strikethrough.class, new MarkwonVisitor.NodeVisitor() { @@ -31,7 +45,7 @@ public class StrikethroughPlugin extends AbstractMarkwonPlugin { public void visit(@NonNull MarkwonVisitor visitor, @NonNull Strikethrough strikethrough) { final int length = visitor.length(); visitor.visitChildren(strikethrough); - visitor.setSpans(length, new StrikethroughSpan()); + visitor.setSpansForNode(strikethrough, length); } }); } diff --git a/markwon-html/build.gradle b/markwon-html/build.gradle index b61b12de..ddf6d5c9 100644 --- a/markwon-html/build.gradle +++ b/markwon-html/build.gradle @@ -20,6 +20,12 @@ dependencies { deps.with { api it['support-annotations'] api it['commonmark'] + + // add a compileOnly dependency, so if this artifact is present + // we will try to obtain a SpanFactory for a Strikethrough node and use + // it to be consistent with markdown (please note that we do not use markwon plugin + // for that in case if different implementation is used) + compileOnly it['commonmark-strikethrough'] } deps.test.with { diff --git a/markwon-html/src/main/java/ru/noties/markwon/html/HtmlPlugin.java b/markwon-html/src/main/java/ru/noties/markwon/html/HtmlPlugin.java index 0ee64b5e..acf20c62 100644 --- a/markwon-html/src/main/java/ru/noties/markwon/html/HtmlPlugin.java +++ b/markwon-html/src/main/java/ru/noties/markwon/html/HtmlPlugin.java @@ -3,7 +3,6 @@ package ru.noties.markwon.html; import android.support.annotation.NonNull; import android.support.annotation.Nullable; -import org.commonmark.node.Document; import org.commonmark.node.HtmlBlock; import org.commonmark.node.HtmlInline; import org.commonmark.node.Node; @@ -43,7 +42,7 @@ public class HtmlPlugin extends AbstractMarkwonPlugin { @Override public void afterRender(@NonNull Node node, @NonNull MarkwonVisitor visitor) { - renderer.render(visitor.configuration(), visitor.builder(), parser); + renderer.render(visitor, parser); } @Override diff --git a/markwon-html/src/main/java/ru/noties/markwon/html/MarkwonHtmlRenderer.java b/markwon-html/src/main/java/ru/noties/markwon/html/MarkwonHtmlRenderer.java index 9e9d222f..f9fed923 100644 --- a/markwon-html/src/main/java/ru/noties/markwon/html/MarkwonHtmlRenderer.java +++ b/markwon-html/src/main/java/ru/noties/markwon/html/MarkwonHtmlRenderer.java @@ -3,25 +3,15 @@ package ru.noties.markwon.html; import android.support.annotation.NonNull; import android.support.annotation.Nullable; -import ru.noties.markwon.MarkwonConfiguration; -import ru.noties.markwon.SpannableBuilder; +import ru.noties.markwon.MarkwonVisitor; /** * @since 2.0.0 */ public abstract class MarkwonHtmlRenderer { - /** - * @since 3.0.0 - */ - @NonNull - public static MarkwonHtmlRenderer noOp() { - return new MarkwonHtmlRendererNoOp(); - } - public abstract void render( - @NonNull MarkwonConfiguration configuration, - @NonNull SpannableBuilder builder, + @NonNull MarkwonVisitor visitor, @NonNull MarkwonHtmlParser parser ); diff --git a/markwon-html/src/main/java/ru/noties/markwon/html/MarkwonHtmlRendererImpl.java b/markwon-html/src/main/java/ru/noties/markwon/html/MarkwonHtmlRendererImpl.java index 399d58ca..33d292c9 100644 --- a/markwon-html/src/main/java/ru/noties/markwon/html/MarkwonHtmlRendererImpl.java +++ b/markwon-html/src/main/java/ru/noties/markwon/html/MarkwonHtmlRendererImpl.java @@ -9,8 +9,7 @@ import java.util.List; import java.util.Locale; import java.util.Map; -import ru.noties.markwon.MarkwonConfiguration; -import ru.noties.markwon.SpannableBuilder; +import ru.noties.markwon.MarkwonVisitor; import ru.noties.markwon.html.tag.BlockquoteHandler; import ru.noties.markwon.html.tag.EmphasisHandler; import ru.noties.markwon.html.tag.HeadingHandler; @@ -100,15 +99,14 @@ public class MarkwonHtmlRendererImpl extends MarkwonHtmlRenderer { @Override public void render( - @NonNull final MarkwonConfiguration configuration, - @NonNull final SpannableBuilder builder, + @NonNull final MarkwonVisitor visitor, @NonNull MarkwonHtmlParser parser) { final int end; if (!allowNonClosedTags) { end = HtmlTag.NO_END; } else { - end = builder.length(); + end = visitor.length(); } parser.flushInlineTags(end, new MarkwonHtmlParser.FlushAction() { @@ -126,7 +124,7 @@ public class MarkwonHtmlRendererImpl extends MarkwonHtmlRenderer { handler = tagHandler(inline.name()); if (handler != null) { - handler.handle(configuration, MarkwonHtmlRendererImpl.this, builder, inline); + handler.handle(visitor, MarkwonHtmlRendererImpl.this, inline); } } } @@ -146,7 +144,7 @@ public class MarkwonHtmlRendererImpl extends MarkwonHtmlRenderer { handler = tagHandler(block.name()); if (handler != null) { - handler.handle(configuration, MarkwonHtmlRendererImpl.this, builder, block); + handler.handle(visitor, MarkwonHtmlRendererImpl.this, block); } else { // see if any of children can be handled apply(block.children()); diff --git a/markwon-html/src/main/java/ru/noties/markwon/html/MarkwonHtmlRendererNoOp.java b/markwon-html/src/main/java/ru/noties/markwon/html/MarkwonHtmlRendererNoOp.java deleted file mode 100644 index 7df5027c..00000000 --- a/markwon-html/src/main/java/ru/noties/markwon/html/MarkwonHtmlRendererNoOp.java +++ /dev/null @@ -1,21 +0,0 @@ -package ru.noties.markwon.html; - -import android.support.annotation.NonNull; -import android.support.annotation.Nullable; - -import ru.noties.markwon.MarkwonConfiguration; -import ru.noties.markwon.SpannableBuilder; - -class MarkwonHtmlRendererNoOp extends MarkwonHtmlRenderer { - - @Override - public void render(@NonNull MarkwonConfiguration configuration, @NonNull SpannableBuilder builder, @NonNull MarkwonHtmlParser parser) { - - } - - @Nullable - @Override - public TagHandler tagHandler(@NonNull String tagName) { - return null; - } -} diff --git a/markwon-html/src/main/java/ru/noties/markwon/html/TagHandler.java b/markwon-html/src/main/java/ru/noties/markwon/html/TagHandler.java index 881e882f..84122946 100644 --- a/markwon-html/src/main/java/ru/noties/markwon/html/TagHandler.java +++ b/markwon-html/src/main/java/ru/noties/markwon/html/TagHandler.java @@ -2,22 +2,19 @@ package ru.noties.markwon.html; import android.support.annotation.NonNull; -import ru.noties.markwon.MarkwonConfiguration; -import ru.noties.markwon.SpannableBuilder; +import ru.noties.markwon.MarkwonVisitor; public abstract class TagHandler { public abstract void handle( - @NonNull MarkwonConfiguration configuration, + @NonNull MarkwonVisitor visitor, @NonNull MarkwonHtmlRenderer renderer, - @NonNull SpannableBuilder builder, @NonNull HtmlTag tag ); protected static void visitChildren( - @NonNull MarkwonConfiguration configuration, + @NonNull MarkwonVisitor visitor, @NonNull MarkwonHtmlRenderer renderer, - @NonNull SpannableBuilder builder, @NonNull HtmlTag.Block block) { TagHandler handler; @@ -30,9 +27,9 @@ public abstract class TagHandler { handler = renderer.tagHandler(child.name()); if (handler != null) { - handler.handle(configuration, renderer, builder, child); + handler.handle(visitor, renderer, child); } else { - visitChildren(configuration, renderer, builder, child); + visitChildren(visitor, renderer, child); } } } diff --git a/markwon-html/src/main/java/ru/noties/markwon/html/tag/BlockquoteHandler.java b/markwon-html/src/main/java/ru/noties/markwon/html/tag/BlockquoteHandler.java index 4cd93fa0..930a8dda 100644 --- a/markwon-html/src/main/java/ru/noties/markwon/html/tag/BlockquoteHandler.java +++ b/markwon-html/src/main/java/ru/noties/markwon/html/tag/BlockquoteHandler.java @@ -2,7 +2,11 @@ package ru.noties.markwon.html.tag; import android.support.annotation.NonNull; +import org.commonmark.node.BlockQuote; + import ru.noties.markwon.MarkwonConfiguration; +import ru.noties.markwon.MarkwonVisitor; +import ru.noties.markwon.SpanFactory; import ru.noties.markwon.SpannableBuilder; import ru.noties.markwon.html.HtmlTag; import ru.noties.markwon.html.MarkwonHtmlRenderer; @@ -12,20 +16,23 @@ public class BlockquoteHandler extends TagHandler { @Override public void handle( - @NonNull MarkwonConfiguration configuration, + @NonNull MarkwonVisitor visitor, @NonNull MarkwonHtmlRenderer renderer, - @NonNull SpannableBuilder builder, @NonNull HtmlTag tag) { if (tag.isBlock()) { - visitChildren(configuration, renderer, builder, tag.getAsBlock()); + visitChildren(visitor, renderer, tag.getAsBlock()); } - SpannableBuilder.setSpans( - builder, - configuration.factory().blockQuote(configuration.theme()), - tag.start(), - tag.end() - ); + final MarkwonConfiguration configuration = visitor.configuration(); + final SpanFactory factory = configuration.spansFactory().get(BlockQuote.class); + if (factory != null) { + SpannableBuilder.setSpans( + visitor.builder(), + factory.getSpans(configuration, visitor.renderProps()), + tag.start(), + tag.end() + ); + } } } diff --git a/markwon-html/src/main/java/ru/noties/markwon/html/tag/EmphasisHandler.java b/markwon-html/src/main/java/ru/noties/markwon/html/tag/EmphasisHandler.java index b0809be6..fe546ee0 100644 --- a/markwon-html/src/main/java/ru/noties/markwon/html/tag/EmphasisHandler.java +++ b/markwon-html/src/main/java/ru/noties/markwon/html/tag/EmphasisHandler.java @@ -3,13 +3,24 @@ package ru.noties.markwon.html.tag; import android.support.annotation.NonNull; import android.support.annotation.Nullable; +import org.commonmark.node.Emphasis; + import ru.noties.markwon.MarkwonConfiguration; +import ru.noties.markwon.RenderProps; +import ru.noties.markwon.SpanFactory; import ru.noties.markwon.html.HtmlTag; public class EmphasisHandler extends SimpleTagHandler { @Nullable @Override - public Object getSpans(@NonNull MarkwonConfiguration configuration, @NonNull HtmlTag tag) { - return configuration.factory().emphasis(); + public Object getSpans( + @NonNull MarkwonConfiguration configuration, + @NonNull RenderProps renderProps, + @NonNull HtmlTag tag) { + final SpanFactory spanFactory = configuration.spansFactory().get(Emphasis.class); + if (spanFactory == null) { + return null; + } + return spanFactory.getSpans(configuration, renderProps); } } diff --git a/markwon-html/src/main/java/ru/noties/markwon/html/tag/HeadingHandler.java b/markwon-html/src/main/java/ru/noties/markwon/html/tag/HeadingHandler.java index 79848625..c3473483 100644 --- a/markwon-html/src/main/java/ru/noties/markwon/html/tag/HeadingHandler.java +++ b/markwon-html/src/main/java/ru/noties/markwon/html/tag/HeadingHandler.java @@ -3,7 +3,12 @@ package ru.noties.markwon.html.tag; import android.support.annotation.NonNull; import android.support.annotation.Nullable; +import org.commonmark.node.Heading; + import ru.noties.markwon.MarkwonConfiguration; +import ru.noties.markwon.RenderProps; +import ru.noties.markwon.SpanFactory; +import ru.noties.markwon.core.CoreProps; import ru.noties.markwon.html.HtmlTag; public class HeadingHandler extends SimpleTagHandler { @@ -16,7 +21,18 @@ public class HeadingHandler extends SimpleTagHandler { @Nullable @Override - public Object getSpans(@NonNull MarkwonConfiguration configuration, @NonNull HtmlTag tag) { - return configuration.factory().heading(configuration.theme(), level); + public Object getSpans( + @NonNull MarkwonConfiguration configuration, + @NonNull RenderProps renderProps, + @NonNull HtmlTag tag) { + + final SpanFactory factory = configuration.spansFactory().get(Heading.class); + if (factory == null) { + return null; + } + + CoreProps.HEADING_LEVEL.set(renderProps, level); + + return factory.getSpans(configuration, renderProps); } } diff --git a/markwon-html/src/main/java/ru/noties/markwon/html/tag/ImageHandler.java b/markwon-html/src/main/java/ru/noties/markwon/html/tag/ImageHandler.java index 8554a645..4e7ffa54 100644 --- a/markwon-html/src/main/java/ru/noties/markwon/html/tag/ImageHandler.java +++ b/markwon-html/src/main/java/ru/noties/markwon/html/tag/ImageHandler.java @@ -4,11 +4,16 @@ import android.support.annotation.NonNull; import android.support.annotation.Nullable; import android.text.TextUtils; +import org.commonmark.node.Image; + import java.util.Map; import ru.noties.markwon.MarkwonConfiguration; -import ru.noties.markwon.html.HtmlTag; +import ru.noties.markwon.RenderProps; +import ru.noties.markwon.SpanFactory; import ru.noties.markwon.html.CssInlineStyleParser; +import ru.noties.markwon.html.HtmlTag; +import ru.noties.markwon.image.ImageProps; import ru.noties.markwon.image.ImageSize; public class ImageHandler extends SimpleTagHandler { @@ -31,7 +36,10 @@ public class ImageHandler extends SimpleTagHandler { @Nullable @Override - public Object getSpans(@NonNull MarkwonConfiguration configuration, @NonNull HtmlTag tag) { + public Object getSpans( + @NonNull MarkwonConfiguration configuration, + @NonNull RenderProps renderProps, + @NonNull HtmlTag tag) { final Map attributes = tag.attributes(); final String src = attributes.get("src"); @@ -39,7 +47,13 @@ public class ImageHandler extends SimpleTagHandler { return null; } + final SpanFactory spanFactory = configuration.spansFactory().get(Image.class); + if (spanFactory == null) { + return null; + } + final String destination = configuration.urlProcessor().process(src); + final ImageSize imageSize = imageSizeParser.parse(tag.attributes()); // todo: replacement text is link... as we are not at block level // and cannot inspect the parent of this node... (img and a are both inlines) @@ -47,15 +61,10 @@ public class ImageHandler extends SimpleTagHandler { // but we can look and see if we are inside a LinkSpan (will have to extend TagHandler // to obtain an instance SpannableBuilder for inspection) - return null; + ImageProps.DESTINATION.set(renderProps, destination); + ImageProps.IMAGE_SIZE.set(renderProps, imageSize); + ImageProps.REPLACEMENT_TEXT_IS_LINK.set(renderProps, false); -// return configuration.factory().image( -// configuration.theme(), -// destination, -// configuration.asyncDrawableLoader(), -// configuration.imageSizeResolver(), -// imageSizeParser.parse(tag.attributes()), -// false -// ); + return spanFactory.getSpans(configuration, renderProps); } } diff --git a/markwon-html/src/main/java/ru/noties/markwon/html/tag/LinkHandler.java b/markwon-html/src/main/java/ru/noties/markwon/html/tag/LinkHandler.java index 8bf78ff0..04e768e9 100644 --- a/markwon-html/src/main/java/ru/noties/markwon/html/tag/LinkHandler.java +++ b/markwon-html/src/main/java/ru/noties/markwon/html/tag/LinkHandler.java @@ -4,20 +4,29 @@ import android.support.annotation.NonNull; import android.support.annotation.Nullable; import android.text.TextUtils; +import org.commonmark.node.Link; + import ru.noties.markwon.MarkwonConfiguration; +import ru.noties.markwon.RenderProps; +import ru.noties.markwon.SpanFactory; +import ru.noties.markwon.core.CoreProps; import ru.noties.markwon.html.HtmlTag; public class LinkHandler extends SimpleTagHandler { @Nullable @Override - public Object getSpans(@NonNull MarkwonConfiguration configuration, @NonNull HtmlTag tag) { + public Object getSpans(@NonNull MarkwonConfiguration configuration, @NonNull RenderProps renderProps, @NonNull HtmlTag tag) { final String destination = tag.attributes().get("href"); if (!TextUtils.isEmpty(destination)) { - return configuration.factory().link( - configuration.theme(), - configuration.urlProcessor().process(destination), - configuration.linkResolver() - ); + final SpanFactory spanFactory = configuration.spansFactory().get(Link.class); + if (spanFactory != null) { + + CoreProps.LINK_DESTINATION.set( + renderProps, + configuration.urlProcessor().process(destination)); + + return spanFactory.getSpans(configuration, renderProps); + } } return null; } diff --git a/markwon-html/src/main/java/ru/noties/markwon/html/tag/ListHandler.java b/markwon-html/src/main/java/ru/noties/markwon/html/tag/ListHandler.java index 3a4dfd7c..66f76cc0 100644 --- a/markwon-html/src/main/java/ru/noties/markwon/html/tag/ListHandler.java +++ b/markwon-html/src/main/java/ru/noties/markwon/html/tag/ListHandler.java @@ -2,8 +2,14 @@ package ru.noties.markwon.html.tag; import android.support.annotation.NonNull; +import org.commonmark.node.ListItem; + import ru.noties.markwon.MarkwonConfiguration; +import ru.noties.markwon.MarkwonVisitor; +import ru.noties.markwon.RenderProps; +import ru.noties.markwon.SpanFactory; import ru.noties.markwon.SpannableBuilder; +import ru.noties.markwon.core.CoreProps; import ru.noties.markwon.html.HtmlTag; import ru.noties.markwon.html.MarkwonHtmlRenderer; import ru.noties.markwon.html.TagHandler; @@ -12,9 +18,8 @@ public class ListHandler extends TagHandler { @Override public void handle( - @NonNull MarkwonConfiguration configuration, + @NonNull MarkwonVisitor visitor, @NonNull MarkwonHtmlRenderer renderer, - @NonNull SpannableBuilder builder, @NonNull HtmlTag tag) { if (!tag.isBlock()) { @@ -29,29 +34,33 @@ public class ListHandler extends TagHandler { return; } + final MarkwonConfiguration configuration = visitor.configuration(); + final RenderProps renderProps = visitor.renderProps(); + final SpanFactory spanFactory = configuration.spansFactory().get(ListItem.class); + int number = 1; final int bulletLevel = currentBulletListLevel(block); - Object spans; - for (HtmlTag.Block child : block.children()) { - visitChildren(configuration, renderer, builder, child); + visitChildren(visitor, renderer, child); + + if (spanFactory != null && "li".equals(child.name())) { - if ("li".equals(child.name())) { // insert list item here if (ol) { - spans = configuration.factory().orderedListItem( - configuration.theme(), - number++ - ); + CoreProps.LIST_ITEM_TYPE.set(renderProps, CoreProps.ListItemType.ORDERED); + CoreProps.ORDERED_LIST_ITEM_NUMBER.set(renderProps, number++); } else { - spans = configuration.factory().bulletListItem( - configuration.theme(), - bulletLevel - ); + CoreProps.LIST_ITEM_TYPE.set(renderProps, CoreProps.ListItemType.BULLET); + CoreProps.BULLET_LIST_ITEM_LEVEL.set(renderProps, bulletLevel); } - SpannableBuilder.setSpans(builder, spans, child.start(), child.end()); + + SpannableBuilder.setSpans( + visitor.builder(), + spanFactory.getSpans(configuration, renderProps), + child.start(), + child.end()); } } } diff --git a/markwon-html/src/main/java/ru/noties/markwon/html/tag/SimpleTagHandler.java b/markwon-html/src/main/java/ru/noties/markwon/html/tag/SimpleTagHandler.java index 784ef8c9..d5717c20 100644 --- a/markwon-html/src/main/java/ru/noties/markwon/html/tag/SimpleTagHandler.java +++ b/markwon-html/src/main/java/ru/noties/markwon/html/tag/SimpleTagHandler.java @@ -4,6 +4,8 @@ import android.support.annotation.NonNull; import android.support.annotation.Nullable; import ru.noties.markwon.MarkwonConfiguration; +import ru.noties.markwon.MarkwonVisitor; +import ru.noties.markwon.RenderProps; import ru.noties.markwon.SpannableBuilder; import ru.noties.markwon.html.HtmlTag; import ru.noties.markwon.html.MarkwonHtmlRenderer; @@ -12,13 +14,16 @@ import ru.noties.markwon.html.TagHandler; public abstract class SimpleTagHandler extends TagHandler { @Nullable - public abstract Object getSpans(@NonNull MarkwonConfiguration configuration, @NonNull HtmlTag tag); + public abstract Object getSpans( + @NonNull MarkwonConfiguration configuration, + @NonNull RenderProps renderProps, + @NonNull HtmlTag tag); @Override - public void handle(@NonNull MarkwonConfiguration configuration, @NonNull MarkwonHtmlRenderer renderer, @NonNull SpannableBuilder builder, @NonNull HtmlTag tag) { - final Object spans = getSpans(configuration, tag); + public void handle(@NonNull MarkwonVisitor visitor, @NonNull MarkwonHtmlRenderer renderer, @NonNull HtmlTag tag) { + final Object spans = getSpans(visitor.configuration(), visitor.renderProps(), tag); if (spans != null) { - SpannableBuilder.setSpans(builder, spans, tag.start(), tag.end()); + SpannableBuilder.setSpans(visitor.builder(), spans, tag.start(), tag.end()); } } } diff --git a/markwon-html/src/main/java/ru/noties/markwon/html/tag/StrikeHandler.java b/markwon-html/src/main/java/ru/noties/markwon/html/tag/StrikeHandler.java index 6df59f28..2771501e 100644 --- a/markwon-html/src/main/java/ru/noties/markwon/html/tag/StrikeHandler.java +++ b/markwon-html/src/main/java/ru/noties/markwon/html/tag/StrikeHandler.java @@ -1,9 +1,12 @@ package ru.noties.markwon.html.tag; import android.support.annotation.NonNull; +import android.support.annotation.Nullable; import android.text.style.StrikethroughSpan; import ru.noties.markwon.MarkwonConfiguration; +import ru.noties.markwon.MarkwonVisitor; +import ru.noties.markwon.SpanFactory; import ru.noties.markwon.SpannableBuilder; import ru.noties.markwon.html.HtmlTag; import ru.noties.markwon.html.MarkwonHtmlRenderer; @@ -11,22 +14,47 @@ import ru.noties.markwon.html.TagHandler; public class StrikeHandler extends TagHandler { + // flag to detect if commonmark-java-strikethrough is in classpath, so we use SpanFactory + // to obtain strikethrough span + private static final boolean HAS_MARKDOWN_IMPLEMENTATION; + + static { + boolean hasMarkdownImplementation; + try { + org.commonmark.ext.gfm.strikethrough.Strikethrough.class.getName(); + hasMarkdownImplementation = true; + } catch (Throwable t) { + hasMarkdownImplementation = false; + } + HAS_MARKDOWN_IMPLEMENTATION = hasMarkdownImplementation; + } + @Override public void handle( - @NonNull MarkwonConfiguration configuration, + @NonNull MarkwonVisitor visitor, @NonNull MarkwonHtmlRenderer renderer, - @NonNull SpannableBuilder builder, @NonNull HtmlTag tag) { if (tag.isBlock()) { - visitChildren(configuration, renderer, builder, tag.getAsBlock()); + visitChildren(visitor, renderer, tag.getAsBlock()); } SpannableBuilder.setSpans( - builder, - new StrikethroughSpan(), + visitor.builder(), + HAS_MARKDOWN_IMPLEMENTATION ? getMarkdownSpans(visitor) : new StrikethroughSpan(), tag.start(), tag.end() ); } + + @Nullable + private static Object getMarkdownSpans(@NonNull MarkwonVisitor visitor) { + final MarkwonConfiguration configuration = visitor.configuration(); + final SpanFactory spanFactory = configuration.spansFactory() + .get(org.commonmark.ext.gfm.strikethrough.Strikethrough.class); + if (spanFactory == null) { + return null; + } + return spanFactory.getSpans(configuration, visitor.renderProps()); + } } diff --git a/markwon-html/src/main/java/ru/noties/markwon/html/tag/StrongEmphasisHandler.java b/markwon-html/src/main/java/ru/noties/markwon/html/tag/StrongEmphasisHandler.java index 31f5aff8..8e604b7f 100644 --- a/markwon-html/src/main/java/ru/noties/markwon/html/tag/StrongEmphasisHandler.java +++ b/markwon-html/src/main/java/ru/noties/markwon/html/tag/StrongEmphasisHandler.java @@ -3,13 +3,24 @@ package ru.noties.markwon.html.tag; import android.support.annotation.NonNull; import android.support.annotation.Nullable; +import org.commonmark.node.StrongEmphasis; + import ru.noties.markwon.MarkwonConfiguration; +import ru.noties.markwon.RenderProps; +import ru.noties.markwon.SpanFactory; import ru.noties.markwon.html.HtmlTag; public class StrongEmphasisHandler extends SimpleTagHandler { @Nullable @Override - public Object getSpans(@NonNull MarkwonConfiguration configuration, @NonNull HtmlTag tag) { - return configuration.factory().strongEmphasis(); + public Object getSpans( + @NonNull MarkwonConfiguration configuration, + @NonNull RenderProps renderProps, + @NonNull HtmlTag tag) { + final SpanFactory spanFactory = configuration.spansFactory().get(StrongEmphasis.class); + if (spanFactory == null) { + return null; + } + return spanFactory.getSpans(configuration, renderProps); } } diff --git a/markwon-html/src/main/java/ru/noties/markwon/html/tag/SubScriptHandler.java b/markwon-html/src/main/java/ru/noties/markwon/html/tag/SubScriptHandler.java index 2dee9f7a..5ddc5697 100644 --- a/markwon-html/src/main/java/ru/noties/markwon/html/tag/SubScriptHandler.java +++ b/markwon-html/src/main/java/ru/noties/markwon/html/tag/SubScriptHandler.java @@ -4,13 +4,14 @@ import android.support.annotation.NonNull; import android.support.annotation.Nullable; import ru.noties.markwon.MarkwonConfiguration; +import ru.noties.markwon.RenderProps; import ru.noties.markwon.html.HtmlTag; import ru.noties.markwon.html.span.SubScriptSpan; public class SubScriptHandler extends SimpleTagHandler { @Nullable @Override - public Object getSpans(@NonNull MarkwonConfiguration configuration, @NonNull HtmlTag tag) { + public Object getSpans(@NonNull MarkwonConfiguration configuration, @NonNull RenderProps renderProps, @NonNull HtmlTag tag) { return new SubScriptSpan(); } } diff --git a/markwon-html/src/main/java/ru/noties/markwon/html/tag/SuperScriptHandler.java b/markwon-html/src/main/java/ru/noties/markwon/html/tag/SuperScriptHandler.java index ccc5c17a..77147c99 100644 --- a/markwon-html/src/main/java/ru/noties/markwon/html/tag/SuperScriptHandler.java +++ b/markwon-html/src/main/java/ru/noties/markwon/html/tag/SuperScriptHandler.java @@ -4,13 +4,14 @@ import android.support.annotation.NonNull; import android.support.annotation.Nullable; import ru.noties.markwon.MarkwonConfiguration; +import ru.noties.markwon.RenderProps; import ru.noties.markwon.html.HtmlTag; import ru.noties.markwon.html.span.SuperScriptSpan; public class SuperScriptHandler extends SimpleTagHandler { @Nullable @Override - public Object getSpans(@NonNull MarkwonConfiguration configuration, @NonNull HtmlTag tag) { + public Object getSpans(@NonNull MarkwonConfiguration configuration, @NonNull RenderProps renderProps, @NonNull HtmlTag tag) { return new SuperScriptSpan(); } } diff --git a/markwon-html/src/main/java/ru/noties/markwon/html/tag/UnderlineHandler.java b/markwon-html/src/main/java/ru/noties/markwon/html/tag/UnderlineHandler.java index 882cae1c..eaa397a6 100644 --- a/markwon-html/src/main/java/ru/noties/markwon/html/tag/UnderlineHandler.java +++ b/markwon-html/src/main/java/ru/noties/markwon/html/tag/UnderlineHandler.java @@ -3,7 +3,7 @@ package ru.noties.markwon.html.tag; import android.support.annotation.NonNull; import android.text.style.UnderlineSpan; -import ru.noties.markwon.MarkwonConfiguration; +import ru.noties.markwon.MarkwonVisitor; import ru.noties.markwon.SpannableBuilder; import ru.noties.markwon.html.HtmlTag; import ru.noties.markwon.html.MarkwonHtmlRenderer; @@ -13,20 +13,19 @@ public class UnderlineHandler extends TagHandler { @Override public void handle( - @NonNull MarkwonConfiguration configuration, + @NonNull MarkwonVisitor visitor, @NonNull MarkwonHtmlRenderer renderer, - @NonNull SpannableBuilder builder, @NonNull HtmlTag tag) { // as parser doesn't treat U tag as an inline one, // thus doesn't allow children, we must visit them first if (tag.isBlock()) { - visitChildren(configuration, renderer, builder, tag.getAsBlock()); + visitChildren(visitor, renderer, tag.getAsBlock()); } SpannableBuilder.setSpans( - builder, + visitor.builder(), new UnderlineSpan(), tag.start(), tag.end() From f86cf1d109d33f5d377b778728e271d94ef3f4b1 Mon Sep 17 00:00:00 2001 From: Dimitry Ivanov Date: Sat, 22 Dec 2018 16:26:40 +0300 Subject: [PATCH 045/103] Update latex extension for latest changes --- .../markwon/ext/latex/JLatexMathPlugin.java | 22 ++++++++----------- .../ru/noties/markwon/MarkwonVisitor.java | 4 ++++ .../ru/noties/markwon/MarkwonVisitorImpl.java | 13 +++++++++++ 3 files changed, 26 insertions(+), 13 deletions(-) diff --git a/markwon-ext-latex/src/main/java/ru/noties/markwon/ext/latex/JLatexMathPlugin.java b/markwon-ext-latex/src/main/java/ru/noties/markwon/ext/latex/JLatexMathPlugin.java index eec628a9..ee653b2f 100644 --- a/markwon-ext-latex/src/main/java/ru/noties/markwon/ext/latex/JLatexMathPlugin.java +++ b/markwon-ext-latex/src/main/java/ru/noties/markwon/ext/latex/JLatexMathPlugin.java @@ -5,6 +5,7 @@ import android.net.Uri; import android.support.annotation.NonNull; import android.support.annotation.Nullable; +import org.commonmark.node.Image; import org.commonmark.parser.Parser; import java.io.ByteArrayInputStream; @@ -14,10 +15,11 @@ import java.util.Scanner; import ru.noties.jlatexmath.JLatexMathDrawable; import ru.noties.markwon.AbstractMarkwonPlugin; -import ru.noties.markwon.MarkwonConfiguration; import ru.noties.markwon.MarkwonVisitor; +import ru.noties.markwon.RenderProps; import ru.noties.markwon.image.AsyncDrawableLoader; import ru.noties.markwon.image.ImageItem; +import ru.noties.markwon.image.ImageProps; import ru.noties.markwon.image.ImageSize; import ru.noties.markwon.image.MediaDecoder; import ru.noties.markwon.image.SchemeHandler; @@ -77,19 +79,13 @@ public class JLatexMathPlugin extends AbstractMarkwonPlugin { final int length = visitor.length(); visitor.builder().append(latex); - final MarkwonConfiguration configuration = visitor.configuration(); + final RenderProps renderProps = visitor.renderProps(); - visitor.setSpans( - length, - configuration.factory().image( - visitor.theme(), - makeDestination(latex), - configuration.asyncDrawableLoader(), - configuration.imageSizeResolver(), - new ImageSize(new ImageSize.Dimension(100, "%"), null), - false - ) - ); + ImageProps.DESTINATION.set(renderProps, makeDestination(latex)); + ImageProps.REPLACEMENT_TEXT_IS_LINK.set(renderProps, false); + ImageProps.IMAGE_SIZE.set(renderProps, new ImageSize(new ImageSize.Dimension(100, "%"), null)); + + visitor.setSpansForNode(Image.class, length); } }); } diff --git a/markwon/src/main/java/ru/noties/markwon/MarkwonVisitor.java b/markwon/src/main/java/ru/noties/markwon/MarkwonVisitor.java index f5e67c1f..81d3138f 100644 --- a/markwon/src/main/java/ru/noties/markwon/MarkwonVisitor.java +++ b/markwon/src/main/java/ru/noties/markwon/MarkwonVisitor.java @@ -49,6 +49,10 @@ public interface MarkwonVisitor extends Visitor { // will throw, if not desired use setSpansForNodeOptional void setSpansForNode(@NonNull N node, int start); + void setSpansForNode(@NonNull Class node, int start); + // does not throw if there is no SpanFactory registered for this node void setSpansForNodeOptional(@NonNull N node, int start); + + void setSpansForNodeOptional(@NonNull Class node, int start); } diff --git a/markwon/src/main/java/ru/noties/markwon/MarkwonVisitorImpl.java b/markwon/src/main/java/ru/noties/markwon/MarkwonVisitorImpl.java index 0421f9e3..2616d843 100644 --- a/markwon/src/main/java/ru/noties/markwon/MarkwonVisitorImpl.java +++ b/markwon/src/main/java/ru/noties/markwon/MarkwonVisitorImpl.java @@ -236,6 +236,11 @@ class MarkwonVisitorImpl implements MarkwonVisitor { setSpans(start, configuration.spansFactory().require(node).getSpans(configuration, renderProps)); } + @Override + public void setSpansForNode(@NonNull Class node, int start) { + setSpans(start, configuration.spansFactory().require(node).getSpans(configuration, renderProps)); + } + @Override public void setSpansForNodeOptional(@NonNull N node, int start) { final SpanFactory factory = configuration.spansFactory().get(node); @@ -244,6 +249,14 @@ class MarkwonVisitorImpl implements MarkwonVisitor { } } + @Override + public void setSpansForNodeOptional(@NonNull Class node, int start) { + final SpanFactory factory = configuration.spansFactory().get(node); + if (factory != null) { + setSpans(start, factory.getSpans(configuration, renderProps)); + } + } + static class BuilderImpl implements Builder { private final Map, NodeVisitor> nodes = new HashMap<>(); From 4f02a793e7beb2eb8f5373bc3e855d2c8d60bd98 Mon Sep 17 00:00:00 2001 From: Dimitry Ivanov Date: Sat, 22 Dec 2018 17:16:46 +0300 Subject: [PATCH 046/103] Fix markwon main artifact tests --- .../ru/noties/markwon/gif/GifAwarePlugin.java | 8 +- .../strikethrough/StrikethroughPlugin.java | 2 +- .../ext/tasklist/TaskListSpanFactory.java | 6 +- .../java/ru/noties/markwon/SpanFactory.java | 2 +- .../ru/noties/markwon/core/CorePlugin.java | 19 ++- .../markwon/core/MarkwonSpannableFactory.java | 51 -------- .../core/MarkwonSpannableFactoryDef.java | 90 ------------- .../core/factory/BlockQuoteSpanFactory.java | 2 +- .../core/factory/CodeBlockSpanFactory.java | 2 +- .../markwon/core/factory/CodeSpanFactory.java | 2 +- .../core/factory/EmphasisSpanFactory.java | 2 +- .../core/factory/HeadingSpanFactory.java | 4 +- .../markwon/core/factory/LinkSpanFactory.java | 4 +- .../core/factory/ListItemSpanFactory.java | 8 +- .../factory/StrongEmphasisSpanFactory.java | 2 +- .../factory/ThematicBreakSpanFactory.java | 2 +- .../markwon/image/ImageSpanFactory.java | 8 +- .../markwon/AbstractMarkwonVisitorImpl.java | 3 +- .../noties/markwon/core/CorePluginBridge.java | 22 ++++ .../java/ru/noties/markwon/core/CoreTest.java | 34 +++-- .../markwon/core/suite/BaseSuiteTest.java | 122 +++++++++++++++++- .../markwon/core/suite/BlockquoteTest.java | 1 - .../markwon/core/suite/BoldItalicTest.java | 2 - .../noties/markwon/core/suite/CodeTest.java | 1 - .../markwon/core/suite/DeeplyNestedTest.java | 3 - .../markwon/core/suite/EmphasisTest.java | 1 - .../noties/markwon/core/suite/FirstTest.java | 3 - .../markwon/core/suite/HeadingTest.java | 1 - .../noties/markwon/core/suite/LinkTest.java | 1 - .../markwon/core/suite/OrderedListTest.java | 1 - .../markwon/core/suite/ParagraphTest.java | 1 - .../noties/markwon/core/suite/SecondTest.java | 5 - .../core/suite/StrongEmphasisTest.java | 1 - .../markwon/core/suite/TestFactory.java | 93 ------------- .../markwon/core/suite/ThematicBreakTest.java | 1 - .../markwon/core/suite/UnOrderedListTest.java | 1 - .../visitor/BlockQuoteNodeVisitorTest.java | 18 --- .../ru/noties/markwon/image/ImageTest.java | 19 ++- .../markwon/syntax/SyntaxHighlightTest.java | 37 +++--- .../markwon/sample/extension/IconPlugin.java | 1 - .../sample/extension/MainActivity.java | 2 + 41 files changed, 246 insertions(+), 342 deletions(-) delete mode 100644 markwon/src/main/java/ru/noties/markwon/core/MarkwonSpannableFactory.java delete mode 100644 markwon/src/main/java/ru/noties/markwon/core/MarkwonSpannableFactoryDef.java create mode 100644 markwon/src/test/java/ru/noties/markwon/core/CorePluginBridge.java delete mode 100644 markwon/src/test/java/ru/noties/markwon/core/suite/TestFactory.java delete mode 100644 markwon/src/test/java/ru/noties/markwon/core/visitor/BlockQuoteNodeVisitorTest.java diff --git a/app/src/main/java/ru/noties/markwon/gif/GifAwarePlugin.java b/app/src/main/java/ru/noties/markwon/gif/GifAwarePlugin.java index ab69ceb7..95509525 100644 --- a/app/src/main/java/ru/noties/markwon/gif/GifAwarePlugin.java +++ b/app/src/main/java/ru/noties/markwon/gif/GifAwarePlugin.java @@ -40,18 +40,18 @@ public class GifAwarePlugin extends AbstractMarkwonPlugin { builder.setFactory(Image.class, new SpanFactory() { @Override - public Object getSpans(@NonNull MarkwonConfiguration configuration, @NonNull RenderProps context) { + public Object getSpans(@NonNull MarkwonConfiguration configuration, @NonNull RenderProps props) { return new AsyncDrawableSpan( configuration.theme(), new GifAwareAsyncDrawable( gifPlaceholder, - ImageProps.DESTINATION.require(context), + ImageProps.DESTINATION.require(props), configuration.asyncDrawableLoader(), configuration.imageSizeResolver(), - ImageProps.IMAGE_SIZE.get(context) + ImageProps.IMAGE_SIZE.get(props) ), AsyncDrawableSpan.ALIGN_BOTTOM, - ImageProps.REPLACEMENT_TEXT_IS_LINK.get(context, false) + ImageProps.REPLACEMENT_TEXT_IS_LINK.get(props, false) ); } }); diff --git a/markwon-ext-strikethrough/src/main/java/ru/noties/markwon/ext/strikethrough/StrikethroughPlugin.java b/markwon-ext-strikethrough/src/main/java/ru/noties/markwon/ext/strikethrough/StrikethroughPlugin.java index eb832e7e..96b76672 100644 --- a/markwon-ext-strikethrough/src/main/java/ru/noties/markwon/ext/strikethrough/StrikethroughPlugin.java +++ b/markwon-ext-strikethrough/src/main/java/ru/noties/markwon/ext/strikethrough/StrikethroughPlugin.java @@ -32,7 +32,7 @@ public class StrikethroughPlugin extends AbstractMarkwonPlugin { public void configureSpansFactory(@NonNull MarkwonSpansFactory.Builder builder) { builder.setFactory(Strikethrough.class, new SpanFactory() { @Override - public Object getSpans(@NonNull MarkwonConfiguration configuration, @NonNull RenderProps context) { + public Object getSpans(@NonNull MarkwonConfiguration configuration, @NonNull RenderProps props) { return new StrikethroughSpan(); } }); diff --git a/markwon-ext-tasklist/src/main/java/ru/noties/markwon/ext/tasklist/TaskListSpanFactory.java b/markwon-ext-tasklist/src/main/java/ru/noties/markwon/ext/tasklist/TaskListSpanFactory.java index b0f4fb20..bd68ede3 100644 --- a/markwon-ext-tasklist/src/main/java/ru/noties/markwon/ext/tasklist/TaskListSpanFactory.java +++ b/markwon-ext-tasklist/src/main/java/ru/noties/markwon/ext/tasklist/TaskListSpanFactory.java @@ -18,12 +18,12 @@ public class TaskListSpanFactory implements SpanFactory { @Nullable @Override - public Object getSpans(@NonNull MarkwonConfiguration configuration, @NonNull RenderProps context) { + public Object getSpans(@NonNull MarkwonConfiguration configuration, @NonNull RenderProps props) { return new TaskListSpan( configuration.theme(), drawable, - TaskListProps.BLOCK_INDENT.get(context, 0), - TaskListProps.DONE.get(context, false) + TaskListProps.BLOCK_INDENT.get(props, 0), + TaskListProps.DONE.get(props, false) ); } } diff --git a/markwon/src/main/java/ru/noties/markwon/SpanFactory.java b/markwon/src/main/java/ru/noties/markwon/SpanFactory.java index 62780776..76e251e7 100644 --- a/markwon/src/main/java/ru/noties/markwon/SpanFactory.java +++ b/markwon/src/main/java/ru/noties/markwon/SpanFactory.java @@ -11,5 +11,5 @@ public interface SpanFactory { @Nullable Object getSpans( @NonNull MarkwonConfiguration configuration, - @NonNull RenderProps context); + @NonNull RenderProps props); } diff --git a/markwon/src/main/java/ru/noties/markwon/core/CorePlugin.java b/markwon/src/main/java/ru/noties/markwon/core/CorePlugin.java index 4d1ff29b..47024866 100644 --- a/markwon/src/main/java/ru/noties/markwon/core/CorePlugin.java +++ b/markwon/src/main/java/ru/noties/markwon/core/CorePlugin.java @@ -2,6 +2,7 @@ package ru.noties.markwon.core; import android.support.annotation.NonNull; import android.support.annotation.Nullable; +import android.support.annotation.VisibleForTesting; import android.widget.TextView; import org.commonmark.node.BlockQuote; @@ -142,9 +143,18 @@ public class CorePlugin extends AbstractMarkwonPlugin { builder.on(BlockQuote.class, new MarkwonVisitor.NodeVisitor
() { @Override public void visit(@NonNull MarkwonVisitor visitor, @NonNull BlockQuote blockQuote) { + + visitor.ensureNewLine(); + final int length = visitor.length(); + visitor.visitChildren(blockQuote); visitor.setSpansForNode(blockQuote, length); + + if (visitor.hasNext(blockQuote)) { + visitor.ensureNewLine(); + visitor.forceNewLine(); + } } }); } @@ -186,7 +196,8 @@ public class CorePlugin extends AbstractMarkwonPlugin { }); } - private static void visitCodeBlock( + @VisibleForTesting + static void visitCodeBlock( @NonNull MarkwonVisitor visitor, @Nullable String info, @NonNull String code, @@ -227,6 +238,11 @@ public class CorePlugin extends AbstractMarkwonPlugin { final int length = visitor.length(); + // it's important to visit children before applying render props ( + // we can have nested children, who are list items also, thus they will + // override out props (if we set them before visiting children) + visitor.visitChildren(listItem); + final Node parent = listItem.getParent(); if (parent instanceof OrderedList) { @@ -244,7 +260,6 @@ public class CorePlugin extends AbstractMarkwonPlugin { CoreProps.BULLET_LIST_ITEM_LEVEL.set(visitor.renderProps(), listLevel(listItem)); } - visitor.visitChildren(listItem); visitor.setSpansForNode(listItem, length); if (visitor.hasNext(listItem)) { diff --git a/markwon/src/main/java/ru/noties/markwon/core/MarkwonSpannableFactory.java b/markwon/src/main/java/ru/noties/markwon/core/MarkwonSpannableFactory.java deleted file mode 100644 index 1e440591..00000000 --- a/markwon/src/main/java/ru/noties/markwon/core/MarkwonSpannableFactory.java +++ /dev/null @@ -1,51 +0,0 @@ -package ru.noties.markwon.core; - -import android.support.annotation.NonNull; -import android.support.annotation.Nullable; - -import ru.noties.markwon.core.spans.LinkSpan; - -/** - * Each method can return null or a Span object or an array of spans - * - * @since 1.1.0 - */ -@Deprecated -public interface MarkwonSpannableFactory { - - @Nullable - Object strongEmphasis(); - - @Nullable - Object emphasis(); - - @Nullable - Object blockQuote(@NonNull MarkwonTheme theme); - - @Nullable - Object code(@NonNull MarkwonTheme theme, boolean multiline); - - @Nullable - Object orderedListItem(@NonNull MarkwonTheme theme, int startNumber); - - @Nullable - Object bulletListItem(@NonNull MarkwonTheme theme, int level); - - @Nullable - Object thematicBreak(@NonNull MarkwonTheme theme); - - @Nullable - Object heading(@NonNull MarkwonTheme theme, int level); - - /** - * @since 1.1.1 - */ - @Nullable - Object paragraph(boolean inTightList); - - @Nullable - Object link( - @NonNull MarkwonTheme theme, - @NonNull String destination, - @NonNull LinkSpan.Resolver resolver); -} diff --git a/markwon/src/main/java/ru/noties/markwon/core/MarkwonSpannableFactoryDef.java b/markwon/src/main/java/ru/noties/markwon/core/MarkwonSpannableFactoryDef.java deleted file mode 100644 index 5765c7b4..00000000 --- a/markwon/src/main/java/ru/noties/markwon/core/MarkwonSpannableFactoryDef.java +++ /dev/null @@ -1,90 +0,0 @@ -package ru.noties.markwon.core; - -import android.support.annotation.NonNull; -import android.support.annotation.Nullable; - -import ru.noties.markwon.core.spans.BlockQuoteSpan; -import ru.noties.markwon.core.spans.BulletListItemSpan; -import ru.noties.markwon.core.spans.CodeSpan; -import ru.noties.markwon.core.spans.EmphasisSpan; -import ru.noties.markwon.core.spans.HeadingSpan; -import ru.noties.markwon.core.spans.LinkSpan; -import ru.noties.markwon.core.spans.OrderedListItemSpan; -import ru.noties.markwon.core.spans.StrongEmphasisSpan; -import ru.noties.markwon.core.spans.ThematicBreakSpan; - -/** - * @since 1.1.0 - */ -@Deprecated -public class MarkwonSpannableFactoryDef implements MarkwonSpannableFactory { - - @NonNull - public static MarkwonSpannableFactoryDef create() { - return new MarkwonSpannableFactoryDef(); - } - - @Nullable - @Override - public Object strongEmphasis() { - return new StrongEmphasisSpan(); - } - - @Nullable - @Override - public Object emphasis() { - return new EmphasisSpan(); - } - - @Nullable - @Override - public Object blockQuote(@NonNull MarkwonTheme theme) { - return new BlockQuoteSpan(theme); - } - - @Nullable - @Override - public Object code(@NonNull MarkwonTheme theme, boolean multiline) { - return new CodeSpan(theme, multiline); - } - - @Nullable - @Override - public Object orderedListItem(@NonNull MarkwonTheme theme, int startNumber) { - // todo| in order to provide real RTL experience there must be a way to provide this string - return new OrderedListItemSpan(theme, String.valueOf(startNumber) + "." + '\u00a0'); - } - - @Nullable - @Override - public Object bulletListItem(@NonNull MarkwonTheme theme, int level) { - return new BulletListItemSpan(theme, level); - } - - @Nullable - @Override - public Object thematicBreak(@NonNull MarkwonTheme theme) { - return new ThematicBreakSpan(theme); - } - - @Nullable - @Override - public Object heading(@NonNull MarkwonTheme theme, int level) { - return new HeadingSpan(theme, level); - } - - /** - * @since 1.1.1 - */ - @Nullable - @Override - public Object paragraph(boolean inTightList) { - return null; - } - - @Nullable - @Override - public Object link(@NonNull MarkwonTheme theme, @NonNull String destination, @NonNull LinkSpan.Resolver resolver) { - return new LinkSpan(theme, destination, resolver); - } -} diff --git a/markwon/src/main/java/ru/noties/markwon/core/factory/BlockQuoteSpanFactory.java b/markwon/src/main/java/ru/noties/markwon/core/factory/BlockQuoteSpanFactory.java index 437c0028..cbe86db6 100644 --- a/markwon/src/main/java/ru/noties/markwon/core/factory/BlockQuoteSpanFactory.java +++ b/markwon/src/main/java/ru/noties/markwon/core/factory/BlockQuoteSpanFactory.java @@ -11,7 +11,7 @@ import ru.noties.markwon.core.spans.BlockQuoteSpan; public class BlockQuoteSpanFactory implements SpanFactory { @Nullable @Override - public Object getSpans(@NonNull MarkwonConfiguration configuration, @NonNull RenderProps context) { + public Object getSpans(@NonNull MarkwonConfiguration configuration, @NonNull RenderProps props) { return new BlockQuoteSpan(configuration.theme()); } } diff --git a/markwon/src/main/java/ru/noties/markwon/core/factory/CodeBlockSpanFactory.java b/markwon/src/main/java/ru/noties/markwon/core/factory/CodeBlockSpanFactory.java index 472bf620..4fd07685 100644 --- a/markwon/src/main/java/ru/noties/markwon/core/factory/CodeBlockSpanFactory.java +++ b/markwon/src/main/java/ru/noties/markwon/core/factory/CodeBlockSpanFactory.java @@ -11,7 +11,7 @@ import ru.noties.markwon.core.spans.CodeSpan; public class CodeBlockSpanFactory implements SpanFactory { @Nullable @Override - public Object getSpans(@NonNull MarkwonConfiguration configuration, @NonNull RenderProps context) { + public Object getSpans(@NonNull MarkwonConfiguration configuration, @NonNull RenderProps props) { return new CodeSpan(configuration.theme(), true); } } diff --git a/markwon/src/main/java/ru/noties/markwon/core/factory/CodeSpanFactory.java b/markwon/src/main/java/ru/noties/markwon/core/factory/CodeSpanFactory.java index 6850ff44..7726bc02 100644 --- a/markwon/src/main/java/ru/noties/markwon/core/factory/CodeSpanFactory.java +++ b/markwon/src/main/java/ru/noties/markwon/core/factory/CodeSpanFactory.java @@ -11,7 +11,7 @@ import ru.noties.markwon.core.spans.CodeSpan; public class CodeSpanFactory implements SpanFactory { @Nullable @Override - public Object getSpans(@NonNull MarkwonConfiguration configuration, @NonNull RenderProps context) { + public Object getSpans(@NonNull MarkwonConfiguration configuration, @NonNull RenderProps props) { return new CodeSpan(configuration.theme(), false); } } diff --git a/markwon/src/main/java/ru/noties/markwon/core/factory/EmphasisSpanFactory.java b/markwon/src/main/java/ru/noties/markwon/core/factory/EmphasisSpanFactory.java index eb49a906..d9d8331d 100644 --- a/markwon/src/main/java/ru/noties/markwon/core/factory/EmphasisSpanFactory.java +++ b/markwon/src/main/java/ru/noties/markwon/core/factory/EmphasisSpanFactory.java @@ -11,7 +11,7 @@ import ru.noties.markwon.core.spans.EmphasisSpan; public class EmphasisSpanFactory implements SpanFactory { @Nullable @Override - public Object getSpans(@NonNull MarkwonConfiguration configuration, @NonNull RenderProps context) { + public Object getSpans(@NonNull MarkwonConfiguration configuration, @NonNull RenderProps props) { return new EmphasisSpan(); } } diff --git a/markwon/src/main/java/ru/noties/markwon/core/factory/HeadingSpanFactory.java b/markwon/src/main/java/ru/noties/markwon/core/factory/HeadingSpanFactory.java index e0b1b517..bd675edd 100644 --- a/markwon/src/main/java/ru/noties/markwon/core/factory/HeadingSpanFactory.java +++ b/markwon/src/main/java/ru/noties/markwon/core/factory/HeadingSpanFactory.java @@ -12,10 +12,10 @@ import ru.noties.markwon.core.spans.HeadingSpan; public class HeadingSpanFactory implements SpanFactory { @Nullable @Override - public Object getSpans(@NonNull MarkwonConfiguration configuration, @NonNull RenderProps context) { + public Object getSpans(@NonNull MarkwonConfiguration configuration, @NonNull RenderProps props) { return new HeadingSpan( configuration.theme(), - CoreProps.HEADING_LEVEL.require(context) + CoreProps.HEADING_LEVEL.require(props) ); } } diff --git a/markwon/src/main/java/ru/noties/markwon/core/factory/LinkSpanFactory.java b/markwon/src/main/java/ru/noties/markwon/core/factory/LinkSpanFactory.java index e0a39dd2..ee97ba34 100644 --- a/markwon/src/main/java/ru/noties/markwon/core/factory/LinkSpanFactory.java +++ b/markwon/src/main/java/ru/noties/markwon/core/factory/LinkSpanFactory.java @@ -12,10 +12,10 @@ import ru.noties.markwon.core.spans.LinkSpan; public class LinkSpanFactory implements SpanFactory { @Nullable @Override - public Object getSpans(@NonNull MarkwonConfiguration configuration, @NonNull RenderProps context) { + public Object getSpans(@NonNull MarkwonConfiguration configuration, @NonNull RenderProps props) { return new LinkSpan( configuration.theme(), - CoreProps.LINK_DESTINATION.require(context), + CoreProps.LINK_DESTINATION.require(props), configuration.linkResolver() ); } diff --git a/markwon/src/main/java/ru/noties/markwon/core/factory/ListItemSpanFactory.java b/markwon/src/main/java/ru/noties/markwon/core/factory/ListItemSpanFactory.java index 87bd4e4e..06d73d2e 100644 --- a/markwon/src/main/java/ru/noties/markwon/core/factory/ListItemSpanFactory.java +++ b/markwon/src/main/java/ru/noties/markwon/core/factory/ListItemSpanFactory.java @@ -14,22 +14,22 @@ public class ListItemSpanFactory implements SpanFactory { @Nullable @Override - public Object getSpans(@NonNull MarkwonConfiguration configuration, @NonNull RenderProps context) { + public Object getSpans(@NonNull MarkwonConfiguration configuration, @NonNull RenderProps props) { // type of list item // bullet : level // ordered: number final Object spans; - if (CoreProps.ListItemType.BULLET == CoreProps.LIST_ITEM_TYPE.require(context)) { + if (CoreProps.ListItemType.BULLET == CoreProps.LIST_ITEM_TYPE.require(props)) { spans = new BulletListItemSpan( configuration.theme(), - CoreProps.BULLET_LIST_ITEM_LEVEL.require(context) + CoreProps.BULLET_LIST_ITEM_LEVEL.require(props) ); } else { // todo| in order to provide real RTL experience there must be a way to provide this string - final String number = String.valueOf(CoreProps.ORDERED_LIST_ITEM_NUMBER.require(context)) + final String number = String.valueOf(CoreProps.ORDERED_LIST_ITEM_NUMBER.require(props)) + "." + '\u00a0'; spans = new OrderedListItemSpan( diff --git a/markwon/src/main/java/ru/noties/markwon/core/factory/StrongEmphasisSpanFactory.java b/markwon/src/main/java/ru/noties/markwon/core/factory/StrongEmphasisSpanFactory.java index e1905e05..c81d6121 100644 --- a/markwon/src/main/java/ru/noties/markwon/core/factory/StrongEmphasisSpanFactory.java +++ b/markwon/src/main/java/ru/noties/markwon/core/factory/StrongEmphasisSpanFactory.java @@ -11,7 +11,7 @@ import ru.noties.markwon.core.spans.StrongEmphasisSpan; public class StrongEmphasisSpanFactory implements SpanFactory { @Nullable @Override - public Object getSpans(@NonNull MarkwonConfiguration configuration, @NonNull RenderProps context) { + public Object getSpans(@NonNull MarkwonConfiguration configuration, @NonNull RenderProps props) { return new StrongEmphasisSpan(); } } diff --git a/markwon/src/main/java/ru/noties/markwon/core/factory/ThematicBreakSpanFactory.java b/markwon/src/main/java/ru/noties/markwon/core/factory/ThematicBreakSpanFactory.java index 82c1dea8..ecd20f32 100644 --- a/markwon/src/main/java/ru/noties/markwon/core/factory/ThematicBreakSpanFactory.java +++ b/markwon/src/main/java/ru/noties/markwon/core/factory/ThematicBreakSpanFactory.java @@ -11,7 +11,7 @@ import ru.noties.markwon.core.spans.ThematicBreakSpan; public class ThematicBreakSpanFactory implements SpanFactory { @Nullable @Override - public Object getSpans(@NonNull MarkwonConfiguration configuration, @NonNull RenderProps context) { + public Object getSpans(@NonNull MarkwonConfiguration configuration, @NonNull RenderProps props) { return new ThematicBreakSpan(configuration.theme()); } } diff --git a/markwon/src/main/java/ru/noties/markwon/image/ImageSpanFactory.java b/markwon/src/main/java/ru/noties/markwon/image/ImageSpanFactory.java index ba73d047..5af5b0ec 100644 --- a/markwon/src/main/java/ru/noties/markwon/image/ImageSpanFactory.java +++ b/markwon/src/main/java/ru/noties/markwon/image/ImageSpanFactory.java @@ -10,17 +10,17 @@ import ru.noties.markwon.SpanFactory; public class ImageSpanFactory implements SpanFactory { @Nullable @Override - public Object getSpans(@NonNull MarkwonConfiguration configuration, @NonNull RenderProps context) { + public Object getSpans(@NonNull MarkwonConfiguration configuration, @NonNull RenderProps props) { return new AsyncDrawableSpan( configuration.theme(), new AsyncDrawable( - ImageProps.DESTINATION.require(context), + ImageProps.DESTINATION.require(props), configuration.asyncDrawableLoader(), configuration.imageSizeResolver(), - ImageProps.IMAGE_SIZE.get(context) + ImageProps.IMAGE_SIZE.get(props) ), AsyncDrawableSpan.ALIGN_BOTTOM, - ImageProps.REPLACEMENT_TEXT_IS_LINK.get(context, false) + ImageProps.REPLACEMENT_TEXT_IS_LINK.get(props, false) ); } } diff --git a/markwon/src/test/java/ru/noties/markwon/AbstractMarkwonVisitorImpl.java b/markwon/src/test/java/ru/noties/markwon/AbstractMarkwonVisitorImpl.java index 99d183ce..f3ad18b3 100644 --- a/markwon/src/test/java/ru/noties/markwon/AbstractMarkwonVisitorImpl.java +++ b/markwon/src/test/java/ru/noties/markwon/AbstractMarkwonVisitorImpl.java @@ -10,7 +10,8 @@ public class AbstractMarkwonVisitorImpl extends MarkwonVisitorImpl { public AbstractMarkwonVisitorImpl( @NonNull MarkwonConfiguration configuration, + @NonNull RenderProps renderProps, @NonNull Map, NodeVisitor> nodes) { - super(configuration, nodes); + super(configuration, renderProps, nodes); } } diff --git a/markwon/src/test/java/ru/noties/markwon/core/CorePluginBridge.java b/markwon/src/test/java/ru/noties/markwon/core/CorePluginBridge.java new file mode 100644 index 00000000..6517873b --- /dev/null +++ b/markwon/src/test/java/ru/noties/markwon/core/CorePluginBridge.java @@ -0,0 +1,22 @@ +package ru.noties.markwon.core; + +import android.support.annotation.NonNull; +import android.support.annotation.Nullable; + +import org.commonmark.node.Node; + +import ru.noties.markwon.MarkwonVisitor; + +public abstract class CorePluginBridge { + + public static void visitCodeBlock( + @NonNull MarkwonVisitor visitor, + @Nullable String info, + @NonNull String code, + @NonNull Node node) { + CorePlugin.visitCodeBlock(visitor, info, code, node); + } + + private CorePluginBridge() { + } +} diff --git a/markwon/src/test/java/ru/noties/markwon/core/CoreTest.java b/markwon/src/test/java/ru/noties/markwon/core/CoreTest.java index 17ee51db..de7c0dd0 100644 --- a/markwon/src/test/java/ru/noties/markwon/core/CoreTest.java +++ b/markwon/src/test/java/ru/noties/markwon/core/CoreTest.java @@ -3,6 +3,8 @@ package ru.noties.markwon.core; import android.support.annotation.NonNull; import android.text.Spanned; +import org.commonmark.node.Emphasis; +import org.commonmark.node.StrongEmphasis; import org.junit.Test; import org.junit.runner.RunWith; import org.robolectric.RobolectricTestRunner; @@ -12,6 +14,9 @@ import org.robolectric.annotation.Config; import ru.noties.markwon.AbstractMarkwonPlugin; import ru.noties.markwon.Markwon; import ru.noties.markwon.MarkwonConfiguration; +import ru.noties.markwon.MarkwonSpansFactory; +import ru.noties.markwon.RenderProps; +import ru.noties.markwon.SpanFactory; import ru.noties.markwon.test.TestSpan; import ru.noties.markwon.test.TestSpanMatcher; @@ -31,23 +36,24 @@ public class CoreTest { span("bold", span("italic", text("bold italic")))); - final Spanned spanned = (Spanned) Markwon.builder(RuntimeEnvironment.application) + final Spanned spanned = Markwon.builder(RuntimeEnvironment.application) .usePlugin(CorePlugin.create()) .usePlugin(new AbstractMarkwonPlugin() { @Override - public void configureConfiguration(@NonNull MarkwonConfiguration.Builder builder) { - builder.factory(new MarkwonSpannableFactoryDef() { - - @Override - public Object strongEmphasis() { - return span("bold"); - } - - @Override - public Object emphasis() { - return span("italic"); - } - }); + public void configureSpansFactory(@NonNull MarkwonSpansFactory.Builder builder) { + builder + .setFactory(StrongEmphasis.class, new SpanFactory() { + @Override + public Object getSpans(@NonNull MarkwonConfiguration configuration, @NonNull RenderProps props) { + return span("bold"); + } + }) + .setFactory(Emphasis.class, new SpanFactory() { + @Override + public Object getSpans(@NonNull MarkwonConfiguration configuration, @NonNull RenderProps props) { + return span("italic"); + } + }); } }) .build() diff --git a/markwon/src/test/java/ru/noties/markwon/core/suite/BaseSuiteTest.java b/markwon/src/test/java/ru/noties/markwon/core/suite/BaseSuiteTest.java index 65555352..f91f2f13 100644 --- a/markwon/src/test/java/ru/noties/markwon/core/suite/BaseSuiteTest.java +++ b/markwon/src/test/java/ru/noties/markwon/core/suite/BaseSuiteTest.java @@ -1,20 +1,55 @@ package ru.noties.markwon.core.suite; import android.support.annotation.NonNull; +import android.support.annotation.Nullable; import android.text.Spanned; +import org.commonmark.node.BlockQuote; +import org.commonmark.node.Code; +import org.commonmark.node.Emphasis; +import org.commonmark.node.FencedCodeBlock; +import org.commonmark.node.Heading; +import org.commonmark.node.IndentedCodeBlock; +import org.commonmark.node.Link; +import org.commonmark.node.ListItem; +import org.commonmark.node.Node; +import org.commonmark.node.Paragraph; +import org.commonmark.node.StrongEmphasis; +import org.commonmark.node.ThematicBreak; import org.robolectric.RuntimeEnvironment; +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; + import ru.noties.markwon.AbstractMarkwonPlugin; import ru.noties.markwon.Markwon; import ru.noties.markwon.MarkwonConfiguration; +import ru.noties.markwon.MarkwonSpansFactory; +import ru.noties.markwon.RenderProps; +import ru.noties.markwon.SpanFactory; import ru.noties.markwon.core.CorePlugin; +import ru.noties.markwon.core.CoreProps; import ru.noties.markwon.test.TestSpan; import ru.noties.markwon.test.TestSpanMatcher; import ru.noties.markwon.test.TestUtil; +import static ru.noties.markwon.test.TestSpan.args; +import static ru.noties.markwon.test.TestSpan.span; + abstract class BaseSuiteTest { + static final String BOLD = "bold"; + static final String ITALIC = "italic"; + static final String CODE = "code"; + static final String LINK = "link"; + static final String BLOCK_QUOTE = "blockquote"; + static final String PARAGRAPH = "paragraph"; + static final String ORDERED_LIST = "ordered-list"; + static final String UN_ORDERED_LIST = "un-ordered-list"; + static final String HEADING = "heading"; + static final String THEMATIC_BREAK = "thematic-break"; + void match(@NonNull String markdown, @NonNull TestSpan.Document document) { final Spanned spanned = markwon().toMarkdown(markdown); TestSpanMatcher.matches(spanned, document); @@ -36,8 +71,20 @@ abstract class BaseSuiteTest { .usePlugin(CorePlugin.create(softBreakAddsNewLine())) .usePlugin(new AbstractMarkwonPlugin() { @Override - public void configureConfiguration(@NonNull MarkwonConfiguration.Builder builder) { - builder.factory(new TestFactory(useParagraphs())); + public void configureSpansFactory(@NonNull MarkwonSpansFactory.Builder builder) { + + for (Map.Entry, SpanFactory> entry : CORE_NODES.entrySet()) { + builder.setFactory(entry.getKey(), entry.getValue()); + } + + if (useParagraphs()) { + builder.setFactory(Paragraph.class, new SpanFactory() { + @Override + public Object getSpans(@NonNull MarkwonConfiguration configuration, @NonNull RenderProps props) { + return span(PARAGRAPH); + } + }); + } } }) .build(); @@ -50,4 +97,75 @@ abstract class BaseSuiteTest { boolean softBreakAddsNewLine() { return false; } + + private static final Map, SpanFactory> CORE_NODES; + + static { + final Map, SpanFactory> factories = new HashMap<>(); + factories.put(BlockQuote.class, new NamedSpanFactory(BLOCK_QUOTE)); + factories.put(Code.class, new SpanFactory() { + @Override + public Object getSpans(@NonNull MarkwonConfiguration configuration, @NonNull RenderProps props) { + return span(CODE, args("multiline", false)); + } + }); + factories.put(FencedCodeBlock.class, new SpanFactory() { + @Override + public Object getSpans(@NonNull MarkwonConfiguration configuration, @NonNull RenderProps props) { + return span(CODE, args("multiline", true)); + } + }); + factories.put(IndentedCodeBlock.class, new SpanFactory() { + @Override + public Object getSpans(@NonNull MarkwonConfiguration configuration, @NonNull RenderProps props) { + return span(CODE, args("multiline", true)); + } + }); + factories.put(Emphasis.class, new NamedSpanFactory(ITALIC)); + factories.put(Heading.class, new SpanFactory() { + @Override + public Object getSpans(@NonNull MarkwonConfiguration configuration, @NonNull RenderProps props) { + return span(HEADING, args("level", CoreProps.HEADING_LEVEL.require(props))); + } + }); + factories.put(Link.class, new SpanFactory() { + @Override + public Object getSpans(@NonNull MarkwonConfiguration configuration, @NonNull RenderProps props) { + return span(LINK, args("href", CoreProps.LINK_DESTINATION.require(props))); + } + }); + factories.put(ListItem.class, new SpanFactory() { + @Override + public Object getSpans(@NonNull MarkwonConfiguration configuration, @NonNull RenderProps props) { + final CoreProps.ListItemType type = CoreProps.LIST_ITEM_TYPE.require(props); + if (CoreProps.ListItemType.BULLET == type) { + return span(UN_ORDERED_LIST, args("level", CoreProps.BULLET_LIST_ITEM_LEVEL.require(props))); + } + return span(ORDERED_LIST, args("start", CoreProps.ORDERED_LIST_ITEM_NUMBER.require(props))); + } + }); + factories.put(StrongEmphasis.class, new NamedSpanFactory(BOLD)); + factories.put(ThematicBreak.class, new NamedSpanFactory(THEMATIC_BREAK)); + CORE_NODES = factories; + } + + private static class NamedSpanFactory implements SpanFactory { + + private final String name; + + private NamedSpanFactory(@NonNull String name) { + this.name = name; + } + + @Nullable + @Override + public Object getSpans(@NonNull MarkwonConfiguration configuration, @NonNull RenderProps props) { + return span(name, extractArgs(props)); + } + + @NonNull + Map extractArgs(@NonNull RenderProps props) { + return Collections.emptyMap(); + } + } } diff --git a/markwon/src/test/java/ru/noties/markwon/core/suite/BlockquoteTest.java b/markwon/src/test/java/ru/noties/markwon/core/suite/BlockquoteTest.java index 02eb2674..21d2c852 100644 --- a/markwon/src/test/java/ru/noties/markwon/core/suite/BlockquoteTest.java +++ b/markwon/src/test/java/ru/noties/markwon/core/suite/BlockquoteTest.java @@ -7,7 +7,6 @@ import org.robolectric.annotation.Config; import ru.noties.markwon.test.TestSpan.Document; -import static ru.noties.markwon.core.suite.TestFactory.BLOCK_QUOTE; import static ru.noties.markwon.test.TestSpan.document; import static ru.noties.markwon.test.TestSpan.span; import static ru.noties.markwon.test.TestSpan.text; diff --git a/markwon/src/test/java/ru/noties/markwon/core/suite/BoldItalicTest.java b/markwon/src/test/java/ru/noties/markwon/core/suite/BoldItalicTest.java index 67cb1a0d..1315136b 100644 --- a/markwon/src/test/java/ru/noties/markwon/core/suite/BoldItalicTest.java +++ b/markwon/src/test/java/ru/noties/markwon/core/suite/BoldItalicTest.java @@ -7,8 +7,6 @@ import org.robolectric.annotation.Config; import ru.noties.markwon.test.TestSpan; -import static ru.noties.markwon.core.suite.TestFactory.BOLD; -import static ru.noties.markwon.core.suite.TestFactory.ITALIC; import static ru.noties.markwon.test.TestSpan.document; import static ru.noties.markwon.test.TestSpan.span; import static ru.noties.markwon.test.TestSpan.text; diff --git a/markwon/src/test/java/ru/noties/markwon/core/suite/CodeTest.java b/markwon/src/test/java/ru/noties/markwon/core/suite/CodeTest.java index 6e5d0c53..52a8f614 100644 --- a/markwon/src/test/java/ru/noties/markwon/core/suite/CodeTest.java +++ b/markwon/src/test/java/ru/noties/markwon/core/suite/CodeTest.java @@ -7,7 +7,6 @@ import org.robolectric.annotation.Config; import ru.noties.markwon.test.TestSpan.Document; -import static ru.noties.markwon.core.suite.TestFactory.CODE; import static ru.noties.markwon.test.TestSpan.args; import static ru.noties.markwon.test.TestSpan.document; import static ru.noties.markwon.test.TestSpan.span; diff --git a/markwon/src/test/java/ru/noties/markwon/core/suite/DeeplyNestedTest.java b/markwon/src/test/java/ru/noties/markwon/core/suite/DeeplyNestedTest.java index 49606f90..e0dcda38 100644 --- a/markwon/src/test/java/ru/noties/markwon/core/suite/DeeplyNestedTest.java +++ b/markwon/src/test/java/ru/noties/markwon/core/suite/DeeplyNestedTest.java @@ -7,9 +7,6 @@ import org.robolectric.annotation.Config; import ru.noties.markwon.test.TestSpan.Document; -import static ru.noties.markwon.core.suite.TestFactory.BOLD; -import static ru.noties.markwon.core.suite.TestFactory.CODE; -import static ru.noties.markwon.core.suite.TestFactory.ITALIC; import static ru.noties.markwon.test.TestSpan.args; import static ru.noties.markwon.test.TestSpan.document; import static ru.noties.markwon.test.TestSpan.span; diff --git a/markwon/src/test/java/ru/noties/markwon/core/suite/EmphasisTest.java b/markwon/src/test/java/ru/noties/markwon/core/suite/EmphasisTest.java index 0e1b1d1c..057acc18 100644 --- a/markwon/src/test/java/ru/noties/markwon/core/suite/EmphasisTest.java +++ b/markwon/src/test/java/ru/noties/markwon/core/suite/EmphasisTest.java @@ -7,7 +7,6 @@ import org.robolectric.annotation.Config; import ru.noties.markwon.test.TestSpan.Document; -import static ru.noties.markwon.core.suite.TestFactory.ITALIC; import static ru.noties.markwon.test.TestSpan.document; import static ru.noties.markwon.test.TestSpan.span; import static ru.noties.markwon.test.TestSpan.text; diff --git a/markwon/src/test/java/ru/noties/markwon/core/suite/FirstTest.java b/markwon/src/test/java/ru/noties/markwon/core/suite/FirstTest.java index 340b5fe9..f67a5062 100644 --- a/markwon/src/test/java/ru/noties/markwon/core/suite/FirstTest.java +++ b/markwon/src/test/java/ru/noties/markwon/core/suite/FirstTest.java @@ -7,9 +7,6 @@ import org.robolectric.annotation.Config; import ru.noties.markwon.test.TestSpan.Document; -import static ru.noties.markwon.core.suite.TestFactory.BOLD; -import static ru.noties.markwon.core.suite.TestFactory.ITALIC; -import static ru.noties.markwon.core.suite.TestFactory.LINK; import static ru.noties.markwon.test.TestSpan.args; import static ru.noties.markwon.test.TestSpan.document; import static ru.noties.markwon.test.TestSpan.span; diff --git a/markwon/src/test/java/ru/noties/markwon/core/suite/HeadingTest.java b/markwon/src/test/java/ru/noties/markwon/core/suite/HeadingTest.java index efc1a090..10fa5f9b 100644 --- a/markwon/src/test/java/ru/noties/markwon/core/suite/HeadingTest.java +++ b/markwon/src/test/java/ru/noties/markwon/core/suite/HeadingTest.java @@ -7,7 +7,6 @@ import org.robolectric.annotation.Config; import ru.noties.markwon.test.TestSpan.Document; -import static ru.noties.markwon.core.suite.TestFactory.HEADING; import static ru.noties.markwon.test.TestSpan.args; import static ru.noties.markwon.test.TestSpan.document; import static ru.noties.markwon.test.TestSpan.span; diff --git a/markwon/src/test/java/ru/noties/markwon/core/suite/LinkTest.java b/markwon/src/test/java/ru/noties/markwon/core/suite/LinkTest.java index b691db9c..ff70f82c 100644 --- a/markwon/src/test/java/ru/noties/markwon/core/suite/LinkTest.java +++ b/markwon/src/test/java/ru/noties/markwon/core/suite/LinkTest.java @@ -7,7 +7,6 @@ import org.robolectric.annotation.Config; import ru.noties.markwon.test.TestSpan.Document; -import static ru.noties.markwon.core.suite.TestFactory.LINK; import static ru.noties.markwon.test.TestSpan.args; import static ru.noties.markwon.test.TestSpan.document; import static ru.noties.markwon.test.TestSpan.span; diff --git a/markwon/src/test/java/ru/noties/markwon/core/suite/OrderedListTest.java b/markwon/src/test/java/ru/noties/markwon/core/suite/OrderedListTest.java index 31fed7d8..9efb46b8 100644 --- a/markwon/src/test/java/ru/noties/markwon/core/suite/OrderedListTest.java +++ b/markwon/src/test/java/ru/noties/markwon/core/suite/OrderedListTest.java @@ -7,7 +7,6 @@ import org.robolectric.annotation.Config; import ru.noties.markwon.test.TestSpan.Document; -import static ru.noties.markwon.core.suite.TestFactory.ORDERED_LIST; import static ru.noties.markwon.test.TestSpan.args; import static ru.noties.markwon.test.TestSpan.document; import static ru.noties.markwon.test.TestSpan.span; diff --git a/markwon/src/test/java/ru/noties/markwon/core/suite/ParagraphTest.java b/markwon/src/test/java/ru/noties/markwon/core/suite/ParagraphTest.java index 79b678c5..877a8818 100644 --- a/markwon/src/test/java/ru/noties/markwon/core/suite/ParagraphTest.java +++ b/markwon/src/test/java/ru/noties/markwon/core/suite/ParagraphTest.java @@ -7,7 +7,6 @@ import org.robolectric.annotation.Config; import ru.noties.markwon.test.TestSpan.Document; -import static ru.noties.markwon.core.suite.TestFactory.PARAGRAPH; import static ru.noties.markwon.test.TestSpan.document; import static ru.noties.markwon.test.TestSpan.span; import static ru.noties.markwon.test.TestSpan.text; diff --git a/markwon/src/test/java/ru/noties/markwon/core/suite/SecondTest.java b/markwon/src/test/java/ru/noties/markwon/core/suite/SecondTest.java index 65f238ed..340df08d 100644 --- a/markwon/src/test/java/ru/noties/markwon/core/suite/SecondTest.java +++ b/markwon/src/test/java/ru/noties/markwon/core/suite/SecondTest.java @@ -7,11 +7,6 @@ import org.robolectric.annotation.Config; import ru.noties.markwon.test.TestSpan.Document; -import static ru.noties.markwon.core.suite.TestFactory.BLOCK_QUOTE; -import static ru.noties.markwon.core.suite.TestFactory.BOLD; -import static ru.noties.markwon.core.suite.TestFactory.CODE; -import static ru.noties.markwon.core.suite.TestFactory.HEADING; -import static ru.noties.markwon.core.suite.TestFactory.ITALIC; import static ru.noties.markwon.test.TestSpan.args; import static ru.noties.markwon.test.TestSpan.document; import static ru.noties.markwon.test.TestSpan.span; diff --git a/markwon/src/test/java/ru/noties/markwon/core/suite/StrongEmphasisTest.java b/markwon/src/test/java/ru/noties/markwon/core/suite/StrongEmphasisTest.java index 12d9358b..d08377a1 100644 --- a/markwon/src/test/java/ru/noties/markwon/core/suite/StrongEmphasisTest.java +++ b/markwon/src/test/java/ru/noties/markwon/core/suite/StrongEmphasisTest.java @@ -7,7 +7,6 @@ import org.robolectric.annotation.Config; import ru.noties.markwon.test.TestSpan.Document; -import static ru.noties.markwon.core.suite.TestFactory.BOLD; import static ru.noties.markwon.test.TestSpan.document; import static ru.noties.markwon.test.TestSpan.span; import static ru.noties.markwon.test.TestSpan.text; diff --git a/markwon/src/test/java/ru/noties/markwon/core/suite/TestFactory.java b/markwon/src/test/java/ru/noties/markwon/core/suite/TestFactory.java deleted file mode 100644 index 693c5cb5..00000000 --- a/markwon/src/test/java/ru/noties/markwon/core/suite/TestFactory.java +++ /dev/null @@ -1,93 +0,0 @@ -package ru.noties.markwon.core.suite; - -import android.support.annotation.NonNull; -import android.support.annotation.Nullable; - -import ru.noties.markwon.core.MarkwonSpannableFactory; -import ru.noties.markwon.core.MarkwonTheme; -import ru.noties.markwon.core.spans.LinkSpan; - -import static ru.noties.markwon.test.TestSpan.args; -import static ru.noties.markwon.test.TestSpan.span; - -class TestFactory implements MarkwonSpannableFactory { - - static final String BOLD = "bold"; - static final String ITALIC = "italic"; - static final String CODE = "code"; - static final String LINK = "link"; - static final String BLOCK_QUOTE = "blockquote"; - static final String PARAGRAPH = "paragraph"; - static final String ORDERED_LIST = "ordered-list"; - static final String UN_ORDERED_LIST = "un-ordered-list"; - static final String HEADING = "heading"; - static final String THEMATIC_BREAK = "thematic-break"; - - private final boolean useParagraphs; - - TestFactory(boolean useParagraphs) { - this.useParagraphs = useParagraphs; - } - - @Nullable - @Override - public Object strongEmphasis() { - return span(BOLD); - } - - @Nullable - @Override - public Object emphasis() { - return span(ITALIC); - } - - @Nullable - @Override - public Object blockQuote(@NonNull MarkwonTheme theme) { - return span(BLOCK_QUOTE); - } - - @Nullable - @Override - public Object code(@NonNull MarkwonTheme theme, boolean multiline) { - return span(CODE, args("multiline", multiline)); - } - - @Nullable - @Override - public Object orderedListItem(@NonNull MarkwonTheme theme, int startNumber) { - return span(ORDERED_LIST, args("start", startNumber)); - } - - @Nullable - @Override - public Object bulletListItem(@NonNull MarkwonTheme theme, int level) { - return span(UN_ORDERED_LIST, args("level", level)); - } - - @Nullable - @Override - public Object thematicBreak(@NonNull MarkwonTheme theme) { - return span(THEMATIC_BREAK); - } - - @Nullable - @Override - public Object heading(@NonNull MarkwonTheme theme, int level) { - return span(HEADING, args("level", level)); - } - - @Nullable - @Override - public Object paragraph(boolean inTightList) { - return useParagraphs - ? span(PARAGRAPH) - : null; - } - - @Nullable - @Override - public Object link(@NonNull MarkwonTheme theme, @NonNull String destination, @NonNull LinkSpan.Resolver resolver) { - return span(LINK, args("href", destination)); - } -} diff --git a/markwon/src/test/java/ru/noties/markwon/core/suite/ThematicBreakTest.java b/markwon/src/test/java/ru/noties/markwon/core/suite/ThematicBreakTest.java index a5005a98..1e0159bc 100644 --- a/markwon/src/test/java/ru/noties/markwon/core/suite/ThematicBreakTest.java +++ b/markwon/src/test/java/ru/noties/markwon/core/suite/ThematicBreakTest.java @@ -7,7 +7,6 @@ import org.robolectric.annotation.Config; import ru.noties.markwon.test.TestSpan.Document; -import static ru.noties.markwon.core.suite.TestFactory.THEMATIC_BREAK; import static ru.noties.markwon.test.TestSpan.document; import static ru.noties.markwon.test.TestSpan.span; import static ru.noties.markwon.test.TestSpan.text; diff --git a/markwon/src/test/java/ru/noties/markwon/core/suite/UnOrderedListTest.java b/markwon/src/test/java/ru/noties/markwon/core/suite/UnOrderedListTest.java index b5aa346e..9b1dfb85 100644 --- a/markwon/src/test/java/ru/noties/markwon/core/suite/UnOrderedListTest.java +++ b/markwon/src/test/java/ru/noties/markwon/core/suite/UnOrderedListTest.java @@ -7,7 +7,6 @@ import org.robolectric.annotation.Config; import ru.noties.markwon.test.TestSpan.Document; -import static ru.noties.markwon.core.suite.TestFactory.UN_ORDERED_LIST; import static ru.noties.markwon.test.TestSpan.args; import static ru.noties.markwon.test.TestSpan.document; import static ru.noties.markwon.test.TestSpan.span; diff --git a/markwon/src/test/java/ru/noties/markwon/core/visitor/BlockQuoteNodeVisitorTest.java b/markwon/src/test/java/ru/noties/markwon/core/visitor/BlockQuoteNodeVisitorTest.java deleted file mode 100644 index f83173ce..00000000 --- a/markwon/src/test/java/ru/noties/markwon/core/visitor/BlockQuoteNodeVisitorTest.java +++ /dev/null @@ -1,18 +0,0 @@ -package ru.noties.markwon.core.visitor; - -import org.junit.Test; -import org.junit.runner.RunWith; -import org.robolectric.RobolectricTestRunner; -import org.robolectric.annotation.Config; - -import static org.junit.Assert.*; - -@RunWith(RobolectricTestRunner.class) -@Config(manifest = Config.NONE) -public class BlockQuoteNodeVisitorTest { - - @Test - public void test() { - fail(); - } -} \ No newline at end of file diff --git a/markwon/src/test/java/ru/noties/markwon/image/ImageTest.java b/markwon/src/test/java/ru/noties/markwon/image/ImageTest.java index 48cd3227..69391899 100644 --- a/markwon/src/test/java/ru/noties/markwon/image/ImageTest.java +++ b/markwon/src/test/java/ru/noties/markwon/image/ImageTest.java @@ -2,14 +2,21 @@ package ru.noties.markwon.image; import android.content.Context; import android.support.annotation.NonNull; +import android.support.annotation.Nullable; +import org.commonmark.node.Image; import org.junit.Test; import org.junit.runner.RunWith; import org.robolectric.RobolectricTestRunner; import org.robolectric.RuntimeEnvironment; import org.robolectric.annotation.Config; +import ru.noties.markwon.AbstractMarkwonPlugin; import ru.noties.markwon.Markwon; +import ru.noties.markwon.MarkwonConfiguration; +import ru.noties.markwon.MarkwonSpansFactory; +import ru.noties.markwon.RenderProps; +import ru.noties.markwon.SpanFactory; import ru.noties.markwon.core.CorePlugin; import ru.noties.markwon.core.MarkwonTheme; import ru.noties.markwon.test.TestSpan.Document; @@ -32,10 +39,16 @@ public class ImageTest { final Context context = RuntimeEnvironment.application; final Markwon markwon = Markwon.builder(context) .usePlugin(CorePlugin.create()) - .usePlugin(new ImagesPlugin(context, false) { + .usePlugin(new ImagesPlugin(context, false)) + .usePlugin(new AbstractMarkwonPlugin() { @Override - protected Object imageSpan(@NonNull MarkwonTheme theme, @NonNull String destination, @NonNull AsyncDrawableLoader loader, @NonNull ImageSizeResolver imageSizeResolver, boolean replacementTextIsLink) { - return span("image", args("href", destination)); + public void configureSpansFactory(@NonNull MarkwonSpansFactory.Builder builder) { + builder.setFactory(Image.class, new SpanFactory() { + @Override + public Object getSpans(@NonNull MarkwonConfiguration configuration, @NonNull RenderProps props) { + return span("image", args("href", ImageProps.DESTINATION.require(props))); + } + }); } }) .build(); diff --git a/markwon/src/test/java/ru/noties/markwon/syntax/SyntaxHighlightTest.java b/markwon/src/test/java/ru/noties/markwon/syntax/SyntaxHighlightTest.java index c15426d2..732a182f 100644 --- a/markwon/src/test/java/ru/noties/markwon/syntax/SyntaxHighlightTest.java +++ b/markwon/src/test/java/ru/noties/markwon/syntax/SyntaxHighlightTest.java @@ -1,6 +1,5 @@ package ru.noties.markwon.syntax; -import android.content.Context; import android.os.Build; import android.support.annotation.NonNull; import android.support.annotation.Nullable; @@ -14,21 +13,24 @@ import org.junit.runner.RunWith; import org.robolectric.RobolectricTestRunner; import org.robolectric.annotation.Config; -import java.util.HashMap; +import java.util.Collections; import java.util.Map; import ru.noties.markwon.AbstractMarkwonVisitorImpl; import ru.noties.markwon.MarkwonConfiguration; +import ru.noties.markwon.MarkwonSpansFactory; import ru.noties.markwon.MarkwonVisitor; +import ru.noties.markwon.RenderProps; +import ru.noties.markwon.RenderPropsImpl; +import ru.noties.markwon.SpanFactory; import ru.noties.markwon.SpannableBuilder; +import ru.noties.markwon.core.CorePluginBridge; import ru.noties.markwon.core.MarkwonTheme; -import ru.noties.markwon.core.MarkwonSpannableFactory; import ru.noties.markwon.image.AsyncDrawableLoader; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertTrue; import static org.mockito.ArgumentMatchers.any; -import static org.mockito.ArgumentMatchers.anyBoolean; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; @@ -53,6 +55,7 @@ public class SyntaxHighlightTest { @Test public void test() { + // code span must be first in the list, then should go highlight spans class Highlight { } @@ -70,19 +73,23 @@ public class SyntaxHighlightTest { } }; - final MarkwonSpannableFactory factory = mock(MarkwonSpannableFactory.class); - when(factory.code(any(MarkwonTheme.class), anyBoolean())).thenReturn(codeSpan); + final MarkwonSpansFactory spansFactory = mock(MarkwonSpansFactory.class); + when(spansFactory.require(any(FencedCodeBlock.class))).thenReturn(new SpanFactory() { + @Override + public Object getSpans(@NonNull MarkwonConfiguration configuration, @NonNull RenderProps props) { + return codeSpan; + } + }); - final MarkwonConfiguration configuration = MarkwonConfiguration.builder(mock(Context.class)) + final MarkwonConfiguration configuration = MarkwonConfiguration.builder() .syntaxHighlight(highlight) - .factory(factory) - .build(mock(MarkwonTheme.class), mock(AsyncDrawableLoader.class)); + .build(mock(MarkwonTheme.class), mock(AsyncDrawableLoader.class), spansFactory); - final Map, MarkwonVisitor.NodeVisitor> visitorMap = new HashMap<>(1); - visitorMap.put(FencedCodeBlock.class, new CodeBlockNodeVisitor.Fenced()); + final Map, MarkwonVisitor.NodeVisitor> visitorMap = Collections.emptyMap(); final MarkwonVisitor visitor = new AbstractMarkwonVisitorImpl( configuration, + new RenderPropsImpl(), visitorMap); final SpannableBuilder builder = visitor.builder(); @@ -96,12 +103,7 @@ public class SyntaxHighlightTest { final FencedCodeBlock fencedCodeBlock = new FencedCodeBlock(); fencedCodeBlock.setLiteral("{code}"); - CodeBlockNodeVisitor.visitCodeBlock( - visitor, - null, - "{code}", - fencedCodeBlock - ); + CorePluginBridge.visitCodeBlock(visitor, null, "{code}", fencedCodeBlock); final int end = builder.length(); @@ -116,6 +118,7 @@ public class SyntaxHighlightTest { assertEquals(length, spans.length); assertEquals(codeSpan, spans[0]); + // each character for (int i = 1; i < length; i++) { assertTrue(spans[i] instanceof Highlight); } diff --git a/sample-custom-extension/src/main/java/ru/noties/markwon/sample/extension/IconPlugin.java b/sample-custom-extension/src/main/java/ru/noties/markwon/sample/extension/IconPlugin.java index e03f8a86..eb821000 100644 --- a/sample-custom-extension/src/main/java/ru/noties/markwon/sample/extension/IconPlugin.java +++ b/sample-custom-extension/src/main/java/ru/noties/markwon/sample/extension/IconPlugin.java @@ -24,7 +24,6 @@ public class IconPlugin extends AbstractMarkwonPlugin { @Override public void configureParser(@NonNull Parser.Builder builder) { builder.customDelimiterProcessor(IconProcessor.create()); - builder.postProcessor() } @Override diff --git a/sample-custom-extension/src/main/java/ru/noties/markwon/sample/extension/MainActivity.java b/sample-custom-extension/src/main/java/ru/noties/markwon/sample/extension/MainActivity.java index 4fa0bac7..4975dc38 100644 --- a/sample-custom-extension/src/main/java/ru/noties/markwon/sample/extension/MainActivity.java +++ b/sample-custom-extension/src/main/java/ru/noties/markwon/sample/extension/MainActivity.java @@ -25,6 +25,8 @@ public class MainActivity extends Activity { .usePlugin(new AbstractMarkwonPlugin() { @Override public void configureTheme(@NonNull MarkwonTheme.Builder builder) { + // this part has nothing to do with actual IconPlugin + // this part is used to showcase that headers can be controlled via Theme final float[] textSizeMultipliers = new float[]{3f, 2f, 1.5f, 1f, .5f, .25f}; builder .headingTypeface(Typeface.MONOSPACE) From ef15ee4bd27b90c3f65b007346915b70ad083084 Mon Sep 17 00:00:00 2001 From: Dimitry Ivanov Date: Sat, 22 Dec 2018 17:39:29 +0300 Subject: [PATCH 047/103] Fix markwon-html tests --- .../{impl => }/CssInlineStyleParserTest.java | 39 +++++++++++-------- .../HtmlEmptyTagReplacementTest.java | 4 +- .../{impl => }/MarkwonHtmlParserImplTest.java | 9 +---- .../html/{impl => }/TrimmingAppenderTest.java | 2 +- .../jsoup/nodes/CommonMarkEntitiesTest.java | 12 ++---- .../tag/ImageSizeParserImplTest.java | 26 ++++++------- .../ru/noties/markwon/test/TestUtils.java | 17 -------- 7 files changed, 41 insertions(+), 68 deletions(-) rename markwon-html/src/test/java/ru/noties/markwon/html/{impl => }/CssInlineStyleParserTest.java (87%) rename markwon-html/src/test/java/ru/noties/markwon/html/{impl => }/HtmlEmptyTagReplacementTest.java (89%) rename markwon-html/src/test/java/ru/noties/markwon/html/{impl => }/MarkwonHtmlParserImplTest.java (99%) rename markwon-html/src/test/java/ru/noties/markwon/html/{impl => }/TrimmingAppenderTest.java (96%) rename markwon-html/src/test/java/ru/noties/markwon/html/{impl => }/jsoup/nodes/CommonMarkEntitiesTest.java (57%) rename markwon-html/src/test/java/ru/noties/markwon/html/{impl => }/tag/ImageSizeParserImplTest.java (87%) delete mode 100644 markwon/src/test/java/ru/noties/markwon/test/TestUtils.java diff --git a/markwon-html/src/test/java/ru/noties/markwon/html/impl/CssInlineStyleParserTest.java b/markwon-html/src/test/java/ru/noties/markwon/html/CssInlineStyleParserTest.java similarity index 87% rename from markwon-html/src/test/java/ru/noties/markwon/html/impl/CssInlineStyleParserTest.java rename to markwon-html/src/test/java/ru/noties/markwon/html/CssInlineStyleParserTest.java index a898d342..cc105137 100644 --- a/markwon-html/src/test/java/ru/noties/markwon/html/impl/CssInlineStyleParserTest.java +++ b/markwon-html/src/test/java/ru/noties/markwon/html/CssInlineStyleParserTest.java @@ -1,4 +1,4 @@ -package ru.noties.markwon.html.impl; +package ru.noties.markwon.html; import android.support.annotation.NonNull; @@ -14,13 +14,9 @@ import java.util.Map; import ix.Ix; import ix.IxFunction; -import ru.noties.markwon.html.CssInlineStyleParser; -import ru.noties.markwon.html.CssProperty; -import ru.noties.markwon.test.TestUtils; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertNotNull; -import static ru.noties.markwon.test.TestUtils.with; @RunWith(RobolectricTestRunner.class) @Config(manifest = Config.NONE) @@ -42,7 +38,7 @@ public class CssInlineStyleParserTest { assertEquals(1, list.size()); - with(list.get(0), new TestUtils.Action() { + with(list.get(0), new Action() { @Override public void apply(@NonNull CssProperty cssProperty) { assertEquals("key", cssProperty.key()); @@ -60,7 +56,7 @@ public class CssInlineStyleParserTest { assertEquals(2, list.size()); - with(list.get(0), new TestUtils.Action() { + with(list.get(0), new Action() { @Override public void apply(@NonNull CssProperty cssProperty) { assertEquals("key1", cssProperty.key()); @@ -68,7 +64,7 @@ public class CssInlineStyleParserTest { } }); - with(list.get(1), new TestUtils.Action() { + with(list.get(1), new Action() { @Override public void apply(@NonNull CssProperty cssProperty) { assertEquals("key2", cssProperty.key()); @@ -84,7 +80,7 @@ public class CssInlineStyleParserTest { final List list = listProperties(input); assertEquals(1, list.size()); - with(list.get(0), new TestUtils.Action() { + with(list.get(0), new Action() { @Override public void apply(@NonNull CssProperty cssProperty) { assertEquals("key", cssProperty.key()); @@ -100,7 +96,7 @@ public class CssInlineStyleParserTest { final List list = listProperties(input); assertEquals(1, list.size()); - with(list.get(0), new TestUtils.Action() { + with(list.get(0), new Action() { @Override public void apply(@NonNull CssProperty cssProperty) { assertEquals("key", cssProperty.key()); @@ -116,7 +112,7 @@ public class CssInlineStyleParserTest { final List list = listProperties(input); assertEquals(2, list.size()); - with(list.get(0), new TestUtils.Action() { + with(list.get(0), new Action() { @Override public void apply(@NonNull CssProperty cssProperty) { assertEquals("key1", cssProperty.key()); @@ -124,7 +120,7 @@ public class CssInlineStyleParserTest { } }); - with(list.get(1), new TestUtils.Action() { + with(list.get(1), new Action() { @Override public void apply(@NonNull CssProperty cssProperty) { assertEquals("key2", cssProperty.key()); @@ -149,7 +145,7 @@ public class CssInlineStyleParserTest { final List list = listProperties(input); assertEquals(1, list.size()); - with(list.get(0), new TestUtils.Action() { + with(list.get(0), new Action() { @Override public void apply(@NonNull CssProperty cssProperty) { assertEquals("key4", cssProperty.key()); @@ -174,7 +170,7 @@ public class CssInlineStyleParserTest { assertEquals(2, list.size()); - with(list.get(0), new TestUtils.Action() { + with(list.get(0), new Action() { @Override public void apply(@NonNull CssProperty cssProperty) { assertEquals("key1", cssProperty.key()); @@ -182,7 +178,7 @@ public class CssInlineStyleParserTest { } }); - with(list.get(1), new TestUtils.Action() { + with(list.get(1), new Action() { @Override public void apply(@NonNull CssProperty cssProperty) { assertEquals("key3", cssProperty.key()); @@ -211,14 +207,14 @@ public class CssInlineStyleParserTest { }}; final StringBuilder builder = new StringBuilder(); - for (Map.Entry entry: map.entrySet()) { + for (Map.Entry entry : map.entrySet()) { builder.append(entry.getKey()) .append(':') .append(entry.getValue()) .append(';'); } - for (CssProperty cssProperty: impl.parse(builder.toString())) { + for (CssProperty cssProperty : impl.parse(builder.toString())) { final String value = map.remove(cssProperty.key()); assertNotNull(cssProperty.key(), value); assertEquals(cssProperty.key(), value, cssProperty.value()); @@ -238,4 +234,13 @@ public class CssInlineStyleParserTest { }) .toList(); } + + public interface Action { + void apply(@NonNull T t); + } + + private static void with(@NonNull T t, @NonNull Action action) { + action.apply(t); + } + } \ No newline at end of file diff --git a/markwon-html/src/test/java/ru/noties/markwon/html/impl/HtmlEmptyTagReplacementTest.java b/markwon-html/src/test/java/ru/noties/markwon/html/HtmlEmptyTagReplacementTest.java similarity index 89% rename from markwon-html/src/test/java/ru/noties/markwon/html/impl/HtmlEmptyTagReplacementTest.java rename to markwon-html/src/test/java/ru/noties/markwon/html/HtmlEmptyTagReplacementTest.java index 62333012..c54fab18 100644 --- a/markwon-html/src/test/java/ru/noties/markwon/html/impl/HtmlEmptyTagReplacementTest.java +++ b/markwon-html/src/test/java/ru/noties/markwon/html/HtmlEmptyTagReplacementTest.java @@ -1,12 +1,10 @@ -package ru.noties.markwon.html.impl; +package ru.noties.markwon.html; import org.junit.Before; import org.junit.Test; import java.util.Collections; -import ru.noties.markwon.html.HtmlEmptyTagReplacement; -import ru.noties.markwon.html.api.HtmlTag; import ru.noties.markwon.html.HtmlTagImpl.InlineImpl; import static org.junit.Assert.assertEquals; diff --git a/markwon-html/src/test/java/ru/noties/markwon/html/impl/MarkwonHtmlParserImplTest.java b/markwon-html/src/test/java/ru/noties/markwon/html/MarkwonHtmlParserImplTest.java similarity index 99% rename from markwon-html/src/test/java/ru/noties/markwon/html/impl/MarkwonHtmlParserImplTest.java rename to markwon-html/src/test/java/ru/noties/markwon/html/MarkwonHtmlParserImplTest.java index 54adb09f..c614d36c 100644 --- a/markwon-html/src/test/java/ru/noties/markwon/html/impl/MarkwonHtmlParserImplTest.java +++ b/markwon-html/src/test/java/ru/noties/markwon/html/MarkwonHtmlParserImplTest.java @@ -1,4 +1,4 @@ -package ru.noties.markwon.html.impl; +package ru.noties.markwon.html; import android.support.annotation.NonNull; import android.support.annotation.Nullable; @@ -15,11 +15,6 @@ import java.util.List; import java.util.Map; import java.util.Set; -import ru.noties.markwon.html.HtmlEmptyTagReplacement; -import ru.noties.markwon.html.MarkwonHtmlParserImpl; -import ru.noties.markwon.html.api.HtmlTag; -import ru.noties.markwon.html.api.MarkwonHtmlParser; - import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertTrue; @@ -847,7 +842,7 @@ public class MarkwonHtmlParserImplTest { "

head #3

in custom-tag" }; - for (String fragment: fragments) { + for (String fragment : fragments) { impl.processFragment(output, fragment); } diff --git a/markwon-html/src/test/java/ru/noties/markwon/html/impl/TrimmingAppenderTest.java b/markwon-html/src/test/java/ru/noties/markwon/html/TrimmingAppenderTest.java similarity index 96% rename from markwon-html/src/test/java/ru/noties/markwon/html/impl/TrimmingAppenderTest.java rename to markwon-html/src/test/java/ru/noties/markwon/html/TrimmingAppenderTest.java index 3d5cf4db..ef82795e 100644 --- a/markwon-html/src/test/java/ru/noties/markwon/html/impl/TrimmingAppenderTest.java +++ b/markwon-html/src/test/java/ru/noties/markwon/html/TrimmingAppenderTest.java @@ -1,4 +1,4 @@ -package ru.noties.markwon.html.impl; +package ru.noties.markwon.html; import org.junit.Before; import org.junit.Test; diff --git a/markwon-html/src/test/java/ru/noties/markwon/html/impl/jsoup/nodes/CommonMarkEntitiesTest.java b/markwon-html/src/test/java/ru/noties/markwon/html/jsoup/nodes/CommonMarkEntitiesTest.java similarity index 57% rename from markwon-html/src/test/java/ru/noties/markwon/html/impl/jsoup/nodes/CommonMarkEntitiesTest.java rename to markwon-html/src/test/java/ru/noties/markwon/html/jsoup/nodes/CommonMarkEntitiesTest.java index 866b3352..3472d8d5 100644 --- a/markwon-html/src/test/java/ru/noties/markwon/html/impl/jsoup/nodes/CommonMarkEntitiesTest.java +++ b/markwon-html/src/test/java/ru/noties/markwon/html/jsoup/nodes/CommonMarkEntitiesTest.java @@ -1,24 +1,20 @@ -package ru.noties.markwon.html.impl.jsoup.nodes; +package ru.noties.markwon.html.jsoup.nodes; +import org.junit.Assert; import org.junit.Test; import org.junit.runner.RunWith; import org.robolectric.RobolectricTestRunner; import org.robolectric.annotation.Config; -import ru.noties.markwon.html.jsoup.nodes.CommonMarkEntities; - -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertTrue; - @RunWith(RobolectricTestRunner.class) @Config(manifest = Config.NONE) public class CommonMarkEntitiesTest { @Test public void can_access_field() { - assertTrue("&", CommonMarkEntities.isNamedEntity("amp")); + Assert.assertTrue("&", CommonMarkEntities.isNamedEntity("amp")); final int[] codepoints = new int[1]; CommonMarkEntities.codepointsForName("amp", codepoints); - assertEquals('&', codepoints[0]); + Assert.assertEquals('&', codepoints[0]); } } \ No newline at end of file diff --git a/markwon-html/src/test/java/ru/noties/markwon/html/impl/tag/ImageSizeParserImplTest.java b/markwon-html/src/test/java/ru/noties/markwon/html/tag/ImageSizeParserImplTest.java similarity index 87% rename from markwon-html/src/test/java/ru/noties/markwon/html/impl/tag/ImageSizeParserImplTest.java rename to markwon-html/src/test/java/ru/noties/markwon/html/tag/ImageSizeParserImplTest.java index c1549747..ffa9fdc6 100644 --- a/markwon-html/src/test/java/ru/noties/markwon/html/impl/tag/ImageSizeParserImplTest.java +++ b/markwon-html/src/test/java/ru/noties/markwon/html/tag/ImageSizeParserImplTest.java @@ -1,8 +1,9 @@ -package ru.noties.markwon.html.impl.tag; +package ru.noties.markwon.html.tag; import android.support.annotation.NonNull; import android.support.annotation.Nullable; +import org.junit.Assert; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; @@ -13,13 +14,8 @@ import java.util.Collections; import java.util.HashMap; import java.util.Map; -import ru.noties.markwon.html.tag.ImageSizeParserImpl; +import ru.noties.markwon.html.CssInlineStyleParser; import ru.noties.markwon.image.ImageSize; -import ru.noties.markwon.renderer.html2.CssInlineStyleParser; - -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertNotNull; -import static org.junit.Assert.assertNull; @RunWith(RobolectricTestRunner.class) @Config(manifest = Config.NONE) @@ -36,7 +32,7 @@ public class ImageSizeParserImplTest { @Test public void nothing() { - assertNull(impl.parse(Collections.emptyMap())); + Assert.assertNull(impl.parse(Collections.emptyMap())); } @Test @@ -153,15 +149,15 @@ public class ImageSizeParserImplTest { }; for (String dimension : dimensions) { - assertNull(dimension, impl.dimension(dimension)); + Assert.assertNull(dimension, impl.dimension(dimension)); } } private static void assertImageSize(@Nullable ImageSize expected, @Nullable ImageSize actual) { if (expected == null) { - assertNull(actual); + Assert.assertNull(actual); } else { - assertNotNull(actual); + Assert.assertNotNull(actual); assertDimension("width", expected.width, actual.width); assertDimension("height", expected.height, actual.height); } @@ -172,11 +168,11 @@ public class ImageSizeParserImplTest { @Nullable ImageSize.Dimension expected, @Nullable ImageSize.Dimension actual) { if (expected == null) { - assertNull(name, actual); + Assert.assertNull(name, actual); } else { - assertNotNull(name, actual); - assertEquals(name, expected.value, actual.value, DELTA); - assertEquals(name, expected.unit, actual.unit); + Assert.assertNotNull(name, actual); + Assert.assertEquals(name, expected.value, actual.value, DELTA); + Assert.assertEquals(name, expected.unit, actual.unit); } } diff --git a/markwon/src/test/java/ru/noties/markwon/test/TestUtils.java b/markwon/src/test/java/ru/noties/markwon/test/TestUtils.java deleted file mode 100644 index 4a8f3890..00000000 --- a/markwon/src/test/java/ru/noties/markwon/test/TestUtils.java +++ /dev/null @@ -1,17 +0,0 @@ -package ru.noties.markwon.test; - -import android.support.annotation.NonNull; - -public abstract class TestUtils { - - public interface Action { - void apply(@NonNull T t); - } - - public static void with(@NonNull T t, @NonNull Action action) { - action.apply(t); - } - - private TestUtils() { - } -} From 286dd5410aa8185d073687de6224be622b187c58 Mon Sep 17 00:00:00 2001 From: Dimitry Ivanov Date: Sun, 23 Dec 2018 16:36:01 +0300 Subject: [PATCH 048/103] Adding javadoc documentation (work in progress) --- markwon-test-util/build.gradle | 13 --- .../java/ru/noties/markwon/test/TestUtil.java | 32 ------ markwon/build.gradle | 5 - .../noties/markwon/AbstractMarkwonPlugin.java | 47 +++++++- .../main/java/ru/noties/markwon/Markwon.java | 26 ++++- .../ru/noties/markwon/MarkwonBuilderImpl.java | 1 - .../java/ru/noties/markwon/MarkwonImpl.java | 16 ++- .../java/ru/noties/markwon/MarkwonPlugin.java | 108 +++++++++++++++++- .../noties/markwon/MarkwonSpansFactory.java | 29 ++++- .../markwon/MarkwonSpansFactoryImpl.java | 17 ++- .../ru/noties/markwon/MarkwonVisitor.java | 77 ++++++++++++- .../src/main/java/ru/noties/markwon/Prop.java | 4 +- .../java/ru/noties/markwon/RenderProps.java | 2 + .../ru/noties/markwon/RenderPropsImpl.java | 1 + .../ru/noties/markwon/core/CorePlugin.java | 21 ++-- .../ru/noties/markwon/core/MarkwonTheme.java | 33 ++++++ .../ru/noties/markwon/image/ImagesPlugin.java | 21 +++- .../markwon/core/suite/BaseSuiteTest.java | 18 +-- .../markwon/syntax/SyntaxHighlightTest.java | 7 +- settings.gradle | 1 - 20 files changed, 375 insertions(+), 104 deletions(-) delete mode 100644 markwon-test-util/build.gradle delete mode 100644 markwon-test-util/src/main/java/ru/noties/markwon/test/TestUtil.java diff --git a/markwon-test-util/build.gradle b/markwon-test-util/build.gradle deleted file mode 100644 index 3909ab27..00000000 --- a/markwon-test-util/build.gradle +++ /dev/null @@ -1,13 +0,0 @@ -apply plugin: 'java-library' - -sourceCompatibility = 1.7 -targetCompatibility = 1.7 - -dependencies { - - api deps['support-annotations'] - - deps['test'].with { - implementation it['commons-io'] - } -} \ No newline at end of file diff --git a/markwon-test-util/src/main/java/ru/noties/markwon/test/TestUtil.java b/markwon-test-util/src/main/java/ru/noties/markwon/test/TestUtil.java deleted file mode 100644 index 2ce00e1f..00000000 --- a/markwon-test-util/src/main/java/ru/noties/markwon/test/TestUtil.java +++ /dev/null @@ -1,32 +0,0 @@ -package ru.noties.markwon.test; - -import android.support.annotation.NonNull; - -import org.apache.commons.io.IOUtils; - -import java.io.IOException; -import java.nio.charset.StandardCharsets; - -public abstract class TestUtil { - - @NonNull - public static String read(@NonNull String path) { - try { - return IOUtils.resourceToString(path, StandardCharsets.UTF_8); - } catch (IOException e) { - throw new RuntimeException(e); - } - } - - @NonNull - public static String read(@NonNull Object who, @NonNull String path) { - try { - return IOUtils.resourceToString(path, StandardCharsets.UTF_8, who.getClass().getClassLoader()); - } catch (IOException e) { - throw new RuntimeException(e); - } - } - - private TestUtil() { - } -} diff --git a/markwon/build.gradle b/markwon/build.gradle index 604519a2..8682d261 100644 --- a/markwon/build.gradle +++ b/markwon/build.gradle @@ -23,17 +23,12 @@ dependencies { deps['test'].with { testImplementation project(':markwon-test-span') - testImplementation project(':markwon-test-util') testImplementation it['junit'] testImplementation it['robolectric'] testImplementation it['mockito'] - // to remove after migration testImplementation it['ix-java'] - testImplementation it['jackson-yaml'] - testImplementation it['jackson-databind'] - testImplementation it['gson'] testImplementation it['commons-io'] } } diff --git a/markwon/src/main/java/ru/noties/markwon/AbstractMarkwonPlugin.java b/markwon/src/main/java/ru/noties/markwon/AbstractMarkwonPlugin.java index 28767521..d9849e5c 100644 --- a/markwon/src/main/java/ru/noties/markwon/AbstractMarkwonPlugin.java +++ b/markwon/src/main/java/ru/noties/markwon/AbstractMarkwonPlugin.java @@ -1,6 +1,7 @@ package ru.noties.markwon; import android.support.annotation.NonNull; +import android.text.Spanned; import android.widget.TextView; import org.commonmark.node.Node; @@ -9,63 +10,107 @@ import org.commonmark.parser.Parser; import ru.noties.markwon.core.MarkwonTheme; import ru.noties.markwon.image.AsyncDrawableLoader; +/** + * Class that extends {@link MarkwonPlugin} with all methods implemented (empty body) + * for easier plugin implementation. Only required methods can be overriden + * + * @see MarkwonPlugin + * @since 3.0.0 + */ public abstract class AbstractMarkwonPlugin implements MarkwonPlugin { + + /** + * @inheritDoc + */ @Override public void configureParser(@NonNull Parser.Builder builder) { } + /** + * @inheritDoc + */ @Override public void configureTheme(@NonNull MarkwonTheme.Builder builder) { } + /** + * @inheritDoc + */ @Override public void configureImages(@NonNull AsyncDrawableLoader.Builder builder) { } + /** + * @inheritDoc + */ @Override public void configureConfiguration(@NonNull MarkwonConfiguration.Builder builder) { } + /** + * @inheritDoc + */ @Override public void configureVisitor(@NonNull MarkwonVisitor.Builder builder) { } + /** + * @inheritDoc + */ @Override public void configureSpansFactory(@NonNull MarkwonSpansFactory.Builder builder) { } + /** + * @inheritDoc + */ @Override public void configureRenderProps(@NonNull RenderProps renderProps) { } + /** + * @inheritDoc + */ @NonNull @Override public String processMarkdown(@NonNull String markdown) { return markdown; } + /** + * @inheritDoc + */ @Override public void beforeRender(@NonNull Node node) { } + /** + * @inheritDoc + */ @Override public void afterRender(@NonNull Node node, @NonNull MarkwonVisitor visitor) { } + /** + * @inheritDoc + */ @Override - public void beforeSetText(@NonNull TextView textView, @NonNull CharSequence markdown) { + public void beforeSetText(@NonNull TextView textView, @NonNull Spanned markdown) { } + /** + * @inheritDoc + */ @Override public void afterSetText(@NonNull TextView textView) { diff --git a/markwon/src/main/java/ru/noties/markwon/Markwon.java b/markwon/src/main/java/ru/noties/markwon/Markwon.java index 6c4e4bea..c5a9ea8c 100644 --- a/markwon/src/main/java/ru/noties/markwon/Markwon.java +++ b/markwon/src/main/java/ru/noties/markwon/Markwon.java @@ -7,9 +7,20 @@ import android.widget.TextView; import org.commonmark.node.Node; +/** + * Class to parse and render markdown. Since version 3.0.0 instance specific (previously consisted + * of static stateless methods). An instance of builder can be obtained via {@link #builder(Context)} + * method. + * + * @see #builder(Context) + * @see Builder + */ public abstract class Markwon { /** + * Factory method to obtain an instance of {@link Builder} + * + * @see Builder * @since 3.0.0 */ @NonNull @@ -17,6 +28,14 @@ public abstract class Markwon { return new MarkwonBuilderImpl(context); } + /** + * Method to simply parse markdown (without rendering) + * + * @param input markdown input to parse + * @return parsed via commonmark-java org.commonmark.node.Node + * @see #render(Node) + * @since 3.0.0 + */ @NonNull public abstract Node parse(@NonNull String input); @@ -29,9 +48,14 @@ public abstract class Markwon { public abstract void setMarkdown(@NonNull TextView textView, @NonNull String markdown); - public abstract void setParsedMarkdown(@NonNull TextView textView, @NonNull CharSequence markdown); + public abstract void setParsedMarkdown(@NonNull TextView textView, @NonNull Spanned markdown); /** + * Builder for {@link Markwon}. + *

+ * Please note that the order in which plugins are supplied is important as this order will be + * used through the whole usage of built Markwon instance + * * @since 3.0.0 */ public interface Builder { diff --git a/markwon/src/main/java/ru/noties/markwon/MarkwonBuilderImpl.java b/markwon/src/main/java/ru/noties/markwon/MarkwonBuilderImpl.java index 6098155a..1cf15c2c 100644 --- a/markwon/src/main/java/ru/noties/markwon/MarkwonBuilderImpl.java +++ b/markwon/src/main/java/ru/noties/markwon/MarkwonBuilderImpl.java @@ -80,7 +80,6 @@ class MarkwonBuilderImpl implements Markwon.Builder { plugin.configureConfiguration(configurationBuilder); plugin.configureVisitor(visitorBuilder); plugin.configureSpansFactory(spanFactoryBuilder); - plugin.configureRenderProps(renderProps); } final MarkwonConfiguration configuration = configurationBuilder.build( diff --git a/markwon/src/main/java/ru/noties/markwon/MarkwonImpl.java b/markwon/src/main/java/ru/noties/markwon/MarkwonImpl.java index 2114f050..8e2d8791 100644 --- a/markwon/src/main/java/ru/noties/markwon/MarkwonImpl.java +++ b/markwon/src/main/java/ru/noties/markwon/MarkwonImpl.java @@ -9,6 +9,9 @@ import org.commonmark.parser.Parser; import java.util.List; +/** + * @since 3.0.0 + */ class MarkwonImpl extends Markwon { private final TextView.BufferType bufferType; @@ -31,6 +34,7 @@ class MarkwonImpl extends Markwon { @Override public Node parse(@NonNull String input) { + // make sure that all plugins are called `processMarkdown` before parsing for (MarkwonPlugin plugin : plugins) { input = plugin.processMarkdown(input); } @@ -42,7 +46,14 @@ class MarkwonImpl extends Markwon { @Override public Spanned render(@NonNull Node node) { + final RenderProps renderProps = visitor.renderProps(); + for (MarkwonPlugin plugin : plugins) { + + // let plugins apply render properties before rendering (as we will clear + // renderProps after rendering) + plugin.configureRenderProps(renderProps); + plugin.beforeRender(node); } @@ -52,6 +63,9 @@ class MarkwonImpl extends Markwon { plugin.afterRender(node, visitor); } + // clear render props after rending + renderProps.clearAll(); + return visitor.builder().spannableStringBuilder(); } @@ -67,7 +81,7 @@ class MarkwonImpl extends Markwon { } @Override - public void setParsedMarkdown(@NonNull TextView textView, @NonNull CharSequence markdown) { + public void setParsedMarkdown(@NonNull TextView textView, @NonNull Spanned markdown) { for (MarkwonPlugin plugin : plugins) { plugin.beforeSetText(textView, markdown); diff --git a/markwon/src/main/java/ru/noties/markwon/MarkwonPlugin.java b/markwon/src/main/java/ru/noties/markwon/MarkwonPlugin.java index 910b044e..fd99b4dd 100644 --- a/markwon/src/main/java/ru/noties/markwon/MarkwonPlugin.java +++ b/markwon/src/main/java/ru/noties/markwon/MarkwonPlugin.java @@ -1,6 +1,7 @@ package ru.noties.markwon; import android.support.annotation.NonNull; +import android.text.Spanned; import android.widget.TextView; import org.commonmark.node.Node; @@ -8,38 +9,137 @@ import org.commonmark.parser.Parser; import ru.noties.markwon.core.MarkwonTheme; import ru.noties.markwon.image.AsyncDrawableLoader; +import ru.noties.markwon.image.MediaDecoder; +import ru.noties.markwon.image.SchemeHandler; /** + * Class represents a plugin (extension) to Markwon to configure how parsing and rendering + * of markdown is carried on. + * + * @see AbstractMarkwonPlugin + * @see ru.noties.markwon.core.CorePlugin + * @see ru.noties.markwon.image.ImagesPlugin * @since 3.0.0 */ public interface MarkwonPlugin { + /** + * Method to configure org.commonmark.parser.Parser (for example register custom + * extension, etc). + */ void configureParser(@NonNull Parser.Builder builder); + /** + * Modify {@link MarkwonTheme} that is used for rendering of markdown. + * + * @see MarkwonTheme + * @see MarkwonTheme.Builder + */ void configureTheme(@NonNull MarkwonTheme.Builder builder); + /** + * Configure image loading functionality. For example add new content-types + * {@link AsyncDrawableLoader.Builder#addMediaDecoder(String, MediaDecoder)}, a transport + * layer (network, file, etc) {@link AsyncDrawableLoader.Builder#addSchemeHandler(String, SchemeHandler)} + * or modify existing properties. + * + * @see AsyncDrawableLoader + * @see AsyncDrawableLoader.Builder + */ void configureImages(@NonNull AsyncDrawableLoader.Builder builder); + /** + * Configure {@link MarkwonConfiguration} + * + * @see MarkwonConfiguration + * @see MarkwonConfiguration.Builder + */ void configureConfiguration(@NonNull MarkwonConfiguration.Builder builder); + /** + * Configure {@link MarkwonVisitor} to accept new node types or override already registered nodes. + * + * @see MarkwonVisitor + * @see MarkwonVisitor.Builder + */ void configureVisitor(@NonNull MarkwonVisitor.Builder builder); + /** + * Configure {@link MarkwonSpansFactory} to change what spans are used for certain node types. + * + * @see MarkwonSpansFactory + * @see MarkwonSpansFactory.Builder + */ void configureSpansFactory(@NonNull MarkwonSpansFactory.Builder builder); // can be used to configure own properties and use between plugins + + /** + * A method to store some arbitrary data in {@link RenderProps}. Although it won\'t make + * much sense to use existing {@link Prop} keys for {@link SpanFactory}, it can be helpful + * to establish a communication channel between multiple plugins in decoupled way (provide + * some initial properties for example or indicate that certain plugin is registered). + *

+ * This method will be called before each rendering step (after rendering {@link RenderProps} + * will be cleared. This method won\'t be called during initialization stage. + * + * @see RenderProps + */ void configureRenderProps(@NonNull RenderProps renderProps); + /** + * Process input markdown and return new string to be used in parsing stage further. + * Can be described as pre-processing of markdown String. + * + * @param markdown String to process + * @return processed markdown String + */ @NonNull String processMarkdown(@NonNull String markdown); + /** + * This method will be called before rendering will occur thus making possible + * to post-process parsed node (make changes for example). + * + * @param node root parsed org.commonmark.node.Node + */ void beforeRender(@NonNull Node node); + /** + * This method will be called after rendering (but before applying markdown to a + * TextView, if such action will happen). It can be used to clean some + * internal state, or trigger certain action. Please note that modifying node won\'t + * have any effect as it has been already visited at this stage. + * + * @param node root parsed org.commonmark.node.Node + * @param visitor {@link MarkwonVisitor} instance used to render markdown + */ void afterRender(@NonNull Node node, @NonNull MarkwonVisitor visitor); - void beforeSetText(@NonNull TextView textView, @NonNull CharSequence markdown); + /** + * This method will be called before calling TextView#setText. + *

+ * It can be useful to prepare a TextView for markdown. For example {@link ru.noties.markwon.image.ImagesPlugin} + * uses this method to unregister previously registered {@link ru.noties.markwon.image.AsyncDrawableSpan} + * (if there are such spans in this TextView at this point). Or {@link ru.noties.markwon.core.CorePlugin} + * which measures ordered list numbers + * + * @param textView TextView to which markdown will be applied + * @param markdown Parsed markdown + */ + void beforeSetText(@NonNull TextView textView, @NonNull Spanned markdown); - // this method do not receive markdown like `beforeSetText` does because at this - // point TextView already has markdown set and to manipulate spans one must - // request them from TextView (getText()) + /** + * This method will be called after markdown was applied. + *

+ * It can be useful to trigger certain action on spans/textView. For example {@link ru.noties.markwon.image.ImagesPlugin} + * uses this method to register {@link ru.noties.markwon.image.AsyncDrawableSpan} and start + * asynchronously loading images. + *

+ * Unlike {@link #beforeSetText(TextView, Spanned)} this method does not receive parsed markdown + * as at this point spans must be queried by calling TextView#getText#getSpans. + * + * @param textView TextView to which markdown was applied + */ void afterSetText(@NonNull TextView textView); } diff --git a/markwon/src/main/java/ru/noties/markwon/MarkwonSpansFactory.java b/markwon/src/main/java/ru/noties/markwon/MarkwonSpansFactory.java index 748ebd57..70545066 100644 --- a/markwon/src/main/java/ru/noties/markwon/MarkwonSpansFactory.java +++ b/markwon/src/main/java/ru/noties/markwon/MarkwonSpansFactory.java @@ -6,27 +6,46 @@ import android.support.annotation.Nullable; import org.commonmark.node.Node; /** + * Class that controls what spans are used for certain Nodes. + * + * @see SpanFactory * @since 3.0.0 */ public interface MarkwonSpansFactory { + /** + * Returns registered {@link SpanFactory} or null if a factory for this node type + * is not registered. There is {@link #require(Class)} method that will throw an exception + * if required {@link SpanFactory} is not registered, thus making return type non-null + * + * @param node type of the node + * @return registered {@link SpanFactory} or null if it\'s not registered + * @see #require(Class) + */ @Nullable - F get(@NonNull Class node); + SpanFactory get(@NonNull Class node); + /** + * @see #get(Class) + * @see #require(Node) + */ @Nullable - F get(@NonNull N node); + SpanFactory get(@NonNull N node); @NonNull - F require(@NonNull Class node); + SpanFactory require(@NonNull Class node); + /** + * @see #require(Class) + */ @NonNull - F require(@NonNull N node); + SpanFactory require(@NonNull N node); interface Builder { @NonNull - Builder setFactory(@NonNull Class node, @NonNull F factory); + Builder setFactory(@NonNull Class node, @NonNull SpanFactory factory); @NonNull MarkwonSpansFactory build(); diff --git a/markwon/src/main/java/ru/noties/markwon/MarkwonSpansFactoryImpl.java b/markwon/src/main/java/ru/noties/markwon/MarkwonSpansFactoryImpl.java index 7bbc8ca8..81b5a0c3 100644 --- a/markwon/src/main/java/ru/noties/markwon/MarkwonSpansFactoryImpl.java +++ b/markwon/src/main/java/ru/noties/markwon/MarkwonSpansFactoryImpl.java @@ -22,21 +22,20 @@ class MarkwonSpansFactoryImpl implements MarkwonSpansFactory { @Nullable @Override - public F get(@NonNull Class node) { - //noinspection unchecked - return (F) factories.get(node); + public SpanFactory get(@NonNull Class node) { + return factories.get(node); } @Nullable @Override - public F get(@NonNull N node) { + public SpanFactory get(@NonNull N node) { return get(node.getClass()); } @NonNull @Override - public F require(@NonNull Class node) { - final F f = get(node); + public SpanFactory require(@NonNull Class node) { + final SpanFactory f = get(node); if (f == null) { throw new NullPointerException(); } @@ -45,8 +44,8 @@ class MarkwonSpansFactoryImpl implements MarkwonSpansFactory { @NonNull @Override - public F require(@NonNull N node) { - final F f = get(node); + public SpanFactory require(@NonNull N node) { + final SpanFactory f = get(node); if (f == null) { throw new NullPointerException(); } @@ -60,7 +59,7 @@ class MarkwonSpansFactoryImpl implements MarkwonSpansFactory { @NonNull @Override - public Builder setFactory(@NonNull Class node, @NonNull F factory) { + public Builder setFactory(@NonNull Class node, @NonNull SpanFactory factory) { factories.put(node, factory); return this; } diff --git a/markwon/src/main/java/ru/noties/markwon/MarkwonVisitor.java b/markwon/src/main/java/ru/noties/markwon/MarkwonVisitor.java index 81d3138f..aec63444 100644 --- a/markwon/src/main/java/ru/noties/markwon/MarkwonVisitor.java +++ b/markwon/src/main/java/ru/noties/markwon/MarkwonVisitor.java @@ -7,16 +7,29 @@ import org.commonmark.node.Node; import org.commonmark.node.Visitor; /** + * Configurable visitor of parsed markdown. Allows visiting certain (registered) nodes without + * need to create own instance of this class. + * + * @see Builder#on(Class, NodeVisitor) + * @see MarkwonPlugin#configureVisitor(Builder) * @since 3.0.0 */ public interface MarkwonVisitor extends Visitor { + /** + * @see Builder#on(Class, NodeVisitor) + */ interface NodeVisitor { void visit(@NonNull MarkwonVisitor visitor, @NonNull N n); } interface Builder { + /** + * @param node to register + * @param nodeVisitor {@link NodeVisitor} to be used or null to ignore previously registered + * visitor for this node + */ @NonNull Builder on(@NonNull Class node, @Nullable NodeVisitor nodeVisitor); @@ -33,26 +46,86 @@ public interface MarkwonVisitor extends Visitor { @NonNull SpannableBuilder builder(); + /** + * Visits all children of supplied node. + * + * @param node to visit + */ void visitChildren(@NonNull Node node); + /** + * Executes a check if there is further content available. + * + * @param node to check + * @return boolean indicating if there are more nodes after supplied one + */ boolean hasNext(@NonNull Node node); + /** + * This method ensures that further content will start at a new line. If current + * last character is already a new line, then it won\'t do anything. + */ void ensureNewLine(); + /** + * This method inserts a new line without any condition checking (unlike {@link #ensureNewLine()}). + */ void forceNewLine(); + /** + * Helper method to call builder().length() + * + * @return current length of underlying {@link SpannableBuilder} + */ int length(); + /** + * Sets spans to underlying {@link SpannableBuilder} from start + * to {@link SpannableBuilder#length()}. + * + * @param start start position of spans + * @param spans to apply + */ void setSpans(int start, @Nullable Object spans); - // will automatically obtain SpanFactory instance and use it, it no SpanFactory is registered, - // will throw, if not desired use setSpansForNodeOptional + /** + * Helper method to obtain and apply spans for supplied Node. Internally queries {@link SpanFactory} + * for the node (via {@link MarkwonSpansFactory#require(Node)} thus throwing an exception + * if there is no {@link SpanFactory} registered for the node). + * + * @param node to retrieve {@link SpanFactory} for + * @param start start position for further {@link #setSpans(int, Object)} call + * @see #setSpansForNodeOptional(Node, int) + */ void setSpansForNode(@NonNull N node, int start); + /** + * The same as {@link #setSpansForNode(Node, int)} but can be used in situations when there is + * no access to a Node instance (for example in HTML rendering which doesn\'t have markdown Nodes). + * + * @see #setSpansForNode(Node, int) + */ void setSpansForNode(@NonNull Class node, int start); // does not throw if there is no SpanFactory registered for this node + + /** + * Helper method to apply spans from a {@link SpanFactory} if it\'s registered in + * {@link MarkwonSpansFactory} instance. Otherwise ignores this call (no spans will be applied). + * If there is a need to ensure that specified node has a {@link SpanFactory} registered, + * then {@link #setSpansForNode(Node, int)} can be used. {@link #setSpansForNode(Node, int)} internally + * uses {@link MarkwonSpansFactory#require(Node)}. This method uses {@link MarkwonSpansFactory#get(Node)}. + * + * @see #setSpansForNode(Node, int) + */ void setSpansForNodeOptional(@NonNull N node, int start); + /** + * The same as {@link #setSpansForNodeOptional(Node, int)} but can be used in situations when + * there is no access to a Node instance (for example in HTML rendering). + * + * @see #setSpansForNodeOptional(Node, int) + */ + @SuppressWarnings("unused") void setSpansForNodeOptional(@NonNull Class node, int start); } diff --git a/markwon/src/main/java/ru/noties/markwon/Prop.java b/markwon/src/main/java/ru/noties/markwon/Prop.java index a930288f..caacbfab 100644 --- a/markwon/src/main/java/ru/noties/markwon/Prop.java +++ b/markwon/src/main/java/ru/noties/markwon/Prop.java @@ -4,9 +4,11 @@ import android.support.annotation.NonNull; import android.support.annotation.Nullable; /** - * Class to hold data in {@link RenderProps} + * Class to hold data in {@link RenderProps}. Represents a certain property. * * @param represents the type that this instance holds + * @see #of(String) + * @see #of(Class, String) * @since 3.0.0 */ public final class Prop { diff --git a/markwon/src/main/java/ru/noties/markwon/RenderProps.java b/markwon/src/main/java/ru/noties/markwon/RenderProps.java index b939acc4..e28edbaf 100644 --- a/markwon/src/main/java/ru/noties/markwon/RenderProps.java +++ b/markwon/src/main/java/ru/noties/markwon/RenderProps.java @@ -17,4 +17,6 @@ public interface RenderProps { void set(@NonNull Prop prop, @Nullable T value); void clear(@NonNull Prop prop); + + void clearAll(); } diff --git a/markwon/src/main/java/ru/noties/markwon/RenderPropsImpl.java b/markwon/src/main/java/ru/noties/markwon/RenderPropsImpl.java index 81605ee9..75b6ac4b 100644 --- a/markwon/src/main/java/ru/noties/markwon/RenderPropsImpl.java +++ b/markwon/src/main/java/ru/noties/markwon/RenderPropsImpl.java @@ -45,6 +45,7 @@ public class RenderPropsImpl implements RenderProps { values.remove(prop); } + @Override public void clearAll() { values.clear(); } diff --git a/markwon/src/main/java/ru/noties/markwon/core/CorePlugin.java b/markwon/src/main/java/ru/noties/markwon/core/CorePlugin.java index 47024866..187f0173 100644 --- a/markwon/src/main/java/ru/noties/markwon/core/CorePlugin.java +++ b/markwon/src/main/java/ru/noties/markwon/core/CorePlugin.java @@ -3,6 +3,7 @@ package ru.noties.markwon.core; import android.support.annotation.NonNull; import android.support.annotation.Nullable; import android.support.annotation.VisibleForTesting; +import android.text.Spanned; import android.widget.TextView; import org.commonmark.node.BlockQuote; @@ -104,7 +105,7 @@ public class CorePlugin extends AbstractMarkwonPlugin { } @Override - public void beforeSetText(@NonNull TextView textView, @NonNull CharSequence markdown) { + public void beforeSetText(@NonNull TextView textView, @NonNull Spanned markdown) { OrderedListItemSpan.measure(textView, markdown); } @@ -123,7 +124,7 @@ public class CorePlugin extends AbstractMarkwonPlugin { public void visit(@NonNull MarkwonVisitor visitor, @NonNull StrongEmphasis strongEmphasis) { final int length = visitor.length(); visitor.visitChildren(strongEmphasis); - visitor.setSpansForNode(strongEmphasis, length); + visitor.setSpansForNodeOptional(strongEmphasis, length); } }); } @@ -134,7 +135,7 @@ public class CorePlugin extends AbstractMarkwonPlugin { public void visit(@NonNull MarkwonVisitor visitor, @NonNull Emphasis emphasis) { final int length = visitor.length(); visitor.visitChildren(emphasis); - visitor.setSpansForNode(emphasis, length); + visitor.setSpansForNodeOptional(emphasis, length); } }); } @@ -149,7 +150,7 @@ public class CorePlugin extends AbstractMarkwonPlugin { final int length = visitor.length(); visitor.visitChildren(blockQuote); - visitor.setSpansForNode(blockQuote, length); + visitor.setSpansForNodeOptional(blockQuote, length); if (visitor.hasNext(blockQuote)) { visitor.ensureNewLine(); @@ -173,7 +174,7 @@ public class CorePlugin extends AbstractMarkwonPlugin { .append(code.getLiteral()) .append('\u00a0'); - visitor.setSpansForNode(code, length); + visitor.setSpansForNodeOptional(code, length); } }); } @@ -215,7 +216,7 @@ public class CorePlugin extends AbstractMarkwonPlugin { visitor.builder().append('\u00a0'); - visitor.setSpansForNode(node, length); + visitor.setSpansForNodeOptional(node, length); if (visitor.hasNext(node)) { visitor.ensureNewLine(); @@ -260,7 +261,7 @@ public class CorePlugin extends AbstractMarkwonPlugin { CoreProps.BULLET_LIST_ITEM_LEVEL.set(visitor.renderProps(), listLevel(listItem)); } - visitor.setSpansForNode(listItem, length); + visitor.setSpansForNodeOptional(listItem, length); if (visitor.hasNext(listItem)) { visitor.ensureNewLine(); @@ -293,7 +294,7 @@ public class CorePlugin extends AbstractMarkwonPlugin { // without space it won't render visitor.builder().append('\u00a0'); - visitor.setSpansForNode(thematicBreak, length); + visitor.setSpansForNodeOptional(thematicBreak, length); if (visitor.hasNext(thematicBreak)) { visitor.ensureNewLine(); @@ -315,7 +316,7 @@ public class CorePlugin extends AbstractMarkwonPlugin { CoreProps.HEADING_LEVEL.set(visitor.renderProps(), heading.getLevel()); - visitor.setSpansForNode(heading, length); + visitor.setSpansForNodeOptional(heading, length); if (visitor.hasNext(heading)) { visitor.ensureNewLine(); @@ -399,7 +400,7 @@ public class CorePlugin extends AbstractMarkwonPlugin { CoreProps.LINK_DESTINATION.set(visitor.renderProps(), destination); - visitor.setSpansForNode(link, length); + visitor.setSpansForNodeOptional(link, length); } }); } diff --git a/markwon/src/main/java/ru/noties/markwon/core/MarkwonTheme.java b/markwon/src/main/java/ru/noties/markwon/core/MarkwonTheme.java index 347a2321..908e200f 100644 --- a/markwon/src/main/java/ru/noties/markwon/core/MarkwonTheme.java +++ b/markwon/src/main/java/ru/noties/markwon/core/MarkwonTheme.java @@ -16,6 +16,22 @@ import java.util.Locale; import ru.noties.markwon.utils.ColorUtils; import ru.noties.markwon.utils.Dip; +/** + * Class to hold theming information for rending of markdown. + *

+ * Since version 3.0.0 this class should be considered as CoreTheme as it\'s + * information holds data for core features only. But based on this other components can still use it + * to display markdown consistently. + *

+ * Since version 3.0.0 this class should not be instantiated manually. Instead a {@link ru.noties.markwon.MarkwonPlugin} + * should be used: {@link ru.noties.markwon.MarkwonPlugin#configureTheme(Builder)} + *

+ * Since version 3.0.0 properties related to strike-through, tables and HTML + * are moved to specific plugins in independent artifacts + * + * @see CorePlugin + * @see ru.noties.markwon.MarkwonPlugin#configureTheme(Builder) + */ @SuppressWarnings("WeakerAccess") public class MarkwonTheme { @@ -41,12 +57,28 @@ public class MarkwonTheme { * @see #builderWithDefaults(Context) * @see #builder(MarkwonTheme) * @since 1.0.0 + * @deprecated 3.0.0 */ @NonNull + @Deprecated public static Builder builder() { return new Builder(); } + /** + * Create an empty instance of {@link Builder} with no default values applied + *

+ * Since version 3.0.0 manual construction of {@link MarkwonTheme} is not required, instead a + * {@link ru.noties.markwon.MarkwonPlugin#configureTheme(Builder)} should be used in order + * to change certain theme properties + * + * @since 3.0.0 + */ + @NonNull + public static Builder builderNoDefaults() { + return new Builder(); + } + /** * Factory method to create a {@link Builder} instance and initialize it with values * from supplied {@link MarkwonTheme} @@ -549,6 +581,7 @@ public class MarkwonTheme { * @return self * @since 1.1.0 */ + @SuppressWarnings("UnusedReturnValue") @NonNull public Builder headingTextSizeMultipliers(@Size(6) @NonNull float[] headingTextSizeMultipliers) { this.headingTextSizeMultipliers = headingTextSizeMultipliers; diff --git a/markwon/src/main/java/ru/noties/markwon/image/ImagesPlugin.java b/markwon/src/main/java/ru/noties/markwon/image/ImagesPlugin.java index 7e91f288..dc51504f 100644 --- a/markwon/src/main/java/ru/noties/markwon/image/ImagesPlugin.java +++ b/markwon/src/main/java/ru/noties/markwon/image/ImagesPlugin.java @@ -2,6 +2,7 @@ package ru.noties.markwon.image; import android.content.Context; import android.support.annotation.NonNull; +import android.text.Spanned; import android.widget.TextView; import org.commonmark.node.Image; @@ -15,6 +16,7 @@ import ru.noties.markwon.MarkwonConfiguration; import ru.noties.markwon.MarkwonSpansFactory; import ru.noties.markwon.MarkwonVisitor; import ru.noties.markwon.RenderProps; +import ru.noties.markwon.SpanFactory; import ru.noties.markwon.image.data.DataUriSchemeHandler; import ru.noties.markwon.image.file.FileSchemeHandler; import ru.noties.markwon.image.network.NetworkSchemeHandler; @@ -68,6 +70,13 @@ public class ImagesPlugin extends AbstractMarkwonPlugin { @Override public void visit(@NonNull MarkwonVisitor visitor, @NonNull Image image) { + // if there is no image spanFactory, ignore + final SpanFactory spanFactory = visitor.configuration().spansFactory().get(image); + if (spanFactory == null) { + visitor.visitChildren(image); + return; + } + final int length = visitor.length(); visitor.visitChildren(image); @@ -86,22 +95,22 @@ public class ImagesPlugin extends AbstractMarkwonPlugin { .urlProcessor() .process(image.getDestination()); - final RenderProps context = visitor.renderProps(); + final RenderProps props = visitor.renderProps(); // apply image properties // Please note that we explicitly set IMAGE_SIZE to null as we do not clear // properties after we applied span (we could though) - ImageProps.DESTINATION.set(context, destination); - ImageProps.REPLACEMENT_TEXT_IS_LINK.set(context, link); - ImageProps.IMAGE_SIZE.set(context, null); + ImageProps.DESTINATION.set(props, destination); + ImageProps.REPLACEMENT_TEXT_IS_LINK.set(props, link); + ImageProps.IMAGE_SIZE.set(props, null); - visitor.setSpansForNode(image, length); + visitor.setSpans(length, spanFactory.getSpans(configuration, props)); } }); } @Override - public void beforeSetText(@NonNull TextView textView, @NonNull CharSequence markdown) { + public void beforeSetText(@NonNull TextView textView, @NonNull Spanned markdown) { AsyncDrawableScheduler.unschedule(textView); } diff --git a/markwon/src/test/java/ru/noties/markwon/core/suite/BaseSuiteTest.java b/markwon/src/test/java/ru/noties/markwon/core/suite/BaseSuiteTest.java index f91f2f13..98734db7 100644 --- a/markwon/src/test/java/ru/noties/markwon/core/suite/BaseSuiteTest.java +++ b/markwon/src/test/java/ru/noties/markwon/core/suite/BaseSuiteTest.java @@ -4,6 +4,7 @@ import android.support.annotation.NonNull; import android.support.annotation.Nullable; import android.text.Spanned; +import org.apache.commons.io.IOUtils; import org.commonmark.node.BlockQuote; import org.commonmark.node.Code; import org.commonmark.node.Emphasis; @@ -18,7 +19,8 @@ import org.commonmark.node.StrongEmphasis; import org.commonmark.node.ThematicBreak; import org.robolectric.RuntimeEnvironment; -import java.util.Collections; +import java.io.IOException; +import java.nio.charset.StandardCharsets; import java.util.HashMap; import java.util.Map; @@ -32,7 +34,6 @@ import ru.noties.markwon.core.CorePlugin; import ru.noties.markwon.core.CoreProps; import ru.noties.markwon.test.TestSpan; import ru.noties.markwon.test.TestSpanMatcher; -import ru.noties.markwon.test.TestUtil; import static ru.noties.markwon.test.TestSpan.args; import static ru.noties.markwon.test.TestSpan.span; @@ -62,7 +63,11 @@ abstract class BaseSuiteTest { @NonNull private String read(@NonNull String name) { - return TestUtil.read(this, "tests/" + name); + try { + return IOUtils.resourceToString("tests/" + name, StandardCharsets.UTF_8, getClass().getClassLoader()); + } catch (IOException e) { + throw new RuntimeException(e); + } } @NonNull @@ -160,12 +165,7 @@ abstract class BaseSuiteTest { @Nullable @Override public Object getSpans(@NonNull MarkwonConfiguration configuration, @NonNull RenderProps props) { - return span(name, extractArgs(props)); - } - - @NonNull - Map extractArgs(@NonNull RenderProps props) { - return Collections.emptyMap(); + return span(name); } } } diff --git a/markwon/src/test/java/ru/noties/markwon/syntax/SyntaxHighlightTest.java b/markwon/src/test/java/ru/noties/markwon/syntax/SyntaxHighlightTest.java index 732a182f..7e0a2af2 100644 --- a/markwon/src/test/java/ru/noties/markwon/syntax/SyntaxHighlightTest.java +++ b/markwon/src/test/java/ru/noties/markwon/syntax/SyntaxHighlightTest.java @@ -13,6 +13,7 @@ import org.junit.runner.RunWith; import org.robolectric.RobolectricTestRunner; import org.robolectric.annotation.Config; +import java.util.Arrays; import java.util.Collections; import java.util.Map; @@ -74,7 +75,7 @@ public class SyntaxHighlightTest { }; final MarkwonSpansFactory spansFactory = mock(MarkwonSpansFactory.class); - when(spansFactory.require(any(FencedCodeBlock.class))).thenReturn(new SpanFactory() { + when(spansFactory.get(any(FencedCodeBlock.class))).thenReturn(new SpanFactory() { @Override public Object getSpans(@NonNull MarkwonConfiguration configuration, @NonNull RenderProps props) { return codeSpan; @@ -103,7 +104,7 @@ public class SyntaxHighlightTest { final FencedCodeBlock fencedCodeBlock = new FencedCodeBlock(); fencedCodeBlock.setLiteral("{code}"); - CorePluginBridge.visitCodeBlock(visitor, null, "{code}", fencedCodeBlock); + CorePluginBridge.visitCodeBlock(visitor, null, fencedCodeBlock.getLiteral(), fencedCodeBlock); final int end = builder.length(); @@ -115,7 +116,7 @@ public class SyntaxHighlightTest { // each character + code span final int length = fencedCodeBlock.getLiteral().length() + 1; - assertEquals(length, spans.length); + assertEquals(Arrays.toString(spans), length, spans.length); assertEquals(codeSpan, spans[0]); // each character diff --git a/settings.gradle b/settings.gradle index ede0753a..d5f56c45 100644 --- a/settings.gradle +++ b/settings.gradle @@ -11,6 +11,5 @@ include ':app', ':markwon-html', ':markwon-view', ':markwon-test-span', - ':markwon-test-util', ':sample-custom-extension', ':sample-latex-math' From 83547d38c3d95982cdced5e7beb0b727493aa0b6 Mon Sep 17 00:00:00 2001 From: Dimitry Ivanov Date: Sun, 23 Dec 2018 16:36:30 +0300 Subject: [PATCH 049/103] Remove module from project --- settings.gradle | 1 - 1 file changed, 1 deletion(-) diff --git a/settings.gradle b/settings.gradle index d5f56c45..a4ab85b4 100644 --- a/settings.gradle +++ b/settings.gradle @@ -9,7 +9,6 @@ include ':app', ':markwon-image-gif', ':markwon-syntax-highlight', ':markwon-html', - ':markwon-view', ':markwon-test-span', ':sample-custom-extension', ':sample-latex-math' From 2aef492e7d52139b4c02a590bf277501a79f04f9 Mon Sep 17 00:00:00 2001 From: Dimitry Ivanov Date: Sun, 23 Dec 2018 16:50:48 +0300 Subject: [PATCH 050/103] Fix build --- app/src/main/java/ru/noties/markwon/MainActivity.java | 3 ++- app/src/main/java/ru/noties/markwon/MarkdownRenderer.java | 5 +++-- .../main/java/ru/noties/markwon/ext/tables/TablePlugin.java | 3 ++- .../src/main/java/ru/noties/markwon/core/MarkwonTheme.java | 3 ++- 4 files changed, 9 insertions(+), 5 deletions(-) diff --git a/app/src/main/java/ru/noties/markwon/MainActivity.java b/app/src/main/java/ru/noties/markwon/MainActivity.java index f55c3ccc..b50fcbd5 100644 --- a/app/src/main/java/ru/noties/markwon/MainActivity.java +++ b/app/src/main/java/ru/noties/markwon/MainActivity.java @@ -6,6 +6,7 @@ import android.net.Uri; import android.os.Bundle; import android.support.annotation.NonNull; import android.support.annotation.Nullable; +import android.text.Spanned; import android.text.method.LinkMovementMethod; import android.view.View; import android.widget.TextView; @@ -71,7 +72,7 @@ public class MainActivity extends Activity { public void apply(final String text) { markdownRenderer.render(MainActivity.this, themes.isLight(), uri(), text, new MarkdownRenderer.MarkdownReadyListener() { @Override - public void onMarkdownReady(@NonNull Markwon markwon, CharSequence markdown) { + public void onMarkdownReady(@NonNull Markwon markwon, Spanned markdown) { markwon.setParsedMarkdown(textView, markdown); diff --git a/app/src/main/java/ru/noties/markwon/MarkdownRenderer.java b/app/src/main/java/ru/noties/markwon/MarkdownRenderer.java index 5c8658e7..c082312a 100644 --- a/app/src/main/java/ru/noties/markwon/MarkdownRenderer.java +++ b/app/src/main/java/ru/noties/markwon/MarkdownRenderer.java @@ -6,6 +6,7 @@ import android.os.Handler; import android.os.SystemClock; import android.support.annotation.NonNull; import android.support.annotation.Nullable; +import android.text.Spanned; import java.util.concurrent.ExecutorService; import java.util.concurrent.Future; @@ -34,7 +35,7 @@ import ru.noties.prism4j.Prism4j; public class MarkdownRenderer { interface MarkdownReadyListener { - void onMarkdownReady(@NonNull Markwon markwon, CharSequence markdown); + void onMarkdownReady(@NonNull Markwon markwon, Spanned markdown); } @Inject @@ -105,7 +106,7 @@ public class MarkdownRenderer { final long start = SystemClock.uptimeMillis(); - final CharSequence text = markwon.toMarkdown(markdown); + final Spanned text = markwon.toMarkdown(markdown); final long end = SystemClock.uptimeMillis(); diff --git a/markwon-ext-tables/src/main/java/ru/noties/markwon/ext/tables/TablePlugin.java b/markwon-ext-tables/src/main/java/ru/noties/markwon/ext/tables/TablePlugin.java index b4bbfb00..d9220b01 100644 --- a/markwon-ext-tables/src/main/java/ru/noties/markwon/ext/tables/TablePlugin.java +++ b/markwon-ext-tables/src/main/java/ru/noties/markwon/ext/tables/TablePlugin.java @@ -2,6 +2,7 @@ package ru.noties.markwon.ext.tables; import android.content.Context; import android.support.annotation.NonNull; +import android.text.Spanned; import android.widget.TextView; import org.commonmark.ext.gfm.tables.TableBody; @@ -49,7 +50,7 @@ public class TablePlugin extends AbstractMarkwonPlugin { } @Override - public void beforeSetText(@NonNull TextView textView, @NonNull CharSequence markdown) { + public void beforeSetText(@NonNull TextView textView, @NonNull Spanned markdown) { TableRowsScheduler.unschedule(textView); } diff --git a/markwon/src/main/java/ru/noties/markwon/core/MarkwonTheme.java b/markwon/src/main/java/ru/noties/markwon/core/MarkwonTheme.java index 908e200f..030c0a29 100644 --- a/markwon/src/main/java/ru/noties/markwon/core/MarkwonTheme.java +++ b/markwon/src/main/java/ru/noties/markwon/core/MarkwonTheme.java @@ -56,6 +56,7 @@ public class MarkwonTheme { * @return {@link Builder instance} * @see #builderWithDefaults(Context) * @see #builder(MarkwonTheme) + * @see #emptyBuilder() * @since 1.0.0 * @deprecated 3.0.0 */ @@ -75,7 +76,7 @@ public class MarkwonTheme { * @since 3.0.0 */ @NonNull - public static Builder builderNoDefaults() { + public static Builder emptyBuilder() { return new Builder(); } From 30532f9e1d8f42f6f1bb161106fa3ff5e8bcf584 Mon Sep 17 00:00:00 2001 From: Dimitry Ivanov Date: Mon, 24 Dec 2018 13:22:31 +0300 Subject: [PATCH 051/103] Configure strikethrough extension artifact --- markwon-ext-strikethrough/README.md | 29 ++++ markwon-ext-strikethrough/build.gradle | 8 ++ markwon-ext-strikethrough/gradle.properties | 3 + .../strikethrough/StrikethroughPlugin.java | 10 +- .../StrikethroughPluginTest.java | 129 ++++++++++++++++++ markwon/build.gradle | 1 - .../ru/noties/markwon/core/MarkwonTheme.java | 3 +- 7 files changed, 180 insertions(+), 3 deletions(-) create mode 100644 markwon-ext-strikethrough/README.md create mode 100644 markwon-ext-strikethrough/gradle.properties create mode 100644 markwon-ext-strikethrough/src/test/java/ru/noties/markwon/ext/strikethrough/StrikethroughPluginTest.java diff --git a/markwon-ext-strikethrough/README.md b/markwon-ext-strikethrough/README.md new file mode 100644 index 00000000..55b9e542 --- /dev/null +++ b/markwon-ext-strikethrough/README.md @@ -0,0 +1,29 @@ +# Strikethrough + +[![markwon-ext-strikethrough](https://img.shields.io/maven-central/v/ru.noties/markwon-ext-strikethrough.svg?label=markwon-ext-strikethrough)](http://search.maven.org/#search|ga|1|g%3A%22ru.noties%22%20AND%20a%3A%22markwon-ext-strikethrough%22) + +This module adds `strikethrough` functionality to `Markwon` via `StrikethroughPlugin`: + +```java +Markwon.builder(context) + .usePlugin(StrikethroughPlugin.create()) +``` + +This plugin registers `SpanFactory` for `Strikethrough` node, so it's possible to customize Strikethrough Span that is used in rendering: + +```java +Markwon.builder(context) + .usePlugin(StrikethroughPlugin.class) + .usePlugin(new AbstractMarkwonPlugin() { + @Override + public void configureSpansFactory(@NonNull MarkwonSpansFactory.Builder builder) { + builder.setFactory(Strikethrough.class, new SpanFactory() { + @Override + public Object getSpans(@NonNull MarkwonConfiguration configuration, @NonNull RenderProps props) { + // will use Underline span instead of Strikethrough + return new UnderlineSpan(); + } + }); + } + }) +``` \ No newline at end of file diff --git a/markwon-ext-strikethrough/build.gradle b/markwon-ext-strikethrough/build.gradle index b4daa0d1..3490b416 100644 --- a/markwon-ext-strikethrough/build.gradle +++ b/markwon-ext-strikethrough/build.gradle @@ -20,6 +20,14 @@ dependencies { deps.with { api it['commonmark-strikethrough'] } + + deps.test.with { + testImplementation project(':markwon-test-span') + testImplementation it['junit'] + testImplementation it['mockito'] + testImplementation it['robolectric'] + testImplementation it['ix-java'] + } } registerArtifact(this) \ No newline at end of file diff --git a/markwon-ext-strikethrough/gradle.properties b/markwon-ext-strikethrough/gradle.properties new file mode 100644 index 00000000..86d609ac --- /dev/null +++ b/markwon-ext-strikethrough/gradle.properties @@ -0,0 +1,3 @@ +POM_NAME=Markwon-Ext-Strikethrough +POM_ARTIFACT_ID=markwon-ext-strikethrough +POM_PACKAGING=aar \ No newline at end of file diff --git a/markwon-ext-strikethrough/src/main/java/ru/noties/markwon/ext/strikethrough/StrikethroughPlugin.java b/markwon-ext-strikethrough/src/main/java/ru/noties/markwon/ext/strikethrough/StrikethroughPlugin.java index 96b76672..f73ccc71 100644 --- a/markwon-ext-strikethrough/src/main/java/ru/noties/markwon/ext/strikethrough/StrikethroughPlugin.java +++ b/markwon-ext-strikethrough/src/main/java/ru/noties/markwon/ext/strikethrough/StrikethroughPlugin.java @@ -16,6 +16,14 @@ import ru.noties.markwon.MarkwonVisitor; import ru.noties.markwon.RenderProps; import ru.noties.markwon.SpanFactory; +/** + * Plugin to add strikethrough markdown feature. This plugin will extend commonmark-java.Parser + * with strikethrough extension, add SpanFactory and register commonmark-java.Strikethrough node + * visitor + * + * @see #create() + * @since 3.0.0 + */ public class StrikethroughPlugin extends AbstractMarkwonPlugin { @NonNull @@ -45,7 +53,7 @@ public class StrikethroughPlugin extends AbstractMarkwonPlugin { public void visit(@NonNull MarkwonVisitor visitor, @NonNull Strikethrough strikethrough) { final int length = visitor.length(); visitor.visitChildren(strikethrough); - visitor.setSpansForNode(strikethrough, length); + visitor.setSpansForNodeOptional(strikethrough, length); } }); } diff --git a/markwon-ext-strikethrough/src/test/java/ru/noties/markwon/ext/strikethrough/StrikethroughPluginTest.java b/markwon-ext-strikethrough/src/test/java/ru/noties/markwon/ext/strikethrough/StrikethroughPluginTest.java new file mode 100644 index 00000000..18cab664 --- /dev/null +++ b/markwon-ext-strikethrough/src/test/java/ru/noties/markwon/ext/strikethrough/StrikethroughPluginTest.java @@ -0,0 +1,129 @@ +package ru.noties.markwon.ext.strikethrough; + +import android.support.annotation.NonNull; +import android.text.style.StrikethroughSpan; + +import org.commonmark.Extension; +import org.commonmark.ext.gfm.strikethrough.Strikethrough; +import org.commonmark.ext.gfm.strikethrough.StrikethroughExtension; +import org.commonmark.parser.Parser; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.ArgumentCaptor; +import org.robolectric.RobolectricTestRunner; +import org.robolectric.RuntimeEnvironment; +import org.robolectric.annotation.Config; + +import java.util.List; + +import ix.Ix; +import ru.noties.markwon.AbstractMarkwonPlugin; +import ru.noties.markwon.Markwon; +import ru.noties.markwon.MarkwonConfiguration; +import ru.noties.markwon.MarkwonSpansFactory; +import ru.noties.markwon.MarkwonVisitor; +import ru.noties.markwon.RenderProps; +import ru.noties.markwon.SpanFactory; +import ru.noties.markwon.core.CorePlugin; +import ru.noties.markwon.test.TestSpan; +import ru.noties.markwon.test.TestSpanMatcher; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertTrue; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static ru.noties.markwon.test.TestSpan.document; +import static ru.noties.markwon.test.TestSpan.span; +import static ru.noties.markwon.test.TestSpan.text; + +@RunWith(RobolectricTestRunner.class) +@Config(manifest = Config.NONE) +public class StrikethroughPluginTest { + + @Test + public void plugin_parser_extension_registered() { + // configure parser is called with proper parser extension + + final StrikethroughPlugin plugin = StrikethroughPlugin.create(); + final Parser.Builder parserBuilder = mock(Parser.Builder.class); + plugin.configureParser(parserBuilder); + + //noinspection unchecked + final ArgumentCaptor> captor = ArgumentCaptor.forClass(Iterable.class); + + //noinspection unchecked + verify(parserBuilder, times(1)).extensions(captor.capture()); + + final List list = Ix.from(captor.getValue()).toList(); + assertEquals(1, list.size()); + + assertTrue(list.get(0) instanceof StrikethroughExtension); + } + + @Test + public void plugin_span_factory_registered() { + // strikethrough has proper spanFactory registered + + final StrikethroughPlugin plugin = StrikethroughPlugin.create(); + final MarkwonSpansFactory.Builder spansFactoryBuilder = mock(MarkwonSpansFactory.Builder.class); + plugin.configureSpansFactory(spansFactoryBuilder); + + final ArgumentCaptor captor = ArgumentCaptor.forClass(SpanFactory.class); + + verify(spansFactoryBuilder, times(1)) + .setFactory(eq(Strikethrough.class), captor.capture()); + + assertTrue(captor.getValue().getSpans(mock(MarkwonConfiguration.class), mock(RenderProps.class)) instanceof StrikethroughSpan); + } + + @Test + public void plugin_node_visitor_registered() { + // visit has strikethrough node visitor registered + + final StrikethroughPlugin plugin = StrikethroughPlugin.create(); + final MarkwonVisitor.Builder visitorBuilder = mock(MarkwonVisitor.Builder.class); + plugin.configureVisitor(visitorBuilder); + + final ArgumentCaptor captor = + ArgumentCaptor.forClass(MarkwonVisitor.NodeVisitor.class); + + //noinspection unchecked + verify(visitorBuilder, times(1)).on(eq(Strikethrough.class), captor.capture()); + + assertNotNull(captor.getValue()); + } + + @Test + public void markdown() { + + final String input = "Hello ~~strike~~ and ~~through~~"; + + final Markwon markwon = Markwon.builder(RuntimeEnvironment.application) + .usePlugin(CorePlugin.create()) + .usePlugin(StrikethroughPlugin.create()) + .usePlugin(new AbstractMarkwonPlugin() { + @Override + public void configureSpansFactory(@NonNull MarkwonSpansFactory.Builder builder) { + builder.setFactory(Strikethrough.class, new SpanFactory() { + @Override + public Object getSpans(@NonNull MarkwonConfiguration configuration, @NonNull RenderProps props) { + return span("strikethrough"); + } + }); + } + }) + .build(); + + final TestSpan.Document document = document( + text("Hello "), + span("strikethrough", text("strike")), + text(" and "), + span("strikethrough", text("through")) + ); + + TestSpanMatcher.matches(markwon.toMarkdown(input), document); + } +} \ No newline at end of file diff --git a/markwon/build.gradle b/markwon/build.gradle index 8682d261..0f6f15db 100644 --- a/markwon/build.gradle +++ b/markwon/build.gradle @@ -28,7 +28,6 @@ dependencies { testImplementation it['robolectric'] testImplementation it['mockito'] - testImplementation it['ix-java'] testImplementation it['commons-io'] } } diff --git a/markwon/src/main/java/ru/noties/markwon/core/MarkwonTheme.java b/markwon/src/main/java/ru/noties/markwon/core/MarkwonTheme.java index 030c0a29..fc97875a 100644 --- a/markwon/src/main/java/ru/noties/markwon/core/MarkwonTheme.java +++ b/markwon/src/main/java/ru/noties/markwon/core/MarkwonTheme.java @@ -75,6 +75,7 @@ public class MarkwonTheme { * * @since 3.0.0 */ + @SuppressWarnings("unused") @NonNull public static Builder emptyBuilder() { return new Builder(); @@ -519,7 +520,7 @@ public class MarkwonTheme { return this; } - @SuppressWarnings("SameParameterValue") + @SuppressWarnings({"SameParameterValue", "UnusedReturnValue"}) @NonNull public Builder codeBackgroundColor(@ColorInt int codeBackgroundColor) { this.codeBackgroundColor = codeBackgroundColor; From eabe1d99944f5085e4386c7666899f8bc40112e9 Mon Sep 17 00:00:00 2001 From: Dimitry Ivanov Date: Mon, 24 Dec 2018 13:39:50 +0300 Subject: [PATCH 052/103] Add image-okhttp module --- markwon-image-okhttp/build.gradle | 25 ++++++++ .../src/main/AndroidManifest.xml | 1 + .../okhttp/MarkwonImageOkHttpPlugin.java | 45 +++++++++++++++ .../image/okhttp/OkHttpSchemeHandler.java | 57 +++++++++++++++++++ .../image/network/NetworkSchemeHandler.java | 6 ++ settings.gradle | 1 + 6 files changed, 135 insertions(+) create mode 100644 markwon-image-okhttp/build.gradle create mode 100644 markwon-image-okhttp/src/main/AndroidManifest.xml create mode 100644 markwon-image-okhttp/src/main/java/ru/noties/markwon/image/okhttp/MarkwonImageOkHttpPlugin.java create mode 100644 markwon-image-okhttp/src/main/java/ru/noties/markwon/image/okhttp/OkHttpSchemeHandler.java diff --git a/markwon-image-okhttp/build.gradle b/markwon-image-okhttp/build.gradle new file mode 100644 index 00000000..cafe07c2 --- /dev/null +++ b/markwon-image-okhttp/build.gradle @@ -0,0 +1,25 @@ +apply plugin: 'com.android.library' + +android { + + compileSdkVersion config['compile-sdk'] + buildToolsVersion config['build-tools'] + + defaultConfig { + minSdkVersion config['min-sdk'] + targetSdkVersion config['target-sdk'] + versionCode 1 + versionName version + } +} + +dependencies { + + api project(':markwon') + + deps.with { + api it['okhttp'] + } +} + +registerArtifact(this) \ No newline at end of file diff --git a/markwon-image-okhttp/src/main/AndroidManifest.xml b/markwon-image-okhttp/src/main/AndroidManifest.xml new file mode 100644 index 00000000..32240579 --- /dev/null +++ b/markwon-image-okhttp/src/main/AndroidManifest.xml @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/markwon-image-okhttp/src/main/java/ru/noties/markwon/image/okhttp/MarkwonImageOkHttpPlugin.java b/markwon-image-okhttp/src/main/java/ru/noties/markwon/image/okhttp/MarkwonImageOkHttpPlugin.java new file mode 100644 index 00000000..bc197c16 --- /dev/null +++ b/markwon-image-okhttp/src/main/java/ru/noties/markwon/image/okhttp/MarkwonImageOkHttpPlugin.java @@ -0,0 +1,45 @@ +package ru.noties.markwon.image.okhttp; + +import android.support.annotation.NonNull; + +import java.util.Arrays; + +import okhttp3.OkHttpClient; +import ru.noties.markwon.AbstractMarkwonPlugin; +import ru.noties.markwon.image.AsyncDrawableLoader; +import ru.noties.markwon.image.network.NetworkSchemeHandler; + +/** + * Plugin to use OkHttpClient to obtain images from network (http and https schemes) + * + * @see #create() + * @see #create(OkHttpClient) + * @since 3.0.0 + */ +@SuppressWarnings("WeakerAccess") +public class MarkwonImageOkHttpPlugin extends AbstractMarkwonPlugin { + + @NonNull + public static MarkwonImageOkHttpPlugin create() { + return new MarkwonImageOkHttpPlugin(new OkHttpClient()); + } + + @NonNull + public static MarkwonImageOkHttpPlugin create(@NonNull OkHttpClient okHttpClient) { + return new MarkwonImageOkHttpPlugin(okHttpClient); + } + + private final OkHttpClient client; + + MarkwonImageOkHttpPlugin(@NonNull OkHttpClient client) { + this.client = client; + } + + @Override + public void configureImages(@NonNull AsyncDrawableLoader.Builder builder) { + builder.addSchemeHandler( + Arrays.asList(NetworkSchemeHandler.SCHEME_HTTP, NetworkSchemeHandler.SCHEME_HTTPS), + new OkHttpSchemeHandler(client) + ); + } +} diff --git a/markwon-image-okhttp/src/main/java/ru/noties/markwon/image/okhttp/OkHttpSchemeHandler.java b/markwon-image-okhttp/src/main/java/ru/noties/markwon/image/okhttp/OkHttpSchemeHandler.java new file mode 100644 index 00000000..eaf73bd6 --- /dev/null +++ b/markwon-image-okhttp/src/main/java/ru/noties/markwon/image/okhttp/OkHttpSchemeHandler.java @@ -0,0 +1,57 @@ +package ru.noties.markwon.image.okhttp; + +import android.net.Uri; +import android.support.annotation.NonNull; +import android.support.annotation.Nullable; + +import java.io.IOException; +import java.io.InputStream; + +import okhttp3.OkHttpClient; +import okhttp3.Request; +import okhttp3.Response; +import okhttp3.ResponseBody; +import ru.noties.markwon.image.ImageItem; +import ru.noties.markwon.image.SchemeHandler; + +class OkHttpSchemeHandler extends SchemeHandler { + + private static final String HEADER_CONTENT_TYPE = "Content-Type"; + + private final OkHttpClient client; + + OkHttpSchemeHandler(@NonNull OkHttpClient client) { + this.client = client; + } + + @Nullable + @Override + public ImageItem handle(@NonNull String raw, @NonNull Uri uri) { + ImageItem out = null; + + final Request request = new Request.Builder() + .url(raw) + .tag(raw) + .build(); + + Response response = null; + try { + response = client.newCall(request).execute(); + } catch (IOException e) { + e.printStackTrace(); + } + + if (response != null) { + final ResponseBody body = response.body(); + if (body != null) { + final InputStream inputStream = body.byteStream(); + if (inputStream != null) { + final String contentType = response.header(HEADER_CONTENT_TYPE); + out = new ImageItem(contentType, inputStream); + } + } + } + + return out; + } +} diff --git a/markwon/src/main/java/ru/noties/markwon/image/network/NetworkSchemeHandler.java b/markwon/src/main/java/ru/noties/markwon/image/network/NetworkSchemeHandler.java index c5352d4b..ebaaf803 100644 --- a/markwon/src/main/java/ru/noties/markwon/image/network/NetworkSchemeHandler.java +++ b/markwon/src/main/java/ru/noties/markwon/image/network/NetworkSchemeHandler.java @@ -13,6 +13,12 @@ import java.net.URL; import ru.noties.markwon.image.ImageItem; import ru.noties.markwon.image.SchemeHandler; +/** + * A simple network scheme handler that is not dependent on any external libraries. + * + * @see #create() + * @since 3.0.0 + */ public class NetworkSchemeHandler extends SchemeHandler { public static final String SCHEME_HTTP = "http"; diff --git a/settings.gradle b/settings.gradle index a4ab85b4..7ae3b4b8 100644 --- a/settings.gradle +++ b/settings.gradle @@ -5,6 +5,7 @@ include ':app', ':markwon-ext-strikethrough', ':markwon-ext-tables', ':markwon-ext-tasklist', + ':markwon-image-okhttp', ':markwon-image-svg', ':markwon-image-gif', ':markwon-syntax-highlight', From c58c31d5fd21b5906c2a6a7abf235062ea6a3ad0 Mon Sep 17 00:00:00 2001 From: Dimitry Ivanov Date: Mon, 24 Dec 2018 15:50:53 +0300 Subject: [PATCH 053/103] Add clear after markdown render action and SpannableBuilder#clear --- markwon/src/main/java/ru/noties/markwon/MarkwonImpl.java | 8 +++++--- .../src/main/java/ru/noties/markwon/MarkwonVisitor.java | 5 +++++ .../main/java/ru/noties/markwon/MarkwonVisitorImpl.java | 6 ++++++ .../src/main/java/ru/noties/markwon/SpannableBuilder.java | 8 ++++++++ 4 files changed, 24 insertions(+), 3 deletions(-) diff --git a/markwon/src/main/java/ru/noties/markwon/MarkwonImpl.java b/markwon/src/main/java/ru/noties/markwon/MarkwonImpl.java index 8e2d8791..d9b74a25 100644 --- a/markwon/src/main/java/ru/noties/markwon/MarkwonImpl.java +++ b/markwon/src/main/java/ru/noties/markwon/MarkwonImpl.java @@ -63,10 +63,12 @@ class MarkwonImpl extends Markwon { plugin.afterRender(node, visitor); } - // clear render props after rending - renderProps.clearAll(); + final Spanned spanned = visitor.builder().spannableStringBuilder(); - return visitor.builder().spannableStringBuilder(); + // clear render props and builder after rending + visitor.clear(); + + return spanned; } @NonNull diff --git a/markwon/src/main/java/ru/noties/markwon/MarkwonVisitor.java b/markwon/src/main/java/ru/noties/markwon/MarkwonVisitor.java index aec63444..01f9b819 100644 --- a/markwon/src/main/java/ru/noties/markwon/MarkwonVisitor.java +++ b/markwon/src/main/java/ru/noties/markwon/MarkwonVisitor.java @@ -88,6 +88,11 @@ public interface MarkwonVisitor extends Visitor { */ void setSpans(int start, @Nullable Object spans); + /** + * Clears state of visitor (both {@link RenderProps} and {@link SpannableBuilder} will be cleared + */ + void clear(); + /** * Helper method to obtain and apply spans for supplied Node. Internally queries {@link SpanFactory} * for the node (via {@link MarkwonSpansFactory#require(Node)} thus throwing an exception diff --git a/markwon/src/main/java/ru/noties/markwon/MarkwonVisitorImpl.java b/markwon/src/main/java/ru/noties/markwon/MarkwonVisitorImpl.java index 2616d843..4c4caff8 100644 --- a/markwon/src/main/java/ru/noties/markwon/MarkwonVisitorImpl.java +++ b/markwon/src/main/java/ru/noties/markwon/MarkwonVisitorImpl.java @@ -231,6 +231,12 @@ class MarkwonVisitorImpl implements MarkwonVisitor { SpannableBuilder.setSpans(builder, spans, start, builder.length()); } + @Override + public void clear() { + renderProps.clearAll(); + builder.clear(); + } + @Override public void setSpansForNode(@NonNull N node, int start) { setSpans(start, configuration.spansFactory().require(node).getSpans(configuration, renderProps)); diff --git a/markwon/src/main/java/ru/noties/markwon/SpannableBuilder.java b/markwon/src/main/java/ru/noties/markwon/SpannableBuilder.java index 556d7cc7..bd583311 100644 --- a/markwon/src/main/java/ru/noties/markwon/SpannableBuilder.java +++ b/markwon/src/main/java/ru/noties/markwon/SpannableBuilder.java @@ -325,6 +325,14 @@ public class SpannableBuilder implements Appendable, CharSequence { return reversed; } + /** + * @since 3.0.0 + */ + public void clear() { + builder.setLength(0); + spans.clear(); + } + private void copySpans(final int index, @Nullable CharSequence cs) { // we must identify already reversed Spanned... From f6ac3fde684bf13fb72d542c938b735f953d3fe7 Mon Sep 17 00:00:00 2001 From: Dimitry Ivanov Date: Mon, 24 Dec 2018 18:16:17 +0300 Subject: [PATCH 054/103] Add markwon-recycler module (work in progress) --- build.gradle | 3 + .../ext/tables/TableRowsScheduler.java | 2 - .../src/main/res/values/ids.xml | 6 + markwon-recycler/build.gradle | 25 ++ markwon-recycler/src/main/AndroidManifest.xml | 1 + .../markwon/recycler/MarkwonAdapter.java | 110 ++++++ .../markwon/recycler/MarkwonAdapterImpl.java | 209 ++++++++++++ .../markwon/recycler/MarkwonRecycler.java | 4 + .../markwon/recycler/SimpleNodeEntry.java | 84 +++++ .../main/res/layout/adapter_simple_entry.xml | 14 + markwon/src/main/res/values/ids.xml | 1 - sample-custom-extension/build.gradle | 7 + .../src/main/AndroidManifest.xml | 13 +- .../src/main/assets/README.md | 313 ++++++++++++++++++ .../recycler/MarkwonRecyclerActivity.java | 143 ++++++++ .../extension/recycler/TableNodeEntry.java | 289 ++++++++++++++++ .../drawable-v24/ic_launcher_foreground.xml | 34 -- .../res/drawable/ic_launcher_background.xml | 170 ---------- .../src/main/res/layout/activity_recycler.xml | 5 + .../res/layout/adapter_fenced_code_block.xml | 24 ++ .../main/res/layout/adapter_table_block.xml | 16 + .../res/mipmap-anydpi-v26/ic_launcher.xml | 5 - .../mipmap-anydpi-v26/ic_launcher_round.xml | 5 - .../src/main/res/mipmap-hdpi/ic_launcher.png | Bin 3056 -> 0 bytes .../res/mipmap-hdpi/ic_launcher_round.png | Bin 5024 -> 0 bytes .../src/main/res/mipmap-mdpi/ic_launcher.png | Bin 2096 -> 0 bytes .../res/mipmap-mdpi/ic_launcher_round.png | Bin 2858 -> 0 bytes .../src/main/res/mipmap-xhdpi/ic_launcher.png | Bin 4569 -> 0 bytes .../res/mipmap-xhdpi/ic_launcher_round.png | Bin 7098 -> 0 bytes .../main/res/mipmap-xxhdpi/ic_launcher.png | Bin 6464 -> 0 bytes .../res/mipmap-xxhdpi/ic_launcher_round.png | Bin 10676 -> 0 bytes .../main/res/mipmap-xxxhdpi/ic_launcher.png | Bin 9250 -> 0 bytes .../res/mipmap-xxxhdpi/ic_launcher_round.png | Bin 15523 -> 0 bytes settings.gradle | 5 +- 34 files changed, 1266 insertions(+), 222 deletions(-) create mode 100644 markwon-ext-tables/src/main/res/values/ids.xml create mode 100644 markwon-recycler/build.gradle create mode 100644 markwon-recycler/src/main/AndroidManifest.xml create mode 100644 markwon-recycler/src/main/java/ru/noties/markwon/recycler/MarkwonAdapter.java create mode 100644 markwon-recycler/src/main/java/ru/noties/markwon/recycler/MarkwonAdapterImpl.java create mode 100644 markwon-recycler/src/main/java/ru/noties/markwon/recycler/MarkwonRecycler.java create mode 100644 markwon-recycler/src/main/java/ru/noties/markwon/recycler/SimpleNodeEntry.java create mode 100644 markwon-recycler/src/main/res/layout/adapter_simple_entry.xml create mode 100644 sample-custom-extension/src/main/assets/README.md create mode 100644 sample-custom-extension/src/main/java/ru/noties/markwon/sample/extension/recycler/MarkwonRecyclerActivity.java create mode 100644 sample-custom-extension/src/main/java/ru/noties/markwon/sample/extension/recycler/TableNodeEntry.java delete mode 100644 sample-custom-extension/src/main/res/drawable-v24/ic_launcher_foreground.xml delete mode 100644 sample-custom-extension/src/main/res/drawable/ic_launcher_background.xml create mode 100644 sample-custom-extension/src/main/res/layout/activity_recycler.xml create mode 100644 sample-custom-extension/src/main/res/layout/adapter_fenced_code_block.xml create mode 100644 sample-custom-extension/src/main/res/layout/adapter_table_block.xml delete mode 100644 sample-custom-extension/src/main/res/mipmap-anydpi-v26/ic_launcher.xml delete mode 100644 sample-custom-extension/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml delete mode 100644 sample-custom-extension/src/main/res/mipmap-hdpi/ic_launcher.png delete mode 100644 sample-custom-extension/src/main/res/mipmap-hdpi/ic_launcher_round.png delete mode 100644 sample-custom-extension/src/main/res/mipmap-mdpi/ic_launcher.png delete mode 100644 sample-custom-extension/src/main/res/mipmap-mdpi/ic_launcher_round.png delete mode 100644 sample-custom-extension/src/main/res/mipmap-xhdpi/ic_launcher.png delete mode 100644 sample-custom-extension/src/main/res/mipmap-xhdpi/ic_launcher_round.png delete mode 100644 sample-custom-extension/src/main/res/mipmap-xxhdpi/ic_launcher.png delete mode 100644 sample-custom-extension/src/main/res/mipmap-xxhdpi/ic_launcher_round.png delete mode 100644 sample-custom-extension/src/main/res/mipmap-xxxhdpi/ic_launcher.png delete mode 100644 sample-custom-extension/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png diff --git a/build.gradle b/build.gradle index f5ccc8a1..05e0927e 100644 --- a/build.gradle +++ b/build.gradle @@ -49,6 +49,8 @@ ext { 'push-aar-gradle': 'https://raw.githubusercontent.com/noties/gradle-mvn-push/master/gradle-mvn-push-aar.gradle' ] + // for now 27.1.1 is used because it's the last one distributed with source files + // next version with sources is androidx one (we wait until migration) final def supportVersion = '27.1.1' final def commonMarkVersion = '0.12.1' final def daggerVersion = '2.10' @@ -56,6 +58,7 @@ ext { deps = [ 'support-annotations' : "com.android.support:support-annotations:$supportVersion", 'support-app-compat' : "com.android.support:appcompat-v7:$supportVersion", + 'support-recycler-view' : "com.android.support:recyclerview-v7:$supportVersion", 'commonmark' : "com.atlassian.commonmark:commonmark:$commonMarkVersion", 'commonmark-strikethrough': "com.atlassian.commonmark:commonmark-ext-gfm-strikethrough:$commonMarkVersion", 'commonmark-table' : "com.atlassian.commonmark:commonmark-ext-gfm-tables:$commonMarkVersion", diff --git a/markwon-ext-tables/src/main/java/ru/noties/markwon/ext/tables/TableRowsScheduler.java b/markwon-ext-tables/src/main/java/ru/noties/markwon/ext/tables/TableRowsScheduler.java index 2f85cf1f..b3b4c773 100644 --- a/markwon-ext-tables/src/main/java/ru/noties/markwon/ext/tables/TableRowsScheduler.java +++ b/markwon-ext-tables/src/main/java/ru/noties/markwon/ext/tables/TableRowsScheduler.java @@ -7,8 +7,6 @@ import android.text.TextUtils; import android.view.View; import android.widget.TextView; -import ru.noties.markwon.renderer.R; - abstract class TableRowsScheduler { static void schedule(@NonNull final TextView view) { diff --git a/markwon-ext-tables/src/main/res/values/ids.xml b/markwon-ext-tables/src/main/res/values/ids.xml new file mode 100644 index 00000000..72ba0ee3 --- /dev/null +++ b/markwon-ext-tables/src/main/res/values/ids.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/markwon-recycler/build.gradle b/markwon-recycler/build.gradle new file mode 100644 index 00000000..8728a5fb --- /dev/null +++ b/markwon-recycler/build.gradle @@ -0,0 +1,25 @@ +apply plugin: 'com.android.library' + +android { + + compileSdkVersion config['compile-sdk'] + buildToolsVersion config['build-tools'] + + defaultConfig { + minSdkVersion config['min-sdk'] + targetSdkVersion config['target-sdk'] + versionCode 1 + versionName version + } +} + +dependencies { + + api project(':markwon') + + deps.with { + api it['support-recycler-view'] + } +} + +registerArtifact(this) \ No newline at end of file diff --git a/markwon-recycler/src/main/AndroidManifest.xml b/markwon-recycler/src/main/AndroidManifest.xml new file mode 100644 index 00000000..4af698d1 --- /dev/null +++ b/markwon-recycler/src/main/AndroidManifest.xml @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/markwon-recycler/src/main/java/ru/noties/markwon/recycler/MarkwonAdapter.java b/markwon-recycler/src/main/java/ru/noties/markwon/recycler/MarkwonAdapter.java new file mode 100644 index 00000000..c5334879 --- /dev/null +++ b/markwon-recycler/src/main/java/ru/noties/markwon/recycler/MarkwonAdapter.java @@ -0,0 +1,110 @@ +package ru.noties.markwon.recycler; + +import android.support.annotation.IdRes; +import android.support.annotation.LayoutRes; +import android.support.annotation.NonNull; +import android.support.annotation.Nullable; +import android.support.v7.widget.RecyclerView; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; + +import org.commonmark.node.Node; + +import java.util.List; + +import ru.noties.markwon.Markwon; + +// each node block will be rendered by a simple TextView, but we can provide own entries for each block +public abstract class MarkwonAdapter extends RecyclerView.Adapter { + + @NonNull + public static Builder builder() { + return new MarkwonAdapterImpl.BuilderImpl(); + } + + @NonNull + public static MarkwonAdapter create() { + return new MarkwonAdapterImpl.BuilderImpl().build(); + } + + // for an adapter with only one entry (all blocks are rendered the same with this entry) + @NonNull + public static MarkwonAdapter create(@NonNull Entry defaultEntry) { + return new MarkwonAdapterImpl.BuilderImpl().defaultEntry(defaultEntry).build(); + } + + @NonNull + public static MarkwonAdapter create(@LayoutRes int layoutResId) { + return new MarkwonAdapterImpl.BuilderImpl().defaultEntry(layoutResId).build(); + } + + public interface Builder { + + @NonNull + Builder include( + @NonNull Class node, + @NonNull Entry entry); + + @NonNull + Builder defaultEntry(@NonNull Entry defaultEntry); + + @NonNull + Builder defaultEntry(@LayoutRes int layoutResId); + + @NonNull + Builder reducer(@NonNull Reducer reducer); + + @NonNull + MarkwonAdapter build(); + } + + public interface Entry { + + @NonNull + H createHolder(@NonNull LayoutInflater inflater, @NonNull ViewGroup parent); + + void bindHolder(@NonNull Markwon markwon, @NonNull H holder, @NonNull N node); + + long id(@NonNull N node); + + // will be called when new content is available (clear internal cache if any) + void clear(); + } + + public interface Reducer { + + @NonNull + List reduce(@NonNull Node root); + } + + public abstract void setMarkdown(@NonNull Markwon markwon, @NonNull String markdown); + + public abstract void setParsedMarkdown(@NonNull Markwon markwon, @NonNull Node document); + + public abstract void setParsedMarkdown(@NonNull Markwon markwon, @NonNull List nodes); + + @SuppressWarnings("WeakerAccess") + public static class Holder extends RecyclerView.ViewHolder { + + public Holder(@NonNull View itemView) { + super(itemView); + } + + // please note that this method should be called after constructor + @Nullable + protected V findView(@IdRes int id) { + return itemView.findViewById(id); + } + + // please note that this method should be called after constructor + @NonNull + protected V requireView(@IdRes int id) { + final V v = itemView.findViewById(id); + if (v == null) { + throw new NullPointerException(); + } + return v; + } + } +} diff --git a/markwon-recycler/src/main/java/ru/noties/markwon/recycler/MarkwonAdapterImpl.java b/markwon-recycler/src/main/java/ru/noties/markwon/recycler/MarkwonAdapterImpl.java new file mode 100644 index 00000000..f95cf129 --- /dev/null +++ b/markwon-recycler/src/main/java/ru/noties/markwon/recycler/MarkwonAdapterImpl.java @@ -0,0 +1,209 @@ +package ru.noties.markwon.recycler; + +import android.support.annotation.NonNull; +import android.util.Log; +import android.util.SparseArray; +import android.view.LayoutInflater; +import android.view.ViewGroup; + +import org.commonmark.node.Node; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +import ru.noties.markwon.Markwon; + +class MarkwonAdapterImpl extends MarkwonAdapter { + + private final SparseArray> entries; + private final Entry defaultEntry; + private final Reducer reducer; + + private LayoutInflater layoutInflater; + + private Markwon markwon; + private List nodes; + + MarkwonAdapterImpl( + @NonNull SparseArray> entries, + @NonNull Entry defaultEntry, + @NonNull Reducer reducer) { + this.entries = entries; + this.defaultEntry = defaultEntry; + this.reducer = reducer; + setHasStableIds(true); + } + + @Override + public void setMarkdown(@NonNull Markwon markwon, @NonNull String markdown) { + setParsedMarkdown(markwon, markwon.parse(markdown)); + } + + @Override + public void setParsedMarkdown(@NonNull Markwon markwon, @NonNull Node document) { + setParsedMarkdown(markwon, reducer.reduce(document)); + } + + @Override + public void setParsedMarkdown(@NonNull Markwon markwon, @NonNull List nodes) { + // clear all entries before applying + + defaultEntry.clear(); + + for (int i = 0, size = entries.size(); i < size; i++) { + entries.valueAt(i).clear(); + } + + this.markwon = markwon; + this.nodes = nodes; + } + + @NonNull + @Override + public Holder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) { + + if (layoutInflater == null) { + layoutInflater = LayoutInflater.from(parent.getContext()); + } + + final Entry entry = viewType == 0 + ? defaultEntry + : entries.get(viewType); + + return entry.createHolder(layoutInflater, parent); + } + + @Override + public void onBindViewHolder(@NonNull Holder holder, int position) { + + final Node node = nodes.get(position); + final int viewType = getNodeViewType(node.getClass()); + + final Entry entry = viewType == 0 + ? defaultEntry + : entries.get(viewType); + + entry.bindHolder(markwon, holder, node); + } + + @Override + public int getItemCount() { + return nodes != null + ? nodes.size() + : 0; + } + + @NonNull + public List getItems() { + return nodes != null + ? Collections.unmodifiableList(nodes) + : Collections.emptyList(); + } + + @Override + public int getItemViewType(int position) { + return getNodeViewType(nodes.get(position).getClass()); + } + + @Override + public long getItemId(int position) { + final Node node = nodes.get(position); + final int type = getNodeViewType(node.getClass()); + final Entry entry = type == 0 + ? defaultEntry + : entries.get(type); + return entry.id(node); + } + + public int getNodeViewType(@NonNull Class node) { + // if has registered -> then return it, else 0 + final int hash = node.hashCode(); + if (entries.indexOfKey(hash) > -1) { + return hash; + } + return 0; + } + + static class BuilderImpl implements Builder { + + private final SparseArray> entries = new SparseArray<>(3); + + private Entry defaultEntry; + private Reducer reducer; + + @NonNull + @Override + public Builder include( + @NonNull Class node, + @NonNull Entry entry) { + //noinspection unchecked + entries.append(node.hashCode(), (Entry) entry); + return this; + } + + @NonNull + @Override + public Builder defaultEntry(@NonNull Entry defaultEntry) { + //noinspection unchecked + this.defaultEntry = (Entry) defaultEntry; + return this; + } + + @NonNull + @Override + public Builder defaultEntry(int layoutResId) { + //noinspection unchecked + this.defaultEntry = (Entry) (Entry) new SimpleNodeEntry(layoutResId); + return this; + } + + @NonNull + @Override + public Builder reducer(@NonNull Reducer reducer) { + this.reducer = reducer; + return this; + } + + @NonNull + @Override + public MarkwonAdapter build() { + + if (defaultEntry == null) { + //noinspection unchecked + defaultEntry = (Entry) (Entry) new SimpleNodeEntry(); + } + + if (reducer == null) { + reducer = new ReducerImpl(); + } + + return new MarkwonAdapterImpl(entries, defaultEntry, reducer); + } + } + + static class ReducerImpl implements Reducer { + + @NonNull + @Override + public List reduce(@NonNull Node root) { + + final List list = new ArrayList<>(); + +// // we will extract all blocks that are direct children of Document + Node node = root.getFirstChild(); + Node temp; + + while (node != null) { + list.add(node); + temp = node.getNext(); + node.unlink(); + node = temp; + } + + Log.e("NODES", list.toString()); + + return list; + } + } +} diff --git a/markwon-recycler/src/main/java/ru/noties/markwon/recycler/MarkwonRecycler.java b/markwon-recycler/src/main/java/ru/noties/markwon/recycler/MarkwonRecycler.java new file mode 100644 index 00000000..2244f37b --- /dev/null +++ b/markwon-recycler/src/main/java/ru/noties/markwon/recycler/MarkwonRecycler.java @@ -0,0 +1,4 @@ +package ru.noties.markwon.recycler; + +public class MarkwonRecycler { +} diff --git a/markwon-recycler/src/main/java/ru/noties/markwon/recycler/SimpleNodeEntry.java b/markwon-recycler/src/main/java/ru/noties/markwon/recycler/SimpleNodeEntry.java new file mode 100644 index 00000000..e5d64590 --- /dev/null +++ b/markwon-recycler/src/main/java/ru/noties/markwon/recycler/SimpleNodeEntry.java @@ -0,0 +1,84 @@ +package ru.noties.markwon.recycler; + +import android.support.annotation.LayoutRes; +import android.support.annotation.NonNull; +import android.text.Spannable; +import android.text.SpannableString; +import android.text.Spanned; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.TextView; + +import org.commonmark.node.Node; + +import java.util.HashMap; +import java.util.Map; + +import ru.noties.markwon.Markwon; + +public class SimpleNodeEntry implements MarkwonAdapter.Entry { + + private static final NoCopySpannableFactory FACTORY = new NoCopySpannableFactory(); + + // small cache, maybe add pre-compute of text, also spannableFactory (so no copying of spans) + private final Map cache = new HashMap<>(); + + private final int layoutResId; + + public SimpleNodeEntry() { + this(R.layout.adapter_simple_entry); + } + + public SimpleNodeEntry(@LayoutRes int layoutResId) { + this.layoutResId = layoutResId; + } + + @NonNull + @Override + public Holder createHolder(@NonNull LayoutInflater inflater, @NonNull ViewGroup parent) { + return new Holder(inflater.inflate(layoutResId, parent, false)); + } + + @Override + public void bindHolder(@NonNull Markwon markwon, @NonNull Holder holder, @NonNull Node node) { + Spanned spanned = cache.get(node); + if (spanned == null) { + spanned = markwon.render(node); + cache.put(node, spanned); + } + markwon.setParsedMarkdown(holder.textView, spanned); + } + + @Override + public long id(@NonNull Node node) { + return node.hashCode(); + } + + @Override + public void clear() { + cache.clear(); + } + + static class Holder extends MarkwonAdapter.Holder { + + final TextView textView; + + Holder(@NonNull View itemView) { + super(itemView); + + this.textView = requireView(R.id.text); + this.textView.setSpannableFactory(FACTORY); + } + } + + private static class NoCopySpannableFactory extends Spannable.Factory { + + @Override + public Spannable newSpannable(CharSequence source) { + return source instanceof Spannable + ? (Spannable) source + : new SpannableString(source); + } + } +} diff --git a/markwon-recycler/src/main/res/layout/adapter_simple_entry.xml b/markwon-recycler/src/main/res/layout/adapter_simple_entry.xml new file mode 100644 index 00000000..7c40a29f --- /dev/null +++ b/markwon-recycler/src/main/res/layout/adapter_simple_entry.xml @@ -0,0 +1,14 @@ + + \ No newline at end of file diff --git a/markwon/src/main/res/values/ids.xml b/markwon/src/main/res/values/ids.xml index de911baf..90fb8322 100644 --- a/markwon/src/main/res/values/ids.xml +++ b/markwon/src/main/res/values/ids.xml @@ -2,6 +2,5 @@ - \ No newline at end of file diff --git a/sample-custom-extension/build.gradle b/sample-custom-extension/build.gradle index 5672dfe9..ac0573a5 100644 --- a/sample-custom-extension/build.gradle +++ b/sample-custom-extension/build.gradle @@ -19,4 +19,11 @@ android { dependencies { implementation project(':markwon') + + implementation project(':markwon-image-svg') + implementation project(':markwon-recycler') + implementation project(':markwon-ext-tables') + implementation project(':markwon-html') + + implementation deps['debug'] } diff --git a/sample-custom-extension/src/main/AndroidManifest.xml b/sample-custom-extension/src/main/AndroidManifest.xml index 1553a0a6..bc18eebe 100644 --- a/sample-custom-extension/src/main/AndroidManifest.xml +++ b/sample-custom-extension/src/main/AndroidManifest.xml @@ -1,14 +1,16 @@ + + + android:theme="@style/AppTheme" + tools:ignore="AllowBackup,GoogleAppIndexingWarning,MissingApplicationIcon"> @@ -16,6 +18,11 @@ + + + \ No newline at end of file diff --git a/sample-custom-extension/src/main/assets/README.md b/sample-custom-extension/src/main/assets/README.md new file mode 100644 index 00000000..be6976a9 --- /dev/null +++ b/sample-custom-extension/src/main/assets/README.md @@ -0,0 +1,313 @@ +![logo](./art/markwon_logo.png) + +# Markwon + +[![markwon](https://img.shields.io/maven-central/v/ru.noties/markwon.svg?label=markwon)](http://search.maven.org/#search|ga|1|g%3A%22ru.noties%22%20AND%20a%3A%22markwon%22) +[![markwon-image-loader](https://img.shields.io/maven-central/v/ru.noties/markwon-image-loader.svg?label=markwon-image-loader)](http://search.maven.org/#search|ga|1|g%3A%22ru.noties%22%20AND%20a%3A%22markwon-image-loader%22) +[![markwon-syntax-highlight](https://img.shields.io/maven-central/v/ru.noties/markwon-syntax-highlight.svg?label=markwon-syntax-highlight)](http://search.maven.org/#search|ga|1|g%3A%22ru.noties%22%20AND%20a%3A%22markwon-syntax-highlight%22) +[![markwon-view](https://img.shields.io/maven-central/v/ru.noties/markwon-view.svg?label=markwon-view)](http://search.maven.org/#search|ga|1|g%3A%22ru.noties%22%20AND%20a%3A%22markwon-view%22) + +[![Build Status](https://travis-ci.org/noties/Markwon.svg?branch=master)](https://travis-ci.org/noties/Markwon) + +**Markwon** is a markdown library for Android. It parses markdown +following [commonmark-spec] with the help of amazing [commonmark-java] +library and renders result as _Android-native_ Spannables. **No HTML** +is involved as an intermediate step. **No WebView** is required. +It's extremely fast, feature-rich and extensible. + +It gives ability to display markdown in all TextView widgets +(**TextView**, **Button**, **Switch**, **CheckBox**, etc), **Toasts** +and all other places that accept **Spanned content**. Library provides +reasonable defaults to display style of a markdown content but also +gives all the means to tweak the appearance if desired. All markdown +features listed in [commonmark-spec] are supported +(including support for **inlined/block HTML code**, **markdown tables**, +**images** and **syntax highlight**). + +[commonmark-spec]: https://spec.commonmark.org/0.28/ +[commonmark-java]: https://github.com/atlassian/commonmark-java/blob/master/README.md + +**This file is displayed by default in the [sample-apk] (`markwon-sample-{latest-version}-debug.apk`) application. Which is a generic markdown viewer with support to display markdown via `http`, `https` & `file` schemes and 2 themes included: Light & Dark* + +[sample-apk]: https://github.com/noties/Markwon/releases + +## Installation +```groovy +implementation "ru.noties:markwon:${markwonVersion}" +implementation "ru.noties:markwon-image-loader:${markwonVersion}" // optional +implementation "ru.noties:markwon-syntax-highlight:${markwonVersion}" // optional +implementation "ru.noties:markwon-view:${markwonVersion}" // optional +``` + +Please visit [documentation] web-site for further reference + +## Supported markdown features: +* Emphasis (`*`, `_`) +* Strong emphasis (`**`, `__`) +* Strike-through (`~~`) +* Headers (`#{1,6}`) +* Links (`[]()` && `[][]`) +* Images +* Thematic break (`---`, `***`, `___`) +* Quotes & nested quotes (`>{1,}`) +* Ordered & non-ordered lists & nested ones +* Inline code +* Code blocks +* Tables (*with limitations*) +* Syntax highlight +* HTML + * Emphasis (``, ``, ``, ``) + * Strong emphasis (``, ``) + * SuperScript (``) + * SubScript (``) + * Underline (``, `ins`) + * Strike-through (``, ``, ``) + * Link (`a`) + * Lists (`ul`, `ol`) + * Images (`img` will require configured image loader) + * Blockquote (`blockquote`) + * Heading (`h1`, `h2`, `h3`, `h4`, `h5`, `h6`) + * there is support to render any HTML tag +* Task lists: +- [ ] Not _done_ + - [X] **Done** with `X` + - [x] ~~and~~ **or** small `x` +--- + +## Screenshots + +Taken with default configuration (except for image loading): + + + + + + +By default configuration uses TextView textColor for styling, so changing textColor changes style + +--- + +## Documentation + +Please visit [documentation] web-site for reference + +[documentation]: https://noties.github.io/Markwon + +--- + +## Applications using Markwon + +* [Partiko](https://partiko.app) +* [FairNote Notepad](https://play.google.com/store/apps/details?id=com.rgiskard.fairnote) + + +--- + +# Demo +Based on [this cheatsheet][cheatsheet] + +--- + +## Headers +--- +# Header 1 +## Header 2 +### Header 3 +#### Header 4 +##### Header 5 +###### Header 6 +--- + +## Emphasis + +Emphasis, aka italics, with *asterisks* or _underscores_. + +Strong emphasis, aka bold, with **asterisks** or __underscores__. + +Combined emphasis with **asterisks and _underscores_**. + +Strikethrough uses two tildes. ~~Scratch this.~~ + +--- + +## Lists +1. First ordered list item +2. Another item + * Unordered sub-list. +1. Actual numbers don't matter, just that it's a number + 1. Ordered sub-list +4. And another item. + + You can have properly indented paragraphs within list items. Notice the blank line above, and the leading spaces (at least one, but we'll use three here to also align the raw Markdown). + + To have a line break without a paragraph, you will need to use two trailing spaces. + Note that this line is separate, but within the same paragraph. + (This is contrary to the typical GFM line break behaviour, where trailing spaces are not required.) + +* Unordered list can use asterisks +- Or minuses ++ Or pluses + +--- + +## Links + +[I'm an inline-style link](https://www.google.com) + +[I'm a reference-style link][Arbitrary case-insensitive reference text] + +[I'm a relative reference to a repository file](../blob/master/LICENSE) + +[You can use numbers for reference-style link definitions][1] + +Or leave it empty and use the [link text itself]. + +--- + +## Code + +Inline `code` has `back-ticks around` it. + +```javascript +var s = "JavaScript syntax highlighting"; +alert(s); +``` + +```python +s = "Python syntax highlighting" +print s +``` + +```java +/** + * Helper method to obtain a Parser with registered strike-through & table extensions + * & task lists (added in 1.0.1) + * + * @return a Parser instance that is supported by this library + * @since 1.0.0 + */ +@NonNull +public static Parser createParser() { + return new Parser.Builder() + .extensions(Arrays.asList( + StrikethroughExtension.create(), + TablesExtension.create(), + TaskListExtension.create() + )) + .build(); +} +``` + +```xml + + + + + +``` + +``` +No language indicated, so no syntax highlighting. +But let's throw in a tag. +``` + +--- + +## Tables + +Colons can be used to align columns. + +| Tables | Are | Cool | +| ------------- |:-------------:| -----:| +| col 3 is | right-aligned | $1600 | +| col 2 is | centered | $12 | +| zebra stripes | are neat | $1 | + +There must be at least 3 dashes separating each header cell. +The outer pipes (|) are optional, and you don't need to make the +raw Markdown line up prettily. You can also use inline Markdown. + +Markdown | Less | Pretty +--- | --- | --- +*Still* | `renders` | **nicely** +1 | 2 | 3 + +--- + +## Blockquotes + +> Blockquotes are very handy in email to emulate reply text. +> This line is part of the same quote. + +Quote break. + +> This is a very long line that will still be quoted properly when it wraps. Oh boy let's keep writing to make sure this is long enough to actually wrap for everyone. Oh, you can *put* **Markdown** into a blockquote. + +Nested quotes +> Hello! +>> And to you! + +--- + +## Inline HTML + +```html +HTML +``` + +HTML + +--- + +## Horizontal Rule + +Three or more... + +--- + +Hyphens (`-`) + +*** + +Asterisks (`*`) + +___ + +Underscores (`_`) + + +## License + +``` + Copyright 2017 Dimitry Ivanov (mail@dimitryivanov.ru) + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +``` + +[cheatsheet]: https://github.com/adam-p/markdown-here/wiki/Markdown-Cheatsheet + +[arbitrary case-insensitive reference text]: https://www.mozilla.org +[1]: http://slashdot.org +[link text itself]: http://www.reddit.com diff --git a/sample-custom-extension/src/main/java/ru/noties/markwon/sample/extension/recycler/MarkwonRecyclerActivity.java b/sample-custom-extension/src/main/java/ru/noties/markwon/sample/extension/recycler/MarkwonRecyclerActivity.java new file mode 100644 index 00000000..4ae85cc5 --- /dev/null +++ b/sample-custom-extension/src/main/java/ru/noties/markwon/sample/extension/recycler/MarkwonRecyclerActivity.java @@ -0,0 +1,143 @@ +package ru.noties.markwon.sample.extension.recycler; + +import android.app.Activity; +import android.content.Context; +import android.net.Uri; +import android.os.Bundle; +import android.support.annotation.NonNull; +import android.support.annotation.Nullable; +import android.support.v7.widget.LinearLayoutManager; +import android.support.v7.widget.RecyclerView; +import android.text.TextUtils; + +import org.commonmark.ext.gfm.tables.TableBlock; +import org.commonmark.node.FencedCodeBlock; + +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; + +import ru.noties.debug.AndroidLogDebugOutput; +import ru.noties.debug.Debug; +import ru.noties.markwon.AbstractMarkwonPlugin; +import ru.noties.markwon.Markwon; +import ru.noties.markwon.MarkwonConfiguration; +import ru.noties.markwon.core.CorePlugin; +import ru.noties.markwon.ext.tables.TablePlugin; +import ru.noties.markwon.html.HtmlPlugin; +import ru.noties.markwon.image.ImagesPlugin; +import ru.noties.markwon.image.svg.SvgPlugin; +import ru.noties.markwon.recycler.MarkwonAdapter; +import ru.noties.markwon.recycler.SimpleNodeEntry; +import ru.noties.markwon.sample.extension.R; +import ru.noties.markwon.urlprocessor.UrlProcessor; +import ru.noties.markwon.urlprocessor.UrlProcessorRelativeToAbsolute; + +// we will create a standalone `sample` module with all these samples, right now this +// module has unrelated things +public class MarkwonRecyclerActivity extends Activity { + + static { + Debug.init(new AndroidLogDebugOutput(true)); + } + + @Override + public void onCreate(@Nullable Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setContentView(R.layout.activity_recycler); + + final MarkwonAdapter adapter = MarkwonAdapter.builder() + .include(FencedCodeBlock.class, new SimpleNodeEntry(R.layout.adapter_fenced_code_block)) + .include(TableBlock.class, new TableNodeEntry()) + .build(); + + final RecyclerView recyclerView = findViewById(R.id.recycler_view); + recyclerView.setLayoutManager(new LinearLayoutManager(this)); + recyclerView.setHasFixedSize(true); + recyclerView.setAdapter(adapter); + + final Markwon markwon = markwon(this); + adapter.setMarkdown(markwon, loadReadMe(this)); + adapter.notifyDataSetChanged(); + } + + @NonNull + private static Markwon markwon(@NonNull Context context) { + return Markwon.builder(context) + .usePlugin(CorePlugin.create()) + .usePlugin(ImagesPlugin.createWithAssets(context)) + .usePlugin(SvgPlugin.create(context.getResources())) + .usePlugin(TablePlugin.create(context)) + .usePlugin(HtmlPlugin.create()) + .usePlugin(new AbstractMarkwonPlugin() { + @Override + public void configureConfiguration(@NonNull MarkwonConfiguration.Builder builder) { + builder.urlProcessor(new UrlProcessorInitialReadme()); + } + }) + .build(); + } + + private static String loadReadMe(@NonNull Context context) { + InputStream stream = null; + try { + stream = context.getAssets().open("README.md"); + } catch (IOException e) { + e.printStackTrace(); + } + return readStream(stream); + } + + private static String readStream(@Nullable InputStream inputStream) { + + String out = null; + + if (inputStream != null) { + BufferedReader reader = null; + try { + reader = new BufferedReader(new InputStreamReader(inputStream)); + final StringBuilder builder = new StringBuilder(); + String line; + while ((line = reader.readLine()) != null) { + builder.append(line) + .append('\n'); + } + out = builder.toString(); + } catch (IOException e) { + e.printStackTrace(); + } finally { + if (reader != null) { + try { + reader.close(); + } catch (IOException e) { + // no op + } + } + } + } + + return out; + } + + private static class UrlProcessorInitialReadme implements UrlProcessor { + + private static final String GITHUB_BASE = "https://github.com/noties/Markwon/raw/master/"; + + private final UrlProcessorRelativeToAbsolute processor + = new UrlProcessorRelativeToAbsolute(GITHUB_BASE); + + @NonNull + @Override + public String process(@NonNull String destination) { + String out; + final Uri uri = Uri.parse(destination); + if (TextUtils.isEmpty(uri.getScheme())) { + out = processor.process(destination); + } else { + out = destination; + } + return out; + } + } +} diff --git a/sample-custom-extension/src/main/java/ru/noties/markwon/sample/extension/recycler/TableNodeEntry.java b/sample-custom-extension/src/main/java/ru/noties/markwon/sample/extension/recycler/TableNodeEntry.java new file mode 100644 index 00000000..31c05783 --- /dev/null +++ b/sample-custom-extension/src/main/java/ru/noties/markwon/sample/extension/recycler/TableNodeEntry.java @@ -0,0 +1,289 @@ +package ru.noties.markwon.sample.extension.recycler; + +import android.support.annotation.NonNull; +import android.util.Log; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.TableLayout; +import android.widget.TableRow; +import android.widget.TextView; + +import org.commonmark.ext.gfm.tables.TableBlock; +import org.commonmark.node.AbstractVisitor; +import org.commonmark.node.BlockQuote; +import org.commonmark.node.BulletList; +import org.commonmark.node.Code; +import org.commonmark.node.CustomBlock; +import org.commonmark.node.CustomNode; +import org.commonmark.node.Document; +import org.commonmark.node.Emphasis; +import org.commonmark.node.FencedCodeBlock; +import org.commonmark.node.HardLineBreak; +import org.commonmark.node.Heading; +import org.commonmark.node.HtmlBlock; +import org.commonmark.node.HtmlInline; +import org.commonmark.node.Image; +import org.commonmark.node.IndentedCodeBlock; +import org.commonmark.node.Link; +import org.commonmark.node.ListItem; +import org.commonmark.node.Node; +import org.commonmark.node.OrderedList; +import org.commonmark.node.Paragraph; +import org.commonmark.node.SoftLineBreak; +import org.commonmark.node.StrongEmphasis; +import org.commonmark.node.Text; +import org.commonmark.node.ThematicBreak; + +import ru.noties.debug.Debug; +import ru.noties.markwon.Markwon; +import ru.noties.markwon.recycler.MarkwonAdapter; +import ru.noties.markwon.sample.extension.R; + +// do not use in real applications, this is just a showcase +public class TableNodeEntry implements MarkwonAdapter.Entry { + + @NonNull + @Override + public TableNodeHolder createHolder(@NonNull LayoutInflater inflater, @NonNull ViewGroup parent) { + return new TableNodeHolder(inflater.inflate(R.layout.adapter_table_block, parent, false)); + } + + @Override + public void bindHolder(@NonNull Markwon markwon, @NonNull TableNodeHolder holder, @NonNull TableBlock node) { + + if (true) { + Debug.e("###############"); + Debug.e("NODE: %s", node); + node.accept(new PrintVisitor()); + Debug.e("NODE: %s", node); + Debug.e("###############"); + } + + final TableLayout layout = holder.layout; + layout.removeAllViews(); + + // each child represents a row (head or regular) + // first direct child is TableHead or TableBody + Node child = node.getFirstChild().getFirstChild(); + Node temp; + + while (child != null) { + Log.e("BIND-ROWS", String.valueOf(child)); + temp = child.getNext(); + addRow(markwon, layout, child); + child = temp; + } + + Log.e("BIND", String.valueOf(layout.getChildCount())); + if (true) { + final ViewGroup group = (ViewGroup) layout.getChildAt(0); + Log.e("BIND-GROUP", String.valueOf(group.getChildCount())); + for (int i = 0; i < group.getChildCount(); i++) { + Log.e("BIND-CHILD-" + i, String.valueOf(group.getChildAt(i)) + ", " + ((TextView) group.getChildAt(i)).getText()); + } + } + + layout.requestLayout(); + } + + private void addRow(@NonNull Markwon markwon, @NonNull TableLayout layout, @NonNull Node node) { + + final TableRow tableRow = new TableRow(layout.getContext()); +// final TableRow.LayoutParams params = new TableRow.LayoutParams(100, 100); + tableRow.setBackgroundColor(0x80ff0000); + + TextView textView; + RenderNode renderNode; + Node temp; + + // each child in a row represents a cell + Node child = node.getFirstChild(); + while (child != null) { + Log.e("BIND-CELL", String.valueOf(child)); + textView = new TextView(layout.getContext()); + textView.setLayoutParams(new TableRow.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT)); + renderNode = new RenderNode(); + temp = child.getNext(); + copy(child, renderNode); + tableRow.addView(textView); + markwon.setParsedMarkdown(textView, markwon.render(renderNode)); + child = temp; + } + + layout.addView(tableRow); + } + + private static void copy(@NonNull Node from, @NonNull Node to) { + Node child = from.getFirstChild(); + Node temp; + while (child != null) { + Log.e("BIND-COPY", String.valueOf(child)); + temp = child.getNext(); + to.appendChild(child); + child = temp; + } + } + + @Override + public long id(@NonNull TableBlock node) { + return node.hashCode(); + } + + @Override + public void clear() { + + } + + static class TableNodeHolder extends MarkwonAdapter.Holder { + + final TableLayout layout; + + TableNodeHolder(@NonNull View itemView) { + super(itemView); + + this.layout = requireView(R.id.table_layout); + } + } + + private static class RenderNode extends CustomBlock { + + } + + private static class PrintVisitor extends AbstractVisitor { + + private final RenderNode renderNode = new RenderNode(); + + @Override + public void visit(BlockQuote blockQuote) { + Debug.i("blockQuote: %s", blockQuote); + super.visit(blockQuote); + } + + @Override + public void visit(BulletList bulletList) { + Debug.i("bulletList: %s", bulletList); + super.visit(bulletList); + } + + @Override + public void visit(Code code) { + Debug.i("code: %s", code); + super.visit(code); + } + + @Override + public void visit(Document document) { + Debug.i("document: %s", document); + super.visit(document); + } + + @Override + public void visit(Emphasis emphasis) { + Debug.i("emphasis: %s", emphasis); + super.visit(emphasis); + } + + @Override + public void visit(FencedCodeBlock fencedCodeBlock) { + Debug.i("fencedCodeBlock: %s", fencedCodeBlock); + super.visit(fencedCodeBlock); + } + + @Override + public void visit(HardLineBreak hardLineBreak) { + Debug.i("hardLineBreak: %s", hardLineBreak); + super.visit(hardLineBreak); + } + + @Override + public void visit(Heading heading) { + Debug.i("heading: %s", heading); + super.visit(heading); + } + + @Override + public void visit(ThematicBreak thematicBreak) { + Debug.i("thematicBreak: %s", thematicBreak); + super.visit(thematicBreak); + } + + @Override + public void visit(HtmlInline htmlInline) { + Debug.i("htmlInline: %s", htmlInline); + super.visit(htmlInline); + } + + @Override + public void visit(HtmlBlock htmlBlock) { + Debug.i("htmlBlock: %s", htmlBlock); + super.visit(htmlBlock); + } + + @Override + public void visit(Image image) { + Debug.i("image: %s", image); + super.visit(image); + } + + @Override + public void visit(IndentedCodeBlock indentedCodeBlock) { + Debug.i("indentedCodeBlock: %s", indentedCodeBlock); + super.visit(indentedCodeBlock); + } + + @Override + public void visit(Link link) { + Debug.i("link: %s", link); + super.visit(link); + } + + @Override + public void visit(ListItem listItem) { + Debug.i("listItem: %s", listItem); + super.visit(listItem); + } + + @Override + public void visit(OrderedList orderedList) { + Debug.i("orderedList: %s", orderedList); + super.visit(orderedList); + } + + @Override + public void visit(Paragraph paragraph) { + Debug.i("paragraph: %s", paragraph); + super.visit(paragraph); + } + + @Override + public void visit(SoftLineBreak softLineBreak) { + Debug.i("softLineBreak: %s", softLineBreak); + super.visit(softLineBreak); + } + + @Override + public void visit(StrongEmphasis strongEmphasis) { + Debug.i("strongEmphasis: %s", strongEmphasis); + super.visit(strongEmphasis); + } + + @Override + public void visit(Text text) { + Debug.i("text: %s", text); + super.visit(text); + } + + @Override + public void visit(CustomBlock customBlock) { + Debug.i("customBlock: %s", customBlock); + super.visit(customBlock); + } + + @Override + public void visit(CustomNode customNode) { + Debug.i("customNode: %s", customNode); + super.visit(customNode); + } + } +} diff --git a/sample-custom-extension/src/main/res/drawable-v24/ic_launcher_foreground.xml b/sample-custom-extension/src/main/res/drawable-v24/ic_launcher_foreground.xml deleted file mode 100644 index c7bd21db..00000000 --- a/sample-custom-extension/src/main/res/drawable-v24/ic_launcher_foreground.xml +++ /dev/null @@ -1,34 +0,0 @@ - - - - - - - - - - - diff --git a/sample-custom-extension/src/main/res/drawable/ic_launcher_background.xml b/sample-custom-extension/src/main/res/drawable/ic_launcher_background.xml deleted file mode 100644 index d5fccc53..00000000 --- a/sample-custom-extension/src/main/res/drawable/ic_launcher_background.xml +++ /dev/null @@ -1,170 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/sample-custom-extension/src/main/res/layout/activity_recycler.xml b/sample-custom-extension/src/main/res/layout/activity_recycler.xml new file mode 100644 index 00000000..1405e07c --- /dev/null +++ b/sample-custom-extension/src/main/res/layout/activity_recycler.xml @@ -0,0 +1,5 @@ + + \ No newline at end of file diff --git a/sample-custom-extension/src/main/res/layout/adapter_fenced_code_block.xml b/sample-custom-extension/src/main/res/layout/adapter_fenced_code_block.xml new file mode 100644 index 00000000..ddc2c802 --- /dev/null +++ b/sample-custom-extension/src/main/res/layout/adapter_fenced_code_block.xml @@ -0,0 +1,24 @@ + + + + + + \ No newline at end of file diff --git a/sample-custom-extension/src/main/res/layout/adapter_table_block.xml b/sample-custom-extension/src/main/res/layout/adapter_table_block.xml new file mode 100644 index 00000000..8574d797 --- /dev/null +++ b/sample-custom-extension/src/main/res/layout/adapter_table_block.xml @@ -0,0 +1,16 @@ + + + + + + \ No newline at end of file diff --git a/sample-custom-extension/src/main/res/mipmap-anydpi-v26/ic_launcher.xml b/sample-custom-extension/src/main/res/mipmap-anydpi-v26/ic_launcher.xml deleted file mode 100644 index eca70cfe..00000000 --- a/sample-custom-extension/src/main/res/mipmap-anydpi-v26/ic_launcher.xml +++ /dev/null @@ -1,5 +0,0 @@ - - - - - \ No newline at end of file diff --git a/sample-custom-extension/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml b/sample-custom-extension/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml deleted file mode 100644 index eca70cfe..00000000 --- a/sample-custom-extension/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml +++ /dev/null @@ -1,5 +0,0 @@ - - - - - \ No newline at end of file diff --git a/sample-custom-extension/src/main/res/mipmap-hdpi/ic_launcher.png b/sample-custom-extension/src/main/res/mipmap-hdpi/ic_launcher.png deleted file mode 100644 index a2f5908281d070150700378b64a84c7db1f97aa1..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 3056 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 axPAxe%7rDusX+w*00006jj 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⁣ zwiG+x#%j}{0gU~J8;<|!B1@-XaB@{KORFwrYg_8rOv({b0EO#DbeQRm;B6_9=mXGf z-x|VL{zd`)#@yN}HkCSJbjbNlE|zL3Wm9Q8HY`sV)}3%pgN>cL^67{Z;PPL(*wT8N zUjXU{@|*hvm}({wsAC=x0^ok0%UAz0;sogW{B!nDqk|JJ5x~4NfTDgP49^zeu`csl?5mY@JdQdISc zFs!E{^grmkLnUk9 zny~m)1vws@5BFI<-0Tuo2JWX(0v`W|t(wg;s--L47WTvTMz-8l#TL^=OJNRS2?_Qj z3AKT+gvbyBi#H*-tJ%tWD|>EV3wy|8qxfzS!5RW;Jpl5*zo&^UBU=fG#2}UvRyNkK zA06Dy9;K1ca@r2T>yThYgI!ont$(G{6q#2QT+00r_x0(b)gsE`lBB?2gr55gq^D3Fi&p%E(p9>U%bv zkg1Jco(RbyTX7FDHOnl7-O@ zI$AaIl?9NJKPm(WiBP`1-#CB1QzU>&hKm)fpa5DKE{2$X0hGz-0uZ?cyTk(YC!Y&| zL=1VrNERSA5NA2jq7FACfX4JfPyj5XXl1yv0>~s;eF7L2$>&oMqeTFT2m$y7FlkON z_yurD1yIOvA;5C6016pyxBznGUt0kJ&k5r#;&>Jow`r)sp9R~PmK~lz$3xH%LT*1U zJdOyABZ3!FvNoR*vN$5ykHS8f`jA4zV+|L}i1C4`B2c{R0;UdYxaU|H)2avz@ z=mEYc|2S<+(B2Tj+FkX+2D+yFI!k9lWMA61DJ{)e;lum$(;O87?vGJJe!KtK04+N_ zI*P~t@dUb>9Xh{dbyl{-ZQ(UMgz7$|QfL5XSPkskt^NgctYC#;4WcZB1@%@wy@2t3 z2z0DI7&%b$*Aw~abe?GxE`ez@+6hOh-6*8fHRV{1os$EL@}uUZeG4h1&Be`98q*7j z=3-v+lhIjfWVo12!<>%V^a6lTgW3+_#W6n|p*~==zOH7z$0{LSZk(Tpd7EaD04hnA zL;#fxS0aD{`5^&D`}>0Uq?byDD-l2=!wm_bLcUl4gc(% za1p|itVANvFF>hghAS07Im1;IK;|b*W)}VDyI;BIp2=K*yu2a)j?B|f<44NI$NbmJ z#dE0>jI$fMr&@>4kN8MLFb4&2O9fEKaQg%(QO$4_1rVQywG^CmBLh#}_7gKW3vd?| z2?1^&KWq8}8I^_S0|)MowU_pw$q@nl@Nkn$z>BQq_KA^9yaR`(R3u{{Ig;cwt z@AJ^{ODQCm^neroM9nKNUAXi9RCK`OsP_LuR0PUR(YZCCX5dNF6VzcoK&=b^r`W?ltt|*F zpkoae%ZT{C1h~EcFui~b7fF`vb<<~j_VquuUA$}QqIKYELPp#;{u?q8Dz}WAG-(3; zjrm$i%7UbyZMM(Y{>!uJ#vNB?R~B{6Htp=>e*<{fQQ5W7V(1coCWlOON!MzZxhum| ztZBQpGR z;~#ur^&PockKdV{Q6R>o`Pl{0x!DEbpZ7y9Y;*ZvE!*gU`V1W3znva{f=?WO5I&>B z&hw6}tjECtaghm5z|C#%M;Yf_*pI^};h}Vl=^r9EN=tVDj86D;C$jIJ?K7VP+00000NkvXXu0mjf D5i!M* diff --git a/sample-custom-extension/src/main/res/mipmap-xhdpi/ic_launcher_round.png b/sample-custom-extension/src/main/res/mipmap-xhdpi/ic_launcher_round.png deleted file mode 100644 index 459ca609d3ae0d3943ab44cdc27feef9256dc6d7..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 7098 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 diff --git a/sample-custom-extension/src/main/res/mipmap-xxhdpi/ic_launcher.png b/sample-custom-extension/src/main/res/mipmap-xxhdpi/ic_launcher.png deleted file mode 100644 index 8ca12fe024be86e868d14e91120a6902f8e88ac6..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 6464 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 B0*L?s diff --git a/sample-custom-extension/src/main/res/mipmap-xxhdpi/ic_launcher_round.png b/sample-custom-extension/src/main/res/mipmap-xxhdpi/ic_launcher_round.png deleted file mode 100644 index 8e19b410a1b15ff180f3dacac19395fe3046cdec..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 10676 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 diff --git a/sample-custom-extension/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png b/sample-custom-extension/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png deleted file mode 100644 index 4c19a13c239cb67b8a2134ddd5f325db1d2d5bee..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 15523 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 BIP(Ai diff --git a/settings.gradle b/settings.gradle index 7ae3b4b8..3a3503f4 100644 --- a/settings.gradle +++ b/settings.gradle @@ -5,11 +5,12 @@ include ':app', ':markwon-ext-strikethrough', ':markwon-ext-tables', ':markwon-ext-tasklist', + ':markwon-html', + ':markwon-image-gif', ':markwon-image-okhttp', ':markwon-image-svg', - ':markwon-image-gif', + ':markwon-recycler', ':markwon-syntax-highlight', - ':markwon-html', ':markwon-test-span', ':sample-custom-extension', ':sample-latex-math' From 24b95e2ffb5756eb380dace4ab33698f24cd3568 Mon Sep 17 00:00:00 2001 From: Dimitry Ivanov Date: Tue, 25 Dec 2018 15:08:28 +0300 Subject: [PATCH 055/103] Table parsing and improvements for recycler artifact --- .../ru/noties/markwon/ext/tables/Table.java | 220 +++++++++++++++ .../markwon/ext/tables/TablePlugin.java | 27 +- .../markwon/recycler/MarkwonAdapter.java | 23 +- .../markwon/recycler/MarkwonAdapterImpl.java | 31 +-- .../markwon/recycler/MarkwonRecycler.java | 4 - ...{SimpleNodeEntry.java => SimpleEntry.java} | 8 +- .../layout/markwon_adapter_simple_entry.xml | 9 + .../java/ru/noties/markwon/MarkwonImpl.java | 2 +- .../recycler/MarkwonRecyclerActivity.java | 5 +- .../extension/recycler/TableNodeEntry.java | 255 ++---------------- .../main/res/layout/adapter_default_entry.xml | 5 +- 11 files changed, 315 insertions(+), 274 deletions(-) create mode 100644 markwon-ext-tables/src/main/java/ru/noties/markwon/ext/tables/Table.java delete mode 100644 markwon-recycler/src/main/java/ru/noties/markwon/recycler/MarkwonRecycler.java rename markwon-recycler/src/main/java/ru/noties/markwon/recycler/{SimpleNodeEntry.java => SimpleEntry.java} (90%) create mode 100644 markwon-recycler/src/main/res/layout/markwon_adapter_simple_entry.xml rename markwon-recycler/src/main/res/layout/adapter_simple_entry.xml => sample-custom-extension/src/main/res/layout/adapter_default_entry.xml (80%) diff --git a/markwon-ext-tables/src/main/java/ru/noties/markwon/ext/tables/Table.java b/markwon-ext-tables/src/main/java/ru/noties/markwon/ext/tables/Table.java new file mode 100644 index 00000000..85e1cfa4 --- /dev/null +++ b/markwon-ext-tables/src/main/java/ru/noties/markwon/ext/tables/Table.java @@ -0,0 +1,220 @@ +package ru.noties.markwon.ext.tables; + +import android.support.annotation.NonNull; +import android.support.annotation.Nullable; +import android.text.Spanned; + +import org.commonmark.ext.gfm.tables.TableBlock; +import org.commonmark.ext.gfm.tables.TableCell; +import org.commonmark.ext.gfm.tables.TableHead; +import org.commonmark.ext.gfm.tables.TableRow; +import org.commonmark.node.AbstractVisitor; +import org.commonmark.node.CustomNode; +import org.commonmark.node.Node; + +import java.util.ArrayList; +import java.util.List; + +import ru.noties.markwon.Markwon; + +/** + * A class to parse TableBlock and return a data-structure that is not dependent + * on commonmark-java table extension. Can be useful when rendering tables require special + * handling (multiple views, specific table view) for example when used with `markwon-recycler` artifact + * + * @see #parse(Markwon, TableBlock) + * @since 3.0.0 + */ +public class Table { + + /** + * Factory method to obtain an instance of {@link Table} + * + * @param markwon Markwon + * @param tableBlock TableBlock to parse + * @return parsed {@link Table} or null + */ + @Nullable + public static Table parse(@NonNull Markwon markwon, @NonNull TableBlock tableBlock) { + + final Table table; + + final ParseVisitor visitor = new ParseVisitor(markwon); + tableBlock.accept(visitor); + final List rows = visitor.rows(); + + if (rows == null) { + table = null; + } else { + table = new Table(rows); + } + + return table; + } + + public static class Row { + + private final boolean isHeader; + private final List columns; + + public Row( + boolean isHeader, + @NonNull List columns) { + this.isHeader = isHeader; + this.columns = columns; + } + + public boolean header() { + return isHeader; + } + + @NonNull + public List columns() { + return columns; + } + + @Override + public String toString() { + return "Row{" + + "isHeader=" + isHeader + + ", columns=" + columns + + '}'; + } + } + + public static class Column { + + private final Alignment alignment; + private final Spanned content; + + public Column(@NonNull Alignment alignment, @NonNull Spanned content) { + this.alignment = alignment; + this.content = content; + } + + @NonNull + public Alignment alignment() { + return alignment; + } + + @NonNull + public Spanned content() { + return content; + } + + @Override + public String toString() { + return "Column{" + + "alignment=" + alignment + + ", content=" + content + + '}'; + } + } + + public enum Alignment { + LEFT, + CENTER, + RIGHT + } + + private final List rows; + + public Table(@NonNull List rows) { + this.rows = rows; + } + + @NonNull + public List rows() { + return rows; + } + + @Override + public String toString() { + return "Table{" + + "rows=" + rows + + '}'; + } + + static class ParseVisitor extends AbstractVisitor { + + private final Markwon markwon; + + private List rows; + + private List pendingRow; + private boolean pendingRowIsHeader; + + ParseVisitor(@NonNull Markwon markwon) { + this.markwon = markwon; + } + + @Nullable + public List rows() { + return rows; + } + + @Override + public void visit(CustomNode customNode) { + + if (customNode instanceof TableCell) { + + final TableCell cell = (TableCell) customNode; + + final Node firstChild = cell.getFirstChild(); + + // need to investigate why... (most likely initial node is modified by someone) + if (firstChild != null) { + + if (pendingRow == null) { + pendingRow = new ArrayList<>(2); + } + + // let's TRY to not visit this node but instead try to render its first child + + pendingRow.add(new Table.Column(alignment(cell.getAlignment()), markwon.render(firstChild))); + pendingRowIsHeader = cell.isHeader(); + } + + return; + } + + if (customNode instanceof TableHead + || customNode instanceof TableRow) { + + visitChildren(customNode); + + // this can happen, ignore such row + if (pendingRow != null && pendingRow.size() > 0) { + + if (rows == null) { + rows = new ArrayList<>(2); + } + + rows.add(new Table.Row(pendingRowIsHeader, pendingRow)); + } + + pendingRow = null; + pendingRowIsHeader = false; + + return; + } + + visitChildren(customNode); + } + + @NonNull + private static Table.Alignment alignment(@NonNull TableCell.Alignment alignment) { + final Table.Alignment out; + if (TableCell.Alignment.RIGHT == alignment) { + out = Table.Alignment.RIGHT; + } else if (TableCell.Alignment.CENTER == alignment) { + out = Table.Alignment.CENTER; + } else { + out = Table.Alignment.LEFT; + } + return out; + } + } + + +} diff --git a/markwon-ext-tables/src/main/java/ru/noties/markwon/ext/tables/TablePlugin.java b/markwon-ext-tables/src/main/java/ru/noties/markwon/ext/tables/TablePlugin.java index d9220b01..ae547435 100644 --- a/markwon-ext-tables/src/main/java/ru/noties/markwon/ext/tables/TablePlugin.java +++ b/markwon-ext-tables/src/main/java/ru/noties/markwon/ext/tables/TablePlugin.java @@ -33,10 +33,11 @@ public class TablePlugin extends AbstractMarkwonPlugin { return new TablePlugin(tableTheme); } - private final TableTheme tableTheme; + private final TableVisitor visitor; + @SuppressWarnings("WeakerAccess") TablePlugin(@NonNull TableTheme tableTheme) { - this.tableTheme = tableTheme; + this.visitor = new TableVisitor(tableTheme); } @Override @@ -46,7 +47,12 @@ public class TablePlugin extends AbstractMarkwonPlugin { @Override public void configureVisitor(@NonNull MarkwonVisitor.Builder builder) { - TableVisitor.configure(tableTheme, builder); + visitor.configure(builder); + } + + @Override + public void beforeRender(@NonNull Node node) { + visitor.clear(); } @Override @@ -61,18 +67,23 @@ public class TablePlugin extends AbstractMarkwonPlugin { private static class TableVisitor { - static void configure(@NonNull TableTheme tableTheme, @NonNull MarkwonVisitor.Builder builder) { - new TableVisitor(tableTheme, builder); - } - private final TableTheme tableTheme; private List pendingTableRow; private boolean tableRowIsHeader; private int tableRows; - private TableVisitor(@NonNull TableTheme tableTheme, @NonNull MarkwonVisitor.Builder builder) { + TableVisitor(@NonNull TableTheme tableTheme) { this.tableTheme = tableTheme; + } + + void clear() { + pendingTableRow = null; + tableRowIsHeader = false; + tableRows = 0; + } + + void configure(@NonNull MarkwonVisitor.Builder builder) { builder .on(TableBody.class, new MarkwonVisitor.NodeVisitor() { @Override diff --git a/markwon-recycler/src/main/java/ru/noties/markwon/recycler/MarkwonAdapter.java b/markwon-recycler/src/main/java/ru/noties/markwon/recycler/MarkwonAdapter.java index c5334879..92f22605 100644 --- a/markwon-recycler/src/main/java/ru/noties/markwon/recycler/MarkwonAdapter.java +++ b/markwon-recycler/src/main/java/ru/noties/markwon/recycler/MarkwonAdapter.java @@ -15,7 +15,25 @@ import java.util.List; import ru.noties.markwon.Markwon; -// each node block will be rendered by a simple TextView, but we can provide own entries for each block +/** + * Class to display markdown in a RecyclerView. It is done by extracting root blocks from a + * parsed markdown document and rendering each block in a standalone RecyclerView entry. Provides + * ability to customize rendering of blocks. For example display certain blocks in a horizontal + * scrolling container or display tables in a specific widget designed for it ({@link Builder#include(Class, Entry)}). + *

+ * By default each node will be rendered in a TextView provided by this artifact. It has no styling + * information and thus must be replaced with your own layout ({@link Builder#defaultEntry(int)} or + * {@link Builder#defaultEntry(Entry)}). + * + * @see #builder() + * @see #create() + * @see #create(int) + * @see #create(Entry) + * @see #setMarkdown(Markwon, String) + * @see #setParsedMarkdown(Markwon, Node) + * @see #setParsedMarkdown(Markwon, List) + * @since 3.0.0 + */ public abstract class MarkwonAdapter extends RecyclerView.Adapter { @NonNull @@ -59,6 +77,9 @@ public abstract class MarkwonAdapter extends RecyclerView.Adapter { @NonNull diff --git a/markwon-recycler/src/main/java/ru/noties/markwon/recycler/MarkwonAdapterImpl.java b/markwon-recycler/src/main/java/ru/noties/markwon/recycler/MarkwonAdapterImpl.java index f95cf129..5a8d2fa6 100644 --- a/markwon-recycler/src/main/java/ru/noties/markwon/recycler/MarkwonAdapterImpl.java +++ b/markwon-recycler/src/main/java/ru/noties/markwon/recycler/MarkwonAdapterImpl.java @@ -1,7 +1,6 @@ package ru.noties.markwon.recycler; import android.support.annotation.NonNull; -import android.util.Log; import android.util.SparseArray; import android.view.LayoutInflater; import android.view.ViewGroup; @@ -25,6 +24,7 @@ class MarkwonAdapterImpl extends MarkwonAdapter { private Markwon markwon; private List nodes; + @SuppressWarnings("WeakerAccess") MarkwonAdapterImpl( @NonNull SparseArray> entries, @NonNull Entry defaultEntry, @@ -67,9 +67,7 @@ class MarkwonAdapterImpl extends MarkwonAdapter { layoutInflater = LayoutInflater.from(parent.getContext()); } - final Entry entry = viewType == 0 - ? defaultEntry - : entries.get(viewType); + final Entry entry = getEntry(viewType); return entry.createHolder(layoutInflater, parent); } @@ -80,9 +78,7 @@ class MarkwonAdapterImpl extends MarkwonAdapter { final Node node = nodes.get(position); final int viewType = getNodeViewType(node.getClass()); - final Entry entry = viewType == 0 - ? defaultEntry - : entries.get(viewType); + final Entry entry = getEntry(viewType); entry.bindHolder(markwon, holder, node); } @@ -94,6 +90,7 @@ class MarkwonAdapterImpl extends MarkwonAdapter { : 0; } + @SuppressWarnings("unused") @NonNull public List getItems() { return nodes != null @@ -110,12 +107,11 @@ class MarkwonAdapterImpl extends MarkwonAdapter { public long getItemId(int position) { final Node node = nodes.get(position); final int type = getNodeViewType(node.getClass()); - final Entry entry = type == 0 - ? defaultEntry - : entries.get(type); + final Entry entry = getEntry(type); return entry.id(node); } + @SuppressWarnings("WeakerAccess") public int getNodeViewType(@NonNull Class node) { // if has registered -> then return it, else 0 final int hash = node.hashCode(); @@ -125,6 +121,13 @@ class MarkwonAdapterImpl extends MarkwonAdapter { return 0; } + @NonNull + private Entry getEntry(int viewType) { + return viewType == 0 + ? defaultEntry + : entries.get(viewType); + } + static class BuilderImpl implements Builder { private final SparseArray> entries = new SparseArray<>(3); @@ -154,7 +157,7 @@ class MarkwonAdapterImpl extends MarkwonAdapter { @Override public Builder defaultEntry(int layoutResId) { //noinspection unchecked - this.defaultEntry = (Entry) (Entry) new SimpleNodeEntry(layoutResId); + this.defaultEntry = (Entry) (Entry) new SimpleEntry(layoutResId); return this; } @@ -171,7 +174,7 @@ class MarkwonAdapterImpl extends MarkwonAdapter { if (defaultEntry == null) { //noinspection unchecked - defaultEntry = (Entry) (Entry) new SimpleNodeEntry(); + defaultEntry = (Entry) (Entry) new SimpleEntry(); } if (reducer == null) { @@ -190,7 +193,7 @@ class MarkwonAdapterImpl extends MarkwonAdapter { final List list = new ArrayList<>(); -// // we will extract all blocks that are direct children of Document + // we will extract all blocks that are direct children of Document Node node = root.getFirstChild(); Node temp; @@ -201,8 +204,6 @@ class MarkwonAdapterImpl extends MarkwonAdapter { node = temp; } - Log.e("NODES", list.toString()); - return list; } } diff --git a/markwon-recycler/src/main/java/ru/noties/markwon/recycler/MarkwonRecycler.java b/markwon-recycler/src/main/java/ru/noties/markwon/recycler/MarkwonRecycler.java deleted file mode 100644 index 2244f37b..00000000 --- a/markwon-recycler/src/main/java/ru/noties/markwon/recycler/MarkwonRecycler.java +++ /dev/null @@ -1,4 +0,0 @@ -package ru.noties.markwon.recycler; - -public class MarkwonRecycler { -} diff --git a/markwon-recycler/src/main/java/ru/noties/markwon/recycler/SimpleNodeEntry.java b/markwon-recycler/src/main/java/ru/noties/markwon/recycler/SimpleEntry.java similarity index 90% rename from markwon-recycler/src/main/java/ru/noties/markwon/recycler/SimpleNodeEntry.java rename to markwon-recycler/src/main/java/ru/noties/markwon/recycler/SimpleEntry.java index e5d64590..d04a7477 100644 --- a/markwon-recycler/src/main/java/ru/noties/markwon/recycler/SimpleNodeEntry.java +++ b/markwon-recycler/src/main/java/ru/noties/markwon/recycler/SimpleEntry.java @@ -17,7 +17,7 @@ import java.util.Map; import ru.noties.markwon.Markwon; -public class SimpleNodeEntry implements MarkwonAdapter.Entry { +public class SimpleEntry implements MarkwonAdapter.Entry { private static final NoCopySpannableFactory FACTORY = new NoCopySpannableFactory(); @@ -26,11 +26,11 @@ public class SimpleNodeEntry implements MarkwonAdapter.Entry + \ No newline at end of file diff --git a/markwon/src/main/java/ru/noties/markwon/MarkwonImpl.java b/markwon/src/main/java/ru/noties/markwon/MarkwonImpl.java index d9b74a25..3cf105b5 100644 --- a/markwon/src/main/java/ru/noties/markwon/MarkwonImpl.java +++ b/markwon/src/main/java/ru/noties/markwon/MarkwonImpl.java @@ -65,7 +65,7 @@ class MarkwonImpl extends Markwon { final Spanned spanned = visitor.builder().spannableStringBuilder(); - // clear render props and builder after rending + // clear render props and builder after rendering visitor.clear(); return spanned; diff --git a/sample-custom-extension/src/main/java/ru/noties/markwon/sample/extension/recycler/MarkwonRecyclerActivity.java b/sample-custom-extension/src/main/java/ru/noties/markwon/sample/extension/recycler/MarkwonRecyclerActivity.java index 4ae85cc5..34d07ec8 100644 --- a/sample-custom-extension/src/main/java/ru/noties/markwon/sample/extension/recycler/MarkwonRecyclerActivity.java +++ b/sample-custom-extension/src/main/java/ru/noties/markwon/sample/extension/recycler/MarkwonRecyclerActivity.java @@ -29,7 +29,7 @@ import ru.noties.markwon.html.HtmlPlugin; import ru.noties.markwon.image.ImagesPlugin; import ru.noties.markwon.image.svg.SvgPlugin; import ru.noties.markwon.recycler.MarkwonAdapter; -import ru.noties.markwon.recycler.SimpleNodeEntry; +import ru.noties.markwon.recycler.SimpleEntry; import ru.noties.markwon.sample.extension.R; import ru.noties.markwon.urlprocessor.UrlProcessor; import ru.noties.markwon.urlprocessor.UrlProcessorRelativeToAbsolute; @@ -48,8 +48,9 @@ public class MarkwonRecyclerActivity extends Activity { setContentView(R.layout.activity_recycler); final MarkwonAdapter adapter = MarkwonAdapter.builder() - .include(FencedCodeBlock.class, new SimpleNodeEntry(R.layout.adapter_fenced_code_block)) + .include(FencedCodeBlock.class, new SimpleEntry(R.layout.adapter_fenced_code_block)) .include(TableBlock.class, new TableNodeEntry()) + .defaultEntry(new SimpleEntry(R.layout.adapter_default_entry)) .build(); final RecyclerView recyclerView = findViewById(R.id.recycler_view); diff --git a/sample-custom-extension/src/main/java/ru/noties/markwon/sample/extension/recycler/TableNodeEntry.java b/sample-custom-extension/src/main/java/ru/noties/markwon/sample/extension/recycler/TableNodeEntry.java index 31c05783..c8c7b67b 100644 --- a/sample-custom-extension/src/main/java/ru/noties/markwon/sample/extension/recycler/TableNodeEntry.java +++ b/sample-custom-extension/src/main/java/ru/noties/markwon/sample/extension/recycler/TableNodeEntry.java @@ -1,48 +1,26 @@ package ru.noties.markwon.sample.extension.recycler; import android.support.annotation.NonNull; -import android.util.Log; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; import android.widget.TableLayout; -import android.widget.TableRow; -import android.widget.TextView; import org.commonmark.ext.gfm.tables.TableBlock; -import org.commonmark.node.AbstractVisitor; -import org.commonmark.node.BlockQuote; -import org.commonmark.node.BulletList; -import org.commonmark.node.Code; -import org.commonmark.node.CustomBlock; -import org.commonmark.node.CustomNode; -import org.commonmark.node.Document; -import org.commonmark.node.Emphasis; -import org.commonmark.node.FencedCodeBlock; -import org.commonmark.node.HardLineBreak; -import org.commonmark.node.Heading; -import org.commonmark.node.HtmlBlock; -import org.commonmark.node.HtmlInline; -import org.commonmark.node.Image; -import org.commonmark.node.IndentedCodeBlock; -import org.commonmark.node.Link; -import org.commonmark.node.ListItem; -import org.commonmark.node.Node; -import org.commonmark.node.OrderedList; -import org.commonmark.node.Paragraph; -import org.commonmark.node.SoftLineBreak; -import org.commonmark.node.StrongEmphasis; -import org.commonmark.node.Text; -import org.commonmark.node.ThematicBreak; -import ru.noties.debug.Debug; +import java.util.HashMap; +import java.util.Map; + import ru.noties.markwon.Markwon; +import ru.noties.markwon.ext.tables.Table; import ru.noties.markwon.recycler.MarkwonAdapter; import ru.noties.markwon.sample.extension.R; // do not use in real applications, this is just a showcase public class TableNodeEntry implements MarkwonAdapter.Entry { + private final Map cache = new HashMap<>(2); + @NonNull @Override public TableNodeHolder createHolder(@NonNull LayoutInflater inflater, @NonNull ViewGroup parent) { @@ -52,77 +30,23 @@ public class TableNodeEntry implements MarkwonAdapter.Entry \ No newline at end of file + android:textColor="#000" + android:textSize="16sp" /> \ No newline at end of file From 2593539b65c5b454d69773f4ef1ce7e6bb7748c3 Mon Sep 17 00:00:00 2001 From: Dimitry Ivanov Date: Wed, 26 Dec 2018 00:06:15 +0300 Subject: [PATCH 056/103] Documenting recycler module (work in progress) --- .../markwon/recycler/MarkwonAdapter.java | 80 ++++++++- .../recycler/MarkwonRecyclerActivity.java | 2 +- .../{TableNodeEntry.java => TableEntry.java} | 16 +- .../extension/recycler/TableEntryView.java | 153 ++++++++++++++++++ .../main/res/layout/adapter_table_block.xml | 13 +- .../main/res/layout/view_table_entry_cell.xml | 11 ++ .../main/res/layout/view_table_entry_row.xml | 5 + .../src/main/res/values/attrs.xml | 8 + 8 files changed, 269 insertions(+), 19 deletions(-) rename sample-custom-extension/src/main/java/ru/noties/markwon/sample/extension/recycler/{TableNodeEntry.java => TableEntry.java} (78%) create mode 100644 sample-custom-extension/src/main/java/ru/noties/markwon/sample/extension/recycler/TableEntryView.java create mode 100644 sample-custom-extension/src/main/res/layout/view_table_entry_cell.xml create mode 100644 sample-custom-extension/src/main/res/layout/view_table_entry_row.xml create mode 100644 sample-custom-extension/src/main/res/values/attrs.xml diff --git a/markwon-recycler/src/main/java/ru/noties/markwon/recycler/MarkwonAdapter.java b/markwon-recycler/src/main/java/ru/noties/markwon/recycler/MarkwonAdapter.java index 92f22605..fc0d4cb9 100644 --- a/markwon-recycler/src/main/java/ru/noties/markwon/recycler/MarkwonAdapter.java +++ b/markwon-recycler/src/main/java/ru/noties/markwon/recycler/MarkwonAdapter.java @@ -16,7 +16,7 @@ import java.util.List; import ru.noties.markwon.Markwon; /** - * Class to display markdown in a RecyclerView. It is done by extracting root blocks from a + * Adapter to display markdown in a RecyclerView. It is done by extracting root blocks from a * parsed markdown document and rendering each block in a standalone RecyclerView entry. Provides * ability to customize rendering of blocks. For example display certain blocks in a horizontal * scrolling container or display tables in a specific widget designed for it ({@link Builder#include(Class, Entry)}). @@ -36,43 +36,119 @@ import ru.noties.markwon.Markwon; */ public abstract class MarkwonAdapter extends RecyclerView.Adapter { + /** + * Factory method to obtain {@link Builder} instance. + * + * @see Builder + */ @NonNull public static Builder builder() { return new MarkwonAdapterImpl.BuilderImpl(); } + /** + * Factory method to create a {@link MarkwonAdapter} for evaluation purposes. Resulting + * adapter will use default layout for all blocks. Default layout has no styling and should + * be specified explicitly. + * + * @see #create(int) + * @see #create(Entry) + */ @NonNull public static MarkwonAdapter create() { return new MarkwonAdapterImpl.BuilderImpl().build(); } - // for an adapter with only one entry (all blocks are rendered the same with this entry) + /** + * Factory method to create a {@link MarkwonAdapter} that uses supplied entry to render all + * nodes. + * + * @param defaultEntry {@link Entry} to be used for node rendering + * @see SimpleEntry + */ @NonNull public static MarkwonAdapter create(@NonNull Entry defaultEntry) { return new MarkwonAdapterImpl.BuilderImpl().defaultEntry(defaultEntry).build(); } + /** + * Factory method to create a {@link MarkwonAdapter} that will use supplied layoutResId view + * to display all nodes. + * + * Please note that supplied layout must have a TextView inside + * with {@code android:id="@+id/text"} + * + * @param layoutResId layout to be used to display all nodes + * @see SimpleEntry + */ @NonNull public static MarkwonAdapter create(@LayoutRes int layoutResId) { return new MarkwonAdapterImpl.BuilderImpl().defaultEntry(layoutResId).build(); } + /** + * Builder to create an instance of {@link MarkwonAdapter} + * + * @see #include(Class, Entry) + * @see #defaultEntry(int) + * @see #defaultEntry(Entry) + * @see #reducer(Reducer) + */ public interface Builder { + /** + * Include a custom {@link Entry} rendering for a Node. Please note that `node` argument + * must be exact type, as internally there is no validation for inheritance. if multiple + * nodes should be rendered with the same {@link Entry} they must specify so explicitly. + * By calling this method for each. + * + * @param node type of the node to register + * @param entry {@link Entry} to be used for `node` rendering + * @return self + */ @NonNull Builder include( @NonNull Class node, @NonNull Entry entry); + /** + * Specify which {@link Entry} to use for all non-explicitly registered nodes + * + * @param defaultEntry {@link Entry} + * @return self + * @see SimpleEntry + */ @NonNull Builder defaultEntry(@NonNull Entry defaultEntry); + /** + * Specify which layout {@link SimpleEntry} will use to render all non-explicitly + * registered nodes. + * + * Please note that supplied layout must have a TextView inside + * with {@code android:id="@+id/text"} + * + * @return self + * @see SimpleEntry + */ @NonNull Builder defaultEntry(@LayoutRes int layoutResId); + /** + * Specify how root Node will be reduced to a list of nodes. There is a default + * {@link Reducer} that will be used if not provided explicitly (there is no need to + * register your own unless you require it). + * + * @param reducer {@link Reducer} + * @return self + * @see Reducer + */ @NonNull Builder reducer(@NonNull Reducer reducer); + /** + * @return {@link MarkwonAdapter} + */ @NonNull MarkwonAdapter build(); } diff --git a/sample-custom-extension/src/main/java/ru/noties/markwon/sample/extension/recycler/MarkwonRecyclerActivity.java b/sample-custom-extension/src/main/java/ru/noties/markwon/sample/extension/recycler/MarkwonRecyclerActivity.java index 34d07ec8..eb4dc039 100644 --- a/sample-custom-extension/src/main/java/ru/noties/markwon/sample/extension/recycler/MarkwonRecyclerActivity.java +++ b/sample-custom-extension/src/main/java/ru/noties/markwon/sample/extension/recycler/MarkwonRecyclerActivity.java @@ -49,7 +49,7 @@ public class MarkwonRecyclerActivity extends Activity { final MarkwonAdapter adapter = MarkwonAdapter.builder() .include(FencedCodeBlock.class, new SimpleEntry(R.layout.adapter_fenced_code_block)) - .include(TableBlock.class, new TableNodeEntry()) + .include(TableBlock.class, new TableEntry()) .defaultEntry(new SimpleEntry(R.layout.adapter_default_entry)) .build(); diff --git a/sample-custom-extension/src/main/java/ru/noties/markwon/sample/extension/recycler/TableNodeEntry.java b/sample-custom-extension/src/main/java/ru/noties/markwon/sample/extension/recycler/TableEntry.java similarity index 78% rename from sample-custom-extension/src/main/java/ru/noties/markwon/sample/extension/recycler/TableNodeEntry.java rename to sample-custom-extension/src/main/java/ru/noties/markwon/sample/extension/recycler/TableEntry.java index c8c7b67b..aa3d79ff 100644 --- a/sample-custom-extension/src/main/java/ru/noties/markwon/sample/extension/recycler/TableNodeEntry.java +++ b/sample-custom-extension/src/main/java/ru/noties/markwon/sample/extension/recycler/TableEntry.java @@ -4,7 +4,6 @@ import android.support.annotation.NonNull; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; -import android.widget.TableLayout; import org.commonmark.ext.gfm.tables.TableBlock; @@ -17,7 +16,7 @@ import ru.noties.markwon.recycler.MarkwonAdapter; import ru.noties.markwon.sample.extension.R; // do not use in real applications, this is just a showcase -public class TableNodeEntry implements MarkwonAdapter.Entry { +public class TableEntry implements MarkwonAdapter.Entry { private final Map cache = new HashMap<>(2); @@ -37,18 +36,11 @@ public class TableNodeEntry implements MarkwonAdapter.Entry rows = new ArrayList<>(); + for (String row : data.split("\\|")) { + final List columns = new ArrayList<>(); + for (String column : row.split(",")) { + columns.add(new Table.Column(Table.Alignment.LEFT, new SpannedString(column))); + } + final boolean header = first; + first = false; + rows.add(new Table.Row(header, columns)); + } + final Table table = new Table(rows); + setTable(table); + } + } + } finally { + array.recycle(); + } + } + } + + public void setTable(@NonNull Table table) { + final List rows = table.rows(); + for (int i = 0, size = rows.size(); i < size; i++) { + addRow(i, rows.get(i)); + } + } + + private void addRow(int index, @NonNull Table.Row row) { + + final ViewGroup group = ensureRow(index); + + final int backgroundColor = !row.header() && (index % 2) == 0 + ? rowEvenBackgroundColor + : 0; + group.setBackgroundColor(backgroundColor); + + final List columns = row.columns(); + + TextView textView; + Table.Column column; + + for (int i = 0, size = columns.size(); i < size; i++) { + textView = ensureCell(group, i); + column = columns.get(i); + textView.setTextAlignment(textAlignment(column.alignment())); + textView.setText(column.content()); + textView.getPaint().setFakeBoldText(row.header()); + } + } + + @NonNull + private ViewGroup ensureRow(int index) { + + final int count = getChildCount(); + if (index >= count) { + + // count=0,index=1, diff=2 + // count=0,index=5, diff=6 + // count=1,index=2, diff=2 + int diff = index - count + 1; + while (diff > 0) { + addView(inflater.inflate(R.layout.view_table_entry_row, this, false)); + diff -= 1; + } + } + + return (ViewGroup) getChildAt(index); + } + + @NonNull + private TextView ensureCell(@NonNull ViewGroup group, int index) { + + final int count = group.getChildCount(); + if (index >= count) { + int diff = index - count + 1; + while (diff > 0) { + group.addView(inflater.inflate(R.layout.view_table_entry_cell, group, false)); + diff -= 1; + } + } + + return (TextView) group.getChildAt(index); + } + + private static int textAlignment(@NonNull Table.Alignment alignment) { + final int out; + switch (alignment) { + case LEFT: + out = TextView.TEXT_ALIGNMENT_TEXT_START; + break; + case CENTER: + out = TextView.TEXT_ALIGNMENT_CENTER; + break; + case RIGHT: + out = TextView.TEXT_ALIGNMENT_TEXT_END; + break; + default: + throw new IllegalStateException("Unexpected alignment: " + alignment); + } + return out; + } +} diff --git a/sample-custom-extension/src/main/res/layout/adapter_table_block.xml b/sample-custom-extension/src/main/res/layout/adapter_table_block.xml index 8574d797..4e032c05 100644 --- a/sample-custom-extension/src/main/res/layout/adapter_table_block.xml +++ b/sample-custom-extension/src/main/res/layout/adapter_table_block.xml @@ -1,16 +1,21 @@ + android:paddingTop="8dip" + android:paddingRight="16dip" + android:paddingBottom="8dip"> - + android:layout_height="wrap_content" + app:tev_debugData="head1,head2,head3|col1,col2,col3|col1,col2,col3|col1,col2,col3" + app:tev_rowEvenBackgroundColor="#40ff0000" /> \ No newline at end of file diff --git a/sample-custom-extension/src/main/res/layout/view_table_entry_cell.xml b/sample-custom-extension/src/main/res/layout/view_table_entry_cell.xml new file mode 100644 index 00000000..88654d3d --- /dev/null +++ b/sample-custom-extension/src/main/res/layout/view_table_entry_cell.xml @@ -0,0 +1,11 @@ + + \ No newline at end of file diff --git a/sample-custom-extension/src/main/res/layout/view_table_entry_row.xml b/sample-custom-extension/src/main/res/layout/view_table_entry_row.xml new file mode 100644 index 00000000..24e7fb9e --- /dev/null +++ b/sample-custom-extension/src/main/res/layout/view_table_entry_row.xml @@ -0,0 +1,5 @@ + + \ No newline at end of file diff --git a/sample-custom-extension/src/main/res/values/attrs.xml b/sample-custom-extension/src/main/res/values/attrs.xml new file mode 100644 index 00000000..05f86e93 --- /dev/null +++ b/sample-custom-extension/src/main/res/values/attrs.xml @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file From 581265a22a65bcd5ce831b85d3d15587e681487b Mon Sep 17 00:00:00 2001 From: Dimitry Ivanov Date: Wed, 26 Dec 2018 19:07:31 +0300 Subject: [PATCH 057/103] Introduce priority abstraction (order and dependency for plugins) --- .../ru/noties/markwon/gif/GifAwarePlugin.java | 8 + .../markwon/ext/latex/JLatexMathPlugin.java | 8 + .../noties/markwon/image/gif/GifPlugin.java | 8 + ...ttpPlugin.java => OkHttpImagesPlugin.java} | 20 +- .../noties/markwon/image/svg/SvgPlugin.java | 8 + .../noties/markwon/AbstractMarkwonPlugin.java | 12 + .../main/java/ru/noties/markwon/Markwon.java | 19 +- .../ru/noties/markwon/MarkwonBuilderImpl.java | 22 + .../java/ru/noties/markwon/MarkwonPlugin.java | 4 + .../ru/noties/markwon/core/CorePlugin.java | 11 +- .../ru/noties/markwon/priority/Priority.java | 96 ++++ .../markwon/priority/PriorityProcessor.java | 18 + .../priority/PriorityProcessorImpl.java | 133 +++++ .../priority/PriorityProcessorTest.java | 468 ++++++++++++++++++ 14 files changed, 824 insertions(+), 11 deletions(-) rename markwon-image-okhttp/src/main/java/ru/noties/markwon/image/okhttp/{MarkwonImageOkHttpPlugin.java => OkHttpImagesPlugin.java} (61%) create mode 100644 markwon/src/main/java/ru/noties/markwon/priority/Priority.java create mode 100644 markwon/src/main/java/ru/noties/markwon/priority/PriorityProcessor.java create mode 100644 markwon/src/main/java/ru/noties/markwon/priority/PriorityProcessorImpl.java create mode 100644 markwon/src/test/java/ru/noties/markwon/priority/PriorityProcessorTest.java diff --git a/app/src/main/java/ru/noties/markwon/gif/GifAwarePlugin.java b/app/src/main/java/ru/noties/markwon/gif/GifAwarePlugin.java index 95509525..89e49384 100644 --- a/app/src/main/java/ru/noties/markwon/gif/GifAwarePlugin.java +++ b/app/src/main/java/ru/noties/markwon/gif/GifAwarePlugin.java @@ -14,6 +14,8 @@ import ru.noties.markwon.RenderProps; import ru.noties.markwon.SpanFactory; import ru.noties.markwon.image.AsyncDrawableSpan; import ru.noties.markwon.image.ImageProps; +import ru.noties.markwon.image.ImagesPlugin; +import ru.noties.markwon.priority.Priority; public class GifAwarePlugin extends AbstractMarkwonPlugin { @@ -57,6 +59,12 @@ public class GifAwarePlugin extends AbstractMarkwonPlugin { }); } + @NonNull + @Override + public Priority priority() { + return Priority.after(ImagesPlugin.class); + } + @Override public void afterSetText(@NonNull TextView textView) { processor.process(textView); diff --git a/markwon-ext-latex/src/main/java/ru/noties/markwon/ext/latex/JLatexMathPlugin.java b/markwon-ext-latex/src/main/java/ru/noties/markwon/ext/latex/JLatexMathPlugin.java index ee653b2f..9bd80400 100644 --- a/markwon-ext-latex/src/main/java/ru/noties/markwon/ext/latex/JLatexMathPlugin.java +++ b/markwon-ext-latex/src/main/java/ru/noties/markwon/ext/latex/JLatexMathPlugin.java @@ -21,8 +21,10 @@ import ru.noties.markwon.image.AsyncDrawableLoader; import ru.noties.markwon.image.ImageItem; import ru.noties.markwon.image.ImageProps; import ru.noties.markwon.image.ImageSize; +import ru.noties.markwon.image.ImagesPlugin; import ru.noties.markwon.image.MediaDecoder; import ru.noties.markwon.image.SchemeHandler; +import ru.noties.markwon.priority.Priority; public class JLatexMathPlugin extends AbstractMarkwonPlugin { @@ -136,4 +138,10 @@ public class JLatexMathPlugin extends AbstractMarkwonPlugin { } }); } + + @NonNull + @Override + public Priority priority() { + return Priority.after(ImagesPlugin.class); + } } diff --git a/markwon-image-gif/src/main/java/ru/noties/markwon/image/gif/GifPlugin.java b/markwon-image-gif/src/main/java/ru/noties/markwon/image/gif/GifPlugin.java index 3ee0c7aa..d0db857e 100644 --- a/markwon-image-gif/src/main/java/ru/noties/markwon/image/gif/GifPlugin.java +++ b/markwon-image-gif/src/main/java/ru/noties/markwon/image/gif/GifPlugin.java @@ -4,6 +4,8 @@ import android.support.annotation.NonNull; import ru.noties.markwon.AbstractMarkwonPlugin; import ru.noties.markwon.image.AsyncDrawableLoader; +import ru.noties.markwon.image.ImagesPlugin; +import ru.noties.markwon.priority.Priority; public class GifPlugin extends AbstractMarkwonPlugin { @@ -27,4 +29,10 @@ public class GifPlugin extends AbstractMarkwonPlugin { public void configureImages(@NonNull AsyncDrawableLoader.Builder builder) { builder.addMediaDecoder(GifMediaDecoder.CONTENT_TYPE, GifMediaDecoder.create(autoPlay)); } + + @NonNull + @Override + public Priority priority() { + return Priority.after(ImagesPlugin.class); + } } diff --git a/markwon-image-okhttp/src/main/java/ru/noties/markwon/image/okhttp/MarkwonImageOkHttpPlugin.java b/markwon-image-okhttp/src/main/java/ru/noties/markwon/image/okhttp/OkHttpImagesPlugin.java similarity index 61% rename from markwon-image-okhttp/src/main/java/ru/noties/markwon/image/okhttp/MarkwonImageOkHttpPlugin.java rename to markwon-image-okhttp/src/main/java/ru/noties/markwon/image/okhttp/OkHttpImagesPlugin.java index bc197c16..fe43c289 100644 --- a/markwon-image-okhttp/src/main/java/ru/noties/markwon/image/okhttp/MarkwonImageOkHttpPlugin.java +++ b/markwon-image-okhttp/src/main/java/ru/noties/markwon/image/okhttp/OkHttpImagesPlugin.java @@ -7,7 +7,9 @@ import java.util.Arrays; import okhttp3.OkHttpClient; import ru.noties.markwon.AbstractMarkwonPlugin; import ru.noties.markwon.image.AsyncDrawableLoader; +import ru.noties.markwon.image.ImagesPlugin; import ru.noties.markwon.image.network.NetworkSchemeHandler; +import ru.noties.markwon.priority.Priority; /** * Plugin to use OkHttpClient to obtain images from network (http and https schemes) @@ -17,21 +19,21 @@ import ru.noties.markwon.image.network.NetworkSchemeHandler; * @since 3.0.0 */ @SuppressWarnings("WeakerAccess") -public class MarkwonImageOkHttpPlugin extends AbstractMarkwonPlugin { +public class OkHttpImagesPlugin extends AbstractMarkwonPlugin { @NonNull - public static MarkwonImageOkHttpPlugin create() { - return new MarkwonImageOkHttpPlugin(new OkHttpClient()); + public static OkHttpImagesPlugin create() { + return new OkHttpImagesPlugin(new OkHttpClient()); } @NonNull - public static MarkwonImageOkHttpPlugin create(@NonNull OkHttpClient okHttpClient) { - return new MarkwonImageOkHttpPlugin(okHttpClient); + public static OkHttpImagesPlugin create(@NonNull OkHttpClient okHttpClient) { + return new OkHttpImagesPlugin(okHttpClient); } private final OkHttpClient client; - MarkwonImageOkHttpPlugin(@NonNull OkHttpClient client) { + OkHttpImagesPlugin(@NonNull OkHttpClient client) { this.client = client; } @@ -42,4 +44,10 @@ public class MarkwonImageOkHttpPlugin extends AbstractMarkwonPlugin { new OkHttpSchemeHandler(client) ); } + + @NonNull + @Override + public Priority priority() { + return Priority.after(ImagesPlugin.class); + } } diff --git a/markwon-image-svg/src/main/java/ru/noties/markwon/image/svg/SvgPlugin.java b/markwon-image-svg/src/main/java/ru/noties/markwon/image/svg/SvgPlugin.java index d2396741..be357480 100644 --- a/markwon-image-svg/src/main/java/ru/noties/markwon/image/svg/SvgPlugin.java +++ b/markwon-image-svg/src/main/java/ru/noties/markwon/image/svg/SvgPlugin.java @@ -5,6 +5,8 @@ import android.support.annotation.NonNull; import ru.noties.markwon.AbstractMarkwonPlugin; import ru.noties.markwon.image.AsyncDrawableLoader; +import ru.noties.markwon.image.ImagesPlugin; +import ru.noties.markwon.priority.Priority; public class SvgPlugin extends AbstractMarkwonPlugin { @@ -23,4 +25,10 @@ public class SvgPlugin extends AbstractMarkwonPlugin { public void configureImages(@NonNull AsyncDrawableLoader.Builder builder) { builder.addMediaDecoder(SvgMediaDecoder.CONTENT_TYPE, SvgMediaDecoder.create(resources)); } + + @NonNull + @Override + public Priority priority() { + return Priority.after(ImagesPlugin.class); + } } diff --git a/markwon/src/main/java/ru/noties/markwon/AbstractMarkwonPlugin.java b/markwon/src/main/java/ru/noties/markwon/AbstractMarkwonPlugin.java index d9849e5c..c5f68fdc 100644 --- a/markwon/src/main/java/ru/noties/markwon/AbstractMarkwonPlugin.java +++ b/markwon/src/main/java/ru/noties/markwon/AbstractMarkwonPlugin.java @@ -7,8 +7,10 @@ import android.widget.TextView; import org.commonmark.node.Node; import org.commonmark.parser.Parser; +import ru.noties.markwon.core.CorePlugin; import ru.noties.markwon.core.MarkwonTheme; import ru.noties.markwon.image.AsyncDrawableLoader; +import ru.noties.markwon.priority.Priority; /** * Class that extends {@link MarkwonPlugin} with all methods implemented (empty body) @@ -75,6 +77,16 @@ public abstract class AbstractMarkwonPlugin implements MarkwonPlugin { } + /** + * @inheritDoc + */ + @NonNull + @Override + public Priority priority() { + // by default all come after CorePlugin + return Priority.after(CorePlugin.class); + } + /** * @inheritDoc */ diff --git a/markwon/src/main/java/ru/noties/markwon/Markwon.java b/markwon/src/main/java/ru/noties/markwon/Markwon.java index c5a9ea8c..ee6e14cd 100644 --- a/markwon/src/main/java/ru/noties/markwon/Markwon.java +++ b/markwon/src/main/java/ru/noties/markwon/Markwon.java @@ -7,6 +7,8 @@ import android.widget.TextView; import org.commonmark.node.Node; +import ru.noties.markwon.core.CorePlugin; + /** * Class to parse and render markdown. Since version 3.0.0 instance specific (previously consisted * of static stateless methods). An instance of builder can be obtained via {@link #builder(Context)} @@ -18,7 +20,22 @@ import org.commonmark.node.Node; public abstract class Markwon { /** - * Factory method to obtain an instance of {@link Builder} + * Factory method to create a minimally functional {@link Markwon} instance. This + * instance will have only {@link CorePlugin} registered. If you wish + * to configure this instance more consider using {@link #builder(Context)} method. + * + * @return {@link Markwon} instance with only CorePlugin registered + * @since 3.0.0 + */ + @NonNull + public static Markwon create(@NonNull Context context) { + return builder(context) + .usePlugin(CorePlugin.create()) + .build(); + } + + /** + * Factory method to obtain an instance of {@link Builder}. * * @see Builder * @since 3.0.0 diff --git a/markwon/src/main/java/ru/noties/markwon/MarkwonBuilderImpl.java b/markwon/src/main/java/ru/noties/markwon/MarkwonBuilderImpl.java index 1cf15c2c..e0eabdf6 100644 --- a/markwon/src/main/java/ru/noties/markwon/MarkwonBuilderImpl.java +++ b/markwon/src/main/java/ru/noties/markwon/MarkwonBuilderImpl.java @@ -2,6 +2,7 @@ package ru.noties.markwon; import android.content.Context; import android.support.annotation.NonNull; +import android.util.Log; import android.widget.TextView; import org.commonmark.parser.Parser; @@ -13,6 +14,7 @@ import java.util.List; import ru.noties.markwon.core.MarkwonTheme; import ru.noties.markwon.image.AsyncDrawableLoader; +import ru.noties.markwon.priority.PriorityProcessor; /** * @since 3.0.0 @@ -22,8 +24,11 @@ class MarkwonBuilderImpl implements Markwon.Builder { private final Context context; private final List plugins = new ArrayList<>(3); + private TextView.BufferType bufferType = TextView.BufferType.SPANNABLE; + private PriorityProcessor priorityProcessor; + MarkwonBuilderImpl(@NonNull Context context) { this.context = context; } @@ -61,6 +66,12 @@ class MarkwonBuilderImpl implements Markwon.Builder { return this; } + @NonNull + public MarkwonBuilderImpl priorityProcessor(@NonNull PriorityProcessor priorityProcessor) { + this.priorityProcessor = priorityProcessor; + return this; + } + @NonNull @Override public Markwon build() { @@ -73,7 +84,18 @@ class MarkwonBuilderImpl implements Markwon.Builder { final MarkwonSpansFactory.Builder spanFactoryBuilder = new MarkwonSpansFactoryImpl.BuilderImpl(); final RenderProps renderProps = new RenderPropsImpl(); + PriorityProcessor priorityProcessor = this.priorityProcessor; + if (priorityProcessor == null) { + // strictly speaking we do not need updating this field + // as we are not building this class to be reused between multiple `build` calls + priorityProcessor = this.priorityProcessor = PriorityProcessor.create(); + } + final List plugins = priorityProcessor.process(this.plugins); + for (MarkwonPlugin plugin : plugins) { + if (true) { + Log.e("PLUGIN", plugin.getClass().getName()); + } plugin.configureParser(parserBuilder); plugin.configureTheme(themeBuilder); plugin.configureImages(asyncDrawableLoaderBuilder); diff --git a/markwon/src/main/java/ru/noties/markwon/MarkwonPlugin.java b/markwon/src/main/java/ru/noties/markwon/MarkwonPlugin.java index fd99b4dd..2f9a6cb1 100644 --- a/markwon/src/main/java/ru/noties/markwon/MarkwonPlugin.java +++ b/markwon/src/main/java/ru/noties/markwon/MarkwonPlugin.java @@ -11,6 +11,7 @@ import ru.noties.markwon.core.MarkwonTheme; import ru.noties.markwon.image.AsyncDrawableLoader; import ru.noties.markwon.image.MediaDecoder; import ru.noties.markwon.image.SchemeHandler; +import ru.noties.markwon.priority.Priority; /** * Class represents a plugin (extension) to Markwon to configure how parsing and rendering @@ -87,6 +88,9 @@ public interface MarkwonPlugin { */ void configureRenderProps(@NonNull RenderProps renderProps); + @NonNull + Priority priority(); + /** * Process input markdown and return new string to be used in parsing stage further. * Can be described as pre-processing of markdown String. diff --git a/markwon/src/main/java/ru/noties/markwon/core/CorePlugin.java b/markwon/src/main/java/ru/noties/markwon/core/CorePlugin.java index 187f0173..2c315486 100644 --- a/markwon/src/main/java/ru/noties/markwon/core/CorePlugin.java +++ b/markwon/src/main/java/ru/noties/markwon/core/CorePlugin.java @@ -39,6 +39,7 @@ import ru.noties.markwon.core.factory.ListItemSpanFactory; import ru.noties.markwon.core.factory.StrongEmphasisSpanFactory; import ru.noties.markwon.core.factory.ThematicBreakSpanFactory; import ru.noties.markwon.core.spans.OrderedListItemSpan; +import ru.noties.markwon.priority.Priority; /** * @since 3.0.0 @@ -55,12 +56,8 @@ public class CorePlugin extends AbstractMarkwonPlugin { return new CorePlugin(softBreakAddsNewLine); } - // todo: can we make it configurable somewhere else? - // even possibility of options that require creating factory method for each configuration... meh private final boolean softBreakAddsNewLine; - // todo: test that visitors are registered for all expected nodes - protected CorePlugin(boolean softBreakAddsNewLine) { this.softBreakAddsNewLine = softBreakAddsNewLine; } @@ -104,6 +101,12 @@ public class CorePlugin extends AbstractMarkwonPlugin { .setFactory(ThematicBreak.class, new ThematicBreakSpanFactory()); } + @NonNull + @Override + public Priority priority() { + return Priority.none(); + } + @Override public void beforeSetText(@NonNull TextView textView, @NonNull Spanned markdown) { OrderedListItemSpan.measure(textView, markdown); diff --git a/markwon/src/main/java/ru/noties/markwon/priority/Priority.java b/markwon/src/main/java/ru/noties/markwon/priority/Priority.java new file mode 100644 index 00000000..5582ff72 --- /dev/null +++ b/markwon/src/main/java/ru/noties/markwon/priority/Priority.java @@ -0,0 +1,96 @@ +package ru.noties.markwon.priority; + +import android.support.annotation.NonNull; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +import ru.noties.markwon.MarkwonPlugin; + +// a small dependency graph also +// what if plugins cannot be constructed into a graph? for example they depend on something +// but not overlap? then it would be hard to sort them (but this doesn't make sense, if +// they do not care about other components, just put them in whatever order, no?) + +/** + * @see MarkwonPlugin#priority() + * @since 3.0.0 + */ +public abstract class Priority { + + @NonNull + public static Priority none() { + return builder().build(); + } + + @NonNull + public static Priority after(@NonNull Class plugin) { + return builder().after(plugin).build(); + } + + @NonNull + public static Priority after( + @NonNull Class plugin1, + @NonNull Class plugin2) { + return builder().after(plugin1).after(plugin2).build(); + } + + @NonNull + public static Builder builder() { + return new Impl.BuilderImpl(); + } + + public interface Builder { + + @NonNull + Builder after(@NonNull Class plugin); + + @NonNull + Priority build(); + } + + @NonNull + public abstract List> after(); + + + static class Impl extends Priority { + + private final List> after; + + Impl(@NonNull List> after) { + this.after = after; + } + + @NonNull + @Override + public List> after() { + return after; + } + + @Override + public String toString() { + return "Priority{" + + "after=" + after + + '}'; + } + + static class BuilderImpl implements Builder { + + private final List> after = new ArrayList<>(0); + + @NonNull + @Override + public Builder after(@NonNull Class plugin) { + after.add(plugin); + return this; + } + + @NonNull + @Override + public Priority build() { + return new Impl(Collections.unmodifiableList(after)); + } + } + } +} diff --git a/markwon/src/main/java/ru/noties/markwon/priority/PriorityProcessor.java b/markwon/src/main/java/ru/noties/markwon/priority/PriorityProcessor.java new file mode 100644 index 00000000..1ba1353d --- /dev/null +++ b/markwon/src/main/java/ru/noties/markwon/priority/PriorityProcessor.java @@ -0,0 +1,18 @@ +package ru.noties.markwon.priority; + +import android.support.annotation.NonNull; + +import java.util.List; + +import ru.noties.markwon.MarkwonPlugin; + +public abstract class PriorityProcessor { + + @NonNull + public static PriorityProcessor create() { + return new PriorityProcessorImpl(); + } + + @NonNull + public abstract List process(@NonNull List plugins); +} diff --git a/markwon/src/main/java/ru/noties/markwon/priority/PriorityProcessorImpl.java b/markwon/src/main/java/ru/noties/markwon/priority/PriorityProcessorImpl.java new file mode 100644 index 00000000..08e820b4 --- /dev/null +++ b/markwon/src/main/java/ru/noties/markwon/priority/PriorityProcessorImpl.java @@ -0,0 +1,133 @@ +package ru.noties.markwon.priority; + +import android.support.annotation.NonNull; + +import java.util.Collections; +import java.util.Comparator; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import ru.noties.markwon.MarkwonPlugin; + +import static java.lang.Math.max; + +class PriorityProcessorImpl extends PriorityProcessor { + + @NonNull + @Override + public List process(@NonNull List plugins) { + + final int size = plugins.size(); + + final Map, Set>> map = + new HashMap<>(size); + + for (MarkwonPlugin plugin : plugins) { + if (map.put(plugin.getClass(), new HashSet<>(plugin.priority().after())) != null) { + throw new IllegalStateException(String.format("Markwon duplicate plugin " + + "found `%s`: %s", plugin.getClass().getName(), plugin)); + } + } + + // change to Map + final Map cache = new HashMap<>(size); + for (MarkwonPlugin plugin : plugins) { + cache.put(plugin, eval(plugin, map)); + } + + Collections.sort(plugins, new PriorityComparator(cache)); + + return plugins; + } + + private static int eval( + @NonNull MarkwonPlugin plugin, + @NonNull Map, Set>> map) { + + final Set> set = map.get(plugin.getClass()); + + // no dependencies + if (set.isEmpty()) { + return 0; + } + + final Class who = plugin.getClass(); + + int max = 0; + + for (Class dependency : set) { + max = max(max, eval(who, dependency, map)); + } + + return 1 + max; + } + + // we need to count the number of steps to a root node (which has no parents) + private static int eval( + @NonNull Class who, + @NonNull Class plugin, + @NonNull Map, Set>> map) { + + // exact match + Set> set = map.get(plugin); + + if (set == null) { + + // let's try to find inexact type (overridden/subclassed) + for (Map.Entry, Set>> entry : map.entrySet()) { + if (plugin.isAssignableFrom(entry.getKey())) { + set = entry.getValue(); + break; + } + } + + if (set == null) { + // unsatisfied dependency + throw new IllegalStateException(String.format("Markwon unsatisfied dependency found. " + + "Plugin `%s` comes after `%s` but it is not added.", + who.getName(), plugin.getName())); + } + } + + if (set.isEmpty()) { + return 0; + } + + int value = 1; + + for (Class dependency : set) { + + // a case when a plugin defines `Priority.after(getClass)` or being + // referenced by own dependency (even indirect) + if (who.equals(dependency)) { + throw new IllegalStateException(String.format("Markwon plugin `%s` defined self " + + "as a dependency or being referenced by own dependency (cycle)", who.getName())); + } + + value += eval(who, dependency, map); + } + + return value; + } + + private static class PriorityComparator implements Comparator { + + private final Map map; + + PriorityComparator(@NonNull Map map) { + this.map = map; + } + + @Override + public int compare(MarkwonPlugin o1, MarkwonPlugin o2) { + return map.get(o1).compareTo(map.get(o2)); + } + } + + private static class NoCorePluginAddedException extends Exception { + + } +} diff --git a/markwon/src/test/java/ru/noties/markwon/priority/PriorityProcessorTest.java b/markwon/src/test/java/ru/noties/markwon/priority/PriorityProcessorTest.java new file mode 100644 index 00000000..28612ca4 --- /dev/null +++ b/markwon/src/test/java/ru/noties/markwon/priority/PriorityProcessorTest.java @@ -0,0 +1,468 @@ +package ru.noties.markwon.priority; + +import android.support.annotation.NonNull; +import android.support.annotation.Nullable; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.robolectric.RobolectricTestRunner; +import org.robolectric.annotation.Config; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.HashSet; +import java.util.List; + +import ru.noties.markwon.AbstractMarkwonPlugin; +import ru.noties.markwon.MarkwonPlugin; +import ru.noties.markwon.core.CorePlugin; +import ru.noties.markwon.image.ImagesPlugin; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; + +@RunWith(RobolectricTestRunner.class) +@Config(manifest = Config.NONE) +public class PriorityProcessorTest { + + private PriorityProcessor processor; + + @Before + public void before() { + processor = PriorityProcessor.create(); + } + + @Test + public void empty_list() { + final List plugins = Collections.emptyList(); + assertEquals(0, processor.process(plugins).size()); + } + + @Test + public void simple_two_plugins() { + + final MarkwonPlugin first = new AbstractMarkwonPlugin() { + @NonNull + @Override + public Priority priority() { + return Priority.none(); + } + }; + + final MarkwonPlugin second = new AbstractMarkwonPlugin() { + @NonNull + @Override + public Priority priority() { + return Priority.after(first.getClass()); + } + }; + + final List plugins = processor.process(Arrays.asList(second, first)); + + assertEquals(2, plugins.size()); + assertEquals(first, plugins.get(0)); + assertEquals(second, plugins.get(1)); + } + + @Test + public void plugin_after_self() { + + final MarkwonPlugin plugin = new AbstractMarkwonPlugin() { + @NonNull + @Override + public Priority priority() { + return Priority.after(getClass()); + } + }; + + try { + processor.process(Collections.singletonList(plugin)); + fail(); + } catch (IllegalStateException e) { + assertTrue(e.getMessage(), e.getMessage().contains("defined self as a dependency")); + } + } + + @Test + public void unsatisfied_dependency() { + + final MarkwonPlugin plugin = new AbstractMarkwonPlugin() { + @NonNull + @Override + public Priority priority() { + return Priority.after(ImagesPlugin.class); + } + }; + + try { + processor.process(Collections.singletonList(plugin)); + fail(); + } catch (IllegalStateException e) { + assertTrue(e.getMessage(), e.getMessage().contains("Markwon unsatisfied dependency found")); + } + } + + @Test + public void subclass_found() { + // when a plugin comes after another, but _another_ was subclassed and placed in the list + + final MarkwonPlugin core = new CorePlugin(false) { + }; + final MarkwonPlugin plugin = new AbstractMarkwonPlugin() { + @NonNull + @Override + public Priority priority() { + return Priority.after(CorePlugin.class); + } + }; + + final List plugins = processor.process(Arrays.asList(plugin, core)); + assertEquals(2, plugins.size()); + assertEquals(core, plugins.get(0)); + assertEquals(plugin, plugins.get(1)); + } + + @Test + public void three_plugins_sequential() { + + final MarkwonPlugin first = new AbstractMarkwonPlugin() { + @NonNull + @Override + public Priority priority() { + return Priority.none(); + } + }; + + final MarkwonPlugin second = new AbstractMarkwonPlugin() { + @NonNull + @Override + public Priority priority() { + return Priority.after(first.getClass()); + } + }; + + final MarkwonPlugin third = new AbstractMarkwonPlugin() { + @NonNull + @Override + public Priority priority() { + return Priority.after(second.getClass()); + } + }; + + final List plugins = processor.process(Arrays.asList(third, second, first)); + assertEquals(3, plugins.size()); + assertEquals(first, plugins.get(0)); + assertEquals(second, plugins.get(1)); + assertEquals(third, plugins.get(2)); + } + + @Test + public void plugin_duplicate() { + + final MarkwonPlugin plugin = new AbstractMarkwonPlugin() { + @NonNull + @Override + public Priority priority() { + return Priority.none(); + } + }; + + try { + processor.process(Arrays.asList(plugin, plugin)); + fail(); + } catch (IllegalStateException e) { + assertTrue(e.getMessage(), e.getMessage().contains("Markwon duplicate plugin found")); + } + } + + @Test + public void multiple_after_3() { + + final MarkwonPlugin a1 = new AbstractMarkwonPlugin() { + @NonNull + @Override + public Priority priority() { + return Priority.none(); + } + }; + + final MarkwonPlugin b1 = new AbstractMarkwonPlugin() { + @NonNull + @Override + public Priority priority() { + return Priority.after(a1.getClass()); + } + }; + + final MarkwonPlugin c1 = new AbstractMarkwonPlugin() { + @NonNull + @Override + public Priority priority() { + return Priority.after(a1.getClass(), b1.getClass()); + } + }; + + final List plugins = processor.process(Arrays.asList(c1, a1, b1)); + assertEquals(3, plugins.size()); + assertEquals(a1, plugins.get(0)); + assertEquals(b1, plugins.get(1)); + assertEquals(c1, plugins.get(2)); + } + + @Test + public void multiple_after_4() { + + final MarkwonPlugin a1 = new AbstractMarkwonPlugin() { + @NonNull + @Override + public Priority priority() { + return Priority.none(); + } + }; + + final MarkwonPlugin b1 = new AbstractMarkwonPlugin() { + @NonNull + @Override + public Priority priority() { + return Priority.after(a1.getClass()); + } + }; + + final MarkwonPlugin c1 = new AbstractMarkwonPlugin() { + @NonNull + @Override + public Priority priority() { + return Priority.after(a1.getClass(), b1.getClass()); + } + }; + + final MarkwonPlugin d1 = new AbstractMarkwonPlugin() { + @NonNull + @Override + public Priority priority() { + return Priority.builder() + .after(a1.getClass()) + .after(b1.getClass()) + .after(c1.getClass()) + .build(); + } + }; + + final List plugins = processor.process(Arrays.asList(c1, d1, a1, b1)); + assertEquals(4, plugins.size()); + assertEquals(a1, plugins.get(0)); + assertEquals(b1, plugins.get(1)); + assertEquals(c1, plugins.get(2)); + assertEquals(d1, plugins.get(3)); + } + + @Test + public void cycle() { + + final class Holder { + Class type; + } + final Holder holder = new Holder(); + + final MarkwonPlugin first = new AbstractMarkwonPlugin() { + @NonNull + @Override + public Priority priority() { + return Priority.after(holder.type); + } + }; + + final MarkwonPlugin second = new AbstractMarkwonPlugin() { + + { + holder.type = getClass(); + } + + @NonNull + @Override + public Priority priority() { + return Priority.after(first.getClass()); + } + }; + + try { + processor.process(Arrays.asList(second, first)); + fail(); + } catch (IllegalStateException e) { + assertTrue(e.getMessage(), e.getMessage().contains("being referenced by own dependency (cycle)")); + } + } + + @Test + public void bigger_cycle() { + + final class Plugin extends NamedPlugin { + + private Priority priority; + + private Plugin(@NonNull String name) { + super(name); + } + + private void set(@NonNull MarkwonPlugin plugin) { + priority = Priority.after(plugin.getClass()); + } + + @NonNull + @Override + public Priority priority() { + return priority; + } + } + + final Plugin a = new Plugin("a"); + + final List plugins = new ArrayList<>(); + plugins.add(a); + plugins.add(new NamedPlugin("b", plugins.get(plugins.size() - 1)) { + }); + plugins.add(new NamedPlugin("c", plugins.get(plugins.size() - 1)) { + }); + plugins.add(new NamedPlugin("d", plugins.get(plugins.size() - 1)) { + }); + plugins.add(new NamedPlugin("e", plugins.get(plugins.size() - 1)) { + }); + plugins.add(new NamedPlugin("f", plugins.get(plugins.size() - 1)) { + }); + plugins.add(new NamedPlugin("g", plugins.get(plugins.size() - 1)) { + }); + + // link with the last one + a.set(plugins.get(plugins.size() - 1)); + + try { + processor.process(plugins); + fail(); + } catch (IllegalStateException e) { + assertTrue(e.getMessage(), e.getMessage().contains("being referenced by own dependency (cycle)")); + } + } + + @Test + public void deep_tree() { + + // we must create subclasses in order to register them like this (otherwise -> duplicates) + final MarkwonPlugin a = new NamedPlugin("a") { + }; + final MarkwonPlugin b1 = new NamedPlugin("b1", a) { + }; + final MarkwonPlugin b2 = new NamedPlugin("b2", a) { + }; + final MarkwonPlugin c1 = new NamedPlugin("c1", b1) { + }; + final MarkwonPlugin c2 = new NamedPlugin("c2", b1) { + }; + final MarkwonPlugin c3 = new NamedPlugin("c3", b2) { + }; + final MarkwonPlugin c4 = new NamedPlugin("c4", b2) { + }; + final MarkwonPlugin d1 = new NamedPlugin("d1", c1) { + }; + final MarkwonPlugin e1 = new NamedPlugin("e1", d1, c2, c3, c4) { + }; + + final List plugins = processor.process(Arrays.asList(b2, b1, + a, e1, c4, c3, c2, c1, d1)); + + // a is first + // b1 + b2 -> second+third + // c1 + c2 + c3 + c4 -> forth, fifth, sixth, seventh + // d1 -> 8th + // e1 -> 9th + + assertEquals(9, plugins.size()); + assertEquals(a, plugins.get(0)); + assertEquals(new HashSet<>(Arrays.asList(b1, b2)), new HashSet<>(plugins.subList(1, 3))); + assertEquals(new HashSet<>(Arrays.asList(c1, c2, c3, c4)), new HashSet<>(plugins.subList(3, 7))); + assertEquals(d1, plugins.get(7)); + assertEquals(e1, plugins.get(8)); + } + + @Test + public void multiple_detached() { + + // when graph has independent elements that are not connected with each other + final MarkwonPlugin a0 = new NamedPlugin("a0") { + }; + final MarkwonPlugin a1 = new NamedPlugin("a1", a0) { + }; + final MarkwonPlugin a2 = new NamedPlugin("a2", a1) { + }; + + final MarkwonPlugin b0 = new NamedPlugin("b0") { + }; + final MarkwonPlugin b1 = new NamedPlugin("b1", b0) { + }; + final MarkwonPlugin b2 = new NamedPlugin("b2", b1) { + }; + + final List plugins = processor.process(Arrays.asList( + b2, a2, a0, b0, b1, a1)); + + assertEquals(6, plugins.size()); + + assertEquals(new HashSet<>(Arrays.asList(a0, b0)), new HashSet<>(plugins.subList(0, 2))); + assertEquals(new HashSet<>(Arrays.asList(a1, b1)), new HashSet<>(plugins.subList(2, 4))); + assertEquals(new HashSet<>(Arrays.asList(a2, b2)), new HashSet<>(plugins.subList(4, 6))); + } + + private static abstract class NamedPlugin extends AbstractMarkwonPlugin { + + private final String name; + private final Priority priority; + + NamedPlugin(@NonNull String name) { + this(name, (Priority) null); + } + + NamedPlugin(@NonNull String name, @Nullable MarkwonPlugin plugin) { + this(name, plugin != null ? Priority.after(plugin.getClass()) : null); + } + + NamedPlugin(@NonNull String name, MarkwonPlugin... plugins) { + this(name, of(plugins)); + } + + NamedPlugin(@NonNull String name, @Nullable Class plugin) { + this(name, plugin != null ? Priority.after(plugin) : null); + } + + NamedPlugin(@NonNull String name, @Nullable Priority priority) { + this.name = name; + this.priority = priority; + } + + @NonNull + @Override + public Priority priority() { + return priority != null + ? priority + : Priority.none(); + } + + @Override + public String toString() { + return "NamedPlugin{" + + "name='" + name + '\'' + + '}'; + } + + @NonNull + private static Priority of(@NonNull MarkwonPlugin... plugins) { + if (plugins.length == 0) return Priority.none(); + final Priority.Builder builder = Priority.builder(); + for (MarkwonPlugin plugin : plugins) { + builder.after(plugin.getClass()); + } + return builder.build(); + } + } +} \ No newline at end of file From c16cdff871cdadc9c928feb08f637a5aa34c36f8 Mon Sep 17 00:00:00 2001 From: Dimitry Ivanov Date: Thu, 27 Dec 2018 18:45:45 +0300 Subject: [PATCH 058/103] MarkwonBuilderImpl test --- .../ru/noties/markwon/MarkdownRenderer.java | 9 + build.gradle | 14 +- .../ru/noties/markwon/MarkwonBuilderImpl.java | 88 ++++++- .../ru/noties/markwon/image/ImagesPlugin.java | 3 + .../priority/PriorityProcessorImpl.java | 11 +- .../markwon/MarkwonBuilderImplTest.java | 231 ++++++++++++++++++ .../priority/PriorityProcessorTest.java | 27 ++ 7 files changed, 357 insertions(+), 26 deletions(-) create mode 100644 markwon/src/test/java/ru/noties/markwon/MarkwonBuilderImplTest.java diff --git a/app/src/main/java/ru/noties/markwon/MarkdownRenderer.java b/app/src/main/java/ru/noties/markwon/MarkdownRenderer.java index c082312a..265b9cd5 100644 --- a/app/src/main/java/ru/noties/markwon/MarkdownRenderer.java +++ b/app/src/main/java/ru/noties/markwon/MarkdownRenderer.java @@ -8,6 +8,7 @@ import android.support.annotation.NonNull; import android.support.annotation.Nullable; import android.text.Spanned; +import java.util.concurrent.Callable; import java.util.concurrent.ExecutorService; import java.util.concurrent.Future; @@ -71,9 +72,17 @@ public class MarkdownRenderer { cancel(); task = service.submit(new Runnable() { + @Override public void run() { + try { + execute(); + } catch (Throwable t) { + Debug.e(t); + } + } + private void execute() { final UrlProcessor urlProcessor; if (uri == null) { urlProcessor = new UrlProcessorInitialReadme(); diff --git a/build.gradle b/build.gradle index 05e0927e..88a298fa 100644 --- a/build.gradle +++ b/build.gradle @@ -77,14 +77,12 @@ ext { ] deps['test'] = [ - 'junit' : 'junit:junit:4.12', - 'robolectric' : 'org.robolectric:robolectric:3.8', - 'ix-java' : 'com.github.akarnokd:ixjava:1.0.0', - 'jackson-yaml' : 'com.fasterxml.jackson.dataformat:jackson-dataformat-yaml:2.9.0', - 'jackson-databind': 'com.fasterxml.jackson.core:jackson-databind:2.9.6', - 'gson' : 'com.google.code.gson:gson:2.8.5', - 'commons-io' : 'commons-io:commons-io:2.6', - 'mockito' : 'org.mockito:mockito-core:2.21.0' + 'junit' : 'junit:junit:4.12', + 'robolectric': 'org.robolectric:robolectric:3.8', + 'ix-java' : 'com.github.akarnokd:ixjava:1.0.0', + 'gson' : 'com.google.code.gson:gson:2.8.5', + 'commons-io' : 'commons-io:commons-io:2.6', + 'mockito' : 'org.mockito:mockito-core:2.21.0' ] registerArtifact = this.®isterArtifact diff --git a/markwon/src/main/java/ru/noties/markwon/MarkwonBuilderImpl.java b/markwon/src/main/java/ru/noties/markwon/MarkwonBuilderImpl.java index e0eabdf6..e4cf56d9 100644 --- a/markwon/src/main/java/ru/noties/markwon/MarkwonBuilderImpl.java +++ b/markwon/src/main/java/ru/noties/markwon/MarkwonBuilderImpl.java @@ -2,7 +2,7 @@ package ru.noties.markwon; import android.content.Context; import android.support.annotation.NonNull; -import android.util.Log; +import android.support.annotation.VisibleForTesting; import android.widget.TextView; import org.commonmark.parser.Parser; @@ -12,6 +12,7 @@ import java.util.Collections; import java.util.Iterator; import java.util.List; +import ru.noties.markwon.core.CorePlugin; import ru.noties.markwon.core.MarkwonTheme; import ru.noties.markwon.image.AsyncDrawableLoader; import ru.noties.markwon.priority.PriorityProcessor; @@ -19,6 +20,7 @@ import ru.noties.markwon.priority.PriorityProcessor; /** * @since 3.0.0 */ +@SuppressWarnings("WeakerAccess") class MarkwonBuilderImpl implements Markwon.Builder { private final Context context; @@ -66,6 +68,7 @@ class MarkwonBuilderImpl implements Markwon.Builder { return this; } + @SuppressWarnings("UnusedReturnValue") @NonNull public MarkwonBuilderImpl priorityProcessor(@NonNull PriorityProcessor priorityProcessor) { this.priorityProcessor = priorityProcessor; @@ -76,6 +79,23 @@ class MarkwonBuilderImpl implements Markwon.Builder { @Override public Markwon build() { + if (plugins.isEmpty()) { + throw new IllegalStateException("No plugins were added to this builder. Use #usePlugin " + + "method to add them"); + } + + // this class will sort plugins to match a priority/dependency graph that we have + PriorityProcessor priorityProcessor = this.priorityProcessor; + if (priorityProcessor == null) { + // strictly speaking we do not need updating this field + // as we are not building this class to be reused between multiple `build` calls + priorityProcessor = this.priorityProcessor = PriorityProcessor.create(); + } + + // please note that this method must not modify supplied collection + // if nothing should be done -> the same collection can be returned + final List plugins = preparePlugins(priorityProcessor, this.plugins); + final Parser.Builder parserBuilder = new Parser.Builder(); final MarkwonTheme.Builder themeBuilder = MarkwonTheme.builderWithDefaults(context); final AsyncDrawableLoader.Builder asyncDrawableLoaderBuilder = new AsyncDrawableLoader.Builder(); @@ -84,18 +104,7 @@ class MarkwonBuilderImpl implements Markwon.Builder { final MarkwonSpansFactory.Builder spanFactoryBuilder = new MarkwonSpansFactoryImpl.BuilderImpl(); final RenderProps renderProps = new RenderPropsImpl(); - PriorityProcessor priorityProcessor = this.priorityProcessor; - if (priorityProcessor == null) { - // strictly speaking we do not need updating this field - // as we are not building this class to be reused between multiple `build` calls - priorityProcessor = this.priorityProcessor = PriorityProcessor.create(); - } - final List plugins = priorityProcessor.process(this.plugins); - for (MarkwonPlugin plugin : plugins) { - if (true) { - Log.e("PLUGIN", plugin.getClass().getName()); - } plugin.configureParser(parserBuilder); plugin.configureTheme(themeBuilder); plugin.configureImages(asyncDrawableLoaderBuilder); @@ -116,4 +125,59 @@ class MarkwonBuilderImpl implements Markwon.Builder { Collections.unmodifiableList(plugins) ); } + + @VisibleForTesting + @NonNull + static List preparePlugins( + @NonNull PriorityProcessor priorityProcessor, + @NonNull List plugins) { + + // with this method we will ensure that CorePlugin is added IF and ONLY IF + // there are plugins that depend on it. If CorePlugin is added, or there are + // no plugins that require it, CorePlugin won't be added + final List out = ensureImplicitCoreIfHasDependents(plugins); + + return priorityProcessor.process(out); + } + + // this method will _implicitly_ add CorePlugin if there is at least one plugin + // that depends on CorePlugin + @VisibleForTesting + @NonNull + static List ensureImplicitCoreIfHasDependents(@NonNull List plugins) { + // loop over plugins -> if CorePlugin is found -> break; + // iterate over all plugins and check if CorePlugin is requested + + boolean hasCore = false; + boolean hasCoreDependents = false; + + for (MarkwonPlugin plugin : plugins) { + + // here we do not check for exact match (a user could've subclasses CorePlugin + // and supplied it. In this case we DO NOT implicitly add CorePlugin + if (CorePlugin.class.isAssignableFrom(plugin.getClass())) { + hasCore = true; + break; + } + + // if plugin has CorePlugin in dependencies -> mark for addition + if (!hasCoreDependents) { + // here we check for direct CorePlugin, if it's not CorePlugin (exact, not a subclass + // or something -> ignore) + if (plugin.priority().after().contains(CorePlugin.class)) { + hasCoreDependents = true; + } + } + } + + if (hasCoreDependents && !hasCore) { + final List out = new ArrayList<>(plugins.size() + 1); + // add default instance of CorePlugin + out.add(CorePlugin.create()); + out.addAll(plugins); + return out; + } + + return plugins; + } } diff --git a/markwon/src/main/java/ru/noties/markwon/image/ImagesPlugin.java b/markwon/src/main/java/ru/noties/markwon/image/ImagesPlugin.java index dc51504f..d32ee10c 100644 --- a/markwon/src/main/java/ru/noties/markwon/image/ImagesPlugin.java +++ b/markwon/src/main/java/ru/noties/markwon/image/ImagesPlugin.java @@ -21,6 +21,9 @@ import ru.noties.markwon.image.data.DataUriSchemeHandler; import ru.noties.markwon.image.file.FileSchemeHandler; import ru.noties.markwon.image.network.NetworkSchemeHandler; +/** + * @since 3.0.0 + */ public class ImagesPlugin extends AbstractMarkwonPlugin { @NonNull diff --git a/markwon/src/main/java/ru/noties/markwon/priority/PriorityProcessorImpl.java b/markwon/src/main/java/ru/noties/markwon/priority/PriorityProcessorImpl.java index 08e820b4..e676a9c9 100644 --- a/markwon/src/main/java/ru/noties/markwon/priority/PriorityProcessorImpl.java +++ b/markwon/src/main/java/ru/noties/markwon/priority/PriorityProcessorImpl.java @@ -2,6 +2,7 @@ package ru.noties.markwon.priority; import android.support.annotation.NonNull; +import java.util.ArrayList; import java.util.Collections; import java.util.Comparator; import java.util.HashMap; @@ -18,7 +19,10 @@ class PriorityProcessorImpl extends PriorityProcessor { @NonNull @Override - public List process(@NonNull List plugins) { + public List process(@NonNull List in) { + + // create new collection based on supplied argument + final List plugins = new ArrayList<>(in); final int size = plugins.size(); @@ -32,7 +36,6 @@ class PriorityProcessorImpl extends PriorityProcessor { } } - // change to Map final Map cache = new HashMap<>(size); for (MarkwonPlugin plugin : plugins) { cache.put(plugin, eval(plugin, map)); @@ -126,8 +129,4 @@ class PriorityProcessorImpl extends PriorityProcessor { return map.get(o1).compareTo(map.get(o2)); } } - - private static class NoCorePluginAddedException extends Exception { - - } } diff --git a/markwon/src/test/java/ru/noties/markwon/MarkwonBuilderImplTest.java b/markwon/src/test/java/ru/noties/markwon/MarkwonBuilderImplTest.java new file mode 100644 index 00000000..2ce7c25e --- /dev/null +++ b/markwon/src/test/java/ru/noties/markwon/MarkwonBuilderImplTest.java @@ -0,0 +1,231 @@ +package ru.noties.markwon; + +import android.support.annotation.NonNull; +import android.text.Spanned; +import android.widget.TextView; + +import org.commonmark.node.Node; +import org.commonmark.parser.Parser; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.ArgumentMatchers; +import org.mockito.invocation.InvocationOnMock; +import org.mockito.stubbing.Answer; +import org.robolectric.RobolectricTestRunner; +import org.robolectric.RuntimeEnvironment; +import org.robolectric.annotation.Config; + +import java.util.Arrays; +import java.util.Collections; +import java.util.List; + +import ru.noties.markwon.core.CorePlugin; +import ru.noties.markwon.core.MarkwonTheme; +import ru.noties.markwon.image.AsyncDrawableLoader; +import ru.noties.markwon.priority.Priority; +import ru.noties.markwon.priority.PriorityProcessor; + +import static org.hamcrest.Matchers.containsString; +import static org.hamcrest.Matchers.hasItem; +import static org.hamcrest.Matchers.hasSize; +import static org.hamcrest.Matchers.isA; +import static org.hamcrest.Matchers.not; +import static org.junit.Assert.assertThat; +import static org.junit.Assert.fail; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.Mockito.atLeast; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; +import static ru.noties.markwon.MarkwonBuilderImpl.ensureImplicitCoreIfHasDependents; +import static ru.noties.markwon.MarkwonBuilderImpl.preparePlugins; + +@RunWith(RobolectricTestRunner.class) +@Config(manifest = Config.NONE) +public class MarkwonBuilderImplTest { + + @Test + public void implicit_core_created() { + // a plugin explicitly requests CorePlugin, but CorePlugin is not added manually by user + // we validate that default CorePlugin instance is added + + final MarkwonPlugin plugin = new AbstractMarkwonPlugin() { + @NonNull + @Override + public Priority priority() { + // strictly speaking we do not need to override this method + // as all children of AbstractMarkwonPlugin specify CorePlugin as a dependency. + // but we still add it to make things explicit and future proof, if this + // behavior changes + return Priority.after(CorePlugin.class); + } + }; + + final List plugins = ensureImplicitCoreIfHasDependents(Collections.singletonList(plugin)); + + assertThat(plugins, hasSize(2)); + assertThat(plugins, hasItem(isA(CorePlugin.class))); + } + + @Test + public void implicit_core_no_dependents_not_added() { + final MarkwonPlugin a = new AbstractMarkwonPlugin() { + @NonNull + @Override + public Priority priority() { + return Priority.none(); + } + }; + + final MarkwonPlugin b = new AbstractMarkwonPlugin() { + @NonNull + @Override + public Priority priority() { + return Priority.after(a.getClass()); + } + }; + + final List plugins = ensureImplicitCoreIfHasDependents(Arrays.asList(a, b)); + assertThat(plugins, hasSize(2)); + assertThat(plugins, not(hasItem(isA(CorePlugin.class)))); + } + + @Test + public void implicit_core_present() { + // if core is present it won't be added (whether or not there are dependents) + + final MarkwonPlugin plugin = new AbstractMarkwonPlugin() { + @NonNull + @Override + public Priority priority() { + return Priority.after(CorePlugin.class); + } + }; + + final CorePlugin corePlugin = CorePlugin.create(); + + final List plugins = ensureImplicitCoreIfHasDependents(Arrays.asList(plugin, corePlugin)); + assertThat(plugins, hasSize(2)); + assertThat(plugins, hasItem(plugin)); + assertThat(plugins, hasItem(corePlugin)); + } + + @Test + public void implicit_core_subclass_present() { + // core was subclassed by a user and provided (implicit core won't be added) + + final MarkwonPlugin plugin = new AbstractMarkwonPlugin() { + @NonNull + @Override + public Priority priority() { + return Priority.after(CorePlugin.class); + } + }; + + // our subclass + final CorePlugin corePlugin = new CorePlugin(false) { + + }; + + final List plugins = ensureImplicitCoreIfHasDependents(Arrays.asList(plugin, corePlugin)); + assertThat(plugins, hasSize(2)); + assertThat(plugins, hasItem(plugin)); + assertThat(plugins, hasItem(corePlugin)); + } + + @Test + public void prepare_plugins() { + // validate that prepare plugins is calling `ensureImplicitCoreIfHasDependents` and + // priority processor + + final PriorityProcessor priorityProcessor = mock(PriorityProcessor.class); + when(priorityProcessor.process(ArgumentMatchers.anyList())) + .thenAnswer(new Answer() { + @Override + public Object answer(InvocationOnMock invocation) { + return invocation.getArgument(0); + } + }); + + final MarkwonPlugin plugin = new AbstractMarkwonPlugin() { + @NonNull + @Override + public Priority priority() { + return Priority.after(CorePlugin.class); + } + }; + + final List plugins = preparePlugins(priorityProcessor, Collections.singletonList(plugin)); + assertThat(plugins, hasSize(2)); + assertThat(plugins, hasItem(plugin)); + assertThat(plugins, hasItem(isA(CorePlugin.class))); + + verify(priorityProcessor, times(1)) + .process(ArgumentMatchers.anyList()); + } + + @Test + public void user_supplied_priority_processor() { + // verify that if user supplied priority processor it will be used + + final PriorityProcessor priorityProcessor = mock(PriorityProcessor.class); + final MarkwonBuilderImpl impl = new MarkwonBuilderImpl(RuntimeEnvironment.application); + + // add some plugin (we do not care which one, but it must be present as we do not + // allow empty plugins list) + impl.usePlugin(new AbstractMarkwonPlugin() { + @NonNull + @Override + public Priority priority() { + return Priority.none(); + } + }); + impl.priorityProcessor(priorityProcessor); + impl.build(); + + verify(priorityProcessor, times(1)).process(ArgumentMatchers.anyList()); + } + + @Test + public void no_plugins_added_throws() { + // there is no sense in having an instance with no plugins registered + + try { + new MarkwonBuilderImpl(RuntimeEnvironment.application).build(); + fail(); + } catch (IllegalStateException e) { + assertThat(e.getMessage(), e.getMessage(), containsString("No plugins were added")); + } + } + + @Test + public void plugin_configured() { + // verify that all configuration methods (applicable) are called + + final MarkwonPlugin plugin = mock(MarkwonPlugin.class); + when(plugin.priority()).thenReturn(Priority.none()); + + final MarkwonBuilderImpl impl = new MarkwonBuilderImpl(RuntimeEnvironment.application); + impl.usePlugin(plugin).build(); + + verify(plugin, times(1)).configureParser(any(Parser.Builder.class)); + verify(plugin, times(1)).configureTheme(any(MarkwonTheme.Builder.class)); + verify(plugin, times(1)).configureImages(any(AsyncDrawableLoader.Builder.class)); + verify(plugin, times(1)).configureConfiguration(any(MarkwonConfiguration.Builder.class)); + verify(plugin, times(1)).configureVisitor(any(MarkwonVisitor.Builder.class)); + verify(plugin, times(1)).configureSpansFactory(any(MarkwonSpansFactory.Builder.class)); + + // we do not know how many times exactly, but at least once it must be called + verify(plugin, atLeast(1)).priority(); + + // note, no render props -> they must be configured on render stage + verify(plugin, times(0)).configureRenderProps(any(RenderProps.class)); + verify(plugin, times(0)).processMarkdown(anyString()); + verify(plugin, times(0)).beforeRender(any(Node.class)); + verify(plugin, times(0)).afterRender(any(Node.class), any(MarkwonVisitor.class)); + verify(plugin, times(0)).beforeSetText(any(TextView.class), any(Spanned.class)); + verify(plugin, times(0)).afterSetText(any(TextView.class)); + } +} \ No newline at end of file diff --git a/markwon/src/test/java/ru/noties/markwon/priority/PriorityProcessorTest.java b/markwon/src/test/java/ru/noties/markwon/priority/PriorityProcessorTest.java index 28612ca4..2274a7f6 100644 --- a/markwon/src/test/java/ru/noties/markwon/priority/PriorityProcessorTest.java +++ b/markwon/src/test/java/ru/noties/markwon/priority/PriorityProcessorTest.java @@ -159,6 +159,33 @@ public class PriorityProcessorTest { assertEquals(third, plugins.get(2)); } + @Test + public void five_plugins_sequential() { + + final MarkwonPlugin a = new NamedPlugin("a") { + }; + + final MarkwonPlugin b = new NamedPlugin("b", a) { + }; + + final MarkwonPlugin c = new NamedPlugin("c", b) { + }; + + final MarkwonPlugin d = new NamedPlugin("d", c) { + }; + + final MarkwonPlugin e = new NamedPlugin("e", d) { + }; + + final List plugins = processor.process(Arrays.asList(d, e, a, c, b)); + assertEquals(5, plugins.size()); + assertEquals(a, plugins.get(0)); + assertEquals(b, plugins.get(1)); + assertEquals(c, plugins.get(2)); + assertEquals(d, plugins.get(3)); + assertEquals(e, plugins.get(4)); + } + @Test public void plugin_duplicate() { From 0505845e4ce259f0b585aefeee0aa25167bcf3da Mon Sep 17 00:00:00 2001 From: Dimitry Ivanov Date: Fri, 28 Dec 2018 14:19:36 +0300 Subject: [PATCH 059/103] Add sample module and update app icons --- app/build.gradle | 2 +- app/src/main/AndroidManifest.xml | 5 ++- .../drawable-v26/ic_launcher_background.xml | 24 ++++++++++ .../res/mipmap-anydpi-v26/ic_launcher.xml | 5 +++ .../mipmap-anydpi-v26/ic_launcher_round.xml | 5 +++ app/src/main/res/mipmap-hdpi/ic_launcher.png | Bin 2561 -> 1929 bytes .../mipmap-hdpi/ic_launcher_foreground.png | Bin 0 -> 1892 bytes .../res/mipmap-hdpi/ic_launcher_round.png | Bin 0 -> 2749 bytes app/src/main/res/mipmap-mdpi/ic_launcher.png | Bin 1369 -> 0 bytes app/src/main/res/mipmap-xhdpi/ic_launcher.png | Bin 3211 -> 2323 bytes .../mipmap-xhdpi/ic_launcher_foreground.png | Bin 0 -> 2603 bytes .../res/mipmap-xhdpi/ic_launcher_round.png | Bin 0 -> 3583 bytes .../main/res/mipmap-xxhdpi/ic_launcher.png | Bin 5946 -> 3501 bytes .../mipmap-xxhdpi/ic_launcher_foreground.png | Bin 0 -> 3934 bytes .../res/mipmap-xxhdpi/ic_launcher_round.png | Bin 0 -> 5225 bytes .../main/res/mipmap-xxxhdpi/ic_launcher.png | Bin 7441 -> 4440 bytes .../mipmap-xxxhdpi/ic_launcher_foreground.png | Bin 0 -> 5250 bytes .../res/mipmap-xxxhdpi/ic_launcher_round.png | Bin 0 -> 7024 bytes sample/build.gradle | 42 ++++++++++++++++++ sample/src/main/AndroidManifest.xml | 23 ++++++++++ .../noties/markwon/sample/MainActivity.java | 12 +++++ .../drawable-v26/ic_launcher_background.xml | 24 ++++++++++ .../res/mipmap-anydpi-v26/ic_launcher.xml | 5 +++ .../mipmap-anydpi-v26/ic_launcher_round.xml | 5 +++ .../src/main/res/mipmap-hdpi/ic_launcher.png | Bin 0 -> 2094 bytes .../mipmap-hdpi/ic_launcher_foreground.png | Bin 0 -> 3165 bytes .../res/mipmap-hdpi/ic_launcher_round.png | Bin 0 -> 3874 bytes .../src/main/res/mipmap-xhdpi/ic_launcher.png | Bin 0 -> 2401 bytes .../mipmap-xhdpi/ic_launcher_foreground.png | Bin 0 -> 4610 bytes .../res/mipmap-xhdpi/ic_launcher_round.png | Bin 0 -> 5527 bytes .../main/res/mipmap-xxhdpi/ic_launcher.png | Bin 0 -> 3851 bytes .../mipmap-xxhdpi/ic_launcher_foreground.png | Bin 0 -> 8468 bytes .../res/mipmap-xxhdpi/ic_launcher_round.png | Bin 0 -> 8957 bytes .../main/res/mipmap-xxxhdpi/ic_launcher.png | Bin 0 -> 4359 bytes .../mipmap-xxxhdpi/ic_launcher_foreground.png | Bin 0 -> 13141 bytes .../res/mipmap-xxxhdpi/ic_launcher_round.png | Bin 0 -> 13141 bytes sample/src/main/res/values/colors.xml | 6 +++ sample/src/main/res/values/strings.xml | 3 ++ sample/src/main/res/values/styles.xml | 8 ++++ settings.gradle | 2 +- 40 files changed, 168 insertions(+), 3 deletions(-) create mode 100644 app/src/main/res/drawable-v26/ic_launcher_background.xml create mode 100644 app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml create mode 100644 app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml create mode 100644 app/src/main/res/mipmap-hdpi/ic_launcher_foreground.png create mode 100644 app/src/main/res/mipmap-hdpi/ic_launcher_round.png delete mode 100644 app/src/main/res/mipmap-mdpi/ic_launcher.png create mode 100644 app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.png create mode 100644 app/src/main/res/mipmap-xhdpi/ic_launcher_round.png create mode 100644 app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.png create mode 100644 app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png create mode 100644 app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.png create mode 100644 app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png create mode 100644 sample/build.gradle create mode 100644 sample/src/main/AndroidManifest.xml create mode 100644 sample/src/main/java/ru/noties/markwon/sample/MainActivity.java create mode 100644 sample/src/main/res/drawable-v26/ic_launcher_background.xml create mode 100644 sample/src/main/res/mipmap-anydpi-v26/ic_launcher.xml create mode 100644 sample/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml create mode 100644 sample/src/main/res/mipmap-hdpi/ic_launcher.png create mode 100644 sample/src/main/res/mipmap-hdpi/ic_launcher_foreground.png create mode 100644 sample/src/main/res/mipmap-hdpi/ic_launcher_round.png create mode 100644 sample/src/main/res/mipmap-xhdpi/ic_launcher.png create mode 100644 sample/src/main/res/mipmap-xhdpi/ic_launcher_foreground.png create mode 100644 sample/src/main/res/mipmap-xhdpi/ic_launcher_round.png create mode 100644 sample/src/main/res/mipmap-xxhdpi/ic_launcher.png create mode 100644 sample/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.png create mode 100644 sample/src/main/res/mipmap-xxhdpi/ic_launcher_round.png create mode 100644 sample/src/main/res/mipmap-xxxhdpi/ic_launcher.png create mode 100644 sample/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.png create mode 100644 sample/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png create mode 100644 sample/src/main/res/values/colors.xml create mode 100644 sample/src/main/res/values/strings.xml create mode 100644 sample/src/main/res/values/styles.xml diff --git a/app/build.gradle b/app/build.gradle index e0d0fbb8..93b59222 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -11,7 +11,7 @@ android { targetSdkVersion config['target-sdk'] versionCode 1 versionName version - setProperty("archivesBaseName", "markwon-sample-$versionName") + setProperty("archivesBaseName", "markwon-$versionName") } lintOptions { diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 2446cea2..ec1059ae 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -1,5 +1,6 @@ @@ -10,8 +11,10 @@ android:allowBackup="true" android:icon="@mipmap/ic_launcher" android:label="@string/app_name" + android:roundIcon="@mipmap/ic_launcher" android:supportsRtl="true" - android:theme="@style/AppThemeLight"> + android:theme="@style/AppThemeLight" + tools:ignore="AllowBackup"> diff --git a/app/src/main/res/drawable-v26/ic_launcher_background.xml b/app/src/main/res/drawable-v26/ic_launcher_background.xml new file mode 100644 index 00000000..49c86ecb --- /dev/null +++ b/app/src/main/res/drawable-v26/ic_launcher_background.xml @@ -0,0 +1,24 @@ + + + + diff --git a/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml b/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml new file mode 100644 index 00000000..c4a603d4 --- /dev/null +++ b/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml b/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml new file mode 100644 index 00000000..c4a603d4 --- /dev/null +++ b/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/mipmap-hdpi/ic_launcher.png b/app/src/main/res/mipmap-hdpi/ic_launcher.png index cac0e405a8e026ffc9ba032d6b4d29a89f2044fc..869b9cbcbe2577e364bce655ef7d6e872a21f90f 100644 GIT binary patch literal 1929 zcmai#c{J4f8^AFHT3Sp@Ol4(dZEbBwM@Myabx24Efj~$}N$KwHHa0fy z>gqBuFbEG14+{$e7I}Gjd3$@OrluAb7r%M)=Fy`^*RNmq^YfFEl4@ybnV+BE*x0yn zTe2OG}%Yni3Neot&J2`OVGE zw{G2vjg7s0`Ldmzor8lzP*4zsLUDI@*VWY}5{Ux?19&{1L?T5*L_B!#z{A4>gTX8> zFGog3Uc7kG%*@Qh#3VaA8w45{7zjel&CRv4vO=TLYHDhknVA_G8J3ooy}i94!M3)x z!otGWuU{J(8TIt^fMgdJ7dtyU*Vfh=8XA(4l9)_pUS6Jyi;J?daz#ajs;cUxOP4-; z`0(Pz3s+ZHMMXt(bMwBwzV!6;g@uI+7cRud$1@m=xVSiwI0Av_@9z%?2=MXoQBY9O z(b18Ymj@J$jEvm9dlwFe1DpV0OG`^A6bgU=Pz1~%k;r%N-rc)*4~N64sHkXbYd?AN zq^71uLqlU_Wd%?nBO?Pqg27;Na&pqr(z3F$p`oF}!^40;I-L%n0%!s70B>7cTYx

M z*Z~1g`$7A`!>^<&e89mea>)Iw5HB0YadxEG2~n~!$(NUj(FD~ea*rmfQ)0KHzcFFJ@S4?ji7k}!#;w1i50ijod$B1yE z3=7IfgdRVcC>iH+93@P2DU8;?H4&_ecUrqhjgGyRq9m0>K6}b9=|~zs!$82N^uN<= zvAa%ch7M3XHL4Vm$zdw2_8&O!F-mM2isgoWej&aNC+?KxN&JRv@Y%NU{*&O}8JPDx z%WY{oQX0Fu+Ed=lJZamj1*yVOo z;SLJ_6xrOG5RAW;`0mx5$?2*{{~q!W#=(Bk;QHHda;|=LwOjN-;v)9o4~Lw{?+N3O zoJqY73;W`bQ7*~3An%3dlW2i^RuT%*KDT8zLzV?u=@E^4XOExfkr)`lx>CMc!*6CPOg8=QsIm0S z(VF0ark|MIV(~0?NH^?;l?RyPv=Jl8)#*8>Gb8Ae^feU5>UV{fxhS0dZ*O*}UmJ3| zdseacTYR0Y*ZySI`Ds+jt9DDZu3mWw-$^Wzu>ao6IsT?j&&@m?-EpZoKS4O+8E54!{qw~1!V{rHc=bc;>DgpJuTIdmPotV&0sy} z@?2Z8EJxZ{K*&PV3G3i3(^R#NFP~lTb=dPKevKBNWD%w|=;c38JJ}RePcF67oAp;J zNpJO%!y?YyE+P#|=*B%&I3H)NC@B=Vfacmx)AE3q#7VOeWfo=xDT0flg66hT1@>Fgn;+Q(tomM@t+%6mA!Z_~?^D0>&SmtNSE z&XE51t(xk%{+8{r-tImts79py{4CU*qNZz^6mLJmQG&_rg+IjPbzMt`U&bB@!nyl? a*=M)H2RN9D^OyYdOENXGG^{ajBmNCt&!>O@ literal 2561 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 bPj>SU%H#~h)DsTB00000NkvXXu0mjf?eL9L diff --git a/app/src/main/res/mipmap-xhdpi/ic_launcher.png b/app/src/main/res/mipmap-xhdpi/ic_launcher.png index c71913957a7d10b49e27e10c56fbe6a1c9564435..8b6c884da54cc3ff6c7bd5966ef6c247f1bdbbe4 100644 GIT binary patch literal 2323 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 bwJgq&-nU?B5z7RBmM|-GJ3{^;^0og0Ts7AE literal 0 HcmV?d00001 diff --git a/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png b/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png new file mode 100644 index 0000000000000000000000000000000000000000..9501337466f164f43ffb91943e2327ec98ad7ccc GIT binary patch literal 3583 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 R5B#-4TI%|^ay6Tf{{hGv->v`v literal 0 HcmV?d00001 diff --git a/app/src/main/res/mipmap-xxhdpi/ic_launcher.png b/app/src/main/res/mipmap-xxhdpi/ic_launcher.png index 5fda70fed23c644b2b29735e075cf3ba22fd4d95..af43070f84ccdafb18f04c8e5c9bb741a4f5e302 100644 GIT binary patch literal 3501 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 literal 5946 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 literal 0 HcmV?d00001 diff --git a/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png b/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png new file mode 100644 index 0000000000000000000000000000000000000000..837bc659cb4565731b6dc8448f0d61b7ffd4cf32 GIT binary patch literal 5225 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 JSE$)V{1^0AAZY*q literal 7441 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 diff --git a/app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.png b/app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.png new file mode 100644 index 0000000000000000000000000000000000000000..0a73d18cb616fbe868472705175ae6a9c1e106fd GIT binary patch literal 5250 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 A3IG5A literal 0 HcmV?d00001 diff --git a/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png b/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png new file mode 100644 index 0000000000000000000000000000000000000000..ab9c90422ce7797ceb72c87e963720cb19d323e3 GIT binary patch literal 7024 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 N)RlFV$`x#${SOLz5WD~Y literal 0 HcmV?d00001 diff --git a/sample/build.gradle b/sample/build.gradle new file mode 100644 index 00000000..3d520a29 --- /dev/null +++ b/sample/build.gradle @@ -0,0 +1,42 @@ +apply plugin: 'com.android.application' + +android { + + compileSdkVersion config['compile-sdk'] + buildToolsVersion config['build-tools'] + + defaultConfig { + applicationId "ru.noties.markwon.sample" + minSdkVersion config['min-sdk'] + targetSdkVersion config['target-sdk'] + versionCode 1 + versionName version + setProperty("archivesBaseName", "markwon-sample-$versionName") + } + + lintOptions { + abortOnError false + } +} + +dependencies { + + implementation project(':markwon') + implementation project(':markwon-ext-strikethrough') + implementation project(':markwon-ext-tables') + implementation project(':markwon-ext-tasklist') + implementation project(':markwon-html') + implementation project(':markwon-image-gif') + implementation project(':markwon-image-svg') + implementation project(':markwon-syntax-highlight') + + deps.with { + implementation it['okhttp'] + implementation it['prism4j'] + implementation it['debug'] + } + + deps['annotationProcessor'].with { + annotationProcessor it['prism4j-bundler'] + } +} diff --git a/sample/src/main/AndroidManifest.xml b/sample/src/main/AndroidManifest.xml new file mode 100644 index 00000000..4907b9ea --- /dev/null +++ b/sample/src/main/AndroidManifest.xml @@ -0,0 +1,23 @@ + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/sample/src/main/java/ru/noties/markwon/sample/MainActivity.java b/sample/src/main/java/ru/noties/markwon/sample/MainActivity.java new file mode 100644 index 00000000..4670347f --- /dev/null +++ b/sample/src/main/java/ru/noties/markwon/sample/MainActivity.java @@ -0,0 +1,12 @@ +package ru.noties.markwon.sample; + +import android.app.Activity; +import android.os.Bundle; + +public class MainActivity extends Activity { + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + } +} diff --git a/sample/src/main/res/drawable-v26/ic_launcher_background.xml b/sample/src/main/res/drawable-v26/ic_launcher_background.xml new file mode 100644 index 00000000..a197b896 --- /dev/null +++ b/sample/src/main/res/drawable-v26/ic_launcher_background.xml @@ -0,0 +1,24 @@ + + + + diff --git a/sample/src/main/res/mipmap-anydpi-v26/ic_launcher.xml b/sample/src/main/res/mipmap-anydpi-v26/ic_launcher.xml new file mode 100644 index 00000000..c4a603d4 --- /dev/null +++ b/sample/src/main/res/mipmap-anydpi-v26/ic_launcher.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/sample/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml b/sample/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml new file mode 100644 index 00000000..c4a603d4 --- /dev/null +++ b/sample/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/sample/src/main/res/mipmap-hdpi/ic_launcher.png b/sample/src/main/res/mipmap-hdpi/ic_launcher.png new file mode 100644 index 0000000000000000000000000000000000000000..5bfd1e426950f1af44b05d2c7029c908b1f66a5e GIT binary patch literal 2094 zcmXX`2{_c<7oJG8%qJxKn9)dfMTEj^mTY58_U!A3WG8!eK8-Cq*@^5)WG(wHWErw& z7yb-le7~Ol=XvgX&V8Tv+~?eT&%F^?4JGgu_A4L|2&|%v#sOaW&!D3Q&|Dnl2Dpnh z2z3MqRQdYqsTmCjL=D2K;W2D%Z1?Wn0}u*@vaqoHML;YhBm|(aurPpsO;l8rot+&J z15+Lz9v}u7Mn*;^CMJLZRsa|X1Ogyn|IYz|fEY-*d-v{NModhMnVA`gSy@^C%7%xB z!^6V?K~YhWpPwHeA76WWyOEJmbaZrfc6LNWL|k0l?c2BO>+9igI8aYrUEQ;1&nhY^ ze0_a+d3keka!N}}8yXt=`}?b_tJBldr>CbwLPB1=co7*HSzBA%($W$Z7FJkTXkubA zI5?P=mR3+uaC&;Ww6uiB$A1B z{r&s5ot<58Z?A`khohroS67$6zyHgZFSoX~YHDg078a_is$yef-@JKKTwE+GD_dDv zDIy}0l9G~?lmr}+gM&j*P*6ulhnkvNW@e_6l2TSy*1^HS#>PfdQ`7wX{K?75_wV0Z zTU*V{%nS_;<>lpjdV2Em@~~L!;o;$@PoEMK6W!e0;^X5L6%}=KboBJ}?CtFh3=C9N zRm;lCyuH11b8|a8JB^Kvy}Z0!U0vPX-Jd^yo|>A9!{LxfZ@fPPl0|Nsn6e>74I5aeLd3kwkY;1IN6zCakZS9{we>OKakBp3P zb93|a^Y8BN5{X1pQ&SR&WMgAvX=(ZN=~HKC=LZiSOioU2Z*QNSodpC0eEj%PK|z5) zAh@`=#Kgp)(dekCC=3Q8B_##)q?VS}@$qqadHKVK4>K|{JUu-T2!yh-^2Ef%&d$!v z%#56zTyk>qqeqX%$H(vAzt73Z$-%)PEiFAYH6@%U9NrSwr)DEH|E;jbNAG0x6yi=1Px0~aaNnyK_Ogq~$qlI);zUG$*f7b9 zJ+NmJ2(R?+ukV+ra_lEuy6oWT!p#*6eb3&PCo{(2bgivsTziyNMBCsa{v2G%6i$TC z2`$$l?+8VOgI7=~SD)=7A|K7-Xq81E{138|7m=kFWfS{7_TjTH?P>!?tdSD{!Fjo^18j3=A_yX^CVPPgi; z9rR57#@wghF`7<@iq9poNU(k){9~h00;`T?+q6xkaOs1d7Mc37=-`vET{ok!iuEEN zJsN4|q>bsX;85n}MB`Mtcf}K0{E?C?MKB(MA#IdkrGqR3)valQ7rFl*OvGdUJF?89 z$;byppZ7{MzBPOzy){NY6jvHjqF@i!*+iaE0$r3%oIy%*Cs$dR!}`if1T32P+*Si_ zxVBS?iQ@nDG9N70p!D+9?7WaM#A3ZN7ao ztEgo(KqpXg!7;o`npVNk;`t9DDBqwhsHa(}4LuZY6o%W~k(zXIzB!a1ppx#G$C|u} z5{}1HB~%;aru;m=sP1+`QIs>oY=Wx<9$uOuO=dGN z8p=pfMx(BpP3>4Vm)GW9zaMSfZt|h$V4vLLP@9e++Q{HWh*l*G>y%@?xd18_F7ndW zQVXWH#9T^quP?oalYHfK>xN=lQE|{ta9aaAOn<(p(}85YpYd<^vH9yXmX{>@j>W2D z`z#8C4Jp6k7B3#L|HqmT2ac^Kc1mn%#kgetdsESh3f@7w9AiOEdNq}ohzTJbrDG?#uuaMbXp&fudeLrv- zPtBfP3Ta4d4A*VpF`}$qLEob3KX$ifdwLvht1JNJ)-j=O9_oZp%af#+)6B~la9y5^ zK6nAGEZptZ5H&>2okUNy0(=}jBJd?1BPkqtPC&__xu=wo`z#}2HwQY2+ZIu4GEv;a zyg#5xLcZR|E)rFz-TA7lqq1Atf;w`Vw*++`wql#h%13+y6hAe}J~@yIMgv`jG!6O>v#!(1 literal 0 HcmV?d00001 diff --git a/sample/src/main/res/mipmap-hdpi/ic_launcher_foreground.png b/sample/src/main/res/mipmap-hdpi/ic_launcher_foreground.png new file mode 100644 index 0000000000000000000000000000000000000000..63bbeda6edeb1bb97afabc33712d3563f0f8842c GIT binary patch literal 3165 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 TltSpe00000NkvXXu0mjf!oG1S literal 0 HcmV?d00001 diff --git a/sample/src/main/res/mipmap-xhdpi/ic_launcher_foreground.png b/sample/src/main/res/mipmap-xhdpi/ic_launcher_foreground.png new file mode 100644 index 0000000000000000000000000000000000000000..605adf677cabab6ec4fd5a4440be021bad15cd9f GIT binary patch literal 4610 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 Z@BhrmzXxvnwi^Hd002ovPDHLkV1jzW)HMJA literal 0 HcmV?d00001 diff --git a/sample/src/main/res/mipmap-xxhdpi/ic_launcher.png b/sample/src/main/res/mipmap-xxhdpi/ic_launcher.png new file mode 100644 index 0000000000000000000000000000000000000000..e7dc9e946fe34b07fa765e78a3e34873918eec4d GIT binary patch literal 3851 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 MQqxzhP<|TxKRFs3IRF3v literal 0 HcmV?d00001 diff --git a/sample/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.png b/sample/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.png new file mode 100644 index 0000000000000000000000000000000000000000..c0ca831d43c3e068d7011b4f2cf37acac945fff4 GIT binary patch literal 8468 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%0AbJ(-ot3n&Y;i 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 Xm^-PXQP@~L00000NkvXXu0mjfPox`q literal 0 HcmV?d00001 diff --git a/sample/src/main/res/mipmap-xxxhdpi/ic_launcher.png b/sample/src/main/res/mipmap-xxxhdpi/ic_launcher.png new file mode 100644 index 0000000000000000000000000000000000000000..4e8bfe4cc750935004149caa348ec76826ac27c9 GIT binary patch literal 4359 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 literal 0 HcmV?d00001 diff --git a/sample/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.png b/sample/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.png new file mode 100644 index 0000000000000000000000000000000000000000..bb544e39dee8bb2613acd1faad843ffc593fbe41 GIT binary patch literal 13141 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 VT-Jv6^Y1bM1sPT83Q2h2{{cr`Aq@Zk literal 0 HcmV?d00001 diff --git a/sample/src/main/res/values/colors.xml b/sample/src/main/res/values/colors.xml new file mode 100644 index 00000000..69b22338 --- /dev/null +++ b/sample/src/main/res/values/colors.xml @@ -0,0 +1,6 @@ + + + #008577 + #00574B + #D81B60 + diff --git a/sample/src/main/res/values/strings.xml b/sample/src/main/res/values/strings.xml new file mode 100644 index 00000000..0081dfc7 --- /dev/null +++ b/sample/src/main/res/values/strings.xml @@ -0,0 +1,3 @@ + + MarkwonSample + diff --git a/sample/src/main/res/values/styles.xml b/sample/src/main/res/values/styles.xml new file mode 100644 index 00000000..9785e0c9 --- /dev/null +++ b/sample/src/main/res/values/styles.xml @@ -0,0 +1,8 @@ + + + + + + diff --git a/settings.gradle b/settings.gradle index 3a3503f4..afe57e0c 100644 --- a/settings.gradle +++ b/settings.gradle @@ -1,5 +1,5 @@ rootProject.name = 'MarkwonProject' -include ':app', +include ':app', ':sample', ':markwon', ':markwon-ext-latex', ':markwon-ext-strikethrough', From e01787982f7e7860c4440db4a824ed5a80c50e53 Mon Sep 17 00:00:00 2001 From: Dimitry Ivanov Date: Fri, 28 Dec 2018 18:58:33 +0300 Subject: [PATCH 060/103] Working on new sample application --- build.gradle | 1 + sample/build.gradle | 20 ++ sample/src/main/AndroidManifest.xml | 4 + .../noties/markwon/sample/MainActivity.java | 84 +++++++ .../ru/noties/markwon/sample/SampleItem.java | 26 +++ .../markwon/sample/SampleItemDecoration.java | 97 ++++++++ .../noties/markwon/sample/SampleItemView.java | 84 +++++++ .../markwon/sample/core/CoreActivity.java | 208 ++++++++++++++++++ .../CustomExtensionActivity.java | 6 + .../markwon/sample/latex/LatexActivity.java | 15 ++ sample/src/main/res/layout/activity_main.xml | 15 ++ .../src/main/res/layout/adapt_sample_item.xml | 13 ++ sample/src/main/res/values-v21/styles.xml | 6 + .../src/main/res/values/strings-samples.xml | 10 + sample/src/main/res/values/styles.xml | 7 +- .../markwon/sample/MainActivityTest.java | 25 +++ 16 files changed, 617 insertions(+), 4 deletions(-) create mode 100644 sample/src/main/java/ru/noties/markwon/sample/SampleItem.java create mode 100644 sample/src/main/java/ru/noties/markwon/sample/SampleItemDecoration.java create mode 100644 sample/src/main/java/ru/noties/markwon/sample/SampleItemView.java create mode 100644 sample/src/main/java/ru/noties/markwon/sample/core/CoreActivity.java create mode 100644 sample/src/main/java/ru/noties/markwon/sample/customextension/CustomExtensionActivity.java create mode 100644 sample/src/main/java/ru/noties/markwon/sample/latex/LatexActivity.java create mode 100644 sample/src/main/res/layout/activity_main.xml create mode 100644 sample/src/main/res/layout/adapt_sample_item.xml create mode 100644 sample/src/main/res/values-v21/styles.xml create mode 100644 sample/src/main/res/values/strings-samples.xml create mode 100644 sample/src/test/java/ru/noties/markwon/sample/MainActivityTest.java diff --git a/build.gradle b/build.gradle index 88a298fa..f0a79879 100644 --- a/build.gradle +++ b/build.gradle @@ -68,6 +68,7 @@ ext { 'okhttp' : 'com.squareup.okhttp3:okhttp:3.9.0', 'prism4j' : 'ru.noties:prism4j:1.1.0', 'debug' : 'ru.noties:debug:3.0.0@jar', + 'adapt' : 'ru.noties:adapt:1.1.0', 'dagger' : "com.google.dagger:dagger:$daggerVersion" ] diff --git a/sample/build.gradle b/sample/build.gradle index 3d520a29..eb169fa3 100644 --- a/sample/build.gradle +++ b/sample/build.gradle @@ -12,11 +12,23 @@ android { versionCode 1 versionName version setProperty("archivesBaseName", "markwon-sample-$versionName") + + resConfig 'en' } lintOptions { abortOnError false } + + dexOptions { + preDexLibraries true + javaMaxHeapSize '5g' + } + + compileOptions { + targetCompatibility JavaVersion.VERSION_1_8 + sourceCompatibility JavaVersion.VERSION_1_8 + } } dependencies { @@ -31,12 +43,20 @@ dependencies { implementation project(':markwon-syntax-highlight') deps.with { + implementation it['support-recycler-view'] implementation it['okhttp'] implementation it['prism4j'] implementation it['debug'] + implementation it['adapt'] } deps['annotationProcessor'].with { annotationProcessor it['prism4j-bundler'] } + + deps['test'].with { + testImplementation it['junit'] + testImplementation it['robolectric'] + testImplementation it['mockito'] + } } diff --git a/sample/src/main/AndroidManifest.xml b/sample/src/main/AndroidManifest.xml index 4907b9ea..73b776c3 100644 --- a/sample/src/main/AndroidManifest.xml +++ b/sample/src/main/AndroidManifest.xml @@ -18,6 +18,10 @@ + + + + \ No newline at end of file diff --git a/sample/src/main/java/ru/noties/markwon/sample/MainActivity.java b/sample/src/main/java/ru/noties/markwon/sample/MainActivity.java index 4670347f..d1fe2853 100644 --- a/sample/src/main/java/ru/noties/markwon/sample/MainActivity.java +++ b/sample/src/main/java/ru/noties/markwon/sample/MainActivity.java @@ -1,12 +1,96 @@ package ru.noties.markwon.sample; import android.app.Activity; +import android.content.Context; +import android.content.Intent; import android.os.Bundle; +import android.support.annotation.NonNull; +import android.support.annotation.VisibleForTesting; +import android.support.v7.widget.LinearLayoutManager; +import android.support.v7.widget.RecyclerView; +import android.view.View; + +import java.util.Arrays; + +import ru.noties.adapt.Adapt; +import ru.noties.adapt.OnClickViewProcessor; +import ru.noties.debug.AndroidLogDebugOutput; +import ru.noties.debug.Debug; +import ru.noties.markwon.Markwon; +import ru.noties.markwon.sample.core.CoreActivity; +import ru.noties.markwon.sample.customextension.CustomExtensionActivity; +import ru.noties.markwon.sample.latex.LatexActivity; public class MainActivity extends Activity { + static { + Debug.init(new AndroidLogDebugOutput(true)); + } + @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); + setContentView(R.layout.activity_main); + + // obtain an instance of Markwon + // here we are creating as core markwon (no additional plugins are registered) + final Markwon markwon = Markwon.create(this); + + final Adapt adapt = Adapt.builder(SampleItem.class) + .include(SampleItem.class, new SampleItemView(markwon), new OnClickViewProcessor() { + @Override + public void onClick(@NonNull SampleItem item, @NonNull View view) { + showSample(item); + } + }) + .build(); + adapt.setItems(Arrays.asList(SampleItem.values())); + + final RecyclerView recyclerView = findViewById(R.id.recycler_view); + recyclerView.setLayoutManager(new LinearLayoutManager(this)); + recyclerView.setHasFixedSize(true); + recyclerView.addItemDecoration(createSampleItemDecoration()); + recyclerView.setAdapter(adapt.recyclerViewAdapter()); + } + + @NonNull + private SampleItemDecoration createSampleItemDecoration() { + final float density = getResources().getDisplayMetrics().density; + return new SampleItemDecoration( + 0xffeeeeee, + (int) (24 * density + .5F), + (int) (1 * density + .5F), + 0xFFBDBDBD + ); + } + + private void showSample(@NonNull SampleItem item) { + startActivity(sampleItemIntent(this, item)); + } + + @VisibleForTesting + static Intent sampleItemIntent(@NonNull Context context, @NonNull SampleItem item) { + + final Class activity; + + switch (item) { + + case CORE: + activity = CoreActivity.class; + break; + + case LATEX: + activity = LatexActivity.class; + break; + + case CUSTOM_EXTENSION: + activity = CustomExtensionActivity.class; + break; + + default: + throw new IllegalStateException("No Activity is associated with sample-item: " + item); + } + + return new Intent(context, activity); } } diff --git a/sample/src/main/java/ru/noties/markwon/sample/SampleItem.java b/sample/src/main/java/ru/noties/markwon/sample/SampleItem.java new file mode 100644 index 00000000..bf3038ac --- /dev/null +++ b/sample/src/main/java/ru/noties/markwon/sample/SampleItem.java @@ -0,0 +1,26 @@ +package ru.noties.markwon.sample; + +import android.support.annotation.StringRes; + +public enum SampleItem { + + // all usages of markwon without plugins (parse, render, setMarkwon, etc) + CORE(R.string.sample_core), + + LATEX(R.string.sample_latex), + + CUSTOM_EXTENSION(R.string.sample_custom_extension), + + ; + + private final int textResId; + + SampleItem(@StringRes int textResId) { + this.textResId = textResId; + } + + @StringRes + public int textResId() { + return textResId; + } +} diff --git a/sample/src/main/java/ru/noties/markwon/sample/SampleItemDecoration.java b/sample/src/main/java/ru/noties/markwon/sample/SampleItemDecoration.java new file mode 100644 index 00000000..580b2763 --- /dev/null +++ b/sample/src/main/java/ru/noties/markwon/sample/SampleItemDecoration.java @@ -0,0 +1,97 @@ +package ru.noties.markwon.sample; + +import android.graphics.Canvas; +import android.graphics.Paint; +import android.graphics.Rect; +import android.support.annotation.ColorInt; +import android.support.annotation.Px; +import android.support.v7.widget.RecyclerView; +import android.view.View; + +class SampleItemDecoration extends RecyclerView.ItemDecoration { + + private final Rect rect = new Rect(); + private final Paint paint = new Paint(Paint.ANTI_ALIAS_FLAG); + + private final int oddItemBackgroundColor; + + private final int bottomPadding; + + private final int dividerHeight; + private final int dividerColor; + + SampleItemDecoration( + @ColorInt int oddItemBackgroundColor, + @Px int bottomPadding, + @Px int dividerHeight, + @ColorInt int dividerColor) { + this.oddItemBackgroundColor = oddItemBackgroundColor; + this.bottomPadding = bottomPadding; + this.dividerHeight = dividerHeight; + this.dividerColor = dividerColor; + + paint.setStyle(Paint.Style.FILL); + } + + @Override + public void onDraw(Canvas c, RecyclerView parent, RecyclerView.State state) { + + // if bottom < parent.getBottom() -> draw bottom background + + paint.setColor(dividerColor); + + View view; + + // we will use this flag afterwards (if we will have to draw bottom background) + // so, if last item is even (no background) -> draw odd + // if last item is odd -> draw no background + // + // let's start with true, so if we have no items no background will be drawn + boolean isOdd = true; + + for (int i = 0, count = parent.getChildCount(); i < count; i++) { + + view = parent.getChildAt(i); + isOdd = parent.getChildAdapterPosition(view) % 2 != 0; + + // odd + if (isOdd) { + rect.set(view.getLeft(), view.getTop(), view.getRight(), view.getBottom()); + paint.setColor(oddItemBackgroundColor); + c.drawRect(rect, paint); + + // set divider color back + paint.setColor(dividerColor); + } + + rect.set(0, view.getBottom(), c.getWidth(), view.getBottom() + dividerHeight); + c.drawRect(rect, paint); + } + + if (!isOdd && rect.bottom < parent.getBottom()) { + + paint.setColor(oddItemBackgroundColor); + + rect.set(0, rect.bottom, c.getWidth(), parent.getBottom()); + c.drawRect(rect, paint); + } + } + + @Override + public void getItemOffsets(Rect outRect, View view, RecyclerView parent, RecyclerView.State state) { + + // divider to bottom + // + {if last} -> bottomPadding + + final int position = parent.getChildAdapterPosition(view); + + final RecyclerView.Adapter adapter = parent.getAdapter(); + final boolean isLast = adapter != null && position == adapter.getItemCount() - 1; + + final int bottom = isLast + ? bottomPadding + dividerHeight + : dividerHeight; + + outRect.set(0, 0, 0, bottom); + } +} diff --git a/sample/src/main/java/ru/noties/markwon/sample/SampleItemView.java b/sample/src/main/java/ru/noties/markwon/sample/SampleItemView.java new file mode 100644 index 00000000..76289022 --- /dev/null +++ b/sample/src/main/java/ru/noties/markwon/sample/SampleItemView.java @@ -0,0 +1,84 @@ +package ru.noties.markwon.sample; + +import android.support.annotation.NonNull; +import android.text.Spannable; +import android.text.SpannableString; +import android.text.Spanned; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.TextView; + +import java.util.EnumMap; + +import ru.noties.adapt.Holder; +import ru.noties.adapt.ItemView; +import ru.noties.markwon.Markwon; + +class SampleItemView extends ItemView { + + private final Markwon markwon; + + // instance specific factory + private final NoCopySpannableFactory factory; + + // instance specific cache + private final EnumMap cache; + + SampleItemView(@NonNull Markwon markwon) { + this.markwon = markwon; + this.factory = new NoCopySpannableFactory(); + this.cache = new EnumMap<>(SampleItem.class); + } + + @NonNull + @Override + public SampleHolder createHolder(@NonNull LayoutInflater inflater, @NonNull ViewGroup parent) { + + final SampleHolder holder = new SampleHolder(inflater.inflate( + R.layout.adapt_sample_item, + parent, + false)); + + // set Spannable.Factory so when TextView will receive a new content + // it won't create new Spannable and copy all the spans but instead + // re-use existing Spannable thus improving performance + holder.textView.setSpannableFactory(factory); + + return holder; + } + + @Override + public void bindHolder(@NonNull SampleHolder holder, @NonNull SampleItem item) { + + // retrieve an item from cache or create new one + // simple lazy loading pattern (cache on first call then re-use) + Spanned spanned = cache.get(item); + if (spanned == null) { + spanned = markwon.toMarkdown(context(holder).getString(item.textResId())); + cache.put(item, spanned); + } + + holder.textView.setText(spanned); + } + + static class SampleHolder extends Holder { + + final TextView textView; + + SampleHolder(@NonNull View view) { + super(view); + + this.textView = requireView(R.id.text); + } + } + + private static class NoCopySpannableFactory extends Spannable.Factory { + @Override + public Spannable newSpannable(CharSequence source) { + return source instanceof Spannable + ? (Spannable) source + : new SpannableString(source); + } + } +} diff --git a/sample/src/main/java/ru/noties/markwon/sample/core/CoreActivity.java b/sample/src/main/java/ru/noties/markwon/sample/core/CoreActivity.java new file mode 100644 index 00000000..79687390 --- /dev/null +++ b/sample/src/main/java/ru/noties/markwon/sample/core/CoreActivity.java @@ -0,0 +1,208 @@ +package ru.noties.markwon.sample.core; + +import android.app.Activity; +import android.graphics.Color; +import android.os.Bundle; +import android.support.annotation.NonNull; +import android.support.annotation.Nullable; +import android.text.Spanned; +import android.text.style.ForegroundColorSpan; +import android.widget.TextView; +import android.widget.Toast; + +import org.commonmark.node.Heading; +import org.commonmark.node.Node; +import org.commonmark.node.Paragraph; + +import ru.noties.markwon.AbstractMarkwonPlugin; +import ru.noties.markwon.Markwon; +import ru.noties.markwon.MarkwonPlugin; +import ru.noties.markwon.MarkwonSpansFactory; +import ru.noties.markwon.MarkwonVisitor; +import ru.noties.markwon.core.CorePlugin; +import ru.noties.markwon.core.MarkwonTheme; + +public class CoreActivity extends Activity { + + private TextView textView; + + @Override + public void onCreate(@Nullable Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + + textView = new TextView(this); + setContentView(textView); + + step_1(); + + step_2(); + + step_3(); + + step_4(); + + step_5(); + + step_6(); + + step_7(); + } + + /** + * Create a simple instance of Markwon with only Core plugin registered + * this will handle all _natively_ supported by commonmark-java nodes: + *

+ *

+ * and basic core functionality: + *

    + *
  • Append text
  • + *
  • Insert new lines (soft and hard breaks)
  • + *
+ */ + private void step_1() { + + // short call + final Markwon markwon = Markwon.create(this); + + // this is the same as calling + final Markwon markwon2 = Markwon.builder(this) + .usePlugin(CorePlugin.create()) + .build(); + } + + /** + * To simply apply raw (non-parsed) markdown call {@link Markwon#setMarkdown(TextView, String)} + */ + private void step_2() { + + // this is raw markdown + final String markdown = "Hello **markdown**!"; + + final Markwon markwon = Markwon.create(this); + + // this will parse raw markdown and set parsed content to specified TextView + markwon.setMarkdown(textView, markdown); + } + + /** + * To apply markdown in a different context (other than textView) use {@link Markwon#toMarkdown(String)} + *

+ * Please note that some features won't work unless they are used in a TextView context. For example + * there might be misplaced ordered lists (ordered list must have TextPaint in order to properly measure + * its number). But also images and tables (they belong to independent modules now). Images and tables + * are using some work-arounds in order to be displayed in relatively limited context without proper way + * of invalidation. But if a Toast for example is created with a custom view + * ({@code new Toast(this).setView(...) }) and has access to a TextView everything should work. + */ + private void step_3() { + + final String markdown = "*Toast* __here__!\n\n> And a quote!"; + + final Markwon markwon = Markwon.create(this); + + final Spanned spanned = markwon.toMarkdown(markdown); + + Toast.makeText(this, spanned, Toast.LENGTH_LONG).show(); + } + + /** + * To apply already parsed markdown use {@link Markwon#setParsedMarkdown(TextView, Spanned)} + */ + private void step_4() { + + final String markdown = "This **is** pre-parsed [markdown](#)"; + + final Markwon markwon = Markwon.create(this); + + // parse markdown to obtain a Node + final Node node = markwon.parse(markdown); + + // create a spanned content from parsed node + final Spanned spanned = markwon.render(node); + + // apply parsed markdown + markwon.setParsedMarkdown(textView, spanned); + } + + /** + * In order to apply paragraph spans a custom plugin should be created (CorePlugin will take care + * of everything else). + *

+ * Please note that when a plugin is registered and it depends on CorePlugin, there is no + * need to explicitly specify it. By default all plugins that extend AbstractMarkwonPlugin do declare + * it\'s dependency on CorePlugin ({@link MarkwonPlugin#priority()}). + *

+ * Order in which plugins are specified to the builder is of little importance as long as each + * plugin clearly states what dependencies it has + */ + private void step_5() { + + final String markdown = "# Hello!\n\nA paragraph?\n\nIt should be!"; + + final Markwon markwon = Markwon.builder(this) + .usePlugin(new AbstractMarkwonPlugin() { + @Override + public void configureSpansFactory(@NonNull MarkwonSpansFactory.Builder builder) { + builder.setFactory(Paragraph.class, (configuration, props) -> + new ForegroundColorSpan(Color.GREEN)); + } + }) + .build(); + + markwon.setMarkdown(textView, markdown); + } + + /** + * To disable some nodes from rendering another custom plugin can be used + */ + private void step_6() { + + final String markdown = "# Heading 1\n\n## Heading 2\n\n**other** content [here](#)"; + + final Markwon markwon = Markwon.builder(this) + .usePlugin(new AbstractMarkwonPlugin() { + @Override + public void configureVisitor(@NonNull MarkwonVisitor.Builder builder) { + // for example to disable rendering of heading: + // try commenting this out to see that otherwise headings will be rendered + builder.on(Heading.class, null); + } + }) + .build(); + + markwon.setMarkdown(textView, markdown); + } + + /** + * To customize core theme plugins can be used again + */ + private void step_7() { + + final String markdown = "`A code` that is rendered differently\n\n```\nHello!\n```"; + + final Markwon markwon = Markwon.builder(this) + .usePlugin(new AbstractMarkwonPlugin() { + @Override + public void configureTheme(@NonNull MarkwonTheme.Builder builder) { + builder + .codeBackgroundColor(Color.BLACK) + .codeTextColor(Color.RED); + } + }) + .build(); + + markwon.setMarkdown(textView, markdown); + } +} diff --git a/sample/src/main/java/ru/noties/markwon/sample/customextension/CustomExtensionActivity.java b/sample/src/main/java/ru/noties/markwon/sample/customextension/CustomExtensionActivity.java new file mode 100644 index 00000000..1b35c0c7 --- /dev/null +++ b/sample/src/main/java/ru/noties/markwon/sample/customextension/CustomExtensionActivity.java @@ -0,0 +1,6 @@ +package ru.noties.markwon.sample.customextension; + +import android.app.Activity; + +public class CustomExtensionActivity extends Activity { +} diff --git a/sample/src/main/java/ru/noties/markwon/sample/latex/LatexActivity.java b/sample/src/main/java/ru/noties/markwon/sample/latex/LatexActivity.java new file mode 100644 index 00000000..179113ea --- /dev/null +++ b/sample/src/main/java/ru/noties/markwon/sample/latex/LatexActivity.java @@ -0,0 +1,15 @@ +package ru.noties.markwon.sample.latex; + +import android.app.Activity; +import android.os.Bundle; +import android.support.annotation.Nullable; + +public class LatexActivity extends Activity { + + @Override + public void onCreate(@Nullable Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + + + } +} diff --git a/sample/src/main/res/layout/activity_main.xml b/sample/src/main/res/layout/activity_main.xml new file mode 100644 index 00000000..26bc9906 --- /dev/null +++ b/sample/src/main/res/layout/activity_main.xml @@ -0,0 +1,15 @@ + + + + + + \ No newline at end of file diff --git a/sample/src/main/res/layout/adapt_sample_item.xml b/sample/src/main/res/layout/adapt_sample_item.xml new file mode 100644 index 00000000..3dfe4e21 --- /dev/null +++ b/sample/src/main/res/layout/adapt_sample_item.xml @@ -0,0 +1,13 @@ + + \ No newline at end of file diff --git a/sample/src/main/res/values-v21/styles.xml b/sample/src/main/res/values-v21/styles.xml new file mode 100644 index 00000000..191be162 --- /dev/null +++ b/sample/src/main/res/values-v21/styles.xml @@ -0,0 +1,6 @@ + + + + + \ No newline at end of file diff --git a/docs/.vuepress/style.styl b/docs/.vuepress/style.styl index e69de29b..71f493f4 100644 --- a/docs/.vuepress/style.styl +++ b/docs/.vuepress/style.styl @@ -0,0 +1,3 @@ +div[class~=language-gradle]:before { + content:"gradle" +} \ No newline at end of file diff --git a/docs/collectArtifacts.js b/docs/collectArtifacts.js new file mode 100644 index 00000000..959999cf --- /dev/null +++ b/docs/collectArtifacts.js @@ -0,0 +1,50 @@ +const fs = require('fs'); +const path = require('path'); + +const PROPERTIES_FILE_NAME = 'gradle.properties'; +const PROP_GROUP = 'GROUP'; +const PROP_DESCRIPTION = 'POM_DESCRIPTION'; +const PROP_ARTIFACT_NAME = 'POM_NAME'; +const PROP_ARTIFACT_ID = 'POM_ARTIFACT_ID'; + +const readProperties = (file) => fs.readFileSync(file, { encoding: 'utf-8' }, 'string') + .split('\n') + // filter-out empty lines + .filter(s => s) + .map(s => s.split('=')) + .reduce((a, s) => { + a[s[0]] = s[1]; + return a; + }, {}); + +const listDirectories = (folder) => fs.readdirSync(folder) + .map(name => path.join(folder, name)) + .filter(f => fs.lstatSync(f).isDirectory()); + +const projectDir = path.resolve(__dirname, '../'); + +const projectProperties = readProperties(path.join(projectDir, PROPERTIES_FILE_NAME)); + +const projectGroup = projectProperties[PROP_GROUP] + +const artifacts = listDirectories(projectDir) + .map(dir => path.join(dir, PROPERTIES_FILE_NAME)) + .filter(f => fs.existsSync(f)) + .map(readProperties) + .map(props => { + return { + id: props[PROP_ARTIFACT_ID], + name: props[PROP_ARTIFACT_NAME], + group: projectGroup, + description: props[PROP_DESCRIPTION] + } + }); + +const artifactsFile = path.join(__dirname, '.vuepress', '.artifacts.js'); +const artifactsJs = ` +// this is a generated file, do not modify. To update it run 'collectArtifacts.js' script +const artifacts = ${JSON.stringify(artifacts)}; +export { artifacts }; +` + +fs.writeFileSync(artifactsFile, artifactsJs); diff --git a/docs/package-lock.json b/docs/package-lock.json index 5cc23227..77f560a6 100644 --- a/docs/package-lock.json +++ b/docs/package-lock.json @@ -5465,9 +5465,9 @@ } }, "linkify-it": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/linkify-it/-/linkify-it-2.0.3.tgz", - "integrity": "sha1-2UpGSPmxwXnWT6lykSaL22zpQ08=", + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/linkify-it/-/linkify-it-2.1.0.tgz", + "integrity": "sha512-4REs8/062kV2DSHxNfq5183zrqXMl7WP0WzABH9IeJI+NLm429FgE1PDecltYfnOoFDFlZGh2T8PfZn0r+GTRg==", "requires": { "uc.micro": "^1.0.1" } diff --git a/docs/package.json b/docs/package.json index d469413c..9ba0ddb5 100644 --- a/docs/package.json +++ b/docs/package.json @@ -1,6 +1,6 @@ { "scripts": { - "docs:build": "vuepress build" + "docs:build": "node ./collectArtifacts.js && vuepress build" }, "dependencies": { "markdown-it-task-lists": "^2.1.1", diff --git a/markwon/gradle.properties b/markwon/gradle.properties index e1d2806a..367bfbc2 100644 --- a/markwon/gradle.properties +++ b/markwon/gradle.properties @@ -1,3 +1,4 @@ POM_NAME=Markwon POM_ARTIFACT_ID=markwon -POM_PACKAGING=aar \ No newline at end of file +POM_PACKAGING=aar +POM_DESCRIPTION=Core Markwon artifact that includes basic markdown parsing and rendering \ No newline at end of file From 702f2a054671597923cd7d388af1cd71be5f1847 Mon Sep 17 00:00:00 2001 From: Dimitry Ivanov Date: Mon, 7 Jan 2019 14:54:29 +0300 Subject: [PATCH 068/103] Move artifacts to 'ru.noties.markwon' group --- app/build.gradle | 2 +- build.gradle | 4 + docs/.vuepress/.artifacts.js | 2 +- docs/.vuepress/components/ArtifactPicker.vue | 3 +- docs/.vuepress/components/MavenBadge.vue | 13 ++- docs/.vuepress/config.js | 18 +++- docs/.vuepress/style.styl | 4 + docs/README.md | 5 +- docs/docs/{ => core}/getting-started.md | 6 ++ docs/docs/image/gif.md | 3 + docs/docs/image/okhttp.md | 9 ++ docs/docs/image/svg.md | 10 ++ docs/docs/install.md | 93 +++---------------- docs/docs/migration-2-3.md | 1 + gradle.properties | 6 +- {markwon => markwon-core}/build.gradle | 0 {markwon => markwon-core}/gradle.properties | 6 +- .../src/main/AndroidManifest.xml | 0 .../noties/markwon/AbstractMarkwonPlugin.java | 42 --------- .../ru/noties/markwon/LinkResolverDef.java | 0 .../main/java/ru/noties/markwon/Markwon.java | 0 .../ru/noties/markwon/MarkwonBuilderImpl.java | 0 .../noties/markwon/MarkwonConfiguration.java | 0 .../java/ru/noties/markwon/MarkwonImpl.java | 0 .../java/ru/noties/markwon/MarkwonPlugin.java | 0 .../noties/markwon/MarkwonSpansFactory.java | 0 .../markwon/MarkwonSpansFactoryImpl.java | 0 .../ru/noties/markwon/MarkwonVisitor.java | 0 .../ru/noties/markwon/MarkwonVisitorImpl.java | 0 .../src/main/java/ru/noties/markwon/Prop.java | 0 .../java/ru/noties/markwon/RenderProps.java | 0 .../ru/noties/markwon/RenderPropsImpl.java | 0 .../java/ru/noties/markwon/SpanFactory.java | 0 .../ru/noties/markwon/SpannableBuilder.java | 0 .../ru/noties/markwon/core/CorePlugin.java | 0 .../ru/noties/markwon/core/CoreProps.java | 0 .../ru/noties/markwon/core/MarkwonTheme.java | 0 .../markwon/core/SimpleBlockNodeVisitor.java | 0 .../core/factory/BlockQuoteSpanFactory.java | 0 .../core/factory/CodeBlockSpanFactory.java | 0 .../markwon/core/factory/CodeSpanFactory.java | 0 .../core/factory/EmphasisSpanFactory.java | 0 .../core/factory/HeadingSpanFactory.java | 0 .../markwon/core/factory/LinkSpanFactory.java | 0 .../core/factory/ListItemSpanFactory.java | 0 .../factory/StrongEmphasisSpanFactory.java | 0 .../factory/ThematicBreakSpanFactory.java | 0 .../markwon/core/spans/BlockQuoteSpan.java | 0 .../core/spans/BulletListItemSpan.java | 0 .../noties/markwon/core/spans/CodeSpan.java | 0 .../markwon/core/spans/EmphasisSpan.java | 0 .../markwon/core/spans/HeadingSpan.java | 0 .../noties/markwon/core/spans/LinkSpan.java | 0 .../markwon/core/spans/ObjectsPool.java | 0 .../core/spans/OrderedListItemSpan.java | 0 .../core/spans/StrongEmphasisSpan.java | 0 .../markwon/core/spans/ThematicBreakSpan.java | 0 .../java/ru/noties/markwon/html/HtmlTag.java | 0 .../markwon/html/MarkwonHtmlParser.java | 0 .../markwon/html/MarkwonHtmlParserNoOp.java | 0 .../markwon/html/MarkwonHtmlRenderer.java | 0 .../markwon/html/MarkwonHtmlRendererImpl.java | 0 .../markwon/html/MarkwonHtmlRendererNoOp.java | 0 .../ru/noties/markwon/html/TagHandler.java | 0 .../noties/markwon/image/AsyncDrawable.java | 0 .../markwon/image/AsyncDrawableLoader.java | 0 .../image/AsyncDrawableLoaderImpl.java | 0 .../image/AsyncDrawableLoaderNoOp.java | 0 .../markwon/image/AsyncDrawableScheduler.java | 0 .../markwon/image/AsyncDrawableSpan.java | 0 .../ru/noties/markwon/image/ImageItem.java | 0 .../markwon/image/ImageMediaDecoder.java | 0 .../ru/noties/markwon/image/ImageProps.java | 0 .../ru/noties/markwon/image/ImageSize.java | 0 .../markwon/image/ImageSizeResolver.java | 0 .../markwon/image/ImageSizeResolverDef.java | 0 .../markwon/image/ImageSpanFactory.java | 0 .../ru/noties/markwon/image/ImagesPlugin.java | 0 .../ru/noties/markwon/image/MediaDecoder.java | 0 .../noties/markwon/image/SchemeHandler.java | 0 .../ru/noties/markwon/image/data/DataUri.java | 0 .../markwon/image/data/DataUriDecoder.java | 0 .../markwon/image/data/DataUriParser.java | 0 .../image/data/DataUriSchemeHandler.java | 0 .../markwon/image/file/FileSchemeHandler.java | 0 .../image/network/NetworkSchemeHandler.java | 0 .../movement/MovementMethodPlugin.java | 0 .../ru/noties/markwon/priority/Priority.java | 0 .../markwon/priority/PriorityProcessor.java | 0 .../priority/PriorityProcessorImpl.java | 0 .../markwon/syntax/SyntaxHighlight.java | 0 .../markwon/syntax/SyntaxHighlightNoOp.java | 0 .../markwon/urlprocessor/UrlProcessor.java | 0 .../UrlProcessorAndroidAssets.java | 0 .../urlprocessor/UrlProcessorNoOp.java | 0 .../UrlProcessorRelativeToAbsolute.java | 0 .../ru/noties/markwon/utils/ColorUtils.java | 0 .../java/ru/noties/markwon/utils/Dip.java | 0 .../noties/markwon/utils/DrawableUtils.java | 0 .../markwon/utils/LeadingMarginUtils.java | 0 .../src/main/res/values/ids.xml | 0 .../markwon/AbstractMarkwonPluginTest.java | 0 .../markwon/AbstractMarkwonVisitorImpl.java | 0 .../markwon/MarkwonBuilderImplTest.java | 0 .../ru/noties/markwon/MarkwonImplTest.java | 0 .../markwon/MarkwonSpansFactoryImplTest.java | 0 .../markwon/MarkwonVisitorImplTest.java | 0 .../test/java/ru/noties/markwon/PropTest.java | 0 .../noties/markwon/RenderPropsImplTest.java | 0 .../noties/markwon/SpannableBuilderTest.java | 0 .../noties/markwon/core/CorePluginBridge.java | 0 .../noties/markwon/core/CorePluginTest.java | 0 .../java/ru/noties/markwon/core/CoreTest.java | 0 .../noties/markwon/core/suite/.editorconfig | 0 .../markwon/core/suite/BaseSuiteTest.java | 0 .../markwon/core/suite/BlockquoteTest.java | 0 .../markwon/core/suite/BoldItalicTest.java | 0 .../noties/markwon/core/suite/CodeTest.java | 0 .../markwon/core/suite/DeeplyNestedTest.java | 0 .../markwon/core/suite/EmphasisTest.java | 0 .../noties/markwon/core/suite/FirstTest.java | 0 .../markwon/core/suite/HeadingTest.java | 0 .../noties/markwon/core/suite/LinkTest.java | 0 .../markwon/core/suite/NoParagraphsTest.java | 0 .../markwon/core/suite/OrderedListTest.java | 0 .../markwon/core/suite/ParagraphTest.java | 0 .../noties/markwon/core/suite/SecondTest.java | 0 .../core/suite/SoftBreakAddsNewLineTest.java | 0 .../markwon/core/suite/SoftBreakTest.java | 0 .../core/suite/StrongEmphasisTest.java | 0 .../markwon/core/suite/ThematicBreakTest.java | 0 .../markwon/core/suite/UnOrderedListTest.java | 0 .../markwon/image/AsyncDrawableTest.java | 0 .../image/ImageSizeResolverDefTest.java | 0 .../ru/noties/markwon/image/ImageTest.java | 0 .../markwon/image/data/DataUriParserTest.java | 0 .../image/data/DataUriSchemeHandlerTest.java | 0 .../priority/PriorityProcessorTest.java | 0 .../markwon/syntax/SyntaxHighlightTest.java | 0 .../UrlProcessorAndroidAssetsTest.java | 0 .../UrlProcessorRelativeToAbsoluteTest.java | 0 .../src/test/resources/tests/code-blocks.md | 0 .../src/test/resources/tests/deeply-nested.md | 0 .../src/test/resources/tests/first.md | 0 .../resources/tests/nested-blockquotes.md | 0 .../src/test/resources/tests/no-paragraphs.md | 0 .../src/test/resources/tests/ol-2-spaces.md | 0 .../test/resources/tests/ol-starts-with-5.md | 0 .../src/test/resources/tests/ol.md | 0 .../src/test/resources/tests/paragraph.md | 0 .../src/test/resources/tests/second.md | 0 .../test/resources/tests/single-code-block.md | 0 .../tests/soft-break-adds-new-line.md | 0 .../src/test/resources/tests/soft-break.md | 0 .../src/test/resources/tests/ul-levels.md | 0 .../src/test/resources/tests/ul.md | 0 markwon-ext-latex/build.gradle | 2 +- markwon-ext-latex/gradle.properties | 4 + markwon-ext-strikethrough/build.gradle | 2 +- markwon-ext-strikethrough/gradle.properties | 5 +- markwon-ext-tables/build.gradle | 2 +- markwon-ext-tables/gradle.properties | 4 + markwon-ext-tasklist/build.gradle | 2 +- markwon-ext-tasklist/gradle.properties | 4 + markwon-html/build.gradle | 2 +- markwon-html/gradle.properties | 5 +- markwon-image-gif/build.gradle | 2 +- markwon-image-gif/gradle.properties | 4 + markwon-image-okhttp/build.gradle | 2 +- markwon-image-okhttp/gradle.properties | 4 + markwon-image-svg/build.gradle | 2 +- markwon-image-svg/gradle.properties | 4 + markwon-recycler/build.gradle | 2 +- markwon-recycler/gradle.properties | 4 + markwon-syntax-highlight/build.gradle | 2 +- markwon-syntax-highlight/gradle.properties | 5 +- markwon-view/gradle.properties | 3 - sample-custom-extension/build.gradle | 2 +- sample-latex-math/build.gradle | 2 +- sample/build.gradle | 2 +- settings.gradle | 2 +- 181 files changed, 137 insertions(+), 164 deletions(-) rename docs/docs/{ => core}/getting-started.md (93%) create mode 100644 docs/docs/image/gif.md create mode 100644 docs/docs/image/okhttp.md create mode 100644 docs/docs/image/svg.md create mode 100644 docs/docs/migration-2-3.md rename {markwon => markwon-core}/build.gradle (100%) rename {markwon => markwon-core}/gradle.properties (55%) rename {markwon => markwon-core}/src/main/AndroidManifest.xml (100%) rename {markwon => markwon-core}/src/main/java/ru/noties/markwon/AbstractMarkwonPlugin.java (81%) rename {markwon => markwon-core}/src/main/java/ru/noties/markwon/LinkResolverDef.java (100%) rename {markwon => markwon-core}/src/main/java/ru/noties/markwon/Markwon.java (100%) rename {markwon => markwon-core}/src/main/java/ru/noties/markwon/MarkwonBuilderImpl.java (100%) rename {markwon => markwon-core}/src/main/java/ru/noties/markwon/MarkwonConfiguration.java (100%) rename {markwon => markwon-core}/src/main/java/ru/noties/markwon/MarkwonImpl.java (100%) rename {markwon => markwon-core}/src/main/java/ru/noties/markwon/MarkwonPlugin.java (100%) rename {markwon => markwon-core}/src/main/java/ru/noties/markwon/MarkwonSpansFactory.java (100%) rename {markwon => markwon-core}/src/main/java/ru/noties/markwon/MarkwonSpansFactoryImpl.java (100%) rename {markwon => markwon-core}/src/main/java/ru/noties/markwon/MarkwonVisitor.java (100%) rename {markwon => markwon-core}/src/main/java/ru/noties/markwon/MarkwonVisitorImpl.java (100%) rename {markwon => markwon-core}/src/main/java/ru/noties/markwon/Prop.java (100%) rename {markwon => markwon-core}/src/main/java/ru/noties/markwon/RenderProps.java (100%) rename {markwon => markwon-core}/src/main/java/ru/noties/markwon/RenderPropsImpl.java (100%) rename {markwon => markwon-core}/src/main/java/ru/noties/markwon/SpanFactory.java (100%) rename {markwon => markwon-core}/src/main/java/ru/noties/markwon/SpannableBuilder.java (100%) rename {markwon => markwon-core}/src/main/java/ru/noties/markwon/core/CorePlugin.java (100%) rename {markwon => markwon-core}/src/main/java/ru/noties/markwon/core/CoreProps.java (100%) rename {markwon => markwon-core}/src/main/java/ru/noties/markwon/core/MarkwonTheme.java (100%) rename {markwon => markwon-core}/src/main/java/ru/noties/markwon/core/SimpleBlockNodeVisitor.java (100%) rename {markwon => markwon-core}/src/main/java/ru/noties/markwon/core/factory/BlockQuoteSpanFactory.java (100%) rename {markwon => markwon-core}/src/main/java/ru/noties/markwon/core/factory/CodeBlockSpanFactory.java (100%) rename {markwon => markwon-core}/src/main/java/ru/noties/markwon/core/factory/CodeSpanFactory.java (100%) rename {markwon => markwon-core}/src/main/java/ru/noties/markwon/core/factory/EmphasisSpanFactory.java (100%) rename {markwon => markwon-core}/src/main/java/ru/noties/markwon/core/factory/HeadingSpanFactory.java (100%) rename {markwon => markwon-core}/src/main/java/ru/noties/markwon/core/factory/LinkSpanFactory.java (100%) rename {markwon => markwon-core}/src/main/java/ru/noties/markwon/core/factory/ListItemSpanFactory.java (100%) rename {markwon => markwon-core}/src/main/java/ru/noties/markwon/core/factory/StrongEmphasisSpanFactory.java (100%) rename {markwon => markwon-core}/src/main/java/ru/noties/markwon/core/factory/ThematicBreakSpanFactory.java (100%) rename {markwon => markwon-core}/src/main/java/ru/noties/markwon/core/spans/BlockQuoteSpan.java (100%) rename {markwon => markwon-core}/src/main/java/ru/noties/markwon/core/spans/BulletListItemSpan.java (100%) rename {markwon => markwon-core}/src/main/java/ru/noties/markwon/core/spans/CodeSpan.java (100%) rename {markwon => markwon-core}/src/main/java/ru/noties/markwon/core/spans/EmphasisSpan.java (100%) rename {markwon => markwon-core}/src/main/java/ru/noties/markwon/core/spans/HeadingSpan.java (100%) rename {markwon => markwon-core}/src/main/java/ru/noties/markwon/core/spans/LinkSpan.java (100%) rename {markwon => markwon-core}/src/main/java/ru/noties/markwon/core/spans/ObjectsPool.java (100%) rename {markwon => markwon-core}/src/main/java/ru/noties/markwon/core/spans/OrderedListItemSpan.java (100%) rename {markwon => markwon-core}/src/main/java/ru/noties/markwon/core/spans/StrongEmphasisSpan.java (100%) rename {markwon => markwon-core}/src/main/java/ru/noties/markwon/core/spans/ThematicBreakSpan.java (100%) rename {markwon => markwon-core}/src/main/java/ru/noties/markwon/html/HtmlTag.java (100%) rename {markwon => markwon-core}/src/main/java/ru/noties/markwon/html/MarkwonHtmlParser.java (100%) rename {markwon => markwon-core}/src/main/java/ru/noties/markwon/html/MarkwonHtmlParserNoOp.java (100%) rename {markwon => markwon-core}/src/main/java/ru/noties/markwon/html/MarkwonHtmlRenderer.java (100%) rename {markwon => markwon-core}/src/main/java/ru/noties/markwon/html/MarkwonHtmlRendererImpl.java (100%) rename {markwon => markwon-core}/src/main/java/ru/noties/markwon/html/MarkwonHtmlRendererNoOp.java (100%) rename {markwon => markwon-core}/src/main/java/ru/noties/markwon/html/TagHandler.java (100%) rename {markwon => markwon-core}/src/main/java/ru/noties/markwon/image/AsyncDrawable.java (100%) rename {markwon => markwon-core}/src/main/java/ru/noties/markwon/image/AsyncDrawableLoader.java (100%) rename {markwon => markwon-core}/src/main/java/ru/noties/markwon/image/AsyncDrawableLoaderImpl.java (100%) rename {markwon => markwon-core}/src/main/java/ru/noties/markwon/image/AsyncDrawableLoaderNoOp.java (100%) rename {markwon => markwon-core}/src/main/java/ru/noties/markwon/image/AsyncDrawableScheduler.java (100%) rename {markwon => markwon-core}/src/main/java/ru/noties/markwon/image/AsyncDrawableSpan.java (100%) rename {markwon => markwon-core}/src/main/java/ru/noties/markwon/image/ImageItem.java (100%) rename {markwon => markwon-core}/src/main/java/ru/noties/markwon/image/ImageMediaDecoder.java (100%) rename {markwon => markwon-core}/src/main/java/ru/noties/markwon/image/ImageProps.java (100%) rename {markwon => markwon-core}/src/main/java/ru/noties/markwon/image/ImageSize.java (100%) rename {markwon => markwon-core}/src/main/java/ru/noties/markwon/image/ImageSizeResolver.java (100%) rename {markwon => markwon-core}/src/main/java/ru/noties/markwon/image/ImageSizeResolverDef.java (100%) rename {markwon => markwon-core}/src/main/java/ru/noties/markwon/image/ImageSpanFactory.java (100%) rename {markwon => markwon-core}/src/main/java/ru/noties/markwon/image/ImagesPlugin.java (100%) rename {markwon => markwon-core}/src/main/java/ru/noties/markwon/image/MediaDecoder.java (100%) rename {markwon => markwon-core}/src/main/java/ru/noties/markwon/image/SchemeHandler.java (100%) rename {markwon => markwon-core}/src/main/java/ru/noties/markwon/image/data/DataUri.java (100%) rename {markwon => markwon-core}/src/main/java/ru/noties/markwon/image/data/DataUriDecoder.java (100%) rename {markwon => markwon-core}/src/main/java/ru/noties/markwon/image/data/DataUriParser.java (100%) rename {markwon => markwon-core}/src/main/java/ru/noties/markwon/image/data/DataUriSchemeHandler.java (100%) rename {markwon => markwon-core}/src/main/java/ru/noties/markwon/image/file/FileSchemeHandler.java (100%) rename {markwon => markwon-core}/src/main/java/ru/noties/markwon/image/network/NetworkSchemeHandler.java (100%) rename {markwon => markwon-core}/src/main/java/ru/noties/markwon/movement/MovementMethodPlugin.java (100%) rename {markwon => markwon-core}/src/main/java/ru/noties/markwon/priority/Priority.java (100%) rename {markwon => markwon-core}/src/main/java/ru/noties/markwon/priority/PriorityProcessor.java (100%) rename {markwon => markwon-core}/src/main/java/ru/noties/markwon/priority/PriorityProcessorImpl.java (100%) rename {markwon => markwon-core}/src/main/java/ru/noties/markwon/syntax/SyntaxHighlight.java (100%) rename {markwon => markwon-core}/src/main/java/ru/noties/markwon/syntax/SyntaxHighlightNoOp.java (100%) rename {markwon => markwon-core}/src/main/java/ru/noties/markwon/urlprocessor/UrlProcessor.java (100%) rename {markwon => markwon-core}/src/main/java/ru/noties/markwon/urlprocessor/UrlProcessorAndroidAssets.java (100%) rename {markwon => markwon-core}/src/main/java/ru/noties/markwon/urlprocessor/UrlProcessorNoOp.java (100%) rename {markwon => markwon-core}/src/main/java/ru/noties/markwon/urlprocessor/UrlProcessorRelativeToAbsolute.java (100%) rename {markwon => markwon-core}/src/main/java/ru/noties/markwon/utils/ColorUtils.java (100%) rename {markwon => markwon-core}/src/main/java/ru/noties/markwon/utils/Dip.java (100%) rename {markwon => markwon-core}/src/main/java/ru/noties/markwon/utils/DrawableUtils.java (100%) rename {markwon => markwon-core}/src/main/java/ru/noties/markwon/utils/LeadingMarginUtils.java (100%) rename {markwon => markwon-core}/src/main/res/values/ids.xml (100%) rename {markwon => markwon-core}/src/test/java/ru/noties/markwon/AbstractMarkwonPluginTest.java (100%) rename {markwon => markwon-core}/src/test/java/ru/noties/markwon/AbstractMarkwonVisitorImpl.java (100%) rename {markwon => markwon-core}/src/test/java/ru/noties/markwon/MarkwonBuilderImplTest.java (100%) rename {markwon => markwon-core}/src/test/java/ru/noties/markwon/MarkwonImplTest.java (100%) rename {markwon => markwon-core}/src/test/java/ru/noties/markwon/MarkwonSpansFactoryImplTest.java (100%) rename {markwon => markwon-core}/src/test/java/ru/noties/markwon/MarkwonVisitorImplTest.java (100%) rename {markwon => markwon-core}/src/test/java/ru/noties/markwon/PropTest.java (100%) rename {markwon => markwon-core}/src/test/java/ru/noties/markwon/RenderPropsImplTest.java (100%) rename {markwon => markwon-core}/src/test/java/ru/noties/markwon/SpannableBuilderTest.java (100%) rename {markwon => markwon-core}/src/test/java/ru/noties/markwon/core/CorePluginBridge.java (100%) rename {markwon => markwon-core}/src/test/java/ru/noties/markwon/core/CorePluginTest.java (100%) rename {markwon => markwon-core}/src/test/java/ru/noties/markwon/core/CoreTest.java (100%) rename {markwon => markwon-core}/src/test/java/ru/noties/markwon/core/suite/.editorconfig (100%) rename {markwon => markwon-core}/src/test/java/ru/noties/markwon/core/suite/BaseSuiteTest.java (100%) rename {markwon => markwon-core}/src/test/java/ru/noties/markwon/core/suite/BlockquoteTest.java (100%) rename {markwon => markwon-core}/src/test/java/ru/noties/markwon/core/suite/BoldItalicTest.java (100%) rename {markwon => markwon-core}/src/test/java/ru/noties/markwon/core/suite/CodeTest.java (100%) rename {markwon => markwon-core}/src/test/java/ru/noties/markwon/core/suite/DeeplyNestedTest.java (100%) rename {markwon => markwon-core}/src/test/java/ru/noties/markwon/core/suite/EmphasisTest.java (100%) rename {markwon => markwon-core}/src/test/java/ru/noties/markwon/core/suite/FirstTest.java (100%) rename {markwon => markwon-core}/src/test/java/ru/noties/markwon/core/suite/HeadingTest.java (100%) rename {markwon => markwon-core}/src/test/java/ru/noties/markwon/core/suite/LinkTest.java (100%) rename {markwon => markwon-core}/src/test/java/ru/noties/markwon/core/suite/NoParagraphsTest.java (100%) rename {markwon => markwon-core}/src/test/java/ru/noties/markwon/core/suite/OrderedListTest.java (100%) rename {markwon => markwon-core}/src/test/java/ru/noties/markwon/core/suite/ParagraphTest.java (100%) rename {markwon => markwon-core}/src/test/java/ru/noties/markwon/core/suite/SecondTest.java (100%) rename {markwon => markwon-core}/src/test/java/ru/noties/markwon/core/suite/SoftBreakAddsNewLineTest.java (100%) rename {markwon => markwon-core}/src/test/java/ru/noties/markwon/core/suite/SoftBreakTest.java (100%) rename {markwon => markwon-core}/src/test/java/ru/noties/markwon/core/suite/StrongEmphasisTest.java (100%) rename {markwon => markwon-core}/src/test/java/ru/noties/markwon/core/suite/ThematicBreakTest.java (100%) rename {markwon => markwon-core}/src/test/java/ru/noties/markwon/core/suite/UnOrderedListTest.java (100%) rename {markwon => markwon-core}/src/test/java/ru/noties/markwon/image/AsyncDrawableTest.java (100%) rename {markwon => markwon-core}/src/test/java/ru/noties/markwon/image/ImageSizeResolverDefTest.java (100%) rename {markwon => markwon-core}/src/test/java/ru/noties/markwon/image/ImageTest.java (100%) rename {markwon => markwon-core}/src/test/java/ru/noties/markwon/image/data/DataUriParserTest.java (100%) rename {markwon => markwon-core}/src/test/java/ru/noties/markwon/image/data/DataUriSchemeHandlerTest.java (100%) rename {markwon => markwon-core}/src/test/java/ru/noties/markwon/priority/PriorityProcessorTest.java (100%) rename {markwon => markwon-core}/src/test/java/ru/noties/markwon/syntax/SyntaxHighlightTest.java (100%) rename {markwon => markwon-core}/src/test/java/ru/noties/markwon/urlprocessor/UrlProcessorAndroidAssetsTest.java (100%) rename {markwon => markwon-core}/src/test/java/ru/noties/markwon/urlprocessor/UrlProcessorRelativeToAbsoluteTest.java (100%) rename {markwon => markwon-core}/src/test/resources/tests/code-blocks.md (100%) rename {markwon => markwon-core}/src/test/resources/tests/deeply-nested.md (100%) rename {markwon => markwon-core}/src/test/resources/tests/first.md (100%) rename {markwon => markwon-core}/src/test/resources/tests/nested-blockquotes.md (100%) rename {markwon => markwon-core}/src/test/resources/tests/no-paragraphs.md (100%) rename {markwon => markwon-core}/src/test/resources/tests/ol-2-spaces.md (100%) rename {markwon => markwon-core}/src/test/resources/tests/ol-starts-with-5.md (100%) rename {markwon => markwon-core}/src/test/resources/tests/ol.md (100%) rename {markwon => markwon-core}/src/test/resources/tests/paragraph.md (100%) rename {markwon => markwon-core}/src/test/resources/tests/second.md (100%) rename {markwon => markwon-core}/src/test/resources/tests/single-code-block.md (100%) rename {markwon => markwon-core}/src/test/resources/tests/soft-break-adds-new-line.md (100%) rename {markwon => markwon-core}/src/test/resources/tests/soft-break.md (100%) rename {markwon => markwon-core}/src/test/resources/tests/ul-levels.md (100%) rename {markwon => markwon-core}/src/test/resources/tests/ul.md (100%) create mode 100644 markwon-ext-latex/gradle.properties create mode 100644 markwon-ext-tables/gradle.properties create mode 100644 markwon-ext-tasklist/gradle.properties create mode 100644 markwon-image-gif/gradle.properties create mode 100644 markwon-image-okhttp/gradle.properties create mode 100644 markwon-image-svg/gradle.properties create mode 100644 markwon-recycler/gradle.properties delete mode 100644 markwon-view/gradle.properties diff --git a/app/build.gradle b/app/build.gradle index 93b59222..51dd6d43 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -28,7 +28,7 @@ android { dependencies { - implementation project(':markwon') + implementation project(':markwon-core') implementation project(':markwon-ext-strikethrough') implementation project(':markwon-ext-tables') implementation project(':markwon-ext-tasklist') diff --git a/build.gradle b/build.gradle index f0a79879..66ba2c0f 100644 --- a/build.gradle +++ b/build.gradle @@ -19,6 +19,10 @@ allprojects { } version = VERSION_NAME group = GROUP + + tasks.withType(Javadoc) { + options.addStringOption('Xdoclint:none', '-quiet') + } } task clean(type: Delete) { diff --git a/docs/.vuepress/.artifacts.js b/docs/.vuepress/.artifacts.js index 8b93cea3..532f38d9 100644 --- a/docs/.vuepress/.artifacts.js +++ b/docs/.vuepress/.artifacts.js @@ -1,4 +1,4 @@ // this is a generated file, do not modify. To update it run 'collectArtifacts.js' script -const artifacts = [{"id":"markwon","name":"Markwon","group":"ru.noties","description":"Core Markwon artifact that includes basic markdown parsing and rendering"},{"id":"markwon-ext-strikethrough","name":"Markwon-Ext-Strikethrough","group":"ru.noties"},{"id":"markwon-html-parser-impl","name":"Markwon","group":"ru.noties"},{"id":"markwon-syntax-highlight","name":"Markwon","group":"ru.noties"},{"id":"markwon-view","name":"Markwon-View","group":"ru.noties"}]; +const artifacts = [{"id":"core","name":"Core","group":"ru.noties.markwon","description":"Core Markwon artifact that includes basic markdown parsing and rendering"},{"id":"ext-latex","name":"LaTeX","group":"ru.noties.markwon","description":"Extension to add LaTeX formulas to Markwon markdown"},{"id":"ext-strikethrough","name":"Strikethrough","group":"ru.noties.markwon","description":"Extension to add strikethrough markup to Markwon markdown"},{"id":"ext-tables","name":"Tables","group":"ru.noties.markwon","description":"Extension to add tables markup (GFM) to Markwon markdown"},{"id":"ext-tasklist","name":"Task List","group":"ru.noties.markwon","description":"Extension to add task lists (GFM) to Markwon markdown"},{"id":"html","name":"HTML","group":"ru.noties.markwon","description":"Provides HTML parsing functionality"},{"id":"image-gif","name":"Image GIF","group":"ru.noties.markwon","description":"Adds GIF media support to Markwon markdown"},{"id":"image-okhttp","name":"Image OkHttp","group":"ru.noties.markwon","description":"Adds OkHttp client to retrieve images data from network"},{"id":"image-svg","name":"Image SVG","group":"ru.noties.markwon","description":"Adds SVG media support to Markwon markdown"},{"id":"recycler","name":"Recycler","group":"ru.noties.markwon","description":"Provides RecyclerView.Adapter to display Markwon markdown"},{"id":"syntax-highlight","name":"Syntax Highlight","group":"ru.noties.markwon","description":"Add syntax highlight to Markwon markdown via Prism4j library"}]; export { artifacts }; diff --git a/docs/.vuepress/components/ArtifactPicker.vue b/docs/.vuepress/components/ArtifactPicker.vue index 83d6c9cb..21e2a560 100644 --- a/docs/.vuepress/components/ArtifactPicker.vue +++ b/docs/.vuepress/components/ArtifactPicker.vue @@ -42,7 +42,7 @@ export default { data() { return { artifacts, - selected: [] + selected: ['core'] }; }, methods: { @@ -86,6 +86,7 @@ export default { display: flex; flex-wrap: wrap; flex-direction: row; + margin-top: 0.5em; } .artifact { flex: 1; diff --git a/docs/.vuepress/components/MavenBadge.vue b/docs/.vuepress/components/MavenBadge.vue index 7c920bfc..3dddbf68 100644 --- a/docs/.vuepress/components/MavenBadge.vue +++ b/docs/.vuepress/components/MavenBadge.vue @@ -1,17 +1,22 @@ + + + + diff --git a/docs/.vuepress/config.js b/docs/.vuepress/config.js index 5c63daf9..ab5c22d5 100644 --- a/docs/.vuepress/config.js +++ b/docs/.vuepress/config.js @@ -13,6 +13,7 @@ module.exports = { nav: [ { text: 'Install', link: '/docs/install.md' }, { text: 'Changelog', link: '/CHANGELOG.md' }, + { text: 'Sandbox', link: '/sandbox.md' }, { text: 'Github', link: 'https://github.com/noties/Markwon' } ], sidebar: [ diff --git a/docs/.vuepress/override.styl b/docs/.vuepress/override.styl index 08764d10..1e96c201 100644 --- a/docs/.vuepress/override.styl +++ b/docs/.vuepress/override.styl @@ -2,7 +2,7 @@ $textColor = #000000 $accentColor = #4CAF50 a.sidebar-link { - font-weight: bold; + font-weight: 500; } .sidebar-sub-headers a.sidebar-link { @@ -15,6 +15,7 @@ a.sidebar-link { .sidebar-heading { color: $textColor; + font-weight: 600; } .sidebar-heading.open, .sidebar-heading:hover { diff --git a/docs/.vuepress/style.styl b/docs/.vuepress/style.styl index 3e7c379a..cae5a235 100644 --- a/docs/.vuepress/style.styl +++ b/docs/.vuepress/style.styl @@ -4,4 +4,8 @@ div[class~=language-gradle]:before { div[class~=language-proguard]:before { content:"proguard" +} + +div[class~=language-groovy]:before { + content:"gradle" } \ No newline at end of file diff --git a/docs/package-lock.json b/docs/package-lock.json index 77f560a6..79159931 100644 --- a/docs/package-lock.json +++ b/docs/package-lock.json @@ -2471,6 +2471,24 @@ "resolved": "https://registry.npmjs.org/commondir/-/commondir-1.0.1.tgz", "integrity": "sha1-3dgA2gxmEnOTzKWVDqloo6rxJTs=" }, + "commonmark": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/commonmark/-/commonmark-0.28.1.tgz", + "integrity": "sha1-Buq41SM4uDn6Gi11rwCF7tGxvq4=", + "requires": { + "entities": "~ 1.1.1", + "mdurl": "~ 1.0.1", + "minimist": "~ 1.2.0", + "string.prototype.repeat": "^0.2.0" + }, + "dependencies": { + "minimist": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.0.tgz", + "integrity": "sha1-o1AIsg9BOD7sH7kU9M1d95omQoQ=" + } + } + }, "component-emitter": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/component-emitter/-/component-emitter-1.2.1.tgz", @@ -9060,6 +9078,11 @@ } } }, + "string.prototype.repeat": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/string.prototype.repeat/-/string.prototype.repeat-0.2.0.tgz", + "integrity": "sha1-q6Nt4I3O5qWjN9SbLqHaGyj8Ds8=" + }, "string_decoder": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", diff --git a/docs/package.json b/docs/package.json index 9ba0ddb5..ba7c3704 100644 --- a/docs/package.json +++ b/docs/package.json @@ -3,6 +3,7 @@ "docs:build": "node ./collectArtifacts.js && vuepress build" }, "dependencies": { + "commonmark": "^0.28.1", "markdown-it-task-lists": "^2.1.1", "vuepress": "^0.14.2" } diff --git a/docs/sandbox.md b/docs/sandbox.md new file mode 100644 index 00000000..87a09091 --- /dev/null +++ b/docs/sandbox.md @@ -0,0 +1,3 @@ +# Commonmark Sandbox + + \ No newline at end of file From b3c685bfbcf7357bc7b55e61db1da73a815d4d96 Mon Sep 17 00:00:00 2001 From: Dimitry Ivanov Date: Tue, 8 Jan 2019 15:08:51 +0300 Subject: [PATCH 072/103] Doc site syntax highlight style --- docs/.vuepress/style.styl | 62 ++++++++++++++++++- docs/README.md | 2 +- .../main/java/ru/noties/markwon/Markwon.java | 25 +++++++- 3 files changed, 85 insertions(+), 4 deletions(-) diff --git a/docs/.vuepress/style.styl b/docs/.vuepress/style.styl index cae5a235..6ec59aba 100644 --- a/docs/.vuepress/style.styl +++ b/docs/.vuepress/style.styl @@ -8,4 +8,64 @@ div[class~=language-proguard]:before { div[class~=language-groovy]:before { content:"gradle" -} \ No newline at end of file +} + +div[class*="language-"] { + background-color: #2d2d2d; +} + +.token.comment, .token.prolog, .token.cdata { + color: #808080; +} + +.token.delimiter, .token.boolean, .token.keyword, .token.selector, .token.important, .token.atrule { + color: #cc7832; +} + +.token.operator, .token.punctuation, .token.attr-name { + color: #a9b7c6; +} + +.token.tag, .token.doctype, .token.builtin { + color: #e8bf6a; +} + +.token.entity, .token.number, .token.symbol { + color: #6897bb; +} + +.token.property, .token.constant, .token.variable { + color: #9876aa; +} + +.token.string, .token.char { + color: #6a8759; +} + +.token.annotation { + color: #bbb438; +} + +.token.attr-value { + color: #a5c261; +} + +.token.url { + color: #287bde; +} + +.token.function { + color: #ffc66d; +} + +.token.regex { + color: #364135; +} + +.token.inserted { + color: #294436; +} + +.token.deleted { + color: #484a4a; +} diff --git a/docs/README.md b/docs/README.md index 498f873d..a2752665 100644 --- a/docs/README.md +++ b/docs/README.md @@ -21,7 +21,7 @@ but also gives all the means to tweak the appearance if desired. All markdown fe listed in are supported (including support for **inlined/block HTML code**, **markdown tables**, **images** and **syntax highlight**). -## Supported markdown features: +## Supported markdown features * Emphasis (`*`, `_`) * Strong emphasis (`**`, `__`) diff --git a/markwon-core/src/main/java/ru/noties/markwon/Markwon.java b/markwon-core/src/main/java/ru/noties/markwon/Markwon.java index ee6e14cd..bf963995 100644 --- a/markwon-core/src/main/java/ru/noties/markwon/Markwon.java +++ b/markwon-core/src/main/java/ru/noties/markwon/Markwon.java @@ -14,6 +14,7 @@ import ru.noties.markwon.core.CorePlugin; * of static stateless methods). An instance of builder can be obtained via {@link #builder(Context)} * method. * + * @see #create(Context) * @see #builder(Context) * @see Builder */ @@ -46,7 +47,7 @@ public abstract class Markwon { } /** - * Method to simply parse markdown (without rendering) + * Method to parse markdown (without rendering) * * @param input markdown input to parse * @return parsed via commonmark-java org.commonmark.node.Node @@ -56,10 +57,30 @@ public abstract class Markwon { @NonNull public abstract Node parse(@NonNull String input); + /** + * Create Spanned markdown from parsed Node (via {@link #parse(String)} call). + *

+ * Please note that returned Spanned has few limitations. For example, images, tables + * and ordered lists require TextView to be properly displayed. This is why images and tables + * most likely won\'t work in this case. Ordered lists might have mis-measurements. Whenever + * possible use {@link #setMarkdown(TextView, String)} or {@link #setParsedMarkdown(TextView, Spanned)} + * as these methods will additionally call specific {@link MarkwonPlugin} methods to prepare + * proper display. + * + * @since 3.0.0 + */ @NonNull public abstract Spanned render(@NonNull Node node); - // parse + render + /** + * This method will {@link #parse(String)} and {@link #render(Node)} supplied markdown. Returned + * Spanned has the same limitations as from {@link #render(Node)} method. + * + * @param input markdown input + * @see #parse(String) + * @see #render(Node) + * @since 3.0.0 + */ @NonNull public abstract Spanned toMarkdown(@NonNull String input); From 7a598829a92825230d7ba1067759ae2428cdd131 Mon Sep 17 00:00:00 2001 From: Dimitry Ivanov Date: Sat, 12 Jan 2019 15:58:24 +0300 Subject: [PATCH 073/103] Bring back legacy 2.x.x documentation --- app/build.gradle | 7 - docs/.vuepress/components/LegacyWarning.vue | 13 + docs/.vuepress/components/MavenBadge2xx.vue | 24 ++ docs/.vuepress/components/MavenBadges2xx.vue | 20 ++ docs/.vuepress/config.js | 116 +++---- docs/docs/core/theme.md | 158 +++++++++ docs/docs/ext-latex/latex.md | 49 ++- docs/docs/ext-strikethrough/strikethrough.md | 32 +- docs/docs/ext-tables/tables.md | 86 ++++- docs/docs/ext-tasklist/tasklist.md | 4 - docs/docs/migration-2-3.md | 2 +- docs/docs/recycler/recycler.md | 4 - .../docs/syntax-highlight/syntax-highlight.md | 72 +++- docs/docs/v2/configure.md | 228 +++++++++++++ docs/docs/v2/factory.md | 63 ++++ docs/docs/v2/getting-started.md | 102 ++++++ docs/docs/v2/html.md | 305 +++++++++++++++++ docs/docs/{ => v2}/image-loader.md | 6 +- docs/docs/v2/install.md | 79 +++++ docs/docs/{ => v2}/syntax-highlight.md | 4 +- docs/docs/{ => v2}/theme.md | 4 +- docs/docs/{ => v2}/view.md | 7 +- markwon-ext-latex/README.md | 5 +- markwon-ext-strikethrough/README.md | 4 +- markwon-ext-tables/README.md | 45 +++ .../markwon/ext/tables/TablePlugin.java | 1 + markwon-view/README.md | 41 --- markwon-view/build.gradle | 25 -- .../debug/DebugConfigurationProvider.java | 31 -- .../res/layout/debug_markwon_preview.xml | 18 - .../layout/debug_markwon_preview_compat.xml | 18 - .../src/debug/res/values/debug_strings.xml | 39 --- markwon-view/src/main/AndroidManifest.xml | 1 - .../ru/noties/markwon/view/IMarkwonView.java | 23 -- .../ru/noties/markwon/view/MarkwonView.java | 50 --- .../markwon/view/MarkwonViewCompat.java | 50 --- .../markwon/view/MarkwonViewHelper.java | 105 ------ markwon-view/src/main/res/values/attrs.xml | 9 - sample-custom-extension/README.md | 26 -- sample-custom-extension/build.gradle | 29 -- .../src/main/AndroidManifest.xml | 28 -- .../src/main/assets/README.md | 313 ------------------ .../sample/extension/MainActivity.java | 40 --- .../recycler/MarkwonRecyclerActivity.java | 144 -------- .../sample/extension/recycler/TableEntry.java | 64 ---- .../extension/recycler/TableEntryView.java | 205 ------------ .../src/main/res/layout/activity_recycler.xml | 5 - .../main/res/layout/adapter_default_entry.xml | 13 - .../res/layout/adapter_fenced_code_block.xml | 24 -- .../main/res/layout/adapter_table_block.xml | 23 -- .../main/res/layout/view_table_entry_cell.xml | 11 - .../main/res/layout/view_table_entry_row.xml | 5 - .../src/main/res/values/attrs.xml | 10 - .../src/main/res/values/strings.xml | 14 - .../src/main/res/values/styles.xml | 5 - sample-latex-math/build.gradle | 24 -- .../src/main/AndroidManifest.xml | 18 - .../sample/jlatexmath/MainActivity.java | 58 ---- .../src/main/res/layout/activity_main.xml | 14 - .../src/main/res/values/strings.xml | 3 - .../src/main/res/values/styles.xml | 6 - sample/build.gradle | 1 + sample/src/main/AndroidManifest.xml | 1 + .../CustomExtensionActivity.java | 30 ++ .../customextension}/IconGroupNode.java | 2 +- .../sample/customextension}/IconNode.java | 2 +- .../sample/customextension}/IconPlugin.java | 11 +- .../customextension}/IconProcessor.java | 2 +- .../sample/customextension}/IconSpan.java | 2 +- .../customextension}/IconSpanProvider.java | 2 +- .../markwon/sample/latex/LatexActivity.java | 43 +++ .../sample/recycler/RecyclerActivity.java | 1 + .../markwon/sample/theme/ThemeActivity.java | 17 + .../res/drawable/ic_android_black_24dp.xml | 0 .../main/res/drawable/ic_home_black_36dp.xml | 0 .../res/drawable/ic_memory_black_48dp.xml | 0 .../ic_sentiment_satisfied_red_64dp.xml | 0 .../main/res/layout/activity_text_view.xml | 8 +- sample/src/main/res/values/strings.xml | 11 + settings.gradle | 4 +- 80 files changed, 1451 insertions(+), 1618 deletions(-) create mode 100644 docs/.vuepress/components/LegacyWarning.vue create mode 100644 docs/.vuepress/components/MavenBadge2xx.vue create mode 100644 docs/.vuepress/components/MavenBadges2xx.vue create mode 100644 docs/docs/core/theme.md create mode 100644 docs/docs/v2/configure.md create mode 100644 docs/docs/v2/factory.md create mode 100644 docs/docs/v2/getting-started.md create mode 100644 docs/docs/v2/html.md rename docs/docs/{ => v2}/image-loader.md (97%) create mode 100644 docs/docs/v2/install.md rename docs/docs/{ => v2}/syntax-highlight.md (96%) rename docs/docs/{ => v2}/theme.md (98%) rename docs/docs/{ => v2}/view.md (95%) create mode 100644 markwon-ext-tables/README.md delete mode 100644 markwon-view/README.md delete mode 100644 markwon-view/build.gradle delete mode 100644 markwon-view/src/debug/java/ru/noties/markwon/view/debug/DebugConfigurationProvider.java delete mode 100644 markwon-view/src/debug/res/layout/debug_markwon_preview.xml delete mode 100644 markwon-view/src/debug/res/layout/debug_markwon_preview_compat.xml delete mode 100644 markwon-view/src/debug/res/values/debug_strings.xml delete mode 100644 markwon-view/src/main/AndroidManifest.xml delete mode 100644 markwon-view/src/main/java/ru/noties/markwon/view/IMarkwonView.java delete mode 100644 markwon-view/src/main/java/ru/noties/markwon/view/MarkwonView.java delete mode 100644 markwon-view/src/main/java/ru/noties/markwon/view/MarkwonViewCompat.java delete mode 100644 markwon-view/src/main/java/ru/noties/markwon/view/MarkwonViewHelper.java delete mode 100644 markwon-view/src/main/res/values/attrs.xml delete mode 100644 sample-custom-extension/README.md delete mode 100644 sample-custom-extension/build.gradle delete mode 100644 sample-custom-extension/src/main/AndroidManifest.xml delete mode 100644 sample-custom-extension/src/main/assets/README.md delete mode 100644 sample-custom-extension/src/main/java/ru/noties/markwon/sample/extension/MainActivity.java delete mode 100644 sample-custom-extension/src/main/java/ru/noties/markwon/sample/extension/recycler/MarkwonRecyclerActivity.java delete mode 100644 sample-custom-extension/src/main/java/ru/noties/markwon/sample/extension/recycler/TableEntry.java delete mode 100644 sample-custom-extension/src/main/java/ru/noties/markwon/sample/extension/recycler/TableEntryView.java delete mode 100644 sample-custom-extension/src/main/res/layout/activity_recycler.xml delete mode 100644 sample-custom-extension/src/main/res/layout/adapter_default_entry.xml delete mode 100644 sample-custom-extension/src/main/res/layout/adapter_fenced_code_block.xml delete mode 100644 sample-custom-extension/src/main/res/layout/adapter_table_block.xml delete mode 100644 sample-custom-extension/src/main/res/layout/view_table_entry_cell.xml delete mode 100644 sample-custom-extension/src/main/res/layout/view_table_entry_row.xml delete mode 100644 sample-custom-extension/src/main/res/values/attrs.xml delete mode 100644 sample-custom-extension/src/main/res/values/strings.xml delete mode 100644 sample-custom-extension/src/main/res/values/styles.xml delete mode 100644 sample-latex-math/build.gradle delete mode 100644 sample-latex-math/src/main/AndroidManifest.xml delete mode 100644 sample-latex-math/src/main/java/ru/noties/markwon/sample/jlatexmath/MainActivity.java delete mode 100644 sample-latex-math/src/main/res/layout/activity_main.xml delete mode 100644 sample-latex-math/src/main/res/values/strings.xml delete mode 100644 sample-latex-math/src/main/res/values/styles.xml rename {sample-custom-extension/src/main/java/ru/noties/markwon/sample/extension => sample/src/main/java/ru/noties/markwon/sample/customextension}/IconGroupNode.java (71%) rename {sample-custom-extension/src/main/java/ru/noties/markwon/sample/extension => sample/src/main/java/ru/noties/markwon/sample/customextension}/IconNode.java (96%) rename {sample-custom-extension/src/main/java/ru/noties/markwon/sample/extension => sample/src/main/java/ru/noties/markwon/sample/customextension}/IconPlugin.java (85%) rename {sample-custom-extension/src/main/java/ru/noties/markwon/sample/extension => sample/src/main/java/ru/noties/markwon/sample/customextension}/IconProcessor.java (98%) rename {sample-custom-extension/src/main/java/ru/noties/markwon/sample/extension => sample/src/main/java/ru/noties/markwon/sample/customextension}/IconSpan.java (97%) rename {sample-custom-extension/src/main/java/ru/noties/markwon/sample/extension => sample/src/main/java/ru/noties/markwon/sample/customextension}/IconSpanProvider.java (97%) create mode 100644 sample/src/main/java/ru/noties/markwon/sample/theme/ThemeActivity.java rename {sample-custom-extension => sample}/src/main/res/drawable/ic_android_black_24dp.xml (100%) rename {sample-custom-extension => sample}/src/main/res/drawable/ic_home_black_36dp.xml (100%) rename {sample-custom-extension => sample}/src/main/res/drawable/ic_memory_black_48dp.xml (100%) rename {sample-custom-extension => sample}/src/main/res/drawable/ic_sentiment_satisfied_red_64dp.xml (100%) rename sample-custom-extension/src/main/res/layout/activity_main.xml => sample/src/main/res/layout/activity_text_view.xml (71%) diff --git a/app/build.gradle b/app/build.gradle index 88169fb2..c6ad59c1 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -1,5 +1,4 @@ apply plugin: 'com.android.application' -apply from: '../markwon-bundle.gradle' android { @@ -27,12 +26,6 @@ android { } } -ext.markwon = [ - 'version': '3.0.0-SNAPSHOT', - 'includeAll': true, - 'exclude': ['ext-latex', 'core'] -] - dependencies { implementation project(':markwon-core') diff --git a/docs/.vuepress/components/LegacyWarning.vue b/docs/.vuepress/components/LegacyWarning.vue new file mode 100644 index 00000000..b84e9cea --- /dev/null +++ b/docs/.vuepress/components/LegacyWarning.vue @@ -0,0 +1,13 @@ + + + + diff --git a/docs/.vuepress/components/MavenBadge2xx.vue b/docs/.vuepress/components/MavenBadge2xx.vue new file mode 100644 index 00000000..15544263 --- /dev/null +++ b/docs/.vuepress/components/MavenBadge2xx.vue @@ -0,0 +1,24 @@ + + + + diff --git a/docs/.vuepress/components/MavenBadges2xx.vue b/docs/.vuepress/components/MavenBadges2xx.vue new file mode 100644 index 00000000..d92b3af3 --- /dev/null +++ b/docs/.vuepress/components/MavenBadges2xx.vue @@ -0,0 +1,20 @@ + + + + + diff --git a/docs/.vuepress/config.js b/docs/.vuepress/config.js index ab5c22d5..c465c245 100644 --- a/docs/.vuepress/config.js +++ b/docs/.vuepress/config.js @@ -3,11 +3,11 @@ module.exports = { title: 'Markwon', description: 'Android markdown library based on commonmark specification', head: [ - ['link', {rel: 'apple-touch-icon', sizes: '180x180', href: '/apple-touch-icon.png?v=1'}], - ['link', {rel: 'icon', type: 'image/png', sizes: '16x16', href: '/favicon-16x16.png?v=1'}], - ['link', {rel: 'icon', href: '/favicon.ico?v=1'}], - ['link', {rel: 'icon', type: 'image/png', sizes: '32x32', href: '/favicon-32x32.png?v=1'}], - ['link', {rel: 'manifest', href: '/manifest.json?v=1'}], + ['link', { rel: 'apple-touch-icon', sizes: '180x180', href: '/apple-touch-icon.png?v=1' }], + ['link', { rel: 'icon', type: 'image/png', sizes: '16x16', href: '/favicon-16x16.png?v=1' }], + ['link', { rel: 'icon', href: '/favicon.ico?v=1' }], + ['link', { rel: 'icon', type: 'image/png', sizes: '32x32', href: '/favicon-32x32.png?v=1' }], + ['link', { rel: 'manifest', href: '/manifest.json?v=1' }], ], themeConfig: { nav: [ @@ -16,67 +16,51 @@ module.exports = { { text: 'Sandbox', link: '/sandbox.md' }, { text: 'Github', link: 'https://github.com/noties/Markwon' } ], - sidebar: [ - '/', - { - title: 'Core', - children: [ - '/docs/core/getting-started.md' - ] - }, - { - title: 'LaTeX extension', - children: [ - '/docs/ext-latex/latex.md' - ] - }, - { - title: 'Strikethrough extension', - children: [ - '/docs/ext-strikethrough/strikethrough.md' - ] - }, - { - title: 'Tables extension', - children: [ - '/docs/ext-tables/tables.md' - ] - }, - { - title: 'Task list extension', - children: [ - '/docs/ext-tasklist/tasklist.md' - ] - }, - { - title: 'HTML', - children: [ - '/docs/html/html.md' - ] - }, - { - title: 'Image', - children: [ - '/docs/image/gif.md', - '/docs/image/okhttp.md', - '/docs/image/svg.md' - ] - }, - { - title: 'Recycler', - children: [ - '/docs/recycler/recycler.md' - ] - }, - { - title: 'Syntax highlight', - children: [ - '/docs/syntax-highlight/syntax-highlight.md' - ] - }, - '/docs/migration-2-3.md' - - ], + sidebar: { + '/docs/v2/': [ + 'install.md', + 'getting-started.md', + 'configure.md', + 'theme.md', + 'factory.md', + 'image-loader.md', + 'syntax-highlight.md', + 'html.md', + 'view.md' + ], + '/': [ + '', + { + title: 'Core', + children: [ + '/docs/core/getting-started.md', + '/docs/core/theme.md' + ] + }, + '/docs/ext-latex/latex.md', + '/docs/ext-strikethrough/strikethrough.md', + '/docs/ext-tables/tables.md', + '/docs/ext-tasklist/tasklist.md', + { + title: 'HTML', + children: [ + '/docs/html/html.md' + ] + }, + { + title: 'Image', + children: [ + '/docs/image/gif.md', + '/docs/image/okhttp.md', + '/docs/image/svg.md' + ] + }, + '/docs/recycler/recycler.md', + '/docs/syntax-highlight/syntax-highlight.md', + '/docs/migration-2-3.md', + ['/docs/v2/install.md', 'Legacy 2.x.x documentation'] + ] + }, sidebarDepth: 2, lastUpdated: true }, diff --git a/docs/docs/core/theme.md b/docs/docs/core/theme.md new file mode 100644 index 00000000..b904fbbe --- /dev/null +++ b/docs/docs/core/theme.md @@ -0,0 +1,158 @@ +# Theme + +Here is the list of properties that can be configured via `MarkwonTheme.Builder` class. + +## Link color + +Controls the color of a [link](#) + + + +* `TextPaint#linkColor` will be used to determine linkColor of a context + +## Block margin + +Starting margin before text content for the: +* lists +* blockquotes +* task lists + + + +## Block quote + +Customizations for the `blockquote` stripe + +> Quote + +### Stripe width + +Width of a blockquote stripe + + + +### Stripe color + +Color of a blockquote stripe + + + +## List + +### List item color + +Controls the color of a list item. For ordered list: leading number, +for unordered list: bullet. + +* UL +1. OL + + + +### Bullet item stroke width + +Border width of a bullet list item (level 2) + +* First +* * Second +* * * Third + + + +### Bullet width + +The width of the bullet item + +* First + * Second + * Third + + + +## Code + +### Inline code text color + +The color of the `code` content + + + +### Inline code background color + +The color of `background` of a code content + + + +### Block code text color + +``` +The color of code block text +``` + + + +### Block code background color + +``` +The color of background of code block text +``` + + + +### Block code leading margin + +Leading margin for the block code content + + + +### Code typeface + +Typeface of code content + + + +### Code text size + +Text size of code content + + + +## Heading + +### Break height + +The height of a brake under H1 & H2 + + + +### Break color + +The color of a brake under H1 & H2 + + + +### Typeface + +The typeface of heading elements + + + +### Text size + +Array of heading text sizes _ratio_ that is applied to text size + + + +## Thematic break + +### Color + +Color of a thematic break + + + +### Height + +Height of a thematic break + + diff --git a/docs/docs/ext-latex/latex.md b/docs/docs/ext-latex/latex.md index d0922cf8..8fa856fc 100644 --- a/docs/docs/ext-latex/latex.md +++ b/docs/docs/ext-latex/latex.md @@ -1,7 +1,46 @@ ---- -title: 'Overview' ---- - # LaTeX extension - \ No newline at end of file + + +This is an extension that will help you display LaTeX formulas in your markdown. +Syntax is pretty simple: pre-fix and post-fix your latex with `$$` (double dollar sign). +`$$` should be the first characters in a line. + +```markdown +$$ +\\text{A long division \\longdiv{12345}{13} +$$ +``` + +```markdown +$$\\text{A long division \\longdiv{12345}{13}$$ +``` + +```java +Markwon.builder(context) + .use(ImagesPlugin.create(context)) + .use(JLatexMathPlugin.create(new Config(textSize)) + .build(); +``` + +This extension uses [jlatexmath-android](https://github.com/noties/jlatexmath-android) artifact to create LaTeX drawable. Then it +registers special `latex` image scheme handler and uses `AsyncDrawableLoader` to display +final result + +## Config + +```java +public static class Config { + + protected final float textSize; + + protected Drawable background; + + @JLatexMathDrawable.Align + protected int align = JLatexMathDrawable.ALIGN_CENTER; + + protected boolean fitCanvas = true; + + protected int padding; +} +``` diff --git a/docs/docs/ext-strikethrough/strikethrough.md b/docs/docs/ext-strikethrough/strikethrough.md index ca78dfaa..9bbfc0e5 100644 --- a/docs/docs/ext-strikethrough/strikethrough.md +++ b/docs/docs/ext-strikethrough/strikethrough.md @@ -1,7 +1,29 @@ ---- -title: 'Overview' ---- - # Strikethrough extension - \ No newline at end of file + + +This module adds `strikethrough` functionality to `Markwon` via `StrikethroughPlugin`: + +```java +Markwon.builder(context) + .usePlugin(StrikethroughPlugin.create()) +``` + +This plugin registers `SpanFactory` for `Strikethrough` node, so it's possible to customize Strikethrough Span that is used in rendering: + +```java +Markwon.builder(context) + .usePlugin(StrikethroughPlugin.create()) + .usePlugin(new AbstractMarkwonPlugin() { + @Override + public void configureSpansFactory(@NonNull MarkwonSpansFactory.Builder builder) { + builder.setFactory(Strikethrough.class, new SpanFactory() { + @Override + public Object getSpans(@NonNull MarkwonConfiguration configuration, @NonNull RenderProps props) { + // will use Underline span instead of Strikethrough + return new UnderlineSpan(); + } + }); + } + }) +``` diff --git a/docs/docs/ext-tables/tables.md b/docs/docs/ext-tables/tables.md index c8aa89d1..afa534ff 100644 --- a/docs/docs/ext-tables/tables.md +++ b/docs/docs/ext-tables/tables.md @@ -1,7 +1,83 @@ ---- -title: 'Overview' ---- - # Tables extension - \ No newline at end of file + + +This extension adds support for GFM tables. + +```java +final Markwon markwon = Markwon.builder(context) + // create default instance of TablePlugin + .usePlugin(TablePlugin.create(context)) +``` + +```java +final TableTheme tableTheme = TableTheme.builder() + .tableBorderColor(Color.RED) + .tableBorderWidth(0) + .tableCellPadding(0) + .tableHeaderRowBackgroundColor(Color.BLACK) + .tableEvenRowBackgroundColor(Color.GREEN) + .tableOddRowBackgroundColor(Color.YELLOW) + .build(); + +final Markwon markwon = Markwon.builder(context) + .usePlugin(TablePlugin.create(tableTheme)) +``` + +Please note, that _by default_ tables have limitations. For example, there is no support +for images inside table cells. And table contents won't be copied to clipboard if a TextView +has such functionality. Table will always take full width of a TextView in which it is displayed. +All columns will always be the of the same width. So, _default_ implementation provides basic +functionality which can answer some needs. These all come from the limited nature of the TextView +to display such content. + +In order to provide full-fledged experience, tables must be displayed in a special widget. +Since version `3.0.0` Markwon provides a special artifact `markwon-recycler` that allows +to render markdown in a set of widgets in a RecyclerView. It also gives ability to change +display widget form TextView to any other. + +```java +final Table table = Table.parse(Markwon, TableBlock); +myTableWidget.setTable(table); +``` + +Unfortunately Markwon does not provide a widget that can be used for tables. But it does +provide API that can be used to achieve desired result. + +## Theme + +### Cell padding + +Padding inside a table cell + + + +### Border color + +The color of table borders + + + +### Border width + +The width of table borders + + + +### Odd row background + +Background of an odd table row + + + +### Even row background + +Background of an even table row + + + +### Header row background + +Background of header table row + + diff --git a/docs/docs/ext-tasklist/tasklist.md b/docs/docs/ext-tasklist/tasklist.md index 0ff1603e..a955500a 100644 --- a/docs/docs/ext-tasklist/tasklist.md +++ b/docs/docs/ext-tasklist/tasklist.md @@ -1,7 +1,3 @@ ---- -title: 'Overview' ---- - # Task list extension \ No newline at end of file diff --git a/docs/docs/migration-2-3.md b/docs/docs/migration-2-3.md index 9a2e5dcc..c7543794 100644 --- a/docs/docs/migration-2-3.md +++ b/docs/docs/migration-2-3.md @@ -1 +1 @@ -# Migration v2 -> v3 \ No newline at end of file +# Migration 2.x.x -> 3.x.x \ No newline at end of file diff --git a/docs/docs/recycler/recycler.md b/docs/docs/recycler/recycler.md index 3c9188a8..1941f4ae 100644 --- a/docs/docs/recycler/recycler.md +++ b/docs/docs/recycler/recycler.md @@ -1,7 +1,3 @@ ---- -title: 'Overview' ---- - # Recycler \ No newline at end of file diff --git a/docs/docs/syntax-highlight/syntax-highlight.md b/docs/docs/syntax-highlight/syntax-highlight.md index beec2993..a5203cf6 100644 --- a/docs/docs/syntax-highlight/syntax-highlight.md +++ b/docs/docs/syntax-highlight/syntax-highlight.md @@ -1,7 +1,69 @@ ---- -title: 'Overview' ---- - # Syntax highlight - \ No newline at end of file + + +This is a simple module to add **syntax highlight** functionality to your markdown rendered with `Markwon` library. It is based on [Prism4j](https://github.com/noties/Prism4j) so lead there to understand how to configure `Prism4j` instance. + +theme-default + + +theme-darkula + +--- + +First, we need to obtain an instance of `Prism4jSyntaxHighlight` which implements Markwon's `SyntaxHighlight`: + +```java +final SyntaxHighlight highlight = + Prism4jSyntaxHighlight.create(Prism4j, Prism4jTheme); +``` + +we also can obtain an instance of `Prism4jSyntaxHighlight` that has a _fallback_ option (if a language is not defined in `Prism4j` instance, fallback language can be used): + +```java +final SyntaxHighlight highlight = + Prism4jSyntaxHighlight.create(Prism4j, Prism4jTheme, String); +``` + +Generally obtaining a `Prism4j` instance is pretty easy: + +```java +final Prism4j prism4j = new Prism4j(new GrammarLocatorDef()); +``` + +Where `GrammarLocatorDef` is a generated grammar locator (if you use `prism4j-bundler` annotation processor) + +`Prism4jTheme` is a specific type that is defined in this module (`prism4j` doesn't know anything about rendering). It has 2 implementations: + +* `Prism4jThemeDefault` +* `Prism4jThemeDarkula` + +Both of them can be obtained via factory method `create`: + +* `Prism4jThemeDefault.create()` +* `Prism4jThemeDarkula.create()` + +But of cause nothing is stopping you from defining your own theme: + +```java +public interface Prism4jTheme { + + @ColorInt + int background(); + + @ColorInt + int textColor(); + + void apply( + @NonNull String language, + @NonNull Prism4j.Syntax syntax, + @NonNull SpannableStringBuilder builder, + int start, + int end + ); +} +``` + +:::tip +You can extend `Prism4jThemeBase` which has some helper methods +::: \ No newline at end of file diff --git a/docs/docs/v2/configure.md b/docs/docs/v2/configure.md new file mode 100644 index 00000000..c8274988 --- /dev/null +++ b/docs/docs/v2/configure.md @@ -0,0 +1,228 @@ + + +# Configuration + +`SpannableConfiguration` is the core component that controls how markdown is parsed and rendered. +It can be obtained via factory methods: + +```java +// creates default implementation +final SpannableConfiguration configuration = SpannableConfiguration.create(context); +``` + +```java +// creates configurablable instance via `#builder` method +final SpannableConfiguration configuration = SpannableConfiguration.builder(context) + .asyncDrawableLoader(AsyncDrawableLoader.create()) + .build(); +``` + +:::tip Note +If `#builder` factory method is used, you do not need to specify default +values as they will be applied automatically +::: + +:::warning Images +If you plan on using images inside your markdown/HTML, you will have to **explicitly** +register an implementation of `AsyncDrawable.Loader` via `#asyncDrawableLoader` builder method. +`Markwon` comes with ready implementation for that and it can be found in +`markwon-image-loader` module. Refer to module [documentation](/docs/v2/image-loader.md) +::: + +## Theme + +`SpannableTheme` controls how markdown is rendered. It has pretty extensive number of +options that can be found [here](/docs/v2/theme.md) + +```java +SpannableConfiguration.builder(context) + .theme(SpannableTheme) + .build(); +``` + +If `SpannableTheme` is not provided explicitly, `SpannableTheme.create(context)` will be used + +## Images + +### Async loader + +`AsyncDrawable.Loader` handles images in your markdown and HTML + +```java +SpannableConfiguration.builder(context) + .asyncDrawableLoader(AsyncDrawable.Loader) + .build(); +``` + +If `AsyncDrawable.Loader` is not provided explicitly, default **no-op** implementation will be used. + +:::tip Implementation +There are no restrictions on what implementation to use, but `Markwon` has artifact that can +answer the most common needs of displaying SVG, GIF and other image formats. It can be found [here](/docs/v2/image-loader.md) +::: + +### Size resolver + +`ImageSizeResolver` controls the size of an image to be displayed. Currently it +handles only HTML images (specified via `img` tag). + +```java +SpannableConfiguration.builder(context) + .imageSizeResolver(ImageSizeResolver) + .build(); +``` + +If not provided explicitly, default `ImageSizeResolverDef` implementation will be used. +It handles 3 dimention units: +* `%` (percent) +* `em` (relative to text size) +* `px` (absolute size, every dimention that is not `%` or `em` is considered to be _absolute_) + +```html + + + +``` + +`ImageSizeResolverDef` keeps the ratio of original image if one of the dimentions is missing. + +:::warning Height% +There is no support for `%` units for `height` dimention. This is due to the fact that +height of an TextView in which markdown is displayed is non-stable and changes with time +(for example when image is loaded and applied to a TextView it will _increase_ TextView's height), +so we will have no point-of-refence from which to _calculate_ image height. +::: + +## Syntax highlight + +`SyntaxHighlight` controls the syntax highlight for code blocks (in markdown). + +```java +SpannableConfiguration.builder(context) + .syntaxHighlight(SyntaxHighlight) + .build(); +``` + +If not provided explicitly, default **no-op** implementation will be used. + +:::tip Syntax highlight +Although `SyntaxHighlight` interface was included with the very first version +of `Markwon` there were no ready-to-use implementations. But starting with +`Markwon` provides one. It can be found in `markwon-syntax-highlight` artifact. Refer +to module [documentation](/docs/v2/syntax-highlight.md) +::: + +## Link resolver + +`LinkSpan.Resolver` is triggered when a link is clicked in markdown/HTML. + +```java +SpannableConfiguration.builder(context) + .linkResolver(LinkSpan.Resolver) + .build(); +``` + +If not provided explicitly, default `LinkResolverDef` implementation will be used. +Underneath it constructs an `Intent` and _tries_ to start an Activity associated with it. +It no Activity is found, it will silently fail (no runtime exceptions) + +## URL processor + +`UrlProcessor` is used to process found URLs in markdown/HTML. + +```java +SpannableConfiguration.builder(context) + .urlProcessor(UrlProcessor) + .build(); +``` + +If not provided explicitly, default **no-op** implementation will be used. + +`Markwon` provides 2 implementations of `UrlProcessor`: +* `UrlProcessorRelativeToAbsolute` +* `UrlProcessorAndroidAssets` + +### UrlProcessorRelativeToAbsolute + +`UrlProcessorRelativeToAbsolute` can be used to make relative URL absolute. For example if an image is +defined like this: `![img](./art/image.JPG)` and `UrlProcessorRelativeToAbsolute` +is created with `https://github.com/noties/Markwon/raw/master/` as the base: +`new UrlProcessorRelativeToAbsolute("https://github.com/noties/Markwon/raw/master/")`, +then final image will have `https://github.com/noties/Markwon/raw/master/art/image.JPG` +as the destination. + +### UrlProcessorAndroidAssets + +`UrlProcessorAndroidAssets` can be used to make processed links to point to Android assets folder. +So an image: `![img](./art/image.JPG)` will have `file:///android_asset/art/image.JPG` as the +destination + +## Factory + +`SpannableFactory` is used to control _what_ span implementations to be used + +```java +SpannableConfiguration.builder(context) + .factory(SpannableFactory) + .build(); +``` + +If not provided explicitly, default `SpannableFactoryDef` implementation will be used. It is documented +in [this section](/docs/v2/factory.md) + +## Soft line break + +`softBreakAddsNewLine` option controls how _soft breaks_ are treated in the final result. +If `true` -> soft break will add a new line, else it will add a ` ` (space) char. + +```java +SpannableConfiguration.builder(context) + .softBreakAddsNewLine(boolean) + .build(); +``` + +If not provided explicitly, default `false` value will be used. + + + +## HTML + +### Parser + +`MarkwonHtmlParser` is used to parse HTML content + +```java +SpannableConfiguration.builder(context) + .htmlParser(MarkwonHtmlParser) + .build(); +``` + +if not provided explicitly, default `MarkwonHtmlParserImpl` will be used +**if** it can be found in classpath, otherwise default **no-op** implementation +wiil be used. Refer to [HTML](/docs/v2/html.md#parser) document for more information about this behavior. + +### Renderer + +`MarkwonHtmlRenderer` controls how parsed HTML content will be rendered. + +```java +SpannableConfiguration.builder(context) + .htmlRenderer(MarkwonHtmlRenderer) + .build(); +``` + +If not provided explicitly, default `MarkwonHtmlRenderer` implementation will be used. +It is documented [here](/docs/v2/html.md#renderer) + +### HTML allow non-closed tags + +`htmlAllowNonClosedTags` option is used to control whether or not to +render non-closed HTML tags + +```java +SpannableConfiguration.builder(context) + .htmlAllowNonClosedTags(boolean) + .build(); +``` + +If not provided explicitly, default value `false` will be used (non-closed tags **won't** be rendered). diff --git a/docs/docs/v2/factory.md b/docs/docs/v2/factory.md new file mode 100644 index 00000000..186dcf3c --- /dev/null +++ b/docs/docs/v2/factory.md @@ -0,0 +1,63 @@ + + +# Factory + +`SpannableFactory` is used to create Span implementations. + +```java +SpannableConfiguration.builder(context) + .factory(SpannableFactory) + .build(); +``` + +`Markwon` provides default `SpannableFactoryDef` implementation that is +used by default. + +Spans: +* `strongEmphasis` +* `emphasis` +* `blockQuote` +* `code` +* `orderedListItem` +* `bulletListItem` +* `thematicBreak` +* `heading` +* `strikethrough` +* `taskListItem` +* `tableRow` +* `paragraph` +* `image` +* `link` +* `superScript` (HTML content only) +* `subScript` (HTML content only) +* `underline` (HTML content only) + +:::tip +`SpannableFactory` can be used to ignore some kinds of text markup. If, for example, +you do not wish to apply _emphasis_ styling to your final result, just return `null` +from `emphasis` factory method: +```java +@Nullable +@Override +public Object emphasis() { + return null; +} +``` +::: + +:::tip +All factory methods in `SpannableFactory` return an `Object`, but you can actually +return an **array of Objects** if you wish to apply multiple Spans to a single styling node. +For example, let's make all _emphasis_ also red: + +```java +@Nullable +@Override +public Object emphasis() { + return new Object[] { + super.emphasis(), + new ForegroundColorSpan(Color.RED) + }; +} +``` +::: \ No newline at end of file diff --git a/docs/docs/v2/getting-started.md b/docs/docs/v2/getting-started.md new file mode 100644 index 00000000..e24ca710 --- /dev/null +++ b/docs/docs/v2/getting-started.md @@ -0,0 +1,102 @@ + + +# Getting started + +:::tip Installation +Please follow [installation](/docs/install.md) instructions +to learn how to add `Markwon` to your project +::: + +## Quick one + +This is the most simple way to set markdown to a `TextView` or any of its siblings: + +```java +Markwon.setMarkdown(textView, "**Hello there!**"); +``` + +The most simple way to obtain markdown to be applied _somewhere_ else: + +```java +// parsed and styled markdown +final CharSequence markdown = Markwon.markdown(context, "**Hello there!**"); + +// use it +Toast.makeText(context, markdown, Toast.LENGTH_LONG).show(); +``` + +## Longer one + +When you need to customize markdown parsing/rendering you can use [SpannableConfiguration](/docs/configure.md): + +```java +final SpannableConfiguration configuration = SpannableConfiguration.builder(context) + .asyncDrawableLoader(AsyncDrawableLoader.create()) + .build(); + +Markwon.setMarkdown(textView, configuration, "Are **you** still there?"); + +final CharSequence markdown = Markwon.markdown(configuration, "Are **you** still there?"); +Toast.makeText(context, markdown, Toast.LENGTH_LONG).show(); +``` + +## No magic one + +In order to understand how previous examples work, let's break them down: + +* construct a `Parser` (see: ) and parse markdown +* construct a `SpannableConfiguration` (if it's not provided) +* *render* parsed markdown to Spannable (via `SpannableRenderer`) +* prepares TextView to display images, tables and links +* sets text + +This flow answers the most simple usage of displaying markdown: one shot parsing +& configuration of relatively small markdown chunks. If your markdown contains +a lot of text or you plan to display multiple UI widgets with markdown you might +consider *stepping in* and taking control of this flow. + +The candidate requirements to *step in*: +* parsing and processing of parsed markdown in a background thread +* reusing `Parser` and/or `SpannableConfiguration` between multiple calls +* ignore images or tables specific logic (you know that markdown won't contain them) + +So, if we expand `Markwon.setMarkdown(textView, markdown)` method we will see the following: + +```java +// create a Parser instance (can be done manually) +// internally creates default Parser instance & registers `strike-through` & `tables` extension +final Parser parser = Markwon.createParser(); + +// core class to display markdown, can be obtained via this method, +// which creates default instance (no images handling though), +// or via `builder` method, which lets you to configure this instance +final SpannableConfiguration configuration = SpannableConfiguration.create(context); + +final SpannableRenderer renderer = new SpannableRenderer(); + +final Node node = parser.parse(markdown); +final CharSequence text = renderer.render(configuration, node); + +// for links in markdown to be clickable +textView.setMovementMethod(LinkMovementMethod.getInstance()); + +// we need these due to the limited nature of Spannables to invalidate TextView +Markwon.unscheduleDrawables(textView); +Markwon.unscheduleTableRows(textView); + +// @since 2.0.1 we must measure ordered list items _before_ they are rendered +OrderedListItemSpan.measure(view, text); + +textView.setText(text); + +Markwon.scheduleDrawables(textView); +Markwon.scheduleTableRows(textView); +``` + +:::tip Note +If you are having trouble with `LinkMovementMethod` you can use +`Markwon.setText(textView, markdown, movementMethod)` method to specify _no_ movement +method (aka `null`) or own implementation. As an alternative to the system `LinkMovementMethod` +you can use [Better-Link-Movement-Method](https://github.com/saket/Better-Link-Movement-Method). +Please note that `Markwon.setText` method expects _parsed_ markdown as the second argument. +::: \ No newline at end of file diff --git a/docs/docs/v2/html.md b/docs/docs/v2/html.md new file mode 100644 index 00000000..bb5e8e59 --- /dev/null +++ b/docs/docs/v2/html.md @@ -0,0 +1,305 @@ + + +# HTML + +Starting with version `2.0.0` `Markwon` brings the whole HTML parsing/rendering +stack _on-site_. The main reason for this are _special_ definitions of HTML nodes +by . More specifically: +and . +These two are _a bit_ different from _native_ HTML understanding. +Well, they are _completely_ different and share only the same names as + and +elements. This leads to situations when for example an `` tag is considered +a block when it's used like this: + +```markdown + +Hello from italics tag + +``` + +:::tip A bit of background +
+ had brought attention to differences between HTML & commonmark implementations.

+::: + +Let's modify code snippet above _a bit_: + +```markdown{3} + +Hello from italics tag + + +``` + +We have just added a `new-line` before closing `
` tag. And this +changes everything as now, according to the , +we have 2 HtmlBlocks: one before `new-line` (containing open `` tag and text content) +and one after (containing as little as closing `` tag). + +If we modify code snippet _a bit_ again: + +```markdown{4} + +Hello from italics tag + +bold> +``` + +We will have 1 HtmlBlock (from previous snippet) and a bunch of HtmlInlines: +* HtmlInline (``) +* HtmlInline (``) +* Text (`bold`) +* HtmlInline (``) + +Those _little_ differences render `Html.fromHtml` (which was used in `1.x.x` versions) +useless. And actually it renders most of the HTML parsers implementations useless, +as most of them do not allow processing of HTML fragments in a raw fashion +without _fixing_ content on-the-fly. + +Both `TagSoup` and `Jsoup` HTML parsers (that were considered for this project) are built to deal with +_malicious_ HTML code (*all HTML code*? :no_mouth:). So, when supplied +with a `italic` fragment they will make it `italic`. +And it's a good thing, but consider these fragments for the sake of markdown: + +* `italic ` +* `bold italic` +* `` + +We will get: + +* `italic ` +* `bold italic` + +_* Or to be precise: `italic ` & +`bold italic`_ + +Which will be rendered in a final document: + + +|expected|actual| +|---|---| +|italic bold italic|italic bold italic| + +This might seem like a minor problem, but add more tags to a document, +introduce some deeply nested structures, spice openning and closing tags up +by adding markdown markup between them and finally write _malicious_ HTML code :laughing:! + +There is no such problem on the _frontend_ for which commonmark specification is mostly +aimed as _frontend_ runs in a web-browser environment. After all _parsed_ markdown +will become HTML tags (most common usage). And web-browser will know how to render final result. + +We, on the other hand, do not posess HTML heritage (*thank :robot:!*), but still +want to display some HTML to style resulting markdown a bit. That's why `Markwon` +incorporated own HTML parsing logic. It is based on the project. +And makes usage of the `Tokekiser` class that allows to _tokenise_ input HTML. +All other code that doesn't follow this purpose was removed. It's safe to use +in projects that already have `jsoup` dependency as `Markwon` repackaged **jsoup** source classes +(which could be found ) + +## Parser + +There are no additional steps to configure HTML parsing. It's enabled by default. +If you wish to _exclude_ it, please follow the [exclude](#exclude-html-parsing) section below. + +The key class here is: `MarkwonHtmlParser` that is defined in `markwon-html-parser-api` module. +`markwon-html-parser-api` is a simple module that defines HTML parsing contract and +does not provide implementation. + +To change what implementation `Markwon` should use, `SpannableConfiguration` can be used: + +```java{2} +SpannableConfiguration.builder(context) + .htmlParser(MarkwonHtmlParser) + .build(); +``` + +`markwon-html-parser-impl` on the other hand provides `MarkwonHtmlParser` implementation. +It's called `MarkwonHtmlParserImpl`. It can be created like this: + +```java +final MarkwonHtmlParser htmlParser = MarkwonHtmlParserImpl.create(); +// or +final MarkwonHtmlParser htmlParser = MarkwonHtmlParserImpl.create(HtmlEmptyTagReplacement); +``` + +### Empty tag replacement + +In order to append text content for self-closing, void or just _empty_ HTML tags, +`HtmlEmptyTagReplacement` can be used. As we cannot set Span for empty content, +we must represent empty tag with text during parsing stage (if we want it to be represented). + +Consider this: +* `` +* `
` +* `` + +By default (`HtmlEmptyTagReplacement.create()`) will handle `img` and `br` tags. +`img` will be replaced with `alt` property if it is present and `\uFFFC` if it is not. +And `br` will insert a new line. + +### Non-closed tags + +It's possible that your HTML can contain non-closed tags. By default `Markwon` will ignore them, +but if you wish to get a bit closer to a web-browser experience, you can allow this behaviour: + +```java{2} +SpannableConfiguration.builder(context) + .htmlAllowNonClosedTags(true) + .build(); +``` + +:::warning Note +If there is (for example) an `` tag at the start of a document and it's not closed +and `Markwon` is configured to **not** ignore non-closed tags (`.htmlAllowNonClosedTags(true)`), +it will make the whole document in italics +::: + +### Implementation note + +`MarkwonHtmlParserImpl` does not create a unified HTML node. Instead it creates +2 collections: inline tags and block tags. Inline tags are represented as a `List` +of inline tags (). And +block tags are structured in a tree. This helps to achieve _browser_-like behaviour, +when open inline tag is applied to all content (even if inside blocks) until closing tag. +All tags that are not _inline_ are considered to be _block_ ones. + +## Renderer + +Unlike `MarkwonHtmlParser` `Markwon` comes with a `MarkwonHtmlRenderer` by default. + +Default implementation can be obtain like this: + +```java +MarkwonHtmlRenderer.create(); +``` + +Default instance have these tags _handled_: +* emphasis + * `i` + * `em` + * `cite` + * `dfn` +* strong emphasis + * `b` + * `strong` +* `sup` (super script) +* `sub` (sub script) +* underline + * `u` + * `ins` +* strike through + * `del` + * `s` + * `strike` +* `a` (link) +* `ul` (unordered list) +* `ol` (ordered list) +* `img` (image) +* `blockquote` (block quote) +* `h{1-6}` (heading) + +If you wish to _extend_ default handling (or override existing), +`#builderWithDefaults` factory method can be used: + +```java +MarkwonHtmlRenderer.builderWithDefaults(); +``` + +For a completely _clean_ configurable instance `#builder` method can be used: + +```java +MarkwonHtmlRenderer.builder(); +``` + +### Custom tag handler + +To configure `MarkwonHtmlRenderer` to handle tags differently or +create a new tag handler - `TagHandler` can be used + +```java +public abstract class TagHandler { + + public abstract void handle( + @NonNull SpannableConfiguration configuration, + @NonNull SpannableBuilder builder, + @NonNull HtmlTag tag + ); +} +``` + +For the most simple _inline_ tag handler a `SimpleTagHandler` can be used: + +```java +public abstract class SimpleTagHandler extends TagHandler { + + @Nullable + public abstract Object getSpans(@NonNull SpannableConfiguration configuration, @NonNull HtmlTag tag); +} +``` + +For example, `EmphasisHandler`: + +```java +public class EmphasisHandler extends SimpleTagHandler { + @Nullable + @Override + public Object getSpans(@NonNull SpannableConfiguration configuration, @NonNull HtmlTag tag) { + return configuration.factory().emphasis(); + } +} +``` + +If you wish to handle a _block_ HTML node (for example `

  • First
  • Second
`) refer +to `ListHandler` source code for reference. + +:::warning +The most important thing when implementing custom `TagHandler` is to know +what type of `HtmlTag` we are dealing with. There are 2: inline & block. +Inline tag cannot contain children. Block _can_ contain children. And they +_most likely_ should also be visited and _handled_ by registered `TagHandler` (if any) +accordingly. See `TagHandler#visitChildren(configuration, builder, child);` +::: + +#### Css inline style parser + +When implementing own `TagHandler` you might want to inspect inline CSS styles +of a HTML element. `Markwon` provides an utility parser for that purpose: + +```java +final CssInlineStyleParser inlineStyleParser = CssInlineStyleParser.create(); +for (CssProperty property: inlineStyleParser.parse("width: 100%; height: 100%;")) { + // [0] = CssProperty({width=100%}), + // [1] = CssProperty({height=100%}) +} +``` + +## Exclude HTML parsing + +If you wish to exclude HTML parsing altogether, you can manually +exclude `markwon-html-parser-impl` artifact from your projects compile classpath. +This can be beneficial if you know that markdown input won't contain +HTML and/or you wish to ignore it. Excluding HTML parsing +can speed up `Markwon` parsing and will decrease final size of +`Markwon` dependency by around `100kb`. + + + +```groovy +dependencies { + implementation("ru.noties:markwon:${markwonVersion}") { + exclude module: 'markwon-html-parser-impl' + } +} +``` + +Excluding `markwon-html-parser-impl` this way will result in +`MarkwonHtmlParser#noOp` implementation. No further steps are +required. + +:::warning Note +Excluding `markwon-html-parser-impl` won't remove *all* the content between +HTML tags. It will if `commonmark` decides that a specific fragment is a +`HtmlBlock`, but it won't if fragment is considered a `HtmlInline` as `HtmlInline` +does not contain content (just a tag definition). +::: \ No newline at end of file diff --git a/docs/docs/image-loader.md b/docs/docs/v2/image-loader.md similarity index 97% rename from docs/docs/image-loader.md rename to docs/docs/v2/image-loader.md index f9642215..75275961 100644 --- a/docs/docs/image-loader.md +++ b/docs/docs/v2/image-loader.md @@ -1,3 +1,5 @@ + + # Images By default `Markwon` doesn't handle images. Although `AsyncDrawable.Loader` is @@ -16,12 +18,12 @@ public interface Loader { ## AsyncDrawableLoader - + `AsyncDrawableLoader` from `markwon-image-loader` artifact can be used. :::tip Install -[Learn how to add](/docs/install.md#image-loader) `markwon-image-loader` to your project +[Learn how to add](/docs/v2/install.md#image-loader) `markwon-image-loader` to your project ::: Default instance of `AsyncDrawableLoader` can be obtain like this: diff --git a/docs/docs/v2/install.md b/docs/docs/v2/install.md new file mode 100644 index 00000000..eab25066 --- /dev/null +++ b/docs/docs/v2/install.md @@ -0,0 +1,79 @@ + + +# Installation + + + +In order to start using `Markwon` add this to your dependencies block +in your projects `build.gradle`: + +```groovy +implementation "ru.noties:markwon:${markwonVersion}" +``` + +This is core artifact that is sufficient to start displaying markdown in your Android applications. + +`Markwon` comes with more artifacts that cover additional functionality, but they are +**not** required to be used, as most of them provide implementations for functionality +that is _interfaced_ in the core artifact + +```groovy +implementation "ru.noties:markwon-image-loader:${markwonVersion}" +implementation "ru.noties:markwon-syntax-highlight:${markwonVersion}" +implementation "ru.noties:markwon-view:${markwonVersion}" +``` + +These artifacts share the same _version_ as the core artifact + +### Image loader + +```groovy +implementation "ru.noties:markwon-image-loader:${markwonVersion}" +``` + +Provides implementation of `AsyncDrawable.Loader` and comes with support for: +* SVG +* GIF +* Other image formats + +Please refer to documentation for [image loader](/docs/v2/image-loader.md) module + +### Syntax highlight + +```groovy +implementation "ru.noties:markwon-syntax-highlight:${markwonVersion}" +``` + +Provides implementation of `SyntaxHighlight` and allows various syntax highlighting +in your markdown based Android applications. Comes with 2 ready-to-be-used themes: `light` and `dark`. +Please refer to documentation for [syntax highlight](/docs/v2/syntax-highlight.md) module + +### View + +```groovy +implementation "ru.noties:markwon-view:${markwonVersion}" +``` + +Provides 2 widgets to display markdown: `MarkwonView` and `MarkwonViewCompat` (subclasses +of `TextView` and `AppCompatTextView` respectively). +Please refer to documentation for [view](/docs/v2/view.md) module + +## Proguard + +When using `markwon-image-loader` artifact and Proguard is enabled, add these rules +to your proguard configuration: + +```proguard +-dontwarn okhttp3.** +-dontwarn okio.** + +-keep class com.caverock.androidsvg.** { *; } +-dontwarn com.caverock.androidsvg.** +``` + +They come from dependencies that `markwon-image-loader` is using. + +:::tip Other artifacts +Other artifacts do not require special Proguard rules +::: + diff --git a/docs/docs/syntax-highlight.md b/docs/docs/v2/syntax-highlight.md similarity index 96% rename from docs/docs/syntax-highlight.md rename to docs/docs/v2/syntax-highlight.md index 64454903..4aa1795e 100644 --- a/docs/docs/syntax-highlight.md +++ b/docs/docs/v2/syntax-highlight.md @@ -1,6 +1,8 @@ + + # Syntax highlight - + This is a simple module to add **syntax highlight** functionality to your markdown rendered with `Markwon` library. It is based on [Prism4j](https://github.com/noties/Prism4j) so lead there to understand how to configure `Prism4j` instance. diff --git a/docs/docs/theme.md b/docs/docs/v2/theme.md similarity index 98% rename from docs/docs/theme.md rename to docs/docs/v2/theme.md index 4bc6cfb0..269155fe 100644 --- a/docs/docs/theme.md +++ b/docs/docs/v2/theme.md @@ -1,3 +1,5 @@ + + # Theme Here is the list of properties that can be configured via `SpannableTheme#builder` factory @@ -209,4 +211,4 @@ Background of header table row Drawable of task list item - + diff --git a/docs/docs/view.md b/docs/docs/v2/view.md similarity index 95% rename from docs/docs/view.md rename to docs/docs/v2/view.md index c43b4e9d..c365fd42 100644 --- a/docs/docs/view.md +++ b/docs/docs/v2/view.md @@ -1,6 +1,8 @@ + + # MarkwonView - + This is simple library containing 2 views that are able to display markdown: * MarkwonView - extends `android.view.TextView` @@ -27,7 +29,8 @@ public interface IMarkwonView { Both views support layout-preview in Android Studio (with some exceptions, for example, bold span is not rendered due to some limitations of layout preview). These are XML attributes: -``` + +```xml app:mv_markdown="string" app:mv_configurationProvider="string" ``` diff --git a/markwon-ext-latex/README.md b/markwon-ext-latex/README.md index 5937fd6b..aa1348c3 100644 --- a/markwon-ext-latex/README.md +++ b/markwon-ext-latex/README.md @@ -1,6 +1,8 @@ # LaTeX -This is a small extension that will help you display LaTeX formulas in your markdown. +[![ext-latex](https://img.shields.io/maven-central/v/ru.noties.markwon/ext-latex.svg?label=ext-latex)](http://search.maven.org/#search|ga|1|g%3A%22ru.noties.markwon%22%20AND%20a%3A%22ext-latex%22) + +This is an extension that will help you display LaTeX formulas in your markdown. Syntax is pretty simple: pre-fix and post-fix your latex with `$$` (double dollar sign). `$$` should be the first characters in a line. @@ -16,7 +18,6 @@ $$\\text{A long division \\longdiv{12345}{13}$$ ```java Markwon.builder(context) - .use(CorePlugin.create()) .use(ImagesPlugin.create(context)) .use(JLatexMathPlugin.create(new Config(textSize)) .build(); diff --git a/markwon-ext-strikethrough/README.md b/markwon-ext-strikethrough/README.md index 55b9e542..acd8b513 100644 --- a/markwon-ext-strikethrough/README.md +++ b/markwon-ext-strikethrough/README.md @@ -1,6 +1,6 @@ # Strikethrough -[![markwon-ext-strikethrough](https://img.shields.io/maven-central/v/ru.noties/markwon-ext-strikethrough.svg?label=markwon-ext-strikethrough)](http://search.maven.org/#search|ga|1|g%3A%22ru.noties%22%20AND%20a%3A%22markwon-ext-strikethrough%22) +[![ext-strikethrough](https://img.shields.io/maven-central/v/ru.noties.markwon/ext-strikethrough.svg?label=ext-strikethrough)](http://search.maven.org/#search|ga|1|g%3A%22ru.noties.markwon%22%20AND%20a%3A%22ext-strikethrough%22) This module adds `strikethrough` functionality to `Markwon` via `StrikethroughPlugin`: @@ -13,7 +13,7 @@ This plugin registers `SpanFactory` for `Strikethrough` node, so it's possible t ```java Markwon.builder(context) - .usePlugin(StrikethroughPlugin.class) + .usePlugin(StrikethroughPlugin.create()) .usePlugin(new AbstractMarkwonPlugin() { @Override public void configureSpansFactory(@NonNull MarkwonSpansFactory.Builder builder) { diff --git a/markwon-ext-tables/README.md b/markwon-ext-tables/README.md new file mode 100644 index 00000000..6f94bd2d --- /dev/null +++ b/markwon-ext-tables/README.md @@ -0,0 +1,45 @@ +# Tables + +[![ext-tables](https://img.shields.io/maven-central/v/ru.noties.markwon/ext-tables.svg?label=ext-tables)](http://search.maven.org/#search|ga|1|g%3A%22ru.noties.markwon%22%20AND%20a%3A%22ext-tables%22) + +This extension adds support for GFM tables. + +```java +final Markwon markwon = Markwon.builder(context) + // create default instance of TablePlugin + .usePlugin(TablePlugin.create(context)) +``` + +```java +final TableTheme tableTheme = TableTheme.builder() + .tableBorderColor(Color.RED) + .tableBorderWidth(0) + .tableCellPadding(0) + .tableHeaderRowBackgroundColor(Color.BLACK) + .tableEvenRowBackgroundColor(Color.GREEN) + .tableOddRowBackgroundColor(Color.YELLOW) + .build(); + +final Markwon markwon = Markwon.builder(context) + .usePlugin(TablePlugin.create(tableTheme)) +``` + +Please note, that _by default_ tables have limitations. For example, there is no support +for images inside table cells. And table contents won't be copied to clipboard if a TextView +has such functionality. Table will always take full width of a TextView in which it is displayed. +All columns will always be the of the same width. So, _default_ implementation provides basic +functionality which can answer some needs. These all come from the limited nature of the TextView +to display such content. + +In order to provide full-fledged experience, tables must be displayed in a special widget. +Since version `3.0.0` Markwon provides a special artifact `markwon-recycler` that allows +to render markdown in a set of widgets in a RecyclerView. It also gives ability to change +display widget form TextView to any other. + +```java +final Table table = Table.parse(Markwon, TableBlock); +myTableWidget.setTable(table); +``` + +Unfortunately Markwon does not provide a widget that can be used for tables. But it does +provide API that can be used to achieve desired result. diff --git a/markwon-ext-tables/src/main/java/ru/noties/markwon/ext/tables/TablePlugin.java b/markwon-ext-tables/src/main/java/ru/noties/markwon/ext/tables/TablePlugin.java index ae547435..d6a5d26e 100644 --- a/markwon-ext-tables/src/main/java/ru/noties/markwon/ext/tables/TablePlugin.java +++ b/markwon-ext-tables/src/main/java/ru/noties/markwon/ext/tables/TablePlugin.java @@ -52,6 +52,7 @@ public class TablePlugin extends AbstractMarkwonPlugin { @Override public void beforeRender(@NonNull Node node) { + // clear before rendering (as visitor has some internal mutable state) visitor.clear(); } diff --git a/markwon-view/README.md b/markwon-view/README.md deleted file mode 100644 index 71f032db..00000000 --- a/markwon-view/README.md +++ /dev/null @@ -1,41 +0,0 @@ -# Markwon View - -[![maven|markwon-view](https://img.shields.io/maven-central/v/ru.noties/markwon-view.svg?label=maven%7Cmarkwon-view)](http://search.maven.org/#search|ga|1|g%3A%22ru.noties%22%20AND%20a%3A%markwon-view%22) - -This is simple library containing 2 views that are able to display markdown: -* MarkwonView - extends `android.view.TextView` -* MarkwonViewCompat - extends `android.support.v7.widget.AppCompatTextView` - -Both of them implement common `IMarkwonView` interface: -```java -public interface IMarkwonView { - - interface ConfigurationProvider { - @NonNull - SpannableConfiguration provide(@NonNull Context context); - } - - void setConfigurationProvider(@NonNull ConfigurationProvider provider); - - void setMarkdown(@Nullable String markdown); - void setMarkdown(@Nullable SpannableConfiguration configuration, @Nullable String markdown); - - @Nullable - String getMarkdown(); -} -``` - -Both views support layout-preview in Android Studio (with some exceptions, for example, bold span is not rendered due to some limitations of layout preview). -These are XML attributes: -``` -app:mv_markdown="string" -app:mv_configurationProvider="string" -``` - -`mv_markdown` accepts a string and represents raw markdown - -`mv_configurationProvider` accepts a string and represents a full class name of a class of type `ConfigurationProvider`, -for example: `com.example.my.package.MyConfigurationProvider` (this class must have an empty constructor -in order to be instantiated via reflection). - -Please note that those views parse markdown in main thread, so their usage must be for relatively small markdown portions only diff --git a/markwon-view/build.gradle b/markwon-view/build.gradle deleted file mode 100644 index 5e4e72ab..00000000 --- a/markwon-view/build.gradle +++ /dev/null @@ -1,25 +0,0 @@ -apply plugin: 'com.android.library' - -android { - - compileSdkVersion config['compile-sdk'] - buildToolsVersion config['build-tools'] - - defaultConfig { - minSdkVersion config['min-sdk'] - targetSdkVersion config['target-sdk'] - versionCode 1 - versionName version - } -} - -dependencies { - - api project(':markwon') - - deps.with { - compileOnly it['support-app-compat'] - } -} - -registerArtifact(this) diff --git a/markwon-view/src/debug/java/ru/noties/markwon/view/debug/DebugConfigurationProvider.java b/markwon-view/src/debug/java/ru/noties/markwon/view/debug/DebugConfigurationProvider.java deleted file mode 100644 index f88ba5ae..00000000 --- a/markwon-view/src/debug/java/ru/noties/markwon/view/debug/DebugConfigurationProvider.java +++ /dev/null @@ -1,31 +0,0 @@ -package ru.noties.markwon.view.debug; - -import android.content.Context; -import android.support.annotation.NonNull; - -import ru.noties.markwon.MarkwonConfiguration; -import ru.noties.markwon.core.MarkwonTheme; -import ru.noties.markwon.view.IMarkwonView; - -public class DebugConfigurationProvider implements IMarkwonView.ConfigurationProvider { - - private MarkwonConfiguration cached; - - @NonNull - @Override - public MarkwonConfiguration provide(@NonNull Context context) { - if (cached == null) { - cached = MarkwonConfiguration.builder(context) - .theme(debugTheme(context)) - .build(); - } - return cached; - } - - private static MarkwonTheme debugTheme(@NonNull Context context) { - return MarkwonTheme.builderWithDefaults(context) - .blockQuoteColor(0xFFff0000) - .codeBackgroundColor(0x40FF0000) - .build(); - } -} diff --git a/markwon-view/src/debug/res/layout/debug_markwon_preview.xml b/markwon-view/src/debug/res/layout/debug_markwon_preview.xml deleted file mode 100644 index ecc7bd3d..00000000 --- a/markwon-view/src/debug/res/layout/debug_markwon_preview.xml +++ /dev/null @@ -1,18 +0,0 @@ - - - - - - \ No newline at end of file diff --git a/markwon-view/src/debug/res/layout/debug_markwon_preview_compat.xml b/markwon-view/src/debug/res/layout/debug_markwon_preview_compat.xml deleted file mode 100644 index 092dbf00..00000000 --- a/markwon-view/src/debug/res/layout/debug_markwon_preview_compat.xml +++ /dev/null @@ -1,18 +0,0 @@ - - - - - - \ No newline at end of file diff --git a/markwon-view/src/debug/res/values/debug_strings.xml b/markwon-view/src/debug/res/values/debug_strings.xml deleted file mode 100644 index e7b87ab2..00000000 --- a/markwon-view/src/debug/res/values/debug_strings.xml +++ /dev/null @@ -1,39 +0,0 @@ - - - - - Quote - \n - >> Quote #2 - \n - >>> Quote `#3` - \n - --- - \n - ``` - \n - // this is some amazing code block - \n - ``` - \n - * First - \n - * Second - \n - * * Second-First - \n - * * Second-Second - \n - * * * Second-Second-First - \n - * And out of blue - Third - ]]> - - - \ No newline at end of file diff --git a/markwon-view/src/main/AndroidManifest.xml b/markwon-view/src/main/AndroidManifest.xml deleted file mode 100644 index 6340e29d..00000000 --- a/markwon-view/src/main/AndroidManifest.xml +++ /dev/null @@ -1 +0,0 @@ - diff --git a/markwon-view/src/main/java/ru/noties/markwon/view/IMarkwonView.java b/markwon-view/src/main/java/ru/noties/markwon/view/IMarkwonView.java deleted file mode 100644 index bc66e8b8..00000000 --- a/markwon-view/src/main/java/ru/noties/markwon/view/IMarkwonView.java +++ /dev/null @@ -1,23 +0,0 @@ -package ru.noties.markwon.view; - -import android.content.Context; -import android.support.annotation.NonNull; -import android.support.annotation.Nullable; - -import ru.noties.markwon.MarkwonConfiguration; - -public interface IMarkwonView { - - interface ConfigurationProvider { - @NonNull - MarkwonConfiguration provide(@NonNull Context context); - } - - void setConfigurationProvider(@NonNull ConfigurationProvider provider); - - void setMarkdown(@Nullable String markdown); - void setMarkdown(@Nullable MarkwonConfiguration configuration, @Nullable String markdown); - - @Nullable - String getMarkdown(); -} diff --git a/markwon-view/src/main/java/ru/noties/markwon/view/MarkwonView.java b/markwon-view/src/main/java/ru/noties/markwon/view/MarkwonView.java deleted file mode 100644 index f42d8727..00000000 --- a/markwon-view/src/main/java/ru/noties/markwon/view/MarkwonView.java +++ /dev/null @@ -1,50 +0,0 @@ -package ru.noties.markwon.view; - -import android.annotation.SuppressLint; -import android.content.Context; -import android.support.annotation.NonNull; -import android.support.annotation.Nullable; -import android.util.AttributeSet; -import android.widget.TextView; - -import ru.noties.markwon.MarkwonConfiguration; - -@SuppressLint("AppCompatCustomView") -public class MarkwonView extends TextView implements IMarkwonView { - - private MarkwonViewHelper helper; - - public MarkwonView(Context context) { - super(context); - init(context, null); - } - - public MarkwonView(Context context, AttributeSet attrs) { - super(context, attrs); - init(context, attrs); - } - - private void init(Context context, AttributeSet attributeSet) { - helper = MarkwonViewHelper.create(this); - helper.init(context, attributeSet); - } - - @Override - public void setConfigurationProvider(@NonNull ConfigurationProvider provider) { - helper.setConfigurationProvider(provider); - } - - public void setMarkdown(@Nullable String markdown) { - helper.setMarkdown(markdown); - } - - public void setMarkdown(@Nullable MarkwonConfiguration configuration, @Nullable String markdown) { - helper.setMarkdown(configuration, markdown); - } - - @Nullable - @Override - public String getMarkdown() { - return helper.getMarkdown(); - } -} diff --git a/markwon-view/src/main/java/ru/noties/markwon/view/MarkwonViewCompat.java b/markwon-view/src/main/java/ru/noties/markwon/view/MarkwonViewCompat.java deleted file mode 100644 index b30ecaa2..00000000 --- a/markwon-view/src/main/java/ru/noties/markwon/view/MarkwonViewCompat.java +++ /dev/null @@ -1,50 +0,0 @@ -package ru.noties.markwon.view; - -import android.content.Context; -import android.support.annotation.NonNull; -import android.support.annotation.Nullable; -import android.support.v7.widget.AppCompatTextView; -import android.util.AttributeSet; - -import ru.noties.markwon.MarkwonConfiguration; - -public class MarkwonViewCompat extends AppCompatTextView implements IMarkwonView { - - private MarkwonViewHelper helper; - - public MarkwonViewCompat(Context context) { - super(context); - init(context, null); - } - - public MarkwonViewCompat(Context context, AttributeSet attrs) { - super(context, attrs); - init(context, attrs); - } - - private void init(Context context, AttributeSet attributeSet) { - helper = MarkwonViewHelper.create(this); - helper.init(context, attributeSet); - } - - @Override - public void setConfigurationProvider(@NonNull ConfigurationProvider provider) { - helper.setConfigurationProvider(provider); - } - - @Override - public void setMarkdown(@Nullable String markdown) { - helper.setMarkdown(markdown); - } - - @Override - public void setMarkdown(@Nullable MarkwonConfiguration configuration, @Nullable String markdown) { - helper.setMarkdown(configuration, markdown); - } - - @Nullable - @Override - public String getMarkdown() { - return helper.getMarkdown(); - } -} diff --git a/markwon-view/src/main/java/ru/noties/markwon/view/MarkwonViewHelper.java b/markwon-view/src/main/java/ru/noties/markwon/view/MarkwonViewHelper.java deleted file mode 100644 index f3669b77..00000000 --- a/markwon-view/src/main/java/ru/noties/markwon/view/MarkwonViewHelper.java +++ /dev/null @@ -1,105 +0,0 @@ -package ru.noties.markwon.view; - -import android.content.Context; -import android.content.res.TypedArray; -import android.support.annotation.NonNull; -import android.support.annotation.Nullable; -import android.text.TextUtils; -import android.util.AttributeSet; -import android.widget.TextView; - -import ru.noties.markwon.Markwon; -import ru.noties.markwon.MarkwonConfiguration; - -public class MarkwonViewHelper implements IMarkwonView { - - public static MarkwonViewHelper create(@NonNull V view) { - return new MarkwonViewHelper(view); - } - - private final TextView textView; - - private ConfigurationProvider provider; - - private MarkwonConfiguration configuration; - private String markdown; - - private MarkwonViewHelper(@NonNull TextView textView) { - this.textView = textView; - } - - public void init(Context context, AttributeSet attributeSet) { - - if (attributeSet != null) { - final TypedArray array = context.obtainStyledAttributes(attributeSet, R.styleable.MarkwonView); - try { - - final String configurationProvider = array.getString(R.styleable.MarkwonView_mv_configurationProvider); - final ConfigurationProvider provider; - if (!TextUtils.isEmpty(configurationProvider)) { - provider = MarkwonViewHelper.obtainProvider(configurationProvider); - } else { - provider = null; - } - if (provider != null) { - setConfigurationProvider(provider); - } - - final String markdown = array.getString(R.styleable.MarkwonView_mv_markdown); - if (!TextUtils.isEmpty(markdown)) { - setMarkdown(markdown); - } - } finally { - array.recycle(); - } - } - } - - @Override - public void setConfigurationProvider(@NonNull ConfigurationProvider provider) { - this.provider = provider; - this.configuration = provider.provide(textView.getContext()); - if (!TextUtils.isEmpty(markdown)) { - // invalidate rendered markdown - setMarkdown(markdown); - } - } - - @Override - public void setMarkdown(@Nullable String markdown) { - setMarkdown(null, markdown); - } - - @Override - public void setMarkdown(@Nullable MarkwonConfiguration configuration, @Nullable String markdown) { - this.markdown = markdown; - if (configuration == null) { - if (this.configuration == null) { - if (provider != null) { - this.configuration = provider.provide(textView.getContext()); - } else { - this.configuration = MarkwonConfiguration.create(textView.getContext()); - } - } - configuration = this.configuration; - } - Markwon.setMarkdown(textView, configuration, markdown); - } - - @Nullable - @Override - public String getMarkdown() { - return markdown; - } - - @Nullable - public static IMarkwonView.ConfigurationProvider obtainProvider(@NonNull String className) { - try { - final Class cl = Class.forName(className); - return (IMarkwonView.ConfigurationProvider) cl.newInstance(); - } catch (Throwable t) { - t.printStackTrace(); - return null; - } - } -} diff --git a/markwon-view/src/main/res/values/attrs.xml b/markwon-view/src/main/res/values/attrs.xml deleted file mode 100644 index 33a532f3..00000000 --- a/markwon-view/src/main/res/values/attrs.xml +++ /dev/null @@ -1,9 +0,0 @@ - - - - - - - - - \ No newline at end of file diff --git a/sample-custom-extension/README.md b/sample-custom-extension/README.md deleted file mode 100644 index 194557ab..00000000 --- a/sample-custom-extension/README.md +++ /dev/null @@ -1,26 +0,0 @@ -# Custom extension - -This module provides a simple implementation for icons that are bundled in your application resources using custom `DelimiterProcessor`. It can be used as a reference when dealing with new functionality based on _delimiters_. - -```markdown -# Hello @ic-android-black-24 - -**Please** click @ic-home-green-24 (home icon) if you want to go home. -``` - -Here we will substitute elements starting with `@ic-` for icons that we have in our resources: - -* `@ic-android-black-24` -> `R.drawable.ic_android_black_24dp` -* `@ic-home-green-24` -> `R.drawable.ic_home_green_24dp` - -In order to provide reliable parsing we need to have delimiters _around_ desired content. So, `@ic-home-green-24` would become `@ic-home-green-24@`. This is current limitation of [commonmark-java](https://github.com/atlassian/commonmark-java) library that Markwon uses underneath. There is an ongoing [issue](https://github.com/atlassian/commonmark-java/issues/113) that might change this in future thought. - -But as we known the pattern beforehand it's pretty easy to pre-process raw markdown and make it the way we want it. Please refer to `IconProcessor#process` method for the reference. - -So, the our steps would be: - -* prepare raw markdown (wrap icons with `@` if it's not already) -* construct a Parser with our registered delimiter processor -* parse markdown and obtain a `Node` -* create a node visitor that will additionally visit custom node (`IconNode`) -* use markdown diff --git a/sample-custom-extension/build.gradle b/sample-custom-extension/build.gradle deleted file mode 100644 index e78a0cf5..00000000 --- a/sample-custom-extension/build.gradle +++ /dev/null @@ -1,29 +0,0 @@ -apply plugin: 'com.android.application' - -android { - - compileSdkVersion config['compile-sdk'] - buildToolsVersion config['build-tools'] - - defaultConfig { - - applicationId "ru.noties.markwon.sample.extension" - - // using 21 as minimum only to be able to vector assets - minSdkVersion 21 - targetSdkVersion config['target-sdk'] - versionCode 1 - versionName version - } -} - -dependencies { - implementation project(':markwon-core') - - implementation project(':markwon-image-svg') - implementation project(':markwon-recycler') - implementation project(':markwon-ext-tables') - implementation project(':markwon-html') - - implementation deps['debug'] -} diff --git a/sample-custom-extension/src/main/AndroidManifest.xml b/sample-custom-extension/src/main/AndroidManifest.xml deleted file mode 100644 index bc18eebe..00000000 --- a/sample-custom-extension/src/main/AndroidManifest.xml +++ /dev/null @@ -1,28 +0,0 @@ - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/sample-custom-extension/src/main/assets/README.md b/sample-custom-extension/src/main/assets/README.md deleted file mode 100644 index be6976a9..00000000 --- a/sample-custom-extension/src/main/assets/README.md +++ /dev/null @@ -1,313 +0,0 @@ -![logo](./art/markwon_logo.png) - -# Markwon - -[![markwon](https://img.shields.io/maven-central/v/ru.noties/markwon.svg?label=markwon)](http://search.maven.org/#search|ga|1|g%3A%22ru.noties%22%20AND%20a%3A%22markwon%22) -[![markwon-image-loader](https://img.shields.io/maven-central/v/ru.noties/markwon-image-loader.svg?label=markwon-image-loader)](http://search.maven.org/#search|ga|1|g%3A%22ru.noties%22%20AND%20a%3A%22markwon-image-loader%22) -[![markwon-syntax-highlight](https://img.shields.io/maven-central/v/ru.noties/markwon-syntax-highlight.svg?label=markwon-syntax-highlight)](http://search.maven.org/#search|ga|1|g%3A%22ru.noties%22%20AND%20a%3A%22markwon-syntax-highlight%22) -[![markwon-view](https://img.shields.io/maven-central/v/ru.noties/markwon-view.svg?label=markwon-view)](http://search.maven.org/#search|ga|1|g%3A%22ru.noties%22%20AND%20a%3A%22markwon-view%22) - -[![Build Status](https://travis-ci.org/noties/Markwon.svg?branch=master)](https://travis-ci.org/noties/Markwon) - -**Markwon** is a markdown library for Android. It parses markdown -following [commonmark-spec] with the help of amazing [commonmark-java] -library and renders result as _Android-native_ Spannables. **No HTML** -is involved as an intermediate step. **No WebView** is required. -It's extremely fast, feature-rich and extensible. - -It gives ability to display markdown in all TextView widgets -(**TextView**, **Button**, **Switch**, **CheckBox**, etc), **Toasts** -and all other places that accept **Spanned content**. Library provides -reasonable defaults to display style of a markdown content but also -gives all the means to tweak the appearance if desired. All markdown -features listed in [commonmark-spec] are supported -(including support for **inlined/block HTML code**, **markdown tables**, -**images** and **syntax highlight**). - -[commonmark-spec]: https://spec.commonmark.org/0.28/ -[commonmark-java]: https://github.com/atlassian/commonmark-java/blob/master/README.md - -**This file is displayed by default in the [sample-apk] (`markwon-sample-{latest-version}-debug.apk`) application. Which is a generic markdown viewer with support to display markdown via `http`, `https` & `file` schemes and 2 themes included: Light & Dark* - -[sample-apk]: https://github.com/noties/Markwon/releases - -## Installation -```groovy -implementation "ru.noties:markwon:${markwonVersion}" -implementation "ru.noties:markwon-image-loader:${markwonVersion}" // optional -implementation "ru.noties:markwon-syntax-highlight:${markwonVersion}" // optional -implementation "ru.noties:markwon-view:${markwonVersion}" // optional -``` - -Please visit [documentation] web-site for further reference - -## Supported markdown features: -* Emphasis (`*`, `_`) -* Strong emphasis (`**`, `__`) -* Strike-through (`~~`) -* Headers (`#{1,6}`) -* Links (`[]()` && `[][]`) -* Images -* Thematic break (`---`, `***`, `___`) -* Quotes & nested quotes (`>{1,}`) -* Ordered & non-ordered lists & nested ones -* Inline code -* Code blocks -* Tables (*with limitations*) -* Syntax highlight -* HTML - * Emphasis (``, ``, ``, ``) - * Strong emphasis (``, ``) - * SuperScript (``) - * SubScript (``) - * Underline (``, `ins`) - * Strike-through (``, ``, ``) - * Link (`a`) - * Lists (`ul`, `ol`) - * Images (`img` will require configured image loader) - * Blockquote (`blockquote`) - * Heading (`h1`, `h2`, `h3`, `h4`, `h5`, `h6`) - * there is support to render any HTML tag -* Task lists: -- [ ] Not _done_ - - [X] **Done** with `X` - - [x] ~~and~~ **or** small `x` ---- - -## Screenshots - -Taken with default configuration (except for image loading): - -
- - - - -By default configuration uses TextView textColor for styling, so changing textColor changes style - ---- - -## Documentation - -Please visit [documentation] web-site for reference - -[documentation]: https://noties.github.io/Markwon - ---- - -## Applications using Markwon - -* [Partiko](https://partiko.app) -* [FairNote Notepad](https://play.google.com/store/apps/details?id=com.rgiskard.fairnote) - - ---- - -# Demo -Based on [this cheatsheet][cheatsheet] - ---- - -## Headers ---- -# Header 1 -## Header 2 -### Header 3 -#### Header 4 -##### Header 5 -###### Header 6 ---- - -## Emphasis - -Emphasis, aka italics, with *asterisks* or _underscores_. - -Strong emphasis, aka bold, with **asterisks** or __underscores__. - -Combined emphasis with **asterisks and _underscores_**. - -Strikethrough uses two tildes. ~~Scratch this.~~ - ---- - -## Lists -1. First ordered list item -2. Another item - * Unordered sub-list. -1. Actual numbers don't matter, just that it's a number - 1. Ordered sub-list -4. And another item. - - You can have properly indented paragraphs within list items. Notice the blank line above, and the leading spaces (at least one, but we'll use three here to also align the raw Markdown). - - To have a line break without a paragraph, you will need to use two trailing spaces. - Note that this line is separate, but within the same paragraph. - (This is contrary to the typical GFM line break behaviour, where trailing spaces are not required.) - -* Unordered list can use asterisks -- Or minuses -+ Or pluses - ---- - -## Links - -[I'm an inline-style link](https://www.google.com) - -[I'm a reference-style link][Arbitrary case-insensitive reference text] - -[I'm a relative reference to a repository file](../blob/master/LICENSE) - -[You can use numbers for reference-style link definitions][1] - -Or leave it empty and use the [link text itself]. - ---- - -## Code - -Inline `code` has `back-ticks around` it. - -```javascript -var s = "JavaScript syntax highlighting"; -alert(s); -``` - -```python -s = "Python syntax highlighting" -print s -``` - -```java -/** - * Helper method to obtain a Parser with registered strike-through & table extensions - * & task lists (added in 1.0.1) - * - * @return a Parser instance that is supported by this library - * @since 1.0.0 - */ -@NonNull -public static Parser createParser() { - return new Parser.Builder() - .extensions(Arrays.asList( - StrikethroughExtension.create(), - TablesExtension.create(), - TaskListExtension.create() - )) - .build(); -} -``` - -```xml - - - - - -``` - -``` -No language indicated, so no syntax highlighting. -But let's throw in a tag. -``` - ---- - -## Tables - -Colons can be used to align columns. - -| Tables | Are | Cool | -| ------------- |:-------------:| -----:| -| col 3 is | right-aligned | $1600 | -| col 2 is | centered | $12 | -| zebra stripes | are neat | $1 | - -There must be at least 3 dashes separating each header cell. -The outer pipes (|) are optional, and you don't need to make the -raw Markdown line up prettily. You can also use inline Markdown. - -Markdown | Less | Pretty ---- | --- | --- -*Still* | `renders` | **nicely** -1 | 2 | 3 - ---- - -## Blockquotes - -> Blockquotes are very handy in email to emulate reply text. -> This line is part of the same quote. - -Quote break. - -> This is a very long line that will still be quoted properly when it wraps. Oh boy let's keep writing to make sure this is long enough to actually wrap for everyone. Oh, you can *put* **Markdown** into a blockquote. - -Nested quotes -> Hello! ->> And to you! - ---- - -## Inline HTML - -```html -HTML -``` - -HTML - ---- - -## Horizontal Rule - -Three or more... - ---- - -Hyphens (`-`) - -*** - -Asterisks (`*`) - -___ - -Underscores (`_`) - - -## License - -``` - Copyright 2017 Dimitry Ivanov (mail@dimitryivanov.ru) - - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. -``` - -[cheatsheet]: https://github.com/adam-p/markdown-here/wiki/Markdown-Cheatsheet - -[arbitrary case-insensitive reference text]: https://www.mozilla.org -[1]: http://slashdot.org -[link text itself]: http://www.reddit.com diff --git a/sample-custom-extension/src/main/java/ru/noties/markwon/sample/extension/MainActivity.java b/sample-custom-extension/src/main/java/ru/noties/markwon/sample/extension/MainActivity.java deleted file mode 100644 index 4975dc38..00000000 --- a/sample-custom-extension/src/main/java/ru/noties/markwon/sample/extension/MainActivity.java +++ /dev/null @@ -1,40 +0,0 @@ -package ru.noties.markwon.sample.extension; - -import android.app.Activity; -import android.graphics.Typeface; -import android.os.Bundle; -import android.support.annotation.NonNull; -import android.widget.TextView; - -import ru.noties.markwon.AbstractMarkwonPlugin; -import ru.noties.markwon.Markwon; -import ru.noties.markwon.core.MarkwonTheme; - -public class MainActivity extends Activity { - - @Override - protected void onCreate(Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - - setContentView(R.layout.activity_main); - - final TextView textView = findViewById(R.id.text_view); - - final Markwon markwon = Markwon.builder(this) - .usePlugin(IconPlugin.create(IconSpanProvider.create(this, 0))) - .usePlugin(new AbstractMarkwonPlugin() { - @Override - public void configureTheme(@NonNull MarkwonTheme.Builder builder) { - // this part has nothing to do with actual IconPlugin - // this part is used to showcase that headers can be controlled via Theme - final float[] textSizeMultipliers = new float[]{3f, 2f, 1.5f, 1f, .5f, .25f}; - builder - .headingTypeface(Typeface.MONOSPACE) - .headingTextSizeMultipliers(textSizeMultipliers); - } - }) - .build(); - - markwon.setMarkdown(textView, getString(R.string.input)); - } -} diff --git a/sample-custom-extension/src/main/java/ru/noties/markwon/sample/extension/recycler/MarkwonRecyclerActivity.java b/sample-custom-extension/src/main/java/ru/noties/markwon/sample/extension/recycler/MarkwonRecyclerActivity.java deleted file mode 100644 index eb4dc039..00000000 --- a/sample-custom-extension/src/main/java/ru/noties/markwon/sample/extension/recycler/MarkwonRecyclerActivity.java +++ /dev/null @@ -1,144 +0,0 @@ -package ru.noties.markwon.sample.extension.recycler; - -import android.app.Activity; -import android.content.Context; -import android.net.Uri; -import android.os.Bundle; -import android.support.annotation.NonNull; -import android.support.annotation.Nullable; -import android.support.v7.widget.LinearLayoutManager; -import android.support.v7.widget.RecyclerView; -import android.text.TextUtils; - -import org.commonmark.ext.gfm.tables.TableBlock; -import org.commonmark.node.FencedCodeBlock; - -import java.io.BufferedReader; -import java.io.IOException; -import java.io.InputStream; -import java.io.InputStreamReader; - -import ru.noties.debug.AndroidLogDebugOutput; -import ru.noties.debug.Debug; -import ru.noties.markwon.AbstractMarkwonPlugin; -import ru.noties.markwon.Markwon; -import ru.noties.markwon.MarkwonConfiguration; -import ru.noties.markwon.core.CorePlugin; -import ru.noties.markwon.ext.tables.TablePlugin; -import ru.noties.markwon.html.HtmlPlugin; -import ru.noties.markwon.image.ImagesPlugin; -import ru.noties.markwon.image.svg.SvgPlugin; -import ru.noties.markwon.recycler.MarkwonAdapter; -import ru.noties.markwon.recycler.SimpleEntry; -import ru.noties.markwon.sample.extension.R; -import ru.noties.markwon.urlprocessor.UrlProcessor; -import ru.noties.markwon.urlprocessor.UrlProcessorRelativeToAbsolute; - -// we will create a standalone `sample` module with all these samples, right now this -// module has unrelated things -public class MarkwonRecyclerActivity extends Activity { - - static { - Debug.init(new AndroidLogDebugOutput(true)); - } - - @Override - public void onCreate(@Nullable Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - setContentView(R.layout.activity_recycler); - - final MarkwonAdapter adapter = MarkwonAdapter.builder() - .include(FencedCodeBlock.class, new SimpleEntry(R.layout.adapter_fenced_code_block)) - .include(TableBlock.class, new TableEntry()) - .defaultEntry(new SimpleEntry(R.layout.adapter_default_entry)) - .build(); - - final RecyclerView recyclerView = findViewById(R.id.recycler_view); - recyclerView.setLayoutManager(new LinearLayoutManager(this)); - recyclerView.setHasFixedSize(true); - recyclerView.setAdapter(adapter); - - final Markwon markwon = markwon(this); - adapter.setMarkdown(markwon, loadReadMe(this)); - adapter.notifyDataSetChanged(); - } - - @NonNull - private static Markwon markwon(@NonNull Context context) { - return Markwon.builder(context) - .usePlugin(CorePlugin.create()) - .usePlugin(ImagesPlugin.createWithAssets(context)) - .usePlugin(SvgPlugin.create(context.getResources())) - .usePlugin(TablePlugin.create(context)) - .usePlugin(HtmlPlugin.create()) - .usePlugin(new AbstractMarkwonPlugin() { - @Override - public void configureConfiguration(@NonNull MarkwonConfiguration.Builder builder) { - builder.urlProcessor(new UrlProcessorInitialReadme()); - } - }) - .build(); - } - - private static String loadReadMe(@NonNull Context context) { - InputStream stream = null; - try { - stream = context.getAssets().open("README.md"); - } catch (IOException e) { - e.printStackTrace(); - } - return readStream(stream); - } - - private static String readStream(@Nullable InputStream inputStream) { - - String out = null; - - if (inputStream != null) { - BufferedReader reader = null; - try { - reader = new BufferedReader(new InputStreamReader(inputStream)); - final StringBuilder builder = new StringBuilder(); - String line; - while ((line = reader.readLine()) != null) { - builder.append(line) - .append('\n'); - } - out = builder.toString(); - } catch (IOException e) { - e.printStackTrace(); - } finally { - if (reader != null) { - try { - reader.close(); - } catch (IOException e) { - // no op - } - } - } - } - - return out; - } - - private static class UrlProcessorInitialReadme implements UrlProcessor { - - private static final String GITHUB_BASE = "https://github.com/noties/Markwon/raw/master/"; - - private final UrlProcessorRelativeToAbsolute processor - = new UrlProcessorRelativeToAbsolute(GITHUB_BASE); - - @NonNull - @Override - public String process(@NonNull String destination) { - String out; - final Uri uri = Uri.parse(destination); - if (TextUtils.isEmpty(uri.getScheme())) { - out = processor.process(destination); - } else { - out = destination; - } - return out; - } - } -} diff --git a/sample-custom-extension/src/main/java/ru/noties/markwon/sample/extension/recycler/TableEntry.java b/sample-custom-extension/src/main/java/ru/noties/markwon/sample/extension/recycler/TableEntry.java deleted file mode 100644 index aa3d79ff..00000000 --- a/sample-custom-extension/src/main/java/ru/noties/markwon/sample/extension/recycler/TableEntry.java +++ /dev/null @@ -1,64 +0,0 @@ -package ru.noties.markwon.sample.extension.recycler; - -import android.support.annotation.NonNull; -import android.view.LayoutInflater; -import android.view.View; -import android.view.ViewGroup; - -import org.commonmark.ext.gfm.tables.TableBlock; - -import java.util.HashMap; -import java.util.Map; - -import ru.noties.markwon.Markwon; -import ru.noties.markwon.ext.tables.Table; -import ru.noties.markwon.recycler.MarkwonAdapter; -import ru.noties.markwon.sample.extension.R; - -// do not use in real applications, this is just a showcase -public class TableEntry implements MarkwonAdapter.Entry { - - private final Map cache = new HashMap<>(2); - - @NonNull - @Override - public TableNodeHolder createHolder(@NonNull LayoutInflater inflater, @NonNull ViewGroup parent) { - return new TableNodeHolder(inflater.inflate(R.layout.adapter_table_block, parent, false)); - } - - @Override - public void bindHolder(@NonNull Markwon markwon, @NonNull TableNodeHolder holder, @NonNull TableBlock node) { - - Table table = cache.get(node); - if (table == null) { - table = Table.parse(markwon, node); - cache.put(node, table); - } - - if (table != null) { - holder.tableEntryView.setTable(table); - // render table - } // we need to do something with null table... - } - - @Override - public long id(@NonNull TableBlock node) { - return node.hashCode(); - } - - @Override - public void clear() { - cache.clear(); - } - - static class TableNodeHolder extends MarkwonAdapter.Holder { - - final TableEntryView tableEntryView; - - TableNodeHolder(@NonNull View itemView) { - super(itemView); - - this.tableEntryView = requireView(R.id.table_entry); - } - } -} diff --git a/sample-custom-extension/src/main/java/ru/noties/markwon/sample/extension/recycler/TableEntryView.java b/sample-custom-extension/src/main/java/ru/noties/markwon/sample/extension/recycler/TableEntryView.java deleted file mode 100644 index 0bf133ee..00000000 --- a/sample-custom-extension/src/main/java/ru/noties/markwon/sample/extension/recycler/TableEntryView.java +++ /dev/null @@ -1,205 +0,0 @@ -package ru.noties.markwon.sample.extension.recycler; - -import android.content.Context; -import android.content.res.TypedArray; -import android.graphics.Canvas; -import android.graphics.Color; -import android.graphics.Paint; -import android.graphics.Rect; -import android.support.annotation.NonNull; -import android.support.annotation.Nullable; -import android.text.SpannedString; -import android.util.AttributeSet; -import android.view.LayoutInflater; -import android.view.View; -import android.view.ViewGroup; -import android.widget.LinearLayout; -import android.widget.TextView; - -import java.util.ArrayList; -import java.util.List; - -import ru.noties.markwon.ext.tables.Table; -import ru.noties.markwon.sample.extension.R; - -public class TableEntryView extends LinearLayout { - - // paint and rect to draw borders - private final Paint paint = new Paint(Paint.ANTI_ALIAS_FLAG); - private final Rect rect = new Rect(); - - private LayoutInflater inflater; - - private int rowEvenBackgroundColor; - - public TableEntryView(Context context) { - super(context); - init(context, null); - } - - public TableEntryView(Context context, AttributeSet attrs) { - super(context, attrs); - init(context, attrs); - } - - private void init(Context context, @Nullable AttributeSet attrs) { - inflater = LayoutInflater.from(context); - setOrientation(VERTICAL); - - if (attrs != null) { - final TypedArray array = context.obtainStyledAttributes(attrs, R.styleable.TableEntryView); - try { - - rowEvenBackgroundColor = array.getColor(R.styleable.TableEntryView_tev_rowEvenBackgroundColor, 0); - - - final int stroke = array.getDimensionPixelSize(R.styleable.TableEntryView_tev_borderWidth, 0); - - // half of requested - final float strokeWidth = stroke > 0 - ? stroke / 2.F - : context.getResources().getDisplayMetrics().density / 2.F; - - paint.setStyle(Paint.Style.STROKE); - paint.setStrokeWidth(strokeWidth); - paint.setColor(array.getColor(R.styleable.TableEntryView_tev_borderColor, Color.BLACK)); - - if (isInEditMode()) { - final String data = array.getString(R.styleable.TableEntryView_tev_debugData); - if (data != null) { - - boolean first = true; - - final List rows = new ArrayList<>(); - for (String row : data.split("\\|")) { - final List columns = new ArrayList<>(); - for (String column : row.split(",")) { - columns.add(new Table.Column(Table.Alignment.LEFT, new SpannedString(column))); - } - final boolean header = first; - first = false; - rows.add(new Table.Row(header, columns)); - } - final Table table = new Table(rows); - setTable(table); - } - } - } finally { - array.recycle(); - } - } - - setWillNotDraw(false); - } - - public void setTable(@NonNull Table table) { - final List rows = table.rows(); - for (int i = 0, size = rows.size(); i < size; i++) { - addRow(i, rows.get(i)); - } - } - - private void addRow(int index, @NonNull Table.Row row) { - - final ViewGroup group = ensureRow(index); - - final int backgroundColor = !row.header() && (index % 2) == 0 - ? rowEvenBackgroundColor - : 0; - group.setBackgroundColor(backgroundColor); - - final List columns = row.columns(); - - TextView textView; - Table.Column column; - - for (int i = 0, size = columns.size(); i < size; i++) { - textView = ensureCell(group, i); - column = columns.get(i); - textView.setTextAlignment(textAlignment(column.alignment())); - textView.setText(column.content()); - textView.getPaint().setFakeBoldText(row.header()); - } - } - - @NonNull - private ViewGroup ensureRow(int index) { - - final int count = getChildCount(); - if (index >= count) { - - // count=0,index=1, diff=2 - // count=0,index=5, diff=6 - // count=1,index=2, diff=2 - int diff = index - count + 1; - while (diff > 0) { - addView(inflater.inflate(R.layout.view_table_entry_row, this, false)); - diff -= 1; - } - } - - return (ViewGroup) getChildAt(index); - } - - @NonNull - private TextView ensureCell(@NonNull ViewGroup group, int index) { - - final int count = group.getChildCount(); - if (index >= count) { - int diff = index - count + 1; - while (diff > 0) { - group.addView(inflater.inflate(R.layout.view_table_entry_cell, group, false)); - diff -= 1; - } - } - - return (TextView) group.getChildAt(index); - } - - @Override - protected void onDraw(Canvas canvas) { - super.onDraw(canvas); - - final int rows = getChildCount(); - if (rows == 0) { - return; - } - - // first draw the whole border - rect.set(0, 0, getWidth(), getHeight()); - canvas.drawRect(rect, paint); - - ViewGroup group; - View view; - - int top; - - for (int row = 0; row < rows; row++) { - group = (ViewGroup) getChildAt(row); - top = group.getTop(); - for (int col = 0, cols = group.getChildCount(); col < cols; col++) { - view = group.getChildAt(col); - rect.set(view.getLeft(), top + view.getTop(), view.getRight(), top + view.getBottom()); - canvas.drawRect(rect, paint); - } - } - } - - private static int textAlignment(@NonNull Table.Alignment alignment) { - final int out; - switch (alignment) { - case LEFT: - out = TextView.TEXT_ALIGNMENT_TEXT_START; - break; - case CENTER: - out = TextView.TEXT_ALIGNMENT_CENTER; - break; - case RIGHT: - out = TextView.TEXT_ALIGNMENT_TEXT_END; - break; - default: - throw new IllegalStateException("Unexpected alignment: " + alignment); - } - return out; - } -} diff --git a/sample-custom-extension/src/main/res/layout/activity_recycler.xml b/sample-custom-extension/src/main/res/layout/activity_recycler.xml deleted file mode 100644 index 1405e07c..00000000 --- a/sample-custom-extension/src/main/res/layout/activity_recycler.xml +++ /dev/null @@ -1,5 +0,0 @@ - - \ No newline at end of file diff --git a/sample-custom-extension/src/main/res/layout/adapter_default_entry.xml b/sample-custom-extension/src/main/res/layout/adapter_default_entry.xml deleted file mode 100644 index 1d302e96..00000000 --- a/sample-custom-extension/src/main/res/layout/adapter_default_entry.xml +++ /dev/null @@ -1,13 +0,0 @@ - - \ No newline at end of file diff --git a/sample-custom-extension/src/main/res/layout/adapter_fenced_code_block.xml b/sample-custom-extension/src/main/res/layout/adapter_fenced_code_block.xml deleted file mode 100644 index ddc2c802..00000000 --- a/sample-custom-extension/src/main/res/layout/adapter_fenced_code_block.xml +++ /dev/null @@ -1,24 +0,0 @@ - - - - - - \ No newline at end of file diff --git a/sample-custom-extension/src/main/res/layout/adapter_table_block.xml b/sample-custom-extension/src/main/res/layout/adapter_table_block.xml deleted file mode 100644 index 287a6b9a..00000000 --- a/sample-custom-extension/src/main/res/layout/adapter_table_block.xml +++ /dev/null @@ -1,23 +0,0 @@ - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/sample-custom-extension/src/main/res/layout/view_table_entry_cell.xml b/sample-custom-extension/src/main/res/layout/view_table_entry_cell.xml deleted file mode 100644 index 88654d3d..00000000 --- a/sample-custom-extension/src/main/res/layout/view_table_entry_cell.xml +++ /dev/null @@ -1,11 +0,0 @@ - - \ No newline at end of file diff --git a/sample-custom-extension/src/main/res/layout/view_table_entry_row.xml b/sample-custom-extension/src/main/res/layout/view_table_entry_row.xml deleted file mode 100644 index 24e7fb9e..00000000 --- a/sample-custom-extension/src/main/res/layout/view_table_entry_row.xml +++ /dev/null @@ -1,5 +0,0 @@ - - \ No newline at end of file diff --git a/sample-custom-extension/src/main/res/values/attrs.xml b/sample-custom-extension/src/main/res/values/attrs.xml deleted file mode 100644 index 5ee8b42b..00000000 --- a/sample-custom-extension/src/main/res/values/attrs.xml +++ /dev/null @@ -1,10 +0,0 @@ - - - - - - - - - - \ No newline at end of file diff --git a/sample-custom-extension/src/main/res/values/strings.xml b/sample-custom-extension/src/main/res/values/strings.xml deleted file mode 100644 index fdc01039..00000000 --- a/sample-custom-extension/src/main/res/values/strings.xml +++ /dev/null @@ -1,14 +0,0 @@ - - - Markwon-SampleCustomExtension - - - - - diff --git a/sample-custom-extension/src/main/res/values/styles.xml b/sample-custom-extension/src/main/res/values/styles.xml deleted file mode 100644 index 49c8cb25..00000000 --- a/sample-custom-extension/src/main/res/values/styles.xml +++ /dev/null @@ -1,5 +0,0 @@ - - - - - diff --git a/sample/build.gradle b/sample/build.gradle index 2ee3691a..35db7389 100644 --- a/sample/build.gradle +++ b/sample/build.gradle @@ -42,6 +42,7 @@ android { dependencies { implementation project(':markwon-core') + implementation project(':markwon-ext-latex') implementation project(':markwon-ext-strikethrough') implementation project(':markwon-ext-tables') implementation project(':markwon-ext-tasklist') diff --git a/sample/src/main/AndroidManifest.xml b/sample/src/main/AndroidManifest.xml index cbccca0d..732928be 100644 --- a/sample/src/main/AndroidManifest.xml +++ b/sample/src/main/AndroidManifest.xml @@ -25,6 +25,7 @@ + diff --git a/sample/src/main/java/ru/noties/markwon/sample/customextension/CustomExtensionActivity.java b/sample/src/main/java/ru/noties/markwon/sample/customextension/CustomExtensionActivity.java index 1b35c0c7..4a9c2fd9 100644 --- a/sample/src/main/java/ru/noties/markwon/sample/customextension/CustomExtensionActivity.java +++ b/sample/src/main/java/ru/noties/markwon/sample/customextension/CustomExtensionActivity.java @@ -1,6 +1,36 @@ package ru.noties.markwon.sample.customextension; import android.app.Activity; +import android.os.Bundle; +import android.support.annotation.Nullable; +import android.widget.TextView; + +import ru.noties.markwon.Markwon; +import ru.noties.markwon.image.ImagesPlugin; +import ru.noties.markwon.sample.R; public class CustomExtensionActivity extends Activity { + + // please note that this sample won't work on a device with SDK level < 21 + // as we are using vector drawables for the sake of brevity. Other than resources + // used, this is fully functional sample on all SDK levels + @Override + public void onCreate(@Nullable Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + + setContentView(R.layout.activity_text_view); + + final TextView textView = findViewById(R.id.text_view); + + // note that we haven't registered CorePlugin, as it's the only one that can be + // implicitly deducted and added automatically. All other plugins require explicit + // `usePlugin` call + final Markwon markwon = Markwon.builder(this) + // try commenting out this line to see runtime dependency resolution + .usePlugin(ImagesPlugin.create(this)) + .usePlugin(IconPlugin.create(IconSpanProvider.create(this, 0))) + .build(); + + markwon.setMarkdown(textView, getString(R.string.input)); + } } diff --git a/sample-custom-extension/src/main/java/ru/noties/markwon/sample/extension/IconGroupNode.java b/sample/src/main/java/ru/noties/markwon/sample/customextension/IconGroupNode.java similarity index 71% rename from sample-custom-extension/src/main/java/ru/noties/markwon/sample/extension/IconGroupNode.java rename to sample/src/main/java/ru/noties/markwon/sample/customextension/IconGroupNode.java index 193b33b8..98560b49 100644 --- a/sample-custom-extension/src/main/java/ru/noties/markwon/sample/extension/IconGroupNode.java +++ b/sample/src/main/java/ru/noties/markwon/sample/customextension/IconGroupNode.java @@ -1,4 +1,4 @@ -package ru.noties.markwon.sample.extension; +package ru.noties.markwon.sample.customextension; import org.commonmark.node.CustomNode; diff --git a/sample-custom-extension/src/main/java/ru/noties/markwon/sample/extension/IconNode.java b/sample/src/main/java/ru/noties/markwon/sample/customextension/IconNode.java similarity index 96% rename from sample-custom-extension/src/main/java/ru/noties/markwon/sample/extension/IconNode.java rename to sample/src/main/java/ru/noties/markwon/sample/customextension/IconNode.java index 3d40ec2f..b297e4a6 100644 --- a/sample-custom-extension/src/main/java/ru/noties/markwon/sample/extension/IconNode.java +++ b/sample/src/main/java/ru/noties/markwon/sample/customextension/IconNode.java @@ -1,4 +1,4 @@ -package ru.noties.markwon.sample.extension; +package ru.noties.markwon.sample.customextension; import android.support.annotation.NonNull; diff --git a/sample-custom-extension/src/main/java/ru/noties/markwon/sample/extension/IconPlugin.java b/sample/src/main/java/ru/noties/markwon/sample/customextension/IconPlugin.java similarity index 85% rename from sample-custom-extension/src/main/java/ru/noties/markwon/sample/extension/IconPlugin.java rename to sample/src/main/java/ru/noties/markwon/sample/customextension/IconPlugin.java index eb821000..3de96315 100644 --- a/sample-custom-extension/src/main/java/ru/noties/markwon/sample/extension/IconPlugin.java +++ b/sample/src/main/java/ru/noties/markwon/sample/customextension/IconPlugin.java @@ -1,4 +1,4 @@ -package ru.noties.markwon.sample.extension; +package ru.noties.markwon.sample.customextension; import android.support.annotation.NonNull; import android.text.TextUtils; @@ -7,6 +7,8 @@ import org.commonmark.parser.Parser; import ru.noties.markwon.AbstractMarkwonPlugin; import ru.noties.markwon.MarkwonVisitor; +import ru.noties.markwon.image.ImagesPlugin; +import ru.noties.markwon.priority.Priority; public class IconPlugin extends AbstractMarkwonPlugin { @@ -21,6 +23,13 @@ public class IconPlugin extends AbstractMarkwonPlugin { this.iconSpanProvider = iconSpanProvider; } + @NonNull + @Override + public Priority priority() { + // define images dependency + return Priority.after(ImagesPlugin.class); + } + @Override public void configureParser(@NonNull Parser.Builder builder) { builder.customDelimiterProcessor(IconProcessor.create()); diff --git a/sample-custom-extension/src/main/java/ru/noties/markwon/sample/extension/IconProcessor.java b/sample/src/main/java/ru/noties/markwon/sample/customextension/IconProcessor.java similarity index 98% rename from sample-custom-extension/src/main/java/ru/noties/markwon/sample/extension/IconProcessor.java rename to sample/src/main/java/ru/noties/markwon/sample/customextension/IconProcessor.java index 500726e0..eb7a79be 100644 --- a/sample-custom-extension/src/main/java/ru/noties/markwon/sample/extension/IconProcessor.java +++ b/sample/src/main/java/ru/noties/markwon/sample/customextension/IconProcessor.java @@ -1,4 +1,4 @@ -package ru.noties.markwon.sample.extension; +package ru.noties.markwon.sample.customextension; import android.support.annotation.NonNull; import android.text.TextUtils; diff --git a/sample-custom-extension/src/main/java/ru/noties/markwon/sample/extension/IconSpan.java b/sample/src/main/java/ru/noties/markwon/sample/customextension/IconSpan.java similarity index 97% rename from sample-custom-extension/src/main/java/ru/noties/markwon/sample/extension/IconSpan.java rename to sample/src/main/java/ru/noties/markwon/sample/customextension/IconSpan.java index 690ba5b2..9d2b8b43 100644 --- a/sample-custom-extension/src/main/java/ru/noties/markwon/sample/extension/IconSpan.java +++ b/sample/src/main/java/ru/noties/markwon/sample/customextension/IconSpan.java @@ -1,4 +1,4 @@ -package ru.noties.markwon.sample.extension; +package ru.noties.markwon.sample.customextension; import android.graphics.Canvas; import android.graphics.Paint; diff --git a/sample-custom-extension/src/main/java/ru/noties/markwon/sample/extension/IconSpanProvider.java b/sample/src/main/java/ru/noties/markwon/sample/customextension/IconSpanProvider.java similarity index 97% rename from sample-custom-extension/src/main/java/ru/noties/markwon/sample/extension/IconSpanProvider.java rename to sample/src/main/java/ru/noties/markwon/sample/customextension/IconSpanProvider.java index cd6e3bf3..25049bc2 100644 --- a/sample-custom-extension/src/main/java/ru/noties/markwon/sample/extension/IconSpanProvider.java +++ b/sample/src/main/java/ru/noties/markwon/sample/customextension/IconSpanProvider.java @@ -1,4 +1,4 @@ -package ru.noties.markwon.sample.extension; +package ru.noties.markwon.sample.customextension; import android.content.Context; import android.content.res.Resources; diff --git a/sample/src/main/java/ru/noties/markwon/sample/latex/LatexActivity.java b/sample/src/main/java/ru/noties/markwon/sample/latex/LatexActivity.java index 179113ea..6e7bd938 100644 --- a/sample/src/main/java/ru/noties/markwon/sample/latex/LatexActivity.java +++ b/sample/src/main/java/ru/noties/markwon/sample/latex/LatexActivity.java @@ -3,6 +3,13 @@ package ru.noties.markwon.sample.latex; import android.app.Activity; import android.os.Bundle; import android.support.annotation.Nullable; +import android.widget.TextView; + +import ru.noties.markwon.Markwon; +import ru.noties.markwon.core.CorePlugin; +import ru.noties.markwon.ext.latex.JLatexMathPlugin; +import ru.noties.markwon.image.ImagesPlugin; +import ru.noties.markwon.sample.R; public class LatexActivity extends Activity { @@ -10,6 +17,42 @@ public class LatexActivity extends Activity { public void onCreate(@Nullable Bundle savedInstanceState) { super.onCreate(savedInstanceState); + setContentView(R.layout.activity_text_view); + final TextView textView = findViewById(R.id.text_view); + + String latex = "\\begin{array}{l}"; + latex += "\\forall\\varepsilon\\in\\mathbb{R}_+^*\\ \\exists\\eta>0\\ |x-x_0|\\leq\\eta\\Longrightarrow|f(x)-f(x_0)|\\leq\\varepsilon\\\\"; + latex += "\\det\\begin{bmatrix}a_{11}&a_{12}&\\cdots&a_{1n}\\\\a_{21}&\\ddots&&\\vdots\\\\\\vdots&&\\ddots&\\vdots\\\\a_{n1}&\\cdots&\\cdots&a_{nn}\\end{bmatrix}\\overset{\\mathrm{def}}{=}\\sum_{\\sigma\\in\\mathfrak{S}_n}\\varepsilon(\\sigma)\\prod_{k=1}^n a_{k\\sigma(k)}\\\\"; + latex += "\\sideset{_\\alpha^\\beta}{_\\gamma^\\delta}{\\begin{pmatrix}a&b\\\\c&d\\end{pmatrix}}\\\\"; + latex += "\\int_0^\\infty{x^{2n} e^{-a x^2}\\,dx} = \\frac{2n-1}{2a} \\int_0^\\infty{x^{2(n-1)} e^{-a x^2}\\,dx} = \\frac{(2n-1)!!}{2^{n+1}} \\sqrt{\\frac{\\pi}{a^{2n+1}}}\\\\"; + latex += "\\int_a^b{f(x)\\,dx} = (b - a) \\sum\\limits_{n = 1}^\\infty {\\sum\\limits_{m = 1}^{2^n - 1} {\\left( { - 1} \\right)^{m + 1} } } 2^{ - n} f(a + m\\left( {b - a} \\right)2^{-n} )\\\\"; + latex += "\\int_{-\\pi}^{\\pi} \\sin(\\alpha x) \\sin^n(\\beta x) dx = \\textstyle{\\left \\{ \\begin{array}{cc} (-1)^{(n+1)/2} (-1)^m \\frac{2 \\pi}{2^n} \\binom{n}{m} & n \\mbox{ odd},\\ \\alpha = \\beta (2m-n) \\\\ 0 & \\mbox{otherwise} \\\\ \\end{array} \\right .}\\\\"; + latex += "L = \\int_a^b \\sqrt{ \\left|\\sum_{i,j=1}^ng_{ij}(\\gamma(t))\\left(\\frac{d}{dt}x^i\\circ\\gamma(t)\\right)\\left(\\frac{d}{dt}x^j\\circ\\gamma(t)\\right)\\right|}\\,dt\\\\"; + latex += "\\begin{array}{rl} s &= \\int_a^b\\left\\|\\frac{d}{dt}\\vec{r}\\,(u(t),v(t))\\right\\|\\,dt \\\\ &= \\int_a^b \\sqrt{u'(t)^2\\,\\vec{r}_u\\cdot\\vec{r}_u + 2u'(t)v'(t)\\, \\vec{r}_u\\cdot\\vec{r}_v+ v'(t)^2\\,\\vec{r}_v\\cdot\\vec{r}_v}\\,\\,\\, dt. \\end{array}\\\\"; + latex += "\\end{array}"; + +// String latex = "\\text{A long division \\longdiv{12345}{13}"; +// String latex = "{a \\bangle b} {c \\brace d} {e \\brack f} {g \\choose h}"; + +// String latex = "\\begin{array}{cc}"; +// latex += "\\fbox{\\text{A framed box with \\textdbend}}&\\shadowbox{\\text{A shadowed box}}\\cr"; +// latex += "\\doublebox{\\text{A double framed box}}&\\ovalbox{\\text{An oval framed box}}\\cr"; +// latex += "\\end{array}"; + + + final JLatexMathPlugin.Config config = new JLatexMathPlugin.Config(textView.getTextSize()) {{ +// align = JLatexMathDrawable.ALIGN_RIGHT; + }}; + + final String markdown = "# Example of LaTeX\n\n$$" + + latex + "$$\n\n something like **this**"; + + final Markwon markwon = Markwon.builder(this) + .usePlugin(ImagesPlugin.create(this)) + .usePlugin(JLatexMathPlugin.create(config)) + .build(); + + markwon.setMarkdown(textView, markdown); } } diff --git a/sample/src/main/java/ru/noties/markwon/sample/recycler/RecyclerActivity.java b/sample/src/main/java/ru/noties/markwon/sample/recycler/RecyclerActivity.java index 53232393..47afb7bd 100644 --- a/sample/src/main/java/ru/noties/markwon/sample/recycler/RecyclerActivity.java +++ b/sample/src/main/java/ru/noties/markwon/sample/recycler/RecyclerActivity.java @@ -31,6 +31,7 @@ import ru.noties.markwon.image.svg.SvgPlugin; import ru.noties.markwon.recycler.MarkwonAdapter; import ru.noties.markwon.recycler.SimpleEntry; import ru.noties.markwon.sample.R; +import ru.noties.markwon.syntax.SyntaxHighlightPlugin; import ru.noties.markwon.urlprocessor.UrlProcessor; import ru.noties.markwon.urlprocessor.UrlProcessorRelativeToAbsolute; diff --git a/sample/src/main/java/ru/noties/markwon/sample/theme/ThemeActivity.java b/sample/src/main/java/ru/noties/markwon/sample/theme/ThemeActivity.java new file mode 100644 index 00000000..33ee500a --- /dev/null +++ b/sample/src/main/java/ru/noties/markwon/sample/theme/ThemeActivity.java @@ -0,0 +1,17 @@ +package ru.noties.markwon.sample.theme; + +import android.app.Activity; +import android.os.Bundle; +import android.support.annotation.Nullable; + +import ru.noties.markwon.sample.R; + +public class ThemeActivity extends Activity { + + @Override + public void onCreate(@Nullable Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + + setContentView(R.layout.activity_text_view); + } +} diff --git a/sample-custom-extension/src/main/res/drawable/ic_android_black_24dp.xml b/sample/src/main/res/drawable/ic_android_black_24dp.xml similarity index 100% rename from sample-custom-extension/src/main/res/drawable/ic_android_black_24dp.xml rename to sample/src/main/res/drawable/ic_android_black_24dp.xml diff --git a/sample-custom-extension/src/main/res/drawable/ic_home_black_36dp.xml b/sample/src/main/res/drawable/ic_home_black_36dp.xml similarity index 100% rename from sample-custom-extension/src/main/res/drawable/ic_home_black_36dp.xml rename to sample/src/main/res/drawable/ic_home_black_36dp.xml diff --git a/sample-custom-extension/src/main/res/drawable/ic_memory_black_48dp.xml b/sample/src/main/res/drawable/ic_memory_black_48dp.xml similarity index 100% rename from sample-custom-extension/src/main/res/drawable/ic_memory_black_48dp.xml rename to sample/src/main/res/drawable/ic_memory_black_48dp.xml diff --git a/sample-custom-extension/src/main/res/drawable/ic_sentiment_satisfied_red_64dp.xml b/sample/src/main/res/drawable/ic_sentiment_satisfied_red_64dp.xml similarity index 100% rename from sample-custom-extension/src/main/res/drawable/ic_sentiment_satisfied_red_64dp.xml rename to sample/src/main/res/drawable/ic_sentiment_satisfied_red_64dp.xml diff --git a/sample-custom-extension/src/main/res/layout/activity_main.xml b/sample/src/main/res/layout/activity_text_view.xml similarity index 71% rename from sample-custom-extension/src/main/res/layout/activity_main.xml rename to sample/src/main/res/layout/activity_text_view.xml index e4f2a936..3eaf3c62 100644 --- a/sample-custom-extension/src/main/res/layout/activity_main.xml +++ b/sample/src/main/res/layout/activity_text_view.xml @@ -1,6 +1,4 @@ - - @@ -12,6 +10,6 @@ android:padding="8dip" android:textAppearance="?android:attr/textAppearanceMedium" android:textSize="15sp" - tools:text="@string/input"/> + tools:text="whatever" /> - + \ No newline at end of file diff --git a/sample/src/main/res/values/strings.xml b/sample/src/main/res/values/strings.xml index 0081dfc7..d1ec3ddc 100644 --- a/sample/src/main/res/values/strings.xml +++ b/sample/src/main/res/values/strings.xml @@ -1,3 +1,14 @@ + MarkwonSample + + + + diff --git a/settings.gradle b/settings.gradle index 8bc72bd1..e3177a74 100644 --- a/settings.gradle +++ b/settings.gradle @@ -11,6 +11,4 @@ include ':app', ':sample', ':markwon-image-svg', ':markwon-recycler', ':markwon-syntax-highlight', - ':markwon-test-span', - ':sample-custom-extension', - ':sample-latex-math' + ':markwon-test-span' From 705dec0571d4ebc17efa4726c41631715ca59fa4 Mon Sep 17 00:00:00 2001 From: Dimitry Ivanov Date: Sat, 12 Jan 2019 16:13:15 +0300 Subject: [PATCH 074/103] Markwon.hasPlugin method --- .../main/java/ru/noties/markwon/Markwon.java | 13 +++++++++ .../java/ru/noties/markwon/MarkwonImpl.java | 12 ++++++++ .../ru/noties/markwon/MarkwonImplTest.java | 28 +++++++++++++++++++ 3 files changed, 53 insertions(+) diff --git a/markwon-core/src/main/java/ru/noties/markwon/Markwon.java b/markwon-core/src/main/java/ru/noties/markwon/Markwon.java index bf963995..6e7e1ba3 100644 --- a/markwon-core/src/main/java/ru/noties/markwon/Markwon.java +++ b/markwon-core/src/main/java/ru/noties/markwon/Markwon.java @@ -88,6 +88,19 @@ public abstract class Markwon { public abstract void setParsedMarkdown(@NonNull TextView textView, @NonNull Spanned markdown); + /** + * Requests information if certain plugin has been registered. Please note that this + * method will check for super classes also, so if supplied with {@code markwon.hasPlugin(MarkwonPlugin.class)} + * this method (if has at least one plugin) will return true. If for example a custom + * (subclassed) version of a {@link CorePlugin} has been registered and given name + * {@code CorePlugin2}, then both {@code markwon.hasPlugin(CorePlugin2.class)} and + * {@code markwon.hasPlugin(CorePlugin.class)} will return true. + * + * @param plugin type to query + * @return true if a plugin is used when configuring this {@link Markwon} instance + */ + public abstract boolean hasPlugin(@NonNull Class plugin); + /** * Builder for {@link Markwon}. *

diff --git a/markwon-core/src/main/java/ru/noties/markwon/MarkwonImpl.java b/markwon-core/src/main/java/ru/noties/markwon/MarkwonImpl.java index 3cf105b5..8d5211d8 100644 --- a/markwon-core/src/main/java/ru/noties/markwon/MarkwonImpl.java +++ b/markwon-core/src/main/java/ru/noties/markwon/MarkwonImpl.java @@ -95,4 +95,16 @@ class MarkwonImpl extends Markwon { plugin.afterSetText(textView); } } + + @Override + public boolean hasPlugin(@NonNull Class type) { + boolean result = false; + for (MarkwonPlugin plugin : plugins) { + if (type.isAssignableFrom(plugin.getClass())) { + result = true; + break; + } + } + return result; + } } diff --git a/markwon-core/src/test/java/ru/noties/markwon/MarkwonImplTest.java b/markwon-core/src/test/java/ru/noties/markwon/MarkwonImplTest.java index 62ab3de0..f0771efa 100644 --- a/markwon-core/src/test/java/ru/noties/markwon/MarkwonImplTest.java +++ b/markwon-core/src/test/java/ru/noties/markwon/MarkwonImplTest.java @@ -16,9 +16,11 @@ import org.robolectric.annotation.Config; import java.util.Arrays; import java.util.Collections; +import java.util.List; import java.util.concurrent.atomic.AtomicBoolean; import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertTrue; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyString; @@ -229,4 +231,30 @@ public class MarkwonImplTest { verify(plugin, times(1)).afterSetText(eq(textView)); } + + @Test + public void has_plugin() { + + final class First extends AbstractMarkwonPlugin { + } + + final class Second extends AbstractMarkwonPlugin { + } + + final List plugins = Collections.singletonList((MarkwonPlugin) new First()); + + final MarkwonImpl impl = new MarkwonImpl( + TextView.BufferType.SPANNABLE, + mock(Parser.class), + mock(MarkwonVisitor.class), + plugins); + + assertTrue("First", impl.hasPlugin(First.class)); + assertFalse("Second", impl.hasPlugin(Second.class)); + + // can use super types. So if we ask if CorePlugin is registered, + // but it was subclassed, we would still have true returned from this method + assertTrue("AbstractMarkwonPlugin", impl.hasPlugin(AbstractMarkwonPlugin.class)); + assertTrue("MarkwonPlugin", impl.hasPlugin(MarkwonPlugin.class)); + } } \ No newline at end of file From 82fe43a921f9101ca598497decb2fbf460cbf21c Mon Sep 17 00:00:00 2001 From: Dimitry Ivanov Date: Sat, 12 Jan 2019 16:32:51 +0300 Subject: [PATCH 075/103] TablePlugin another create factory method --- app/src/main/java/ru/noties/markwon/AppBarItem.java | 4 ++-- .../main/java/ru/noties/markwon/MainActivity.java | 8 +------- .../java/ru/noties/markwon/MarkdownRenderer.java | 1 - app/src/main/java/ru/noties/markwon/Views.java | 12 ------------ .../ru/noties/markwon/ext/tables/TablePlugin.java | 11 +++++++++++ .../ru/noties/markwon/recycler/MarkwonAdapter.java | 2 ++ .../noties/markwon/recycler/MarkwonAdapterImpl.java | 2 +- 7 files changed, 17 insertions(+), 23 deletions(-) diff --git a/app/src/main/java/ru/noties/markwon/AppBarItem.java b/app/src/main/java/ru/noties/markwon/AppBarItem.java index c4960a2c..bf83e658 100644 --- a/app/src/main/java/ru/noties/markwon/AppBarItem.java +++ b/app/src/main/java/ru/noties/markwon/AppBarItem.java @@ -23,8 +23,8 @@ abstract class AppBarItem { final TextView subtitle; Renderer(@NonNull View view, @NonNull View.OnClickListener themeChangeClicked) { - this.title = Views.findView(view, R.id.app_bar_title); - this.subtitle = Views.findView(view, R.id.app_bar_subtitle); + this.title = view.findViewById(R.id.app_bar_title); + this.subtitle = view.findViewById(R.id.app_bar_subtitle); view.findViewById(R.id.app_bar_theme_changer) .setOnClickListener(themeChangeClicked); } diff --git a/app/src/main/java/ru/noties/markwon/MainActivity.java b/app/src/main/java/ru/noties/markwon/MainActivity.java index b50fcbd5..d0a11d07 100644 --- a/app/src/main/java/ru/noties/markwon/MainActivity.java +++ b/app/src/main/java/ru/noties/markwon/MainActivity.java @@ -28,9 +28,6 @@ public class MainActivity extends Activity { @Inject UriProcessor uriProcessor; -// -// @Inject -// GifProcessor gifProcessor; @Override protected void onCreate(final Bundle savedInstanceState) { @@ -42,9 +39,6 @@ public class MainActivity extends Activity { themes.apply(this); - // how can we obtain MarkwonConfiguration after theme was applied? - // as we inject `themes` we won't be able to inject configuration, as it requires theme set - setContentView(R.layout.activity_main); // we process additionally github urls, as if url has in path `blob`, we won't receive @@ -60,7 +54,7 @@ public class MainActivity extends Activity { } }); - final TextView textView = Views.findView(this, R.id.text); + final TextView textView = findViewById(R.id.text); final View progress = findViewById(R.id.progress); appBarRenderer.render(appBarState()); diff --git a/app/src/main/java/ru/noties/markwon/MarkdownRenderer.java b/app/src/main/java/ru/noties/markwon/MarkdownRenderer.java index 265b9cd5..cf2ab04c 100644 --- a/app/src/main/java/ru/noties/markwon/MarkdownRenderer.java +++ b/app/src/main/java/ru/noties/markwon/MarkdownRenderer.java @@ -8,7 +8,6 @@ import android.support.annotation.NonNull; import android.support.annotation.Nullable; import android.text.Spanned; -import java.util.concurrent.Callable; import java.util.concurrent.ExecutorService; import java.util.concurrent.Future; diff --git a/app/src/main/java/ru/noties/markwon/Views.java b/app/src/main/java/ru/noties/markwon/Views.java index b9f692d0..3c172e4b 100644 --- a/app/src/main/java/ru/noties/markwon/Views.java +++ b/app/src/main/java/ru/noties/markwon/Views.java @@ -1,7 +1,5 @@ package ru.noties.markwon; -import android.app.Activity; -import android.support.annotation.IdRes; import android.support.annotation.IntDef; import android.support.annotation.NonNull; import android.view.View; @@ -13,16 +11,6 @@ public abstract class Views { @interface NotVisible { } - public static V findView(@NonNull View view, @IdRes int id) { - //noinspection unchecked - return (V) view.findViewById(id); - } - - public static V findView(@NonNull Activity activity, @IdRes int id) { - //noinspection unchecked - return (V) activity.findViewById(id); - } - public static void setVisible(@NonNull View view, boolean visible) { setVisible(view, visible, View.GONE); } diff --git a/markwon-ext-tables/src/main/java/ru/noties/markwon/ext/tables/TablePlugin.java b/markwon-ext-tables/src/main/java/ru/noties/markwon/ext/tables/TablePlugin.java index d6a5d26e..7a944665 100644 --- a/markwon-ext-tables/src/main/java/ru/noties/markwon/ext/tables/TablePlugin.java +++ b/markwon-ext-tables/src/main/java/ru/noties/markwon/ext/tables/TablePlugin.java @@ -23,6 +23,10 @@ import ru.noties.markwon.SpannableBuilder; public class TablePlugin extends AbstractMarkwonPlugin { + public interface ThemeConfigure { + void configureTheme(@NonNull TableTheme.Builder builder); + } + @NonNull public static TablePlugin create(@NonNull Context context) { return new TablePlugin(TableTheme.create(context)); @@ -33,6 +37,13 @@ public class TablePlugin extends AbstractMarkwonPlugin { return new TablePlugin(tableTheme); } + @NonNull + public static TablePlugin create(@NonNull ThemeConfigure themeConfigure) { + final TableTheme.Builder builder = new TableTheme.Builder(); + themeConfigure.configureTheme(builder); + return new TablePlugin(builder.build()); + } + private final TableVisitor visitor; @SuppressWarnings("WeakerAccess") diff --git a/markwon-recycler/src/main/java/ru/noties/markwon/recycler/MarkwonAdapter.java b/markwon-recycler/src/main/java/ru/noties/markwon/recycler/MarkwonAdapter.java index fc0d4cb9..e37c2a75 100644 --- a/markwon-recycler/src/main/java/ru/noties/markwon/recycler/MarkwonAdapter.java +++ b/markwon-recycler/src/main/java/ru/noties/markwon/recycler/MarkwonAdapter.java @@ -181,6 +181,8 @@ public abstract class MarkwonAdapter extends RecyclerView.Adapter nodes); + public abstract int getNodeViewType(@NonNull Class node); + @SuppressWarnings("WeakerAccess") public static class Holder extends RecyclerView.ViewHolder { diff --git a/markwon-recycler/src/main/java/ru/noties/markwon/recycler/MarkwonAdapterImpl.java b/markwon-recycler/src/main/java/ru/noties/markwon/recycler/MarkwonAdapterImpl.java index 5a8d2fa6..afd1aa71 100644 --- a/markwon-recycler/src/main/java/ru/noties/markwon/recycler/MarkwonAdapterImpl.java +++ b/markwon-recycler/src/main/java/ru/noties/markwon/recycler/MarkwonAdapterImpl.java @@ -111,7 +111,7 @@ class MarkwonAdapterImpl extends MarkwonAdapter { return entry.id(node); } - @SuppressWarnings("WeakerAccess") + @Override public int getNodeViewType(@NonNull Class node) { // if has registered -> then return it, else 0 final int hash = node.hashCode(); From ba5bb9bfc759ec12cc6db7145d5c65569bd3dc38 Mon Sep 17 00:00:00 2001 From: Dimitry Ivanov Date: Sat, 12 Jan 2019 16:50:37 +0300 Subject: [PATCH 076/103] Improve latex entension plugin module --- .../markwon/ext/latex/JLatexMathPlugin.java | 90 +++++++++++++++++-- .../markwon/ext/tables/TablePlugin.java | 3 + .../noties/markwon/ext/tables/TableTheme.java | 14 +-- .../markwon/sample/latex/LatexActivity.java | 8 +- 4 files changed, 95 insertions(+), 20 deletions(-) diff --git a/markwon-ext-latex/src/main/java/ru/noties/markwon/ext/latex/JLatexMathPlugin.java b/markwon-ext-latex/src/main/java/ru/noties/markwon/ext/latex/JLatexMathPlugin.java index 9bd80400..c4761d0c 100644 --- a/markwon-ext-latex/src/main/java/ru/noties/markwon/ext/latex/JLatexMathPlugin.java +++ b/markwon-ext-latex/src/main/java/ru/noties/markwon/ext/latex/JLatexMathPlugin.java @@ -4,6 +4,7 @@ import android.graphics.drawable.Drawable; import android.net.Uri; import android.support.annotation.NonNull; import android.support.annotation.Nullable; +import android.support.annotation.Px; import org.commonmark.node.Image; import org.commonmark.parser.Parser; @@ -26,28 +27,56 @@ import ru.noties.markwon.image.MediaDecoder; import ru.noties.markwon.image.SchemeHandler; import ru.noties.markwon.priority.Priority; +/** + * @since 3.0.0 + */ public class JLatexMathPlugin extends AbstractMarkwonPlugin { + public interface BuilderConfigure { + void configureBuilder(@NonNull Builder builder); + } + + @NonNull + public static JLatexMathPlugin create(float textSize) { + return new JLatexMathPlugin(builder(textSize).build()); + } + @NonNull public static JLatexMathPlugin create(@NonNull Config config) { return new JLatexMathPlugin(config); } + @NonNull + public static JLatexMathPlugin create(float textSize, @NonNull BuilderConfigure builderConfigure) { + final Builder builder = new Builder(textSize); + builderConfigure.configureBuilder(builder); + return new JLatexMathPlugin(builder.build()); + } + + @NonNull + public static JLatexMathPlugin.Builder builder(float textSize) { + return new Builder(textSize); + } + public static class Config { - protected final float textSize; + private final float textSize; - protected Drawable background; + private final Drawable background; @JLatexMathDrawable.Align - protected int align = JLatexMathDrawable.ALIGN_CENTER; + private final int align; - protected boolean fitCanvas = true; + private final boolean fitCanvas; - protected int padding; + private final int padding; - public Config(float textSize) { - this.textSize = textSize; + Config(@NonNull Builder builder) { + this.textSize = builder.textSize; + this.background = builder.background; + this.align = builder.align; + this.fitCanvas = builder.fitCanvas; + this.padding = builder.padding; } } @@ -144,4 +173,51 @@ public class JLatexMathPlugin extends AbstractMarkwonPlugin { public Priority priority() { return Priority.after(ImagesPlugin.class); } + + public static class Builder { + + private final float textSize; + + private Drawable background; + + @JLatexMathDrawable.Align + private int align = JLatexMathDrawable.ALIGN_CENTER; + + private boolean fitCanvas = true; + + private int padding; + + Builder(float textSize) { + this.textSize = textSize; + } + + @NonNull + public Builder background(@NonNull Drawable background) { + this.background = background; + return this; + } + + @NonNull + public Builder align(@JLatexMathDrawable.Align int align) { + this.align = align; + return this; + } + + @NonNull + public Builder fitCanvas(boolean fitCanvas) { + this.fitCanvas = fitCanvas; + return this; + } + + @NonNull + public Builder padding(@Px int padding) { + this.padding = padding; + return this; + } + + @NonNull + public Config build() { + return new Config(this); + } + } } diff --git a/markwon-ext-tables/src/main/java/ru/noties/markwon/ext/tables/TablePlugin.java b/markwon-ext-tables/src/main/java/ru/noties/markwon/ext/tables/TablePlugin.java index 7a944665..67faecb2 100644 --- a/markwon-ext-tables/src/main/java/ru/noties/markwon/ext/tables/TablePlugin.java +++ b/markwon-ext-tables/src/main/java/ru/noties/markwon/ext/tables/TablePlugin.java @@ -21,6 +21,9 @@ import ru.noties.markwon.AbstractMarkwonPlugin; import ru.noties.markwon.MarkwonVisitor; import ru.noties.markwon.SpannableBuilder; +/** + * @since 3.0.0 + */ public class TablePlugin extends AbstractMarkwonPlugin { public interface ThemeConfigure { diff --git a/markwon-ext-tables/src/main/java/ru/noties/markwon/ext/tables/TableTheme.java b/markwon-ext-tables/src/main/java/ru/noties/markwon/ext/tables/TableTheme.java index a868c7c7..6752e729 100644 --- a/markwon-ext-tables/src/main/java/ru/noties/markwon/ext/tables/TableTheme.java +++ b/markwon-ext-tables/src/main/java/ru/noties/markwon/ext/tables/TableTheme.java @@ -2,7 +2,9 @@ package ru.noties.markwon.ext.tables; import android.content.Context; import android.graphics.Paint; +import android.support.annotation.ColorInt; import android.support.annotation.NonNull; +import android.support.annotation.Px; import ru.noties.markwon.utils.ColorUtils; import ru.noties.markwon.utils.Dip; @@ -121,37 +123,37 @@ public class TableTheme { private int tableHeaderRowBackgroundColor; // @since 1.1.1 @NonNull - public Builder tableCellPadding(int tableCellPadding) { + public Builder tableCellPadding(@Px int tableCellPadding) { this.tableCellPadding = tableCellPadding; return this; } @NonNull - public Builder tableBorderColor(int tableBorderColor) { + public Builder tableBorderColor(@ColorInt int tableBorderColor) { this.tableBorderColor = tableBorderColor; return this; } @NonNull - public Builder tableBorderWidth(int tableBorderWidth) { + public Builder tableBorderWidth(@Px int tableBorderWidth) { this.tableBorderWidth = tableBorderWidth; return this; } @NonNull - public Builder tableOddRowBackgroundColor(int tableOddRowBackgroundColor) { + public Builder tableOddRowBackgroundColor(@ColorInt int tableOddRowBackgroundColor) { this.tableOddRowBackgroundColor = tableOddRowBackgroundColor; return this; } @NonNull - public Builder tableEvenRowBackgroundColor(int tableEvenRowBackgroundColor) { + public Builder tableEvenRowBackgroundColor(@ColorInt int tableEvenRowBackgroundColor) { this.tableEvenRowBackgroundColor = tableEvenRowBackgroundColor; return this; } @NonNull - public Builder tableHeaderRowBackgroundColor(int tableHeaderRowBackgroundColor) { + public Builder tableHeaderRowBackgroundColor(@ColorInt int tableHeaderRowBackgroundColor) { this.tableHeaderRowBackgroundColor = tableHeaderRowBackgroundColor; return this; } diff --git a/sample/src/main/java/ru/noties/markwon/sample/latex/LatexActivity.java b/sample/src/main/java/ru/noties/markwon/sample/latex/LatexActivity.java index 6e7bd938..d4143a76 100644 --- a/sample/src/main/java/ru/noties/markwon/sample/latex/LatexActivity.java +++ b/sample/src/main/java/ru/noties/markwon/sample/latex/LatexActivity.java @@ -6,7 +6,6 @@ import android.support.annotation.Nullable; import android.widget.TextView; import ru.noties.markwon.Markwon; -import ru.noties.markwon.core.CorePlugin; import ru.noties.markwon.ext.latex.JLatexMathPlugin; import ru.noties.markwon.image.ImagesPlugin; import ru.noties.markwon.sample.R; @@ -40,17 +39,12 @@ public class LatexActivity extends Activity { // latex += "\\doublebox{\\text{A double framed box}}&\\ovalbox{\\text{An oval framed box}}\\cr"; // latex += "\\end{array}"; - - final JLatexMathPlugin.Config config = new JLatexMathPlugin.Config(textView.getTextSize()) {{ -// align = JLatexMathDrawable.ALIGN_RIGHT; - }}; - final String markdown = "# Example of LaTeX\n\n$$" + latex + "$$\n\n something like **this**"; final Markwon markwon = Markwon.builder(this) .usePlugin(ImagesPlugin.create(this)) - .usePlugin(JLatexMathPlugin.create(config)) + .usePlugin(JLatexMathPlugin.create(textView.getTextSize())) .build(); markwon.setMarkdown(textView, markdown); From 02e7539881c146cd4a4cea6e7de8c8bfc288aca5 Mon Sep 17 00:00:00 2001 From: Dimitry Ivanov Date: Sun, 13 Jan 2019 16:57:16 +0300 Subject: [PATCH 077/103] Implicit LinkMovementMethod if not supplied explicitly --- .../java/ru/noties/markwon/MainActivity.java | 3 - docs/.vuepress/components/Link.vue | 35 +++--- docs/.vuepress/config.js | 2 +- docs/docs/core/getting-started.md | 110 ++++++++++-------- docs/docs/ext-latex/{latex.md => README.md} | 28 ++--- docs/docs/v2/getting-started.md | 5 - .../ru/noties/markwon/core/CorePlugin.java | 11 ++ markwon-ext-latex/README.md | 46 +------- .../main/res/layout/activity_text_view.xml | 3 +- 9 files changed, 112 insertions(+), 131 deletions(-) rename docs/docs/ext-latex/{latex.md => README.md} (56%) diff --git a/app/src/main/java/ru/noties/markwon/MainActivity.java b/app/src/main/java/ru/noties/markwon/MainActivity.java index d0a11d07..fd3965b6 100644 --- a/app/src/main/java/ru/noties/markwon/MainActivity.java +++ b/app/src/main/java/ru/noties/markwon/MainActivity.java @@ -7,7 +7,6 @@ import android.os.Bundle; import android.support.annotation.NonNull; import android.support.annotation.Nullable; import android.text.Spanned; -import android.text.method.LinkMovementMethod; import android.view.View; import android.widget.TextView; @@ -59,8 +58,6 @@ public class MainActivity extends Activity { appBarRenderer.render(appBarState()); - textView.setMovementMethod(LinkMovementMethod.getInstance()); - markdownLoader.load(uri(), new MarkdownLoader.OnMarkdownTextLoaded() { @Override public void apply(final String text) { diff --git a/docs/.vuepress/components/Link.vue b/docs/.vuepress/components/Link.vue index 07e549ef..542f1081 100644 --- a/docs/.vuepress/components/Link.vue +++ b/docs/.vuepress/components/Link.vue @@ -1,5 +1,8 @@ @@ -74,8 +97,8 @@ export default { background-color: rgba(0, 0, 0, 0.05); } .footer { - color: #666; - font-size: 0.85em; + color: #666; + font-size: 0.85em; } diff --git a/docs/docs/v3/README.md b/docs/docs/v3/README.md index a406b6e1..936d3aed 100644 --- a/docs/docs/v3/README.md +++ b/docs/docs/v3/README.md @@ -70,3 +70,24 @@ Screenshots are taken from sample application. It is a generic markdown viewer with support to display markdown content via `http`, `https` & `file` schemes and 2 themes included: Light & Dark. It can be downloaded from [releases](https://github.com/noties/Markwon/releases) ::: + + +## Awesome Markwon + +Applications using Markwon: + +* [Partico](https://partiko.app/) - Partiko is a censorship free social network. +* [FairNote](https://play.google.com/store/apps/details?id=com.rgiskard.fairnote) - simple and intuitive notepad app. It gives you speed and efficiency when you write notes, to-do lists, e-mails, or jot down quick ideas. + + +Extension/plugins: + +* [MarkwonCodeEx](https://github.com/kingideayou/MarkwonCodeEx) - Markwon extension support elegant code background. + +--- + +[Help to improve][awesome_link] this section by submitting your application/library/anything +that is using `Markwon` + + +[awesome_link]: https://github.com/noties/Markwon/issues/new?labels=awesome&body=Please%20provide%20the%20following%3A%0A*%20Project%20name%0A*%20Project%20URL%20(repository%2C%20store%20listing%2C%20web%20page)%0A*%20Optionally%20_brand_%20image%20URL%0A%0APlease%20make%20sure%20that%20there%20is%20the%20**awesome**%20label%20selected%20for%20this%20issue.%0A%0A%F0%9F%99%8C%20 From 1d3255ed591076cd485339a15e3f3e2749f9ea44 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=81ukasz=20Rutkowski?= Date: Mon, 18 Feb 2019 12:36:53 +0100 Subject: [PATCH 087/103] Extend task list parsing (#99) * Handle task lists that start with * or + symbol * Handle task lists that start with numbers * Limit numbers valid for task lists to 9 digits --- .../markwon/tasklist/TaskListBlockParser.java | 6 +- .../src/test/resources/tests/task-list.yaml | 57 +++++++++++++++++++ 2 files changed, 60 insertions(+), 3 deletions(-) create mode 100644 markwon/src/test/resources/tests/task-list.yaml diff --git a/markwon/src/main/java/ru/noties/markwon/tasklist/TaskListBlockParser.java b/markwon/src/main/java/ru/noties/markwon/tasklist/TaskListBlockParser.java index 4c2ab99a..436de188 100644 --- a/markwon/src/main/java/ru/noties/markwon/tasklist/TaskListBlockParser.java +++ b/markwon/src/main/java/ru/noties/markwon/tasklist/TaskListBlockParser.java @@ -23,7 +23,7 @@ import java.util.regex.Pattern; @SuppressWarnings("WeakerAccess") class TaskListBlockParser extends AbstractBlockParser { - private static final Pattern PATTERN = Pattern.compile("\\s*-\\s+\\[(x|X|\\s)\\]\\s+(.*)"); + private static final Pattern PATTERN = Pattern.compile("\\s*([-*+]|\\d{1,9}[.)])\\s+\\[(x|X|\\s)]\\s+(.*)"); private final TaskListBlock block = new TaskListBlock(); @@ -88,9 +88,9 @@ class TaskListBlockParser extends AbstractBlockParser { continue; } listItem = new TaskListItem() - .done(isDone(matcher.group(1))) + .done(isDone(matcher.group(2))) .indent(item.indent / 2); - inlineParser.parse(matcher.group(2), listItem); + inlineParser.parse(matcher.group(3), listItem); block.appendChild(listItem); } } diff --git a/markwon/src/test/resources/tests/task-list.yaml b/markwon/src/test/resources/tests/task-list.yaml new file mode 100644 index 00000000..1663940c --- /dev/null +++ b/markwon/src/test/resources/tests/task-list.yaml @@ -0,0 +1,57 @@ +input: |- + - [ ] First + - [x] Second + - [X] Third + * [ ] First star + * [x] Second star + * [X] Third star + + [ ] First plus + + [x] Second plus + + [X] Third plus + 1. [x] Number with dot + 3) [ ] Number + +output: + - task-list: "First" + blockIdent: 1 + done: false + - text: "\n" + - task-list: "Second" + blockIdent: 1 + done: true + - text: "\n" + - task-list: "Third" + blockIdent: 1 + done: true + - text: "\n" + - task-list: "First star" + blockIdent: 1 + done: false + - text: "\n" + - task-list: "Second star" + blockIdent: 1 + done: true + - text: "\n" + - task-list: "Third star" + blockIdent: 1 + done: true + - text: "\n" + - task-list: "First plus" + blockIdent: 1 + done: false + - text: "\n" + - task-list: "Second plus" + blockIdent: 1 + done: true + - text: "\n" + - task-list: "Third plus" + blockIdent: 1 + done: true + - text: "\n" + - task-list: "Number with dot" + blockIdent: 1 + done: true + - text: "\n" + - task-list: "Number" + blockIdent: 1 + done: false From e1d553096240b74f37f9be91c2d74a893171932f Mon Sep 17 00:00:00 2001 From: Dimitry Ivanov Date: Sun, 24 Feb 2019 17:10:58 +0300 Subject: [PATCH 088/103] Working with documentation --- docs/.vuepress/config.js | 3 +- docs/docs/v3/README.md | 2 +- docs/docs/v3/core/configuration.md | 182 +++++++++++++++++- docs/docs/v3/core/core-plugin.md | 2 + docs/docs/v3/core/images.md | 34 ++++ docs/docs/v3/core/plugins.md | 2 +- docs/docs/v3/core/render-props.md | 75 ++++++++ docs/docs/v3/core/visitor.md | 74 ++++++- .../ru/noties/markwon/MarkwonVisitor.java | 10 +- .../ru/noties/markwon/utils/DumpNodes.java | 110 +++++++++++ .../sample/recycler/RecyclerActivity.java | 34 ++++ 11 files changed, 518 insertions(+), 10 deletions(-) create mode 100644 docs/docs/v3/core/render-props.md create mode 100644 markwon-core/src/main/java/ru/noties/markwon/utils/DumpNodes.java diff --git a/docs/.vuepress/config.js b/docs/.vuepress/config.js index 50b76e0d..29ab8971 100644 --- a/docs/.vuepress/config.js +++ b/docs/.vuepress/config.js @@ -40,7 +40,8 @@ module.exports = { '/docs/v3/core/spans-factory.md', '/docs/v3/core/html-renderer.md', '/docs/v3/core/core-plugin.md', - '/docs/v3/core/movement-method-plugin.md' + '/docs/v3/core/movement-method-plugin.md', + '/docs/v3/core/render-props.md' ] }, '/docs/v3/ext-latex/', diff --git a/docs/docs/v3/README.md b/docs/docs/v3/README.md index 936d3aed..ef82ddba 100644 --- a/docs/docs/v3/README.md +++ b/docs/docs/v3/README.md @@ -86,7 +86,7 @@ and 2 themes included: Light & Dark. It can be downloaded from [releases](ht --- -[Help to improve][awesome_link] this section by submitting your application/library/anything +[Help to improve][awesome_link] this section by submitting your application or library that is using `Markwon` diff --git a/docs/docs/v3/core/configuration.md b/docs/docs/v3/core/configuration.md index af4abbfe..245b3310 100644 --- a/docs/docs/v3/core/configuration.md +++ b/docs/docs/v3/core/configuration.md @@ -1 +1,181 @@ -# Configuration \ No newline at end of file +# Configuration + +`MarkwonConfiguration` class holds common Markwon functionality. +These are _configurable_ properties: +* `SyntaxHighlight` +* `LinkSpan.Resolver` +* `UrlProcessor` +* `ImageSizeResolver` +* `MarkwonHtmlParser` + +:::tip +Additionally `MarkwonConfiguration` holds: +* `MarkwonTheme` +* `AsyncDrawableLoader` +* `MarkwonHtmlRenderer` +* `MarkwonSpansFactory` + +Please note that these values can be retrieved from `MarkwonConfiguration` +instance, but their _configuration_ must be done by a `Plugin` by overriding +one of the methods: +* `Plugin#configureTheme` +* `Plugin#configureImages` +* `Plugin#configureHtmlRenderer` +* `Plugin#configureSpansFactory` +::: + +## SyntaxHighlight + +```java +final Markwon markwon = Markwon.builder(this) + .usePlugin(new AbstractMarkwonPlugin() { + @Override + public void configureConfiguration(@NonNull MarkwonConfiguration.Builder builder) { + builder.syntaxHighlight(new SyntaxHighlightNoOp()); + } + }) + .build(); +``` + +:::tip +Use [syntax-highlight](/docs/v3/syntax-highlight/) to add syntax highlighting +to your application +::: + +## LinkSpan.Resolver + +React to a link click event. By default `LinkResolverDef` is used, +which tries to start an Activity given the `link` argument. If no +Activity can handle `link` `LinkResolverDef` silently ignores click event + +```java +final Markwon markwon = Markwon.builder(this) + .usePlugin(new AbstractMarkwonPlugin() { + @Override + public void configureConfiguration(@NonNull MarkwonConfiguration.Builder builder) { + builder.linkResolver(new LinkSpan.Resolver() { + @Override + public void resolve(View view, @NonNull String link) { + // react to link click here + } + }); + } + }) + .build(); +``` + +:::tip +Please note that `Markwon` will apply `LinkMovementMethod` to a resulting TextView +if there is none registered. if you wish to register own instance of a `MovementMethod` +apply it directly to a TextView or use [MovementMethodPlugin](/docs/v3/core/movement-method-plugin.md) +::: + +## UrlProcessor + +Process URLs in your markdown (for links and images). If not provided explicitly, +default **no-op** implementation will be used, which does not modify URLs (keeping them as-is). + +`Markwon` provides 2 implementations of `UrlProcessor`: +* `UrlProcessorRelativeToAbsolute` +* `UrlProcessorAndroidAssets` + +### UrlProcessorRelativeToAbsolute + +`UrlProcessorRelativeToAbsolute` can be used to make relative URL absolute. For example if an image is +defined like this: `![img](./art/image.JPG)` and `UrlProcessorRelativeToAbsolute` +is created with `https://github.com/noties/Markwon/raw/master/` as the base: +`new UrlProcessorRelativeToAbsolute("https://github.com/noties/Markwon/raw/master/")`, +then final image will have `https://github.com/noties/Markwon/raw/master/art/image.JPG` +as the destination. + +### UrlProcessorAndroidAssets + +`UrlProcessorAndroidAssets` can be used to make processed links to point to Android assets folder. +So an image: `![img](./art/image.JPG)` will have `file:///android_asset/art/image.JPG` as the +destination. + +:::tip +Please note that `UrlProcessorAndroidAssets` will process only URLs that have no `scheme` information, +so a `./art/image.png` will become `file:///android_asset/art/image.JPG` whilst `https://so.me/where.png` +will be kept as-is. +::: + +:::warning +In order to display an image from assets you still need to register `ImagesPlugin#createWithAssets(Context)` +plugin in resulting `Markwon` instance. As `UrlProcessorAndroidAssets` only +_processes_ URLs and doesn't take any part in displaying an image. +::: + + +## ImageSizeResolver + +`ImageSizeResolver` controls the size of an image to be displayed. Currently it +handles only HTML images (specified via `img` tag). + +```java +final Markwon markwon = Markwon.builder(this) + .usePlugin(new AbstractMarkwonPlugin() { + @Override + public void configureConfiguration(@NonNull MarkwonConfiguration.Builder builder) { + builder.imageSizeResolver(new ImageSizeResolver() { + @NonNull + @Override + public Rect resolveImageSize( + @Nullable ImageSize imageSize, + @NonNull Rect imageBounds, + int canvasWidth, + float textSize) { + return null; + } + }); + } + }) + .build(); +``` + +If not provided explicitly, default `ImageSizeResolverDef` implementation will be used. +It handles 3 dimension units: +* `%` (percent, relative to Canvas width) +* `em` (relative to text size) +* `px` (absolute size, every dimension that is not `%` or `em` is considered to be _absolute_) + +```html + + + +``` + +`ImageSizeResolverDef` keeps the ratio of original image if one of the dimensions is missing. + +:::warning Height% +There is no support for `%` units for `height` dimension. This is due to the fact that +height of an TextView in which markdown is displayed is non-stable and changes with time +(for example when image is loaded and applied to a TextView it will _increase_ TextView's height), +so we will have no point-of-reference from which to _calculate_ image height. +::: + +:::tip +`ImageSizeResolverDef` also takes care for an image to **not** exceed +canvas width. If an image has greater width than a TextView Canvas, then +image will be _scaled-down_ to fit the canvas. Please note that this rule +applies only if image has no absolute sizes (for example width is specified +in pixels). +::: + +## MarkwonHtmlParser + +Specify which HTML parser to use. Default implementation is **no-op**. + +:::warning +One must explicitly use [HtmlPlugin](/docs/v3/html/) in order to display +HTML content in markdown. Without specified HTML parser **no HTML content +will be rendered**. + +```java +Markwon.builder(context) + .usePlugin(HtmlPlugin.create()) +``` + +Please note that adding `HtmlPlugin` will take care of initializing parser, +so after `HtmlPlugin` is used, no additional configuration steps are required. +::: \ No newline at end of file diff --git a/docs/docs/v3/core/core-plugin.md b/docs/docs/v3/core/core-plugin.md index e6da8eab..3a63c2c5 100644 --- a/docs/docs/v3/core/core-plugin.md +++ b/docs/docs/v3/core/core-plugin.md @@ -75,6 +75,8 @@ Beware of this if you would like to override only one of the list types. This is done to correspond to `commonmark-java` implementation. ::: +More information about props can be found [here](/docs/v3/core/render-props.md) + --- :::tip Soft line break diff --git a/docs/docs/v3/core/images.md b/docs/docs/v3/core/images.md index b2f93c05..01ca81ce 100644 --- a/docs/docs/v3/core/images.md +++ b/docs/docs/v3/core/images.md @@ -113,6 +113,40 @@ be used. ## MediaDecoder +By default `core` artifact comes with _default image decoder_ only. It's called +`ImageMediaDecoder` and it can decode all the formats that `BitmapFactory#decodeStream(InputStream)` +can. + +```java +final Markwon markwon = Markwon.builder(this) + .usePlugin(ImagesPlugin.create(this)) + .usePlugin(new AbstractMarkwonPlugin() { + @Override + public void configureImages(@NonNull AsyncDrawableLoader.Builder builder) { + builder.addMediaDecoder("text/plain", new TextPlainMediaDecoder()); + } + }) + .build(); + +``` + +`MediaDecoder` is a class to turn `InputStream` into a `Drawable`: + +```java +public abstract class MediaDecoder { + + @Nullable + public abstract Drawable decode(@NonNull InputStream inputStream); +} +``` + +:::tip +If you want to display GIF or SVG images also, you can use [image-gif](/docs/v3/image/gif.md) +and [image-svg](/docs/v3/image/svg.md) modules. +::: + +## + :::tip If you are using [html](/docs/v3/html/) you do not have to additionally setup images displayed via `` tag, as `HtmlPlugin` automatically uses configured diff --git a/docs/docs/v3/core/plugins.md b/docs/docs/v3/core/plugins.md index 32e4c51b..dd83ab76 100644 --- a/docs/docs/v3/core/plugins.md +++ b/docs/docs/v3/core/plugins.md @@ -425,7 +425,7 @@ queried directly from a TextView. ## What happens underneath -Here is an approximation of how a `Markwon` instance will handle plugins: +Here is what happens inside `Markwon` when `setMarkdown` method is called: ```java // `Markwon#create` implicitly uses CorePlugin diff --git a/docs/docs/v3/core/render-props.md b/docs/docs/v3/core/render-props.md new file mode 100644 index 00000000..9dd18004 --- /dev/null +++ b/docs/docs/v3/core/render-props.md @@ -0,0 +1,75 @@ +# RenderProps + +`RenderProps` encapsulates passing arguments from a node visitor to a node renderer. +Without hardcoding arguments into an API method calls. + +`RenderProps` is the state collection for `Props` that are set by a node visitor and +retrieved by a node renderer. + +```java +public class Prop { + + @NonNull + public static Prop of(@NonNull String name) { + return new Prop<>(name); + } + + /* ... */ +} +``` + +For example `CorePlugin` defines a _Heading level_ prop (inside `CoreProps` class): + +```java +public static final Prop HEADING_LEVEL = Prop.of("heading-level"); +``` + +Then CorePlugin registers a `Heading` node visitor and applies heading value: + +```java +@Override +public void configureVisitor(@NonNull MarkwonVisitor.Builder builder) { + builder.on(Heading.class, new MarkwonVisitor.NodeVisitor() { + @Override + public void visit(@NonNull MarkwonVisitor visitor, @NonNull Heading heading) { + + /* Heading node handling logic */ + + // set heading level + CoreProps.HEADING_LEVEL.set(visitor.renderProps(), heading.getLevel()); + + // a helper method to apply span(s) for a node + // (internally obtains a SpanFactory for Heading or silently ignores + // this call if no factory for a Heading is registered) + visitor.setSpansForNodeOptional(heading, start); + + /* Heading node handling logic */ + } + }); +} +``` + +And finally `HeadingSpanFactory` (which is also registered by `CorePlugin`): + +```java +public class HeadingSpanFactory implements SpanFactory { + @Nullable + @Override + public Object getSpans(@NonNull MarkwonConfiguration configuration, @NonNull RenderProps props) { + return new HeadingSpan( + configuration.theme(), + CoreProps.HEADING_LEVEL.require(props) + ); + } +} +``` + +--- + +`Prop` has these methods: + +* `@Nullable T get(RenderProps)` - returns value stored in RenderProps or `null` if none is present +* `@NonNull T get(RenderProps, @NonNull T defValue)` - returns value stored in RenderProps or default value (this method always return non-null value) +* `@NonNull T require(RenderProps)` - returns value stored in RenderProps or _throws an exception_ if none is present +* `void set(RenderProps, @Nullable T value)` - updates value stored in RenderProps, passing `null` as value is the same as calling `clear` +* `void clear(RenderProps)` - clears value stored in RenderProps diff --git a/docs/docs/v3/core/visitor.md b/docs/docs/v3/core/visitor.md index 3e8ba057..f0cce0f2 100644 --- a/docs/docs/v3/core/visitor.md +++ b/docs/docs/v3/core/visitor.md @@ -1 +1,73 @@ -# Visitor \ No newline at end of file +# Visitor + +Starting with _visiting_ of parsed markdown +nodes does not require creating own instance of commonmark-java `Visitor`, +instead a composable/configurable `MarkwonVisitor` is used. + +## Visitor.Builder +There is no need to create own instance of `MarkwonVisitor.Builder` as +it is done by `Markwon` itself. One still can configure it as one wishes: + +```java +final Markwon markwon = Markwon.builder(contex) + .usePlugin(new AbstractMarkwonPlugin() { + @Override + public void configureVisitor(@NonNull MarkwonVisitor.Builder builder) { + builder.on(SoftLineBreak.class, new MarkwonVisitor.NodeVisitor() { + @Override + public void visit(@NonNull MarkwonVisitor visitor, @NonNull SoftLineBreak softLineBreak) { + visitor.forceNewLine(); + } + }); + } + }); +``` + +--- + +`MarkwonVisitor` encapsulates most of the functionality of rendering parsed markdown. + +It holds rendering configuration: +* `MarkwonVisitor#configuration` - getter for current [MarkwonConfiguration](/docs/v3/core/configuration.md) +* `MarkwonVisitor#renderProps` - getter for current [RenderProps](/docs/v3/core/render-props.md) +* `MarkwonVisitor#builder` - getter for current `SpannableBuilder` + +It contains also a number of utility functions: +* `visitChildren(Node)` - will visit all children of supplied Node +* `hasNext(Node)` - utility function to check if supplied Node has a Node after it (useful for white-space management, so there should be no blank new line after last BlockNode) +* `ensureNewLine` - will insert a new line at current `SpannableBuilder` position only if current (last) character is not a new-line +* `forceNewLine` - will insert a new line character without any condition checking +* `length` - helper function to call `visitor.builder().length()`, returns current length of `SpannableBuilder` +* `clear` - will clear state for `RenderProps` and `SpannableBuilder`, this is done by `Markwon` automatically after each render call + +And some utility functions to control the spans: +* `setSpans(int start, Object spans)` - will apply supplied `spans` on `SpannableBuilder` starting at `start` position and ending at `SpannableBuilder#length`. `spans` can be `null` (no spans will be applied) or an array of spans (each span of this array will be applied) +* `setSpansForNodeOptional(N node, int start)` - helper method to set spans for specified `node` (internally obtains `SpanFactory` for that node and uses it to apply spans) +* `setSpansForNode(N node, int start)` - almost the same as `setSpansForNodeOptional` but instead of silently ignoring call if none `SpanFactory` is registered, this method will throw an exception. + +```java +@Override +public void configureVisitor(@NonNull MarkwonVisitor.Builder builder) { + builder.on(Heading.class, new MarkwonVisitor.NodeVisitor() { + @Override + public void visit(@NonNull MarkwonVisitor visitor, @NonNull Heading heading) { + + // or just `visitor.length()` + final int start = visitor.builder().length(); + + visitor.visitChildren(heading); + + // or just `visitor.setSpansForNodeOptional(heading, start)` + final SpanFactory factory = visitor.configuration().spansFactory().get(heading.getClass()); + if (factory != null) { + visitor.setSpans(start, factory.getSpans(visitor.configuration(), visitor.renderProps())); + } + + if (visitor.hasNext(heading)) { + visitor.ensureNewLine(); + visitor.forceNewLine(); + } + } + }); +} +``` \ No newline at end of file diff --git a/markwon-core/src/main/java/ru/noties/markwon/MarkwonVisitor.java b/markwon-core/src/main/java/ru/noties/markwon/MarkwonVisitor.java index 52cc6cb6..e7a83db0 100644 --- a/markwon-core/src/main/java/ru/noties/markwon/MarkwonVisitor.java +++ b/markwon-core/src/main/java/ru/noties/markwon/MarkwonVisitor.java @@ -79,6 +79,11 @@ public interface MarkwonVisitor extends Visitor { */ int length(); + /** + * Clears state of visitor (both {@link RenderProps} and {@link SpannableBuilder} will be cleared + */ + void clear(); + /** * Sets spans to underlying {@link SpannableBuilder} from start * to {@link SpannableBuilder#length()}. @@ -88,11 +93,6 @@ public interface MarkwonVisitor extends Visitor { */ void setSpans(int start, @Nullable Object spans); - /** - * Clears state of visitor (both {@link RenderProps} and {@link SpannableBuilder} will be cleared - */ - void clear(); - /** * Helper method to obtain and apply spans for supplied Node. Internally queries {@link SpanFactory} * for the node (via {@link MarkwonSpansFactory#require(Class)} thus throwing an exception diff --git a/markwon-core/src/main/java/ru/noties/markwon/utils/DumpNodes.java b/markwon-core/src/main/java/ru/noties/markwon/utils/DumpNodes.java new file mode 100644 index 00000000..3b45d238 --- /dev/null +++ b/markwon-core/src/main/java/ru/noties/markwon/utils/DumpNodes.java @@ -0,0 +1,110 @@ +package ru.noties.markwon.utils; + +import android.support.annotation.NonNull; +import android.support.annotation.Nullable; + +import org.commonmark.node.Block; +import org.commonmark.node.Node; +import org.commonmark.node.Visitor; + +import java.lang.reflect.InvocationHandler; +import java.lang.reflect.Method; +import java.lang.reflect.Proxy; + +// utility class to print parsed Nodes hierarchy +public abstract class DumpNodes { + + public interface NodeProcessor { + @NonNull + String process(@NonNull Node node); + } + + @NonNull + public static String dump(@NonNull Node node) { + return dump(node, null); + } + + @NonNull + public static String dump(@NonNull Node node, @Nullable NodeProcessor nodeProcessor) { + + final NodeProcessor processor = nodeProcessor != null + ? nodeProcessor + : new NodeProcessorToString(); + + final Indent indent = new Indent(); + final StringBuilder builder = new StringBuilder(); + final Visitor visitor = (Visitor) Proxy.newProxyInstance( + Visitor.class.getClassLoader(), + new Class[]{Visitor.class}, + new InvocationHandler() { + @Override + public Object invoke(Object proxy, Method method, Object[] args) { + + final Node argument = (Node) args[0]; + + // initial indent + indent.appendTo(builder); + + // node info + builder.append(processor.process(argument)); + + if (argument instanceof Block) { + builder.append(" [\n"); + indent.increment(); + visitChildren((Visitor) proxy, argument); + indent.decrement(); + indent.appendTo(builder); + builder.append("]\n"); + } else { + builder.append('\n'); + } + return null; + } + }); + node.accept(visitor); + return builder.toString(); + } + + private DumpNodes() { + } + + private static class Indent { + + private int count; + + void increment() { + count += 1; + } + + void decrement() { + count -= 1; + } + + void appendTo(@NonNull StringBuilder builder) { + for (int i = 0; i < count; i++) { + builder + .append(' ') + .append(' '); + } + } + } + + private static void visitChildren(@NonNull Visitor visitor, @NonNull Node parent) { + Node node = parent.getFirstChild(); + while (node != null) { + // A subclass of this visitor might modify the node, resulting in getNext returning a different node or no + // node after visiting it. So get the next node before visiting. + Node next = node.getNext(); + node.accept(visitor); + node = next; + } + } + + private static class NodeProcessorToString implements NodeProcessor { + @NonNull + @Override + public String process(@NonNull Node node) { + return node.toString(); + } + } +} diff --git a/sample/src/main/java/ru/noties/markwon/sample/recycler/RecyclerActivity.java b/sample/src/main/java/ru/noties/markwon/sample/recycler/RecyclerActivity.java index c230aa6c..d366a64e 100644 --- a/sample/src/main/java/ru/noties/markwon/sample/recycler/RecyclerActivity.java +++ b/sample/src/main/java/ru/noties/markwon/sample/recycler/RecyclerActivity.java @@ -13,6 +13,8 @@ import android.text.TextUtils; import org.commonmark.ext.gfm.tables.TableBlock; import org.commonmark.ext.gfm.tables.TablesExtension; import org.commonmark.node.FencedCodeBlock; +import org.commonmark.node.Heading; +import org.commonmark.node.SoftLineBreak; import org.commonmark.parser.Parser; import java.io.BufferedReader; @@ -27,7 +29,9 @@ import ru.noties.markwon.AbstractMarkwonPlugin; import ru.noties.markwon.Markwon; import ru.noties.markwon.MarkwonConfiguration; import ru.noties.markwon.MarkwonVisitor; +import ru.noties.markwon.SpanFactory; import ru.noties.markwon.core.CorePlugin; +import ru.noties.markwon.core.CoreProps; import ru.noties.markwon.html.HtmlPlugin; import ru.noties.markwon.image.ImagesPlugin; import ru.noties.markwon.image.svg.SvgPlugin; @@ -48,6 +52,36 @@ public class RecyclerActivity extends Activity { super.onCreate(savedInstanceState); setContentView(R.layout.activity_recycler); + { +final Markwon markwon = Markwon.builder(contex) + .usePlugin(new AbstractMarkwonPlugin() { +@Override +public void configureVisitor(@NonNull MarkwonVisitor.Builder builder) { + builder.on(Heading.class, new MarkwonVisitor.NodeVisitor() { + @Override + public void visit(@NonNull MarkwonVisitor visitor, @NonNull Heading heading) { + + // or just `visitor.length()` + final int start = visitor.builder().length(); + + visitor.visitChildren(heading); + + // or just `visitor.setSpansForNodeOptional(heading, start)` + final SpanFactory factory = visitor.configuration().spansFactory().get(heading.getClass()); + if (factory != null) { + visitor.setSpans(start, factory.getSpans(visitor.configuration(), visitor.renderProps())); + } + + if (visitor.hasNext(heading)) { + visitor.ensureNewLine(); + visitor.forceNewLine(); + } + } + }); +} + }); + } + // create MarkwonAdapter and register two blocks that will be rendered differently // * fenced code block (can also specify the same Entry for indended code block) // * table block From 0a965e4cbc9c498cc5e04cc159966c3963ff7b5a Mon Sep 17 00:00:00 2001 From: Dimitry Ivanov Date: Sat, 2 Mar 2019 14:41:01 +0300 Subject: [PATCH 089/103] Split code and codeBlock spans and factories --- docs/docs/theme.md | 2 +- docs/docs/v3/core/theme.md | 2 +- .../ru/noties/markwon/core/MarkwonTheme.java | 149 +++++++++++++----- .../core/factory/CodeBlockSpanFactory.java | 4 +- .../markwon/core/factory/CodeSpanFactory.java | 2 +- .../markwon/core/spans/CodeBlockSpan.java | 66 ++++++++ .../noties/markwon/core/spans/CodeSpan.java | 52 +----- .../sample/recycler/RecyclerActivity.java | 34 ---- 8 files changed, 191 insertions(+), 120 deletions(-) create mode 100644 markwon-core/src/main/java/ru/noties/markwon/core/spans/CodeBlockSpan.java diff --git a/docs/docs/theme.md b/docs/docs/theme.md index 4612a63d..4baf954c 100644 --- a/docs/docs/theme.md +++ b/docs/docs/theme.md @@ -116,7 +116,7 @@ The color of background of code block text Leading margin for the block code content - + ### Code typeface diff --git a/docs/docs/v3/core/theme.md b/docs/docs/v3/core/theme.md index 93c31e85..bfb2b71d 100644 --- a/docs/docs/v3/core/theme.md +++ b/docs/docs/v3/core/theme.md @@ -120,7 +120,7 @@ The color of background of code block text Leading margin for the block code content - + ### Code typeface diff --git a/markwon-core/src/main/java/ru/noties/markwon/core/MarkwonTheme.java b/markwon-core/src/main/java/ru/noties/markwon/core/MarkwonTheme.java index fc97875a..43848d75 100644 --- a/markwon-core/src/main/java/ru/noties/markwon/core/MarkwonTheme.java +++ b/markwon-core/src/main/java/ru/noties/markwon/core/MarkwonTheme.java @@ -108,7 +108,7 @@ public class MarkwonTheme { final Dip dip = Dip.create(context); return new Builder() - .codeMultilineMargin(dip.toPx(8)) + .codeBlockMargin(dip.toPx(8)) .blockMargin(dip.toPx(24)) .blockQuoteWidth(dip.toPx(4)) .bulletListItemStrokeWidth(dip.toPx(1)) @@ -165,15 +165,19 @@ public class MarkwonTheme { // by default `width` of a space char... it's fun and games, but span doesn't have access to paint in `getLeadingMargin` // so, we need to set this value explicitly (think of an utility method, that takes TextView/TextPaint and measures space char) - protected final int codeMultilineMargin; + protected final int codeBlockMargin; // by default Typeface.MONOSPACE protected final Typeface codeTypeface; + protected final Typeface codeBlockTypeface; + // by default a bit (how much?!) smaller than normal text // applied ONLY if default typeface was used, otherwise, not applied protected final int codeTextSize; + protected final int codeBlockTextSize; + // by default paint.getStrokeWidth protected final int headingBreakHeight; @@ -207,9 +211,11 @@ public class MarkwonTheme { this.codeBlockTextColor = builder.codeBlockTextColor; this.codeBackgroundColor = builder.codeBackgroundColor; this.codeBlockBackgroundColor = builder.codeBlockBackgroundColor; - this.codeMultilineMargin = builder.codeMultilineMargin; + this.codeBlockMargin = builder.codeBlockMargin; this.codeTypeface = builder.codeTypeface; + this.codeBlockTypeface = builder.codeBlockTypeface; this.codeTextSize = builder.codeTextSize; + this.codeBlockTextSize = builder.codeBlockTextSize; this.headingBreakHeight = builder.headingBreakHeight; this.headingBreakColor = builder.headingBreakColor; this.headingTypeface = builder.headingTypeface; @@ -301,66 +307,117 @@ public class MarkwonTheme { } /** - * Modified in 1.0.5 to accept `multiline` argument + * @since 3.0.0 */ - public void applyCodeTextStyle(@NonNull Paint paint, boolean multiline) { + public void applyCodeTextStyle(@NonNull Paint paint) { - // @since 1.0.5 added handling of multiline code blocks - if (multiline - && codeBlockTextColor != 0) { - paint.setColor(codeBlockTextColor); - } else if (codeTextColor != 0) { + if (codeTextColor != 0) { paint.setColor(codeTextColor); } - // custom typeface was set if (codeTypeface != null) { paint.setTypeface(codeTypeface); - // please note that we won't be calculating textSize - // (like we do when no Typeface is provided), if it's some specific typeface - // we would confuse users about textSize - if (codeTextSize != 0) { + if (codeTextSize > 0) { paint.setTextSize(codeTextSize); } } else { + paint.setTypeface(Typeface.MONOSPACE); - final float textSize; - if (codeTextSize != 0) { - textSize = codeTextSize; + + if (codeTextSize > 0) { + paint.setTextSize(codeTextSize); } else { - textSize = paint.getTextSize() * CODE_DEF_TEXT_SIZE_RATIO; + paint.setTextSize(paint.getTextSize() * CODE_DEF_TEXT_SIZE_RATIO); } - paint.setTextSize(textSize); } } - public int getCodeMultilineMargin() { - return codeMultilineMargin; + /** + * @since 3.0.0 + */ + public void applyCodeBlockTextStyle(@NonNull Paint paint) { + + // apply text color, first check for block specific value, + // then check for code (inline), else do nothing (keep original color of text) + final int textColor = codeBlockTextColor != 0 + ? codeBlockTextColor + : codeTextColor; + + if (textColor != 0) { + paint.setColor(textColor); + } + + final Typeface typeface = codeBlockTypeface != null + ? codeBlockTypeface + : codeTypeface; + + if (typeface != null) { + + paint.setTypeface(typeface); + + // please note that we won't be calculating textSize + // (like we do when no Typeface is provided), if it's some specific typeface + // we would confuse users about textSize + final int textSize = codeBlockTextSize > 0 + ? codeBlockTextSize + : codeTextSize; + + if (textSize > 0) { + paint.setTextSize(textSize); + } + } else { + + // by default use monospace + paint.setTypeface(Typeface.MONOSPACE); + + final int textSize = codeBlockTextSize > 0 + ? codeBlockTextSize + : codeTextSize; + + if (textSize > 0) { + paint.setTextSize(textSize); + } else { + // calculate default value + paint.setTextSize(paint.getTextSize() * CODE_DEF_TEXT_SIZE_RATIO); + } + } + } + + + public int getCodeBlockMargin() { + return codeBlockMargin; } /** - * Modified in 1.0.5 to accept `multiline` argument + * @since 3.0.0 */ - public int getCodeBackgroundColor(@NonNull Paint paint, boolean multiline) { - + public int getCodeBackgroundColor(@NonNull Paint paint) { final int color; - - // @since 1.0.5 added handling of multiline code blocks - if (multiline - && codeBlockBackgroundColor != 0) { - color = codeBlockBackgroundColor; - } else if (codeBackgroundColor != 0) { + if (codeBackgroundColor != 0) { color = codeBackgroundColor; } else { color = ColorUtils.applyAlpha(paint.getColor(), CODE_DEF_BACKGROUND_COLOR_ALPHA); } - return color; } + /** + * @since 3.0.0 + */ + public int getCodeBlockBackgroundColor(@NonNull Paint paint) { + + final int color = codeBlockBackgroundColor != 0 + ? codeBlockBackgroundColor + : codeBackgroundColor; + + return color != 0 + ? color + : ColorUtils.applyAlpha(paint.getColor(), CODE_DEF_BACKGROUND_COLOR_ALPHA); + } + public void applyHeadingTextStyle(@NonNull Paint paint, @IntRange(from = 1, to = 6) int level) { if (headingTypeface == null) { paint.setFakeBoldText(true); @@ -426,9 +483,11 @@ public class MarkwonTheme { private int codeBlockTextColor; // @since 1.0.5 private int codeBackgroundColor; private int codeBlockBackgroundColor; // @since 1.0.5 - private int codeMultilineMargin; + private int codeBlockMargin; private Typeface codeTypeface; + private Typeface codeBlockTypeface; // @since 3.0.0 private int codeTextSize; + private int codeBlockTextSize; // @since 3.0.0 private int headingBreakHeight = -1; private int headingBreakColor; private Typeface headingTypeface; @@ -451,7 +510,7 @@ public class MarkwonTheme { this.codeBlockTextColor = theme.codeBlockTextColor; this.codeBackgroundColor = theme.codeBackgroundColor; this.codeBlockBackgroundColor = theme.codeBlockBackgroundColor; - this.codeMultilineMargin = theme.codeMultilineMargin; + this.codeBlockMargin = theme.codeBlockMargin; this.codeTypeface = theme.codeTypeface; this.codeTextSize = theme.codeTextSize; this.headingBreakHeight = theme.headingBreakHeight; @@ -537,8 +596,8 @@ public class MarkwonTheme { } @NonNull - public Builder codeMultilineMargin(@Px int codeMultilineMargin) { - this.codeMultilineMargin = codeMultilineMargin; + public Builder codeBlockMargin(@Px int codeBlockMargin) { + this.codeBlockMargin = codeBlockMargin; return this; } @@ -548,12 +607,30 @@ public class MarkwonTheme { return this; } + /** + * @since 3.0.0 + */ + @NonNull + public Builder codeBlockTypeface(@NonNull Typeface typeface) { + this.codeBlockTypeface = typeface; + return this; + } + @NonNull public Builder codeTextSize(@Px int codeTextSize) { this.codeTextSize = codeTextSize; return this; } + /** + * @since 3.0.0 + */ + @NonNull + public Builder codeBlockTextSize(@Px int codeTextSize) { + this.codeBlockTextSize = codeTextSize; + return this; + } + @NonNull public Builder headingBreakHeight(@Px int headingBreakHeight) { this.headingBreakHeight = headingBreakHeight; diff --git a/markwon-core/src/main/java/ru/noties/markwon/core/factory/CodeBlockSpanFactory.java b/markwon-core/src/main/java/ru/noties/markwon/core/factory/CodeBlockSpanFactory.java index 4fd07685..2bf9383e 100644 --- a/markwon-core/src/main/java/ru/noties/markwon/core/factory/CodeBlockSpanFactory.java +++ b/markwon-core/src/main/java/ru/noties/markwon/core/factory/CodeBlockSpanFactory.java @@ -6,12 +6,12 @@ import android.support.annotation.Nullable; import ru.noties.markwon.MarkwonConfiguration; import ru.noties.markwon.RenderProps; import ru.noties.markwon.SpanFactory; -import ru.noties.markwon.core.spans.CodeSpan; +import ru.noties.markwon.core.spans.CodeBlockSpan; public class CodeBlockSpanFactory implements SpanFactory { @Nullable @Override public Object getSpans(@NonNull MarkwonConfiguration configuration, @NonNull RenderProps props) { - return new CodeSpan(configuration.theme(), true); + return new CodeBlockSpan(configuration.theme()); } } diff --git a/markwon-core/src/main/java/ru/noties/markwon/core/factory/CodeSpanFactory.java b/markwon-core/src/main/java/ru/noties/markwon/core/factory/CodeSpanFactory.java index 7726bc02..944556fb 100644 --- a/markwon-core/src/main/java/ru/noties/markwon/core/factory/CodeSpanFactory.java +++ b/markwon-core/src/main/java/ru/noties/markwon/core/factory/CodeSpanFactory.java @@ -12,6 +12,6 @@ public class CodeSpanFactory implements SpanFactory { @Nullable @Override public Object getSpans(@NonNull MarkwonConfiguration configuration, @NonNull RenderProps props) { - return new CodeSpan(configuration.theme(), false); + return new CodeSpan(configuration.theme()); } } diff --git a/markwon-core/src/main/java/ru/noties/markwon/core/spans/CodeBlockSpan.java b/markwon-core/src/main/java/ru/noties/markwon/core/spans/CodeBlockSpan.java new file mode 100644 index 00000000..00109766 --- /dev/null +++ b/markwon-core/src/main/java/ru/noties/markwon/core/spans/CodeBlockSpan.java @@ -0,0 +1,66 @@ +package ru.noties.markwon.core.spans; + +import android.graphics.Canvas; +import android.graphics.Paint; +import android.graphics.Rect; +import android.support.annotation.NonNull; +import android.text.Layout; +import android.text.TextPaint; +import android.text.style.LeadingMarginSpan; +import android.text.style.MetricAffectingSpan; + +import ru.noties.markwon.core.MarkwonTheme; + +/** + * @since 3.0.0 split inline and block spans + */ +public class CodeBlockSpan extends MetricAffectingSpan implements LeadingMarginSpan { + + private final MarkwonTheme theme; + private final Rect rect = ObjectsPool.rect(); + private final Paint paint = ObjectsPool.paint(); + + public CodeBlockSpan(@NonNull MarkwonTheme theme) { + this.theme = theme; + } + + @Override + public void updateMeasureState(TextPaint p) { + apply(p); + } + + @Override + public void updateDrawState(TextPaint ds) { + apply(ds); + } + + private void apply(TextPaint p) { + theme.applyCodeBlockTextStyle(p); + } + + @Override + public int getLeadingMargin(boolean first) { + return theme.getCodeBlockMargin(); + } + + @Override + public void drawLeadingMargin(Canvas c, Paint p, int x, int dir, int top, int baseline, int bottom, CharSequence text, int start, int end, boolean first, Layout layout) { + + paint.setStyle(Paint.Style.FILL); + paint.setColor(theme.getCodeBlockBackgroundColor(p)); + + final int left; + final int right; + if (dir > 0) { + left = x; + right = c.getWidth(); + } else { + left = x - c.getWidth(); + right = x; + } + + rect.set(left, top, right, bottom); + + c.drawRect(rect, paint); + } +} diff --git a/markwon-core/src/main/java/ru/noties/markwon/core/spans/CodeSpan.java b/markwon-core/src/main/java/ru/noties/markwon/core/spans/CodeSpan.java index dc233538..856d5807 100644 --- a/markwon-core/src/main/java/ru/noties/markwon/core/spans/CodeSpan.java +++ b/markwon-core/src/main/java/ru/noties/markwon/core/spans/CodeSpan.java @@ -1,27 +1,20 @@ package ru.noties.markwon.core.spans; -import android.graphics.Canvas; -import android.graphics.Paint; -import android.graphics.Rect; import android.support.annotation.NonNull; -import android.text.Layout; import android.text.TextPaint; -import android.text.style.LeadingMarginSpan; import android.text.style.MetricAffectingSpan; import ru.noties.markwon.core.MarkwonTheme; -public class CodeSpan extends MetricAffectingSpan implements LeadingMarginSpan { +/** + * @since 3.0.0 split inline and block spans + */ +public class CodeSpan extends MetricAffectingSpan { private final MarkwonTheme theme; - private final Rect rect = ObjectsPool.rect(); - private final Paint paint = ObjectsPool.paint(); - private final boolean multiline; - - public CodeSpan(@NonNull MarkwonTheme theme, boolean multiline) { + public CodeSpan(@NonNull MarkwonTheme theme) { this.theme = theme; - this.multiline = multiline; } @Override @@ -32,41 +25,10 @@ public class CodeSpan extends MetricAffectingSpan implements LeadingMarginSpan { @Override public void updateDrawState(TextPaint ds) { apply(ds); - if (!multiline) { - ds.bgColor = theme.getCodeBackgroundColor(ds, false); - } + ds.bgColor = theme.getCodeBackgroundColor(ds); } private void apply(TextPaint p) { - theme.applyCodeTextStyle(p, multiline); - } - - @Override - public int getLeadingMargin(boolean first) { - return multiline ? theme.getCodeMultilineMargin() : 0; - } - - @Override - public void drawLeadingMargin(Canvas c, Paint p, int x, int dir, int top, int baseline, int bottom, CharSequence text, int start, int end, boolean first, Layout layout) { - - if (multiline) { - - paint.setStyle(Paint.Style.FILL); - paint.setColor(theme.getCodeBackgroundColor(p, true)); - - final int left; - final int right; - if (dir > 0) { - left = x; - right = c.getWidth(); - } else { - left = x - c.getWidth(); - right = x; - } - - rect.set(left, top, right, bottom); - - c.drawRect(rect, paint); - } + theme.applyCodeTextStyle(p); } } diff --git a/sample/src/main/java/ru/noties/markwon/sample/recycler/RecyclerActivity.java b/sample/src/main/java/ru/noties/markwon/sample/recycler/RecyclerActivity.java index d366a64e..c230aa6c 100644 --- a/sample/src/main/java/ru/noties/markwon/sample/recycler/RecyclerActivity.java +++ b/sample/src/main/java/ru/noties/markwon/sample/recycler/RecyclerActivity.java @@ -13,8 +13,6 @@ import android.text.TextUtils; import org.commonmark.ext.gfm.tables.TableBlock; import org.commonmark.ext.gfm.tables.TablesExtension; import org.commonmark.node.FencedCodeBlock; -import org.commonmark.node.Heading; -import org.commonmark.node.SoftLineBreak; import org.commonmark.parser.Parser; import java.io.BufferedReader; @@ -29,9 +27,7 @@ import ru.noties.markwon.AbstractMarkwonPlugin; import ru.noties.markwon.Markwon; import ru.noties.markwon.MarkwonConfiguration; import ru.noties.markwon.MarkwonVisitor; -import ru.noties.markwon.SpanFactory; import ru.noties.markwon.core.CorePlugin; -import ru.noties.markwon.core.CoreProps; import ru.noties.markwon.html.HtmlPlugin; import ru.noties.markwon.image.ImagesPlugin; import ru.noties.markwon.image.svg.SvgPlugin; @@ -52,36 +48,6 @@ public class RecyclerActivity extends Activity { super.onCreate(savedInstanceState); setContentView(R.layout.activity_recycler); - { -final Markwon markwon = Markwon.builder(contex) - .usePlugin(new AbstractMarkwonPlugin() { -@Override -public void configureVisitor(@NonNull MarkwonVisitor.Builder builder) { - builder.on(Heading.class, new MarkwonVisitor.NodeVisitor() { - @Override - public void visit(@NonNull MarkwonVisitor visitor, @NonNull Heading heading) { - - // or just `visitor.length()` - final int start = visitor.builder().length(); - - visitor.visitChildren(heading); - - // or just `visitor.setSpansForNodeOptional(heading, start)` - final SpanFactory factory = visitor.configuration().spansFactory().get(heading.getClass()); - if (factory != null) { - visitor.setSpans(start, factory.getSpans(visitor.configuration(), visitor.renderProps())); - } - - if (visitor.hasNext(heading)) { - visitor.ensureNewLine(); - visitor.forceNewLine(); - } - } - }); -} - }); - } - // create MarkwonAdapter and register two blocks that will be rendered differently // * fenced code block (can also specify the same Entry for indended code block) // * table block From 12ef0b57034ee30e66b2698157cb05db0b387114 Mon Sep 17 00:00:00 2001 From: Dimitry Ivanov Date: Sat, 2 Mar 2019 15:29:17 +0300 Subject: [PATCH 090/103] Finished core documentation --- docs/docs/v3/core/html-renderer.md | 83 ++++++++++++++++++- docs/docs/v3/core/spans-factory.md | 62 +++++++++++++- docs/docs/v3/core/theme.md | 12 +++ .../noties/markwon/MarkwonSpansFactory.java | 2 +- .../markwon/MarkwonSpansFactoryImpl.java | 8 +- .../markwon/html/MarkwonHtmlRenderer.java | 4 +- .../markwon/html/MarkwonHtmlRendererImpl.java | 4 +- .../ru/noties/markwon/html/HtmlPlugin.java | 22 ++--- 8 files changed, 177 insertions(+), 20 deletions(-) diff --git a/docs/docs/v3/core/html-renderer.md b/docs/docs/v3/core/html-renderer.md index 3734eff5..70b8598e 100644 --- a/docs/docs/v3/core/html-renderer.md +++ b/docs/docs/v3/core/html-renderer.md @@ -1 +1,82 @@ -# HTML Renderer \ No newline at end of file +# HTML Renderer + +Starting with `MarkwonHtmlRenderer` controls how HTML +is rendered: + +```java +Markwon.builder(context) + .usePlugin(new AbstractMarkwonPlugin() { + @Override + public void configureHtmlRenderer(@NonNull MarkwonHtmlRenderer.Builder builder) { + builder.setHandler("a", new MyTagHandler()); + } + }); +``` + +:::danger +Customizing `MarkwonHtmlRenderer` is not enough to include HTML content in your application. +You must explicitly include [markwon-html](/docs/v3/html/) artifact (include HtmlParser) +to you project and register `HtmlPlugin`: + +```java +Markwon.builder(context) + .usePlugin(HtmlPlugin.create()) +``` +::: + +For example, to create an `` HTML tag handler: + +```java +builder.setHandler("a", new SimpleTagHandler() { + @Override + public Object getSpans( + @NonNull MarkwonConfiguration configuration, + @NonNull RenderProps renderProps, + @NonNull HtmlTag tag) { + return new LinkSpan( + configuration.theme(), + tag.attributes().get("href"), + configuration.linkResolver()); + } +}); +``` + +`SimpleTagHandler` can be used for simple cases when a tag does not require any special +handling (like visiting it's children) + +:::tip +One can return `null` a single span or an array of spans from `getSpans` method +::: + +For a more advanced usage `TagHandler` can be used directly: + +```java +builder.setHandler("a", new TagHandler() { + @Override + public void handle(@NonNull MarkwonVisitor visitor, @NonNull MarkwonHtmlRenderer renderer, @NonNull HtmlTag tag) { + + // obtain default spanFactory for Link node + final SpanFactory factory = visitor.configuration().spansFactory().get(Link.class); + + if (factory != null) { + + // set destination property + CoreProps.LINK_DESTINATION.set( + visitor.renderProps(), + tag.attributes().get("href")); + + // Obtain spans from the factory + final Object spans = factory.getSpans( + visitor.configuration(), + visitor.renderProps()); + + // apply spans to SpannableBuilder + SpannableBuilder.setSpans( + visitor.builder(), + spans, + tag.start(), + tag.end()); + } + } +}); +``` \ No newline at end of file diff --git a/docs/docs/v3/core/spans-factory.md b/docs/docs/v3/core/spans-factory.md index 3b326736..6044de9a 100644 --- a/docs/docs/v3/core/spans-factory.md +++ b/docs/docs/v3/core/spans-factory.md @@ -1 +1,61 @@ -# Spans Factory \ No newline at end of file +# Spans Factory + +Starting with `MarkwonSpansFactory` controls what spans are displayed +for markdown nodes. + +```java +Markwon.builder(context) + .usePlugin(new AbstractMarkwonPlugin() { + @Override + public void configureSpansFactory(@NonNull MarkwonSpansFactory.Builder builder) { + // passing null as second argument will remove previously added + // factory for the Link node + builder.setFactory(Link.class, null); + } + }); +``` + +## SpanFactory + +In order to create a _generic_ interface for all possible Nodes, a `SpanFactory` +was added: + +```java +builder.setFactory(Link.class, new SpanFactory() { + @Nullable + @Override + public Object getSpans(@NonNull MarkwonConfiguration configuration, @NonNull RenderProps props) { + return null; + } +}); +``` + +All possible arguments are passed via [RenderProps](/docs/v3/core/render-props.md): + +```java +builder.setFactory(Link.class, new SpanFactory() { + @Override + public Object getSpans(@NonNull MarkwonConfiguration configuration, @NonNull RenderProps props) { + final String href = CoreProps.LINK_DESTINATION.require(props); + return new LinkSpan(configuration.theme(), href, configuration.linkResolver()); + } +}); +``` + +`SpanFactory` allows returning `null` for a certain span (no span will be applied). +Or an array of spans: + +```java +builder.setFactory(Link.class, new SpanFactory() { + @Override + public Object getSpans(@NonNull MarkwonConfiguration configuration, @NonNull RenderProps props) { + return new Object[]{ + new LinkSpan( + configuration.theme(), + CoreProps.LINK_DESTINATION.require(props), + configuration.linkResolver()), + new ForegroundColorSpan(Color.RED) + }; + } +}); +``` \ No newline at end of file diff --git a/docs/docs/v3/core/theme.md b/docs/docs/v3/core/theme.md index bfb2b71d..672babc6 100644 --- a/docs/docs/v3/core/theme.md +++ b/docs/docs/v3/core/theme.md @@ -128,12 +128,24 @@ Typeface of code content +### Block code typeface + +Typeface of block code content + + + ### Code text size Text size of code content +### Block code text size + +Text size of block code content + + + ## Heading ### Break height diff --git a/markwon-core/src/main/java/ru/noties/markwon/MarkwonSpansFactory.java b/markwon-core/src/main/java/ru/noties/markwon/MarkwonSpansFactory.java index 326af3e6..dbc6960d 100644 --- a/markwon-core/src/main/java/ru/noties/markwon/MarkwonSpansFactory.java +++ b/markwon-core/src/main/java/ru/noties/markwon/MarkwonSpansFactory.java @@ -32,7 +32,7 @@ public interface MarkwonSpansFactory { interface Builder { @NonNull - Builder setFactory(@NonNull Class node, @NonNull SpanFactory factory); + Builder setFactory(@NonNull Class node, @Nullable SpanFactory factory); @NonNull MarkwonSpansFactory build(); diff --git a/markwon-core/src/main/java/ru/noties/markwon/MarkwonSpansFactoryImpl.java b/markwon-core/src/main/java/ru/noties/markwon/MarkwonSpansFactoryImpl.java index bc472bb4..f3cd0dee 100644 --- a/markwon-core/src/main/java/ru/noties/markwon/MarkwonSpansFactoryImpl.java +++ b/markwon-core/src/main/java/ru/noties/markwon/MarkwonSpansFactoryImpl.java @@ -43,8 +43,12 @@ class MarkwonSpansFactoryImpl implements MarkwonSpansFactory { @NonNull @Override - public Builder setFactory(@NonNull Class node, @NonNull SpanFactory factory) { - factories.put(node, factory); + public Builder setFactory(@NonNull Class node, @Nullable SpanFactory factory) { + if (factory == null) { + factories.remove(node); + } else { + factories.put(node, factory); + } return this; } diff --git a/markwon-core/src/main/java/ru/noties/markwon/html/MarkwonHtmlRenderer.java b/markwon-core/src/main/java/ru/noties/markwon/html/MarkwonHtmlRenderer.java index 931fd092..234d53ac 100644 --- a/markwon-core/src/main/java/ru/noties/markwon/html/MarkwonHtmlRenderer.java +++ b/markwon-core/src/main/java/ru/noties/markwon/html/MarkwonHtmlRenderer.java @@ -42,10 +42,10 @@ public abstract class MarkwonHtmlRenderer { Builder allowNonClosedTags(boolean allowNonClosedTags); @NonNull - Builder addHandler(@NonNull String tagName, @NonNull TagHandler tagHandler); + Builder setHandler(@NonNull String tagName, @NonNull TagHandler tagHandler); @NonNull - Builder addHandler(@NonNull Collection tagNames, @NonNull TagHandler tagHandler); + Builder setHandler(@NonNull Collection tagNames, @NonNull TagHandler tagHandler); @NonNull Builder removeHandler(@NonNull String tagName); diff --git a/markwon-core/src/main/java/ru/noties/markwon/html/MarkwonHtmlRendererImpl.java b/markwon-core/src/main/java/ru/noties/markwon/html/MarkwonHtmlRendererImpl.java index ca94f902..7547e1cc 100644 --- a/markwon-core/src/main/java/ru/noties/markwon/html/MarkwonHtmlRendererImpl.java +++ b/markwon-core/src/main/java/ru/noties/markwon/html/MarkwonHtmlRendererImpl.java @@ -100,14 +100,14 @@ class MarkwonHtmlRendererImpl extends MarkwonHtmlRenderer { @NonNull @Override - public Builder addHandler(@NonNull String tagName, @NonNull TagHandler tagHandler) { + public Builder setHandler(@NonNull String tagName, @NonNull TagHandler tagHandler) { tagHandlers.put(tagName, tagHandler); return this; } @NonNull @Override - public Builder addHandler(@NonNull Collection tagNames, @NonNull TagHandler tagHandler) { + public Builder setHandler(@NonNull Collection tagNames, @NonNull TagHandler tagHandler) { for (String tagName : tagNames) { if (tagName != null) { tagHandlers.put(tagName, tagHandler); diff --git a/markwon-html/src/main/java/ru/noties/markwon/html/HtmlPlugin.java b/markwon-html/src/main/java/ru/noties/markwon/html/HtmlPlugin.java index 032fd68e..d087058e 100644 --- a/markwon-html/src/main/java/ru/noties/markwon/html/HtmlPlugin.java +++ b/markwon-html/src/main/java/ru/noties/markwon/html/HtmlPlugin.java @@ -45,37 +45,37 @@ public class HtmlPlugin extends AbstractMarkwonPlugin { public void configureHtmlRenderer(@NonNull MarkwonHtmlRenderer.Builder builder) { builder - .addHandler( + .setHandler( "img", ImageHandler.create()) - .addHandler( + .setHandler( "a", new LinkHandler()) - .addHandler( + .setHandler( "blockquote", new BlockquoteHandler()) - .addHandler( + .setHandler( "sub", new SubScriptHandler()) - .addHandler( + .setHandler( "sup", new SuperScriptHandler()) - .addHandler( + .setHandler( asList("b", "strong"), new StrongEmphasisHandler()) - .addHandler( + .setHandler( asList("s", "del"), new StrikeHandler()) - .addHandler( + .setHandler( asList("u", "ins"), new UnderlineHandler()) - .addHandler( + .setHandler( asList("ul", "ol"), new ListHandler()) - .addHandler( + .setHandler( asList("i", "em", "cite", "dfn"), new EmphasisHandler()) - .addHandler( + .setHandler( asList("h1", "h2", "h3", "h4", "h5", "h6"), new HeadingHandler()); } From 128612d5b26fa02d10ca56fd194f98c582122c02 Mon Sep 17 00:00:00 2001 From: Dimitry Ivanov Date: Sun, 3 Mar 2019 16:25:23 +0300 Subject: [PATCH 091/103] Add reducer and nodeRenderer --- docs/.vuepress/components/ArtifactPicker.vue | 5 +- docs/docs/v3/core/html-renderer.md | 4 +- .../noties/markwon/MarkwonNodeRenderer.java | 184 ++++++++++++++++++ .../ru/noties/markwon/MarkwonReducer.java | 57 ++++++ .../markwon/html/MarkwonHtmlRenderer.java | 10 +- .../markwon/html/MarkwonHtmlRendererImpl.java | 34 ++-- .../markwon/ext/tasklist/TaskListPlugin.java | 16 +- .../markwon/recycler/MarkwonAdapter.java | 17 +- .../markwon/recycler/MarkwonAdapterImpl.java | 35 +--- sample/src/main/res/values/strings.xml | 11 +- 10 files changed, 280 insertions(+), 93 deletions(-) create mode 100644 markwon-core/src/main/java/ru/noties/markwon/MarkwonNodeRenderer.java create mode 100644 markwon-core/src/main/java/ru/noties/markwon/MarkwonReducer.java diff --git a/docs/.vuepress/components/ArtifactPicker.vue b/docs/.vuepress/components/ArtifactPicker.vue index a19ec829..463d230a 100644 --- a/docs/.vuepress/components/ArtifactPicker.vue +++ b/docs/.vuepress/components/ArtifactPicker.vue @@ -19,7 +19,7 @@ @click="selectAll" >


@@ -42,7 +42,8 @@ export default { data() { return { artifacts, - selected: ['core'] + selected: ['core'], + latestVersion: 'latest_version' }; }, methods: { diff --git a/docs/docs/v3/core/html-renderer.md b/docs/docs/v3/core/html-renderer.md index 70b8598e..d0756b12 100644 --- a/docs/docs/v3/core/html-renderer.md +++ b/docs/docs/v3/core/html-renderer.md @@ -15,8 +15,8 @@ Markwon.builder(context) :::danger Customizing `MarkwonHtmlRenderer` is not enough to include HTML content in your application. -You must explicitly include [markwon-html](/docs/v3/html/) artifact (include HtmlParser) -to you project and register `HtmlPlugin`: +You must explicitly include [markwon-html](/docs/v3/html/) artifact (includes HtmlParser) +to your project and register `HtmlPlugin`: ```java Markwon.builder(context) diff --git a/markwon-core/src/main/java/ru/noties/markwon/MarkwonNodeRenderer.java b/markwon-core/src/main/java/ru/noties/markwon/MarkwonNodeRenderer.java new file mode 100644 index 00000000..94de3c20 --- /dev/null +++ b/markwon-core/src/main/java/ru/noties/markwon/MarkwonNodeRenderer.java @@ -0,0 +1,184 @@ +package ru.noties.markwon; + +import android.content.Context; +import android.support.annotation.IdRes; +import android.support.annotation.LayoutRes; +import android.support.annotation.NonNull; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.TextView; + +import org.commonmark.node.Node; + +import java.util.HashMap; +import java.util.Map; + +/** + * @since 3.0.0 + */ +public abstract class MarkwonNodeRenderer { + + public interface ViewProvider { + + /** + * Please note that you should not attach created View to specified group. It will be done + * automatically. + */ + @NonNull + View provide( + @NonNull LayoutInflater inflater, + @NonNull ViewGroup group, + @NonNull Markwon markwon, + @NonNull N n); + } + + @NonNull + public static Builder builder(@NonNull ViewProvider defaultViewProvider) { + return new Builder(defaultViewProvider); + } + + /** + * @param defaultViewProviderLayoutResId layout resource id to be used in default view provider + * @param defaultViewProviderTextViewId id of a TextView in specified layout + * @return Builder + * @see SimpleTextViewProvider + */ + @NonNull + public static Builder builder( + @LayoutRes int defaultViewProviderLayoutResId, + @IdRes int defaultViewProviderTextViewId) { + return new Builder(new SimpleTextViewProvider( + defaultViewProviderLayoutResId, + defaultViewProviderTextViewId)); + } + + public abstract void render(@NonNull ViewGroup group, @NonNull Markwon markwon, @NonNull String markdown); + + public abstract void render(@NonNull ViewGroup group, @NonNull Markwon markwon, @NonNull Node root); + + + public static class Builder { + + private final ViewProvider defaultViewProvider; + + private MarkwonReducer reducer; + private Map, ViewProvider> viewProviders; + private LayoutInflater inflater; + + public Builder(@NonNull ViewProvider defaultViewProvider) { + this.defaultViewProvider = defaultViewProvider; + this.viewProviders = new HashMap<>(3); + } + + @NonNull + public Builder reducer(@NonNull MarkwonReducer reducer) { + this.reducer = reducer; + return this; + } + + @NonNull + public Builder viewProvider( + @NonNull Class type, + @NonNull ViewProvider viewProvider) { + //noinspection unchecked + viewProviders.put(type, (ViewProvider) viewProvider); + return this; + } + + @NonNull + public Builder inflater(@NonNull LayoutInflater inflater) { + this.inflater = inflater; + return this; + } + + @NonNull + public MarkwonNodeRenderer build() { + if (reducer == null) { + reducer = MarkwonReducer.directChildren(); + } + return new Impl(this); + } + } + + public static class SimpleTextViewProvider implements ViewProvider { + + private final int layoutResId; + private final int textViewId; + + public SimpleTextViewProvider(@LayoutRes int layoutResId, @IdRes int textViewId) { + this.layoutResId = layoutResId; + this.textViewId = textViewId; + } + + @NonNull + @Override + public View provide( + @NonNull LayoutInflater inflater, + @NonNull ViewGroup group, + @NonNull Markwon markwon, + @NonNull Node node) { + final View view = inflater.inflate(layoutResId, group, false); + final TextView textView = view.findViewById(textViewId); + markwon.setParsedMarkdown(textView, markwon.render(node)); + return view; + } + } + + static class Impl extends MarkwonNodeRenderer { + + private final MarkwonReducer reducer; + private final Map, ViewProvider> viewProviders; + private final ViewProvider defaultViewProvider; + + private LayoutInflater inflater; + + Impl(@NonNull Builder builder) { + this.reducer = builder.reducer; + this.viewProviders = builder.viewProviders; + this.defaultViewProvider = builder.defaultViewProvider; + this.inflater = builder.inflater; + } + + @Override + public void render(@NonNull ViewGroup group, @NonNull Markwon markwon, @NonNull String markdown) { + render(group, markwon, markwon.parse(markdown)); + } + + @Override + public void render(@NonNull ViewGroup group, @NonNull Markwon markwon, @NonNull Node root) { + + final LayoutInflater inflater = ensureLayoutInflater(group.getContext()); + + ViewProvider viewProvider; + + for (Node node : reducer.reduce(root)) { + viewProvider = viewProvider(node); + group.addView(viewProvider.provide(inflater, group, markwon, node)); + } + } + + @NonNull + private LayoutInflater ensureLayoutInflater(@NonNull Context context) { + LayoutInflater inflater = this.inflater; + if (inflater == null) { + inflater = this.inflater = LayoutInflater.from(context); + } + return inflater; + } + + @NonNull + private ViewProvider viewProvider(@NonNull Node node) { + + // check for specific node view provider + final ViewProvider provider = viewProviders.get(node.getClass()); + if (provider != null) { + return provider; + } + + // if it's not present, then we can return a default one + return defaultViewProvider; + } + } + +} diff --git a/markwon-core/src/main/java/ru/noties/markwon/MarkwonReducer.java b/markwon-core/src/main/java/ru/noties/markwon/MarkwonReducer.java new file mode 100644 index 00000000..251cf1fe --- /dev/null +++ b/markwon-core/src/main/java/ru/noties/markwon/MarkwonReducer.java @@ -0,0 +1,57 @@ +package ru.noties.markwon; + +import android.support.annotation.NonNull; + +import org.commonmark.node.Node; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +/** + * @since 3.0.0 + */ +public abstract class MarkwonReducer { + + @NonNull + public static MarkwonReducer directChildren() { + return new DirectChildren(); + } + + @NonNull + public abstract List reduce(@NonNull Node node); + + + static class DirectChildren extends MarkwonReducer { + + @NonNull + @Override + public List reduce(@NonNull Node root) { + + final List list; + + // we will extract all blocks that are direct children of Document + Node node = root.getFirstChild(); + + // please note, that if there are no children -> we will return a list with + // single element (which was supplied) + if (node == null) { + list = Collections.singletonList(root); + } else { + + list = new ArrayList<>(); + + Node temp; + + while (node != null) { + list.add(node); + temp = node.getNext(); + node.unlink(); + node = temp; + } + } + + return list; + } + } +} diff --git a/markwon-core/src/main/java/ru/noties/markwon/html/MarkwonHtmlRenderer.java b/markwon-core/src/main/java/ru/noties/markwon/html/MarkwonHtmlRenderer.java index 234d53ac..19d355f0 100644 --- a/markwon-core/src/main/java/ru/noties/markwon/html/MarkwonHtmlRenderer.java +++ b/markwon-core/src/main/java/ru/noties/markwon/html/MarkwonHtmlRenderer.java @@ -42,16 +42,10 @@ public abstract class MarkwonHtmlRenderer { Builder allowNonClosedTags(boolean allowNonClosedTags); @NonNull - Builder setHandler(@NonNull String tagName, @NonNull TagHandler tagHandler); + Builder setHandler(@NonNull String tagName, @Nullable TagHandler tagHandler); @NonNull - Builder setHandler(@NonNull Collection tagNames, @NonNull TagHandler tagHandler); - - @NonNull - Builder removeHandler(@NonNull String tagName); - - @NonNull - Builder removeHandlers(@NonNull String... tagNames); + Builder setHandler(@NonNull Collection tagNames, @Nullable TagHandler tagHandler); @NonNull MarkwonHtmlRenderer build(); diff --git a/markwon-core/src/main/java/ru/noties/markwon/html/MarkwonHtmlRendererImpl.java b/markwon-core/src/main/java/ru/noties/markwon/html/MarkwonHtmlRendererImpl.java index 7547e1cc..973342e2 100644 --- a/markwon-core/src/main/java/ru/noties/markwon/html/MarkwonHtmlRendererImpl.java +++ b/markwon-core/src/main/java/ru/noties/markwon/html/MarkwonHtmlRendererImpl.java @@ -100,36 +100,26 @@ class MarkwonHtmlRendererImpl extends MarkwonHtmlRenderer { @NonNull @Override - public Builder setHandler(@NonNull String tagName, @NonNull TagHandler tagHandler) { - tagHandlers.put(tagName, tagHandler); - return this; - } - - @NonNull - @Override - public Builder setHandler(@NonNull Collection tagNames, @NonNull TagHandler tagHandler) { - for (String tagName : tagNames) { - if (tagName != null) { - tagHandlers.put(tagName, tagHandler); - } + public Builder setHandler(@NonNull String tagName, @Nullable TagHandler tagHandler) { + if (tagHandler == null) { + tagHandlers.remove(tagName); + } else { + tagHandlers.put(tagName, tagHandler); } return this; } @NonNull @Override - public Builder removeHandler(@NonNull String tagName) { - tagHandlers.remove(tagName); - return this; - } - - @NonNull - @Override - public Builder removeHandlers(@NonNull String... tagNames) { - for (String tagName : tagNames) { - if (tagName != null) { + public Builder setHandler(@NonNull Collection tagNames, @Nullable TagHandler tagHandler) { + if (tagHandler == null) { + for (String tagName : tagNames) { tagHandlers.remove(tagName); } + } else { + for (String tagName : tagNames) { + tagHandlers.put(tagName, tagHandler); + } } return this; } diff --git a/markwon-ext-tasklist/src/main/java/ru/noties/markwon/ext/tasklist/TaskListPlugin.java b/markwon-ext-tasklist/src/main/java/ru/noties/markwon/ext/tasklist/TaskListPlugin.java index 5a787dc9..9f74c206 100644 --- a/markwon-ext-tasklist/src/main/java/ru/noties/markwon/ext/tasklist/TaskListPlugin.java +++ b/markwon-ext-tasklist/src/main/java/ru/noties/markwon/ext/tasklist/TaskListPlugin.java @@ -15,6 +15,7 @@ import ru.noties.markwon.AbstractMarkwonPlugin; import ru.noties.markwon.MarkwonSpansFactory; import ru.noties.markwon.MarkwonVisitor; import ru.noties.markwon.RenderProps; +import ru.noties.markwon.core.SimpleBlockNodeVisitor; /** * @since 3.0.0 @@ -75,20 +76,7 @@ public class TaskListPlugin extends AbstractMarkwonPlugin { @Override public void configureVisitor(@NonNull MarkwonVisitor.Builder builder) { builder - .on(TaskListBlock.class, new MarkwonVisitor.NodeVisitor() { - @Override - public void visit(@NonNull MarkwonVisitor visitor, @NonNull TaskListBlock taskListBlock) { - - visitor.ensureNewLine(); - - visitor.visitChildren(taskListBlock); - - if (visitor.hasNext(taskListBlock)) { - visitor.ensureNewLine(); - visitor.forceNewLine(); - } - } - }) + .on(TaskListBlock.class, new SimpleBlockNodeVisitor()) .on(TaskListItem.class, new MarkwonVisitor.NodeVisitor() { @Override public void visit(@NonNull MarkwonVisitor visitor, @NonNull TaskListItem taskListItem) { diff --git a/markwon-recycler/src/main/java/ru/noties/markwon/recycler/MarkwonAdapter.java b/markwon-recycler/src/main/java/ru/noties/markwon/recycler/MarkwonAdapter.java index 5d6ddc8b..d288a44c 100644 --- a/markwon-recycler/src/main/java/ru/noties/markwon/recycler/MarkwonAdapter.java +++ b/markwon-recycler/src/main/java/ru/noties/markwon/recycler/MarkwonAdapter.java @@ -14,6 +14,7 @@ import org.commonmark.node.Node; import java.util.List; import ru.noties.markwon.Markwon; +import ru.noties.markwon.MarkwonReducer; /** * Adapter to display markdown in a RecyclerView. It is done by extracting root blocks from a @@ -92,7 +93,7 @@ public abstract class MarkwonAdapter extends RecyclerView.Adapterreduced to a list of nodes. There is a default - * {@link Reducer} that will be used if not provided explicitly (there is no need to + * {@link MarkwonReducer} that will be used if not provided explicitly (there is no need to * register your own unless you require it). * - * @param reducer {@link Reducer} + * @param reducer {@link MarkwonReducer} * @return self - * @see Reducer + * @see MarkwonReducer */ @NonNull - Builder reducer(@NonNull Reducer reducer); + Builder reducer(@NonNull MarkwonReducer reducer); /** * @return {@link MarkwonAdapter} @@ -169,12 +170,6 @@ public abstract class MarkwonAdapter extends RecyclerView.Adapter reduce(@NonNull Node root); - } - public abstract void setMarkdown(@NonNull Markwon markwon, @NonNull String markdown); public abstract void setParsedMarkdown(@NonNull Markwon markwon, @NonNull Node document); diff --git a/markwon-recycler/src/main/java/ru/noties/markwon/recycler/MarkwonAdapterImpl.java b/markwon-recycler/src/main/java/ru/noties/markwon/recycler/MarkwonAdapterImpl.java index 0e29c2d4..70406e78 100644 --- a/markwon-recycler/src/main/java/ru/noties/markwon/recycler/MarkwonAdapterImpl.java +++ b/markwon-recycler/src/main/java/ru/noties/markwon/recycler/MarkwonAdapterImpl.java @@ -7,17 +7,17 @@ import android.view.ViewGroup; import org.commonmark.node.Node; -import java.util.ArrayList; import java.util.Collections; import java.util.List; import ru.noties.markwon.Markwon; +import ru.noties.markwon.MarkwonReducer; class MarkwonAdapterImpl extends MarkwonAdapter { private final SparseArray> entries; private final Entry defaultEntry; - private final Reducer reducer; + private final MarkwonReducer reducer; private LayoutInflater layoutInflater; @@ -28,7 +28,7 @@ class MarkwonAdapterImpl extends MarkwonAdapter { MarkwonAdapterImpl( @NonNull SparseArray> entries, @NonNull Entry defaultEntry, - @NonNull Reducer reducer) { + @NonNull MarkwonReducer reducer) { this.entries = entries; this.defaultEntry = defaultEntry; this.reducer = reducer; @@ -133,7 +133,7 @@ class MarkwonAdapterImpl extends MarkwonAdapter { private final SparseArray> entries = new SparseArray<>(3); private Entry defaultEntry; - private Reducer reducer; + private MarkwonReducer reducer; @NonNull @Override @@ -163,7 +163,7 @@ class MarkwonAdapterImpl extends MarkwonAdapter { @NonNull @Override - public Builder reducer(@NonNull Reducer reducer) { + public Builder reducer(@NonNull MarkwonReducer reducer) { this.reducer = reducer; return this; } @@ -178,33 +178,10 @@ class MarkwonAdapterImpl extends MarkwonAdapter { } if (reducer == null) { - reducer = new ReducerImpl(); + reducer = MarkwonReducer.directChildren(); } return new MarkwonAdapterImpl(entries, defaultEntry, reducer); } } - - static class ReducerImpl implements Reducer { - - @NonNull - @Override - public List reduce(@NonNull Node root) { - - final List list = new ArrayList<>(); - - // we will extract all blocks that are direct children of Document - Node node = root.getFirstChild(); - Node temp; - - while (node != null) { - list.add(node); - temp = node.getNext(); - node.unlink(); - node = temp; - } - - return list; - } - } } diff --git a/sample/src/main/res/values/strings.xml b/sample/src/main/res/values/strings.xml index d1ec3ddc..5505eb0c 100644 --- a/sample/src/main/res/values/strings.xml +++ b/sample/src/main/res/values/strings.xml @@ -2,12 +2,13 @@ MarkwonSample + From 0a54ebbd6ffc838108a77a177e20b2e0ee12ff13 Mon Sep 17 00:00:00 2001 From: Dimitry Ivanov Date: Sun, 3 Mar 2019 16:33:39 +0300 Subject: [PATCH 092/103] Documentation for task list extension --- docs/docs/v3/ext-tasklist/README.md | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/docs/docs/v3/ext-tasklist/README.md b/docs/docs/v3/ext-tasklist/README.md index a955500a..7dde3d9a 100644 --- a/docs/docs/v3/ext-tasklist/README.md +++ b/docs/docs/v3/ext-tasklist/README.md @@ -1,3 +1,10 @@ # Task list extension - \ No newline at end of file + + +Adds support for GFM (Github-flavored markdown) task-lists: + +```java +Markwon.builder(context) + .usePlugin(TaskListPlugin.create(context)); +``` \ No newline at end of file From cd8016eb68d5006748bedd0551ff17d575c5adda Mon Sep 17 00:00:00 2001 From: Dimitry Ivanov Date: Mon, 11 Mar 2019 15:45:32 +0300 Subject: [PATCH 093/103] Add CustomTypefaceSpan --- .../core/spans/CustomTypefaceSpan.java | 43 +++++++++++++++++++ 1 file changed, 43 insertions(+) create mode 100644 markwon-core/src/main/java/ru/noties/markwon/core/spans/CustomTypefaceSpan.java diff --git a/markwon-core/src/main/java/ru/noties/markwon/core/spans/CustomTypefaceSpan.java b/markwon-core/src/main/java/ru/noties/markwon/core/spans/CustomTypefaceSpan.java new file mode 100644 index 00000000..99ae2111 --- /dev/null +++ b/markwon-core/src/main/java/ru/noties/markwon/core/spans/CustomTypefaceSpan.java @@ -0,0 +1,43 @@ +package ru.noties.markwon.core.spans; + +import android.graphics.Typeface; +import android.support.annotation.NonNull; +import android.text.TextPaint; +import android.text.style.MetricAffectingSpan; + +/** + * A span implementation that allow applying custom Typeface. Although it is + * not used directly by the library, it\'s helpful for customizations. + *

+ * Please note that this implementation does not validate current paint state + * and won\'t be updating/modifying supplied Typeface. + * + * @since 3.0.0 + */ +public class CustomTypefaceSpan extends MetricAffectingSpan { + + @NonNull + public static CustomTypefaceSpan create(@NonNull Typeface typeface) { + return new CustomTypefaceSpan(typeface); + } + + private final Typeface typeface; + + public CustomTypefaceSpan(@NonNull Typeface typeface) { + this.typeface = typeface; + } + + @Override + public void updateMeasureState(@NonNull TextPaint p) { + updatePaint(p); + } + + @Override + public void updateDrawState(TextPaint tp) { + updatePaint(tp); + } + + private void updatePaint(@NonNull TextPaint paint) { + paint.setTypeface(typeface); + } +} From 6d2881721530a19eb5b71f542a3d4b6ea09a9daa Mon Sep 17 00:00:00 2001 From: Dimitry Ivanov Date: Wed, 13 Mar 2019 16:30:43 +0300 Subject: [PATCH 094/103] Add recycler-table module --- docs/.vuepress/.artifacts.js | 2 +- docs/.vuepress/config.js | 1 + .../assets/recycler-table-screenshot.png | Bin 0 -> 78822 bytes docs/docs/v3/ext-tables/README.md | 7 +- docs/docs/v3/ext-tasklist/README.md | 136 +++++ docs/docs/v3/install.md | 56 -- docs/docs/v3/recycler-table/README.md | 90 +++ markwon-bundle.gradle | 59 -- .../main/java/ru/noties/markwon/Markwon.java | 4 + .../java/ru/noties/markwon/MarkwonImpl.java | 15 +- .../ru/noties/markwon/MarkwonReducer.java | 4 + .../noties/markwon/MarkwonSpansFactory.java | 6 + .../markwon/MarkwonSpansFactoryImpl.java | 6 + .../markwon/utils/NoCopySpannableFactory.java | 30 + .../markwon/ext/tables/TablePlugin.java | 7 + .../noties/markwon/ext/tables/TableTheme.java | 30 +- markwon-recycler-table/build.gradle | 29 + markwon-recycler-table/gradle.properties | 4 + .../src/main/AndroidManifest.xml | 1 + .../recycler/table/TableBorderDrawable.java | 48 ++ .../markwon/recycler/table/TableEntry.java | 530 ++++++++++++++++++ .../recycler/table/TableEntryPlugin.java | 65 +++ .../recycler/table/TableEntryTheme.java | 67 +++ .../recycler/table/TableEntryTest.java | 103 ++++ .../markwon/recycler/MarkwonAdapter.java | 123 ++-- .../markwon/recycler/MarkwonAdapterImpl.java | 37 +- .../noties/markwon/recycler/SimpleEntry.java | 61 +- .../layout/markwon_adapter_simple_entry.xml | 9 - sample/build.gradle | 9 +- sample/src/main/AndroidManifest.xml | 4 +- sample/src/main/assets/README.md | 1 + .../sample/recycler/RecyclerActivity.java | 33 +- .../markwon/sample/recycler/TableEntry.java | 67 --- .../markwon/sample/recycler/TableEntry2.java | 121 ---- .../sample/recycler/TableEntryView.java | 219 -------- sample/src/main/recycler/assets/README.md | 1 - .../res/layout/adapter_table_block.xml | 21 - sample/src/main/recycler/res/values/attrs.xml | 9 - .../drawable-v26/ic_launcher_background.xml | 24 - .../res/layout/activity_recycler.xml | 0 .../res/layout/adapter_default_entry.xml | 0 .../res/layout/adapter_fenced_code_block.xml | 0 .../layout/adapter_table_block.xml} | 3 +- .../res/layout/view_table_entry_cell.xml | 4 +- .../res/layout/view_table_entry_row.xml | 0 .../res/mipmap-anydpi-v26/ic_launcher.xml | 5 - .../ic_launcher_foreground.png | Bin 3366 -> 0 bytes .../src/main/res/mipmap-hdpi/ic_launcher.png | Bin 2381 -> 0 bytes .../ic_launcher_foreground.png | Bin 4727 -> 0 bytes .../src/main/res/mipmap-xhdpi/ic_launcher.png | Bin 3319 -> 0 bytes .../ic_launcher_foreground.png | Bin 7773 -> 0 bytes .../main/res/mipmap-xxhdpi/ic_launcher.png | Bin 5093 -> 0 bytes .../ic_launcher_foreground.png | Bin 11195 -> 0 bytes .../main/res/mipmap-xxxhdpi/ic_launcher.png | Bin 6783 -> 0 bytes settings.gradle | 1 + 55 files changed, 1298 insertions(+), 754 deletions(-) create mode 100644 docs/.vuepress/public/assets/recycler-table-screenshot.png create mode 100644 docs/docs/v3/recycler-table/README.md delete mode 100644 markwon-bundle.gradle create mode 100644 markwon-core/src/main/java/ru/noties/markwon/utils/NoCopySpannableFactory.java create mode 100644 markwon-recycler-table/build.gradle create mode 100644 markwon-recycler-table/gradle.properties create mode 100644 markwon-recycler-table/src/main/AndroidManifest.xml create mode 100644 markwon-recycler-table/src/main/java/ru/noties/markwon/recycler/table/TableBorderDrawable.java create mode 100644 markwon-recycler-table/src/main/java/ru/noties/markwon/recycler/table/TableEntry.java create mode 100644 markwon-recycler-table/src/main/java/ru/noties/markwon/recycler/table/TableEntryPlugin.java create mode 100644 markwon-recycler-table/src/main/java/ru/noties/markwon/recycler/table/TableEntryTheme.java create mode 100644 markwon-recycler-table/src/test/java/ru/noties/markwon/recycler/table/TableEntryTest.java delete mode 100644 markwon-recycler/src/main/res/layout/markwon_adapter_simple_entry.xml create mode 120000 sample/src/main/assets/README.md delete mode 100644 sample/src/main/java/ru/noties/markwon/sample/recycler/TableEntry.java delete mode 100644 sample/src/main/java/ru/noties/markwon/sample/recycler/TableEntry2.java delete mode 100644 sample/src/main/java/ru/noties/markwon/sample/recycler/TableEntryView.java delete mode 120000 sample/src/main/recycler/assets/README.md delete mode 100644 sample/src/main/recycler/res/layout/adapter_table_block.xml delete mode 100644 sample/src/main/recycler/res/values/attrs.xml delete mode 100644 sample/src/main/res/drawable-v26/ic_launcher_background.xml rename sample/src/main/{recycler => }/res/layout/activity_recycler.xml (100%) rename sample/src/main/{recycler => }/res/layout/adapter_default_entry.xml (100%) rename sample/src/main/{recycler => }/res/layout/adapter_fenced_code_block.xml (100%) rename sample/src/main/{recycler/res/layout/adapter_table_block_2.xml => res/layout/adapter_table_block.xml} (83%) rename sample/src/main/{recycler => }/res/layout/view_table_entry_cell.xml (69%) rename sample/src/main/{recycler => }/res/layout/view_table_entry_row.xml (100%) delete mode 100644 sample/src/main/res/mipmap-anydpi-v26/ic_launcher.xml delete mode 100644 sample/src/main/res/mipmap-hdpi-v26/ic_launcher_foreground.png delete mode 100644 sample/src/main/res/mipmap-hdpi/ic_launcher.png delete mode 100644 sample/src/main/res/mipmap-xhdpi-v26/ic_launcher_foreground.png delete mode 100644 sample/src/main/res/mipmap-xhdpi/ic_launcher.png delete mode 100644 sample/src/main/res/mipmap-xxhdpi-v26/ic_launcher_foreground.png delete mode 100644 sample/src/main/res/mipmap-xxhdpi/ic_launcher.png delete mode 100644 sample/src/main/res/mipmap-xxxhdpi-v26/ic_launcher_foreground.png delete mode 100644 sample/src/main/res/mipmap-xxxhdpi/ic_launcher.png diff --git a/docs/.vuepress/.artifacts.js b/docs/.vuepress/.artifacts.js index 532f38d9..2a1c3f43 100644 --- a/docs/.vuepress/.artifacts.js +++ b/docs/.vuepress/.artifacts.js @@ -1,4 +1,4 @@ // this is a generated file, do not modify. To update it run 'collectArtifacts.js' script -const artifacts = [{"id":"core","name":"Core","group":"ru.noties.markwon","description":"Core Markwon artifact that includes basic markdown parsing and rendering"},{"id":"ext-latex","name":"LaTeX","group":"ru.noties.markwon","description":"Extension to add LaTeX formulas to Markwon markdown"},{"id":"ext-strikethrough","name":"Strikethrough","group":"ru.noties.markwon","description":"Extension to add strikethrough markup to Markwon markdown"},{"id":"ext-tables","name":"Tables","group":"ru.noties.markwon","description":"Extension to add tables markup (GFM) to Markwon markdown"},{"id":"ext-tasklist","name":"Task List","group":"ru.noties.markwon","description":"Extension to add task lists (GFM) to Markwon markdown"},{"id":"html","name":"HTML","group":"ru.noties.markwon","description":"Provides HTML parsing functionality"},{"id":"image-gif","name":"Image GIF","group":"ru.noties.markwon","description":"Adds GIF media support to Markwon markdown"},{"id":"image-okhttp","name":"Image OkHttp","group":"ru.noties.markwon","description":"Adds OkHttp client to retrieve images data from network"},{"id":"image-svg","name":"Image SVG","group":"ru.noties.markwon","description":"Adds SVG media support to Markwon markdown"},{"id":"recycler","name":"Recycler","group":"ru.noties.markwon","description":"Provides RecyclerView.Adapter to display Markwon markdown"},{"id":"syntax-highlight","name":"Syntax Highlight","group":"ru.noties.markwon","description":"Add syntax highlight to Markwon markdown via Prism4j library"}]; +const artifacts = [{"id":"core","name":"Core","group":"ru.noties.markwon","description":"Core Markwon artifact that includes basic markdown parsing and rendering"},{"id":"ext-latex","name":"LaTeX","group":"ru.noties.markwon","description":"Extension to add LaTeX formulas to Markwon markdown"},{"id":"ext-strikethrough","name":"Strikethrough","group":"ru.noties.markwon","description":"Extension to add strikethrough markup to Markwon markdown"},{"id":"ext-tables","name":"Tables","group":"ru.noties.markwon","description":"Extension to add tables markup (GFM) to Markwon markdown"},{"id":"ext-tasklist","name":"Task List","group":"ru.noties.markwon","description":"Extension to add task lists (GFM) to Markwon markdown"},{"id":"html","name":"HTML","group":"ru.noties.markwon","description":"Provides HTML parsing functionality"},{"id":"image-gif","name":"Image GIF","group":"ru.noties.markwon","description":"Adds GIF media support to Markwon markdown"},{"id":"image-okhttp","name":"Image OkHttp","group":"ru.noties.markwon","description":"Adds OkHttp client to retrieve images data from network"},{"id":"image-svg","name":"Image SVG","group":"ru.noties.markwon","description":"Adds SVG media support to Markwon markdown"},{"id":"recycler","name":"Recycler","group":"ru.noties.markwon","description":"Provides RecyclerView.Adapter to display Markwon markdown"},{"id":"recycler-table","name":"Recycler Table","group":"ru.noties.markwon","description":"Provides MarkwonAdapter.Entry to render TableBlocks inside Android-native TableLayout widget"},{"id":"syntax-highlight","name":"Syntax Highlight","group":"ru.noties.markwon","description":"Add syntax highlight to Markwon markdown via Prism4j library"}]; export { artifacts }; diff --git a/docs/.vuepress/config.js b/docs/.vuepress/config.js index 29ab8971..85ebf762 100644 --- a/docs/.vuepress/config.js +++ b/docs/.vuepress/config.js @@ -60,6 +60,7 @@ module.exports = { '/docs/v3/image/okhttp.md', '/docs/v3/image/svg.md', '/docs/v3/recycler/', + '/docs/v3/recycler-table/', '/docs/v3/syntax-highlight/', '/docs/v3/migration-2-3.md' ], diff --git a/docs/.vuepress/public/assets/recycler-table-screenshot.png b/docs/.vuepress/public/assets/recycler-table-screenshot.png new file mode 100644 index 0000000000000000000000000000000000000000..f609c65fedb6b9a6dc3ccfb35160ae677f13161a GIT binary patch literal 78822 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 LWMx=ja3cJl#b=Jg literal 0 HcmV?d00001 diff --git a/docs/docs/v3/ext-tables/README.md b/docs/docs/v3/ext-tables/README.md index a728645c..fe149ed2 100644 --- a/docs/docs/v3/ext-tables/README.md +++ b/docs/docs/v3/ext-tables/README.md @@ -54,8 +54,11 @@ final Table table = Table.parse(Markwon, TableBlock); myTableWidget.setTable(table); ``` -Unfortunately Markwon does not provide a widget that can be used for tables. But it does -provide API that can be used to achieve desired result. +:::tip +To take advantage of this functionality and render tables without limitations (including +horizontally scrollable layout when its contents exceed screen width), refer to [recycler-table](/docs/v3/recycler-table) +module documentation that adds support for rendering `TableBlock` markdown node inside Android-native `TableLayout` widget. +::: ## Theme diff --git a/docs/docs/v3/ext-tasklist/README.md b/docs/docs/v3/ext-tasklist/README.md index 7dde3d9a..3fb332f4 100644 --- a/docs/docs/v3/ext-tasklist/README.md +++ b/docs/docs/v3/ext-tasklist/README.md @@ -7,4 +7,140 @@ Adds support for GFM (Github-flavored markdown) task-lists: ```java Markwon.builder(context) .usePlugin(TaskListPlugin.create(context)); +``` + +--- + +Create a default instance of `TaskListPlugin` with `TaskListDrawable` initialized to use +`android.R.attr.textColorLink` as primary color and `android.R.attr.colorBackground` as background +```java +TaskListPlugin.create(context); +``` + +--- + +Create an instance of `TaskListPlugin` with exact color values to use: +```java +// obtain color values +final int checkedFillColor = /* */; +final int normalOutlineColor = /* */; +final int checkMarkColor = /* */; + +TaskListPlugin.create(checkedFillColor, normalOutlineColor, checkMarkColor); +``` + +--- + +Specify own drawable for a task list item: + +```java +// obtain drawable +final Drawable drawable = /* */; + +TaskListPlugin.create(drawable); +``` + +:::warning +Please note that custom drawable for a task list item must correctly handle state +in order to display done/not-done: + +```java +public class MyTaskListDrawable extends Drawable { + + private boolean isChecked; + + @Override + public void draw(@NonNull Canvas canvas) { + // draw accordingly to the isChecked value + } + + /* implementation omitted */ + + @Override + protected boolean onStateChange(int[] state) { + final boolean isChecked = contains(state, android.R.attr.state_checked); + final boolean result = this.isChecked != isChecked; + if (result) { + this.isChecked = isChecked; + } + return result; + } + + private static boolean contains(@Nullable int[] states, int value) { + if (states != null) { + for (int state : states) { + if (state == value) { + // NB return here + return true; + } + } + } + return false; + } +} +``` +::: + +## Task list mutation + +It is possible to mutate task list item state (toggle done/not-done). But note +that `Markwon` won't handle state change internally by any means and this change +is merely a visual one. If you need to persist state of a task list +item change you have to implement it yourself. This should get your started: + +```java +final Markwon markwon = Markwon.builder(context) + .usePlugin(TaskListPlugin.create(context)) + .usePlugin(new AbstractMarkwonPlugin() { + @Override + public void configureSpansFactory(@NonNull MarkwonSpansFactory.Builder builder) { + + // obtain original SpanFactory set by TaskListPlugin + final SpanFactory origin = builder.getFactory(TaskListItem.class); + if (origin == null) { + // or throw, as it's a bit weird state and we expect + // this factory to be present + return; + } + + builder.setFactory(TaskListItem.class, new SpanFactory() { + @Override + public Object getSpans(@NonNull MarkwonConfiguration configuration, @NonNull RenderProps props) { + // it's a bit non-secure behavior and we should validate + // the type of returned span first, but for the sake of brevity + // we skip this step + final TaskListSpan span = (TaskListSpan) origin.getSpans(configuration, props); + + if (span == null) { + // or throw + return null; + } + + // return an array of spans + return new Object[]{ + span, + new ClickableSpan() { + @Override + public void onClick(@NonNull View widget) { + // toggle VISUAL state + span.setDone(!span.isDone()); + + // do not forget to invalidate widget + widget.invalidate(); + + // execute your persistence logic + } + + @Override + public void updateDrawState(@NonNull TextPaint ds) { + // no-op, so appearance is not changed (otherwise + // task list item will look like a link) + } + } + }; + } + }); + } + }) + .build(); ``` \ No newline at end of file diff --git a/docs/docs/v3/install.md b/docs/docs/v3/install.md index 93b6de88..7f7184b7 100644 --- a/docs/docs/v3/install.md +++ b/docs/docs/v3/install.md @@ -10,62 +10,6 @@ next: /docs/v3/core/getting-started.md -# Bundle -If you wish to include all Markwon artifacts or add specific artifacts -in a different manner than explicit gradle dependency definition, you can -use `markwon-bundle.gradle` gradle script: - -*(in your `build.gradle`)* -```groovy -apply plugin: 'com.android.application' -apply from: 'https://raw.githubusercontent.com/noties/Markwon/master/markwon-bundle.gradle' - -android { /* */ } - -ext.markwon = [ - 'version': '3.0.0', - 'includeAll': true -] - -dependencies { /* */ } -``` - -`markwon` object can have these properties: -* `version` - (required) version of `Markwon` -* `includeAll` - if _true_ will add all known Markwon artifacts. Can be used with `exclude` -* * `exclude` - an array of artifacts to _exclude_ (cannot exclude `core`) -* `artifacts` - an array of artifacts (can omit `core`, as it will be added implicitly anyway) - -If `includeAll` property is present and is `true`, then `artifacts` property won't be used. -If there is no `includeAll` property or if it is `false`, `exclude` property won't be used. - -These 2 markwon objects are equal: - -```groovy -// #1 -ext.markwon = [ - 'version': '3.0.0', - 'artifacts': [ - 'ext-latex', - 'ext-strikethrough', - 'ext-tables', - 'ext-tasklist', - 'html', - 'image-gif', - 'image-okhttp', - 'image-svg', - 'recycler', - 'syntax-highlight' - ] -] - -// #2 -ext.markwon = [ - 'version': '3.0.0', - 'includeAll': true -] -``` - ## Snapshot In order to use latest `SNAPSHOT` version add snapshot repository diff --git a/docs/docs/v3/recycler-table/README.md b/docs/docs/v3/recycler-table/README.md new file mode 100644 index 00000000..0cd90583 --- /dev/null +++ b/docs/docs/v3/recycler-table/README.md @@ -0,0 +1,90 @@ +# Recycler Table + +Artifact that provides [MarkwonAdapter.Entry](/docs/v3/recycler/) to render `TableBlock` inside +Android-native `TableLayout` widget. + +screenshot +
+* It's possible to wrap `TableLayout` inside a `HorizontalScrollView` to include all table content + +--- + +Register instance of `TableEntry` with `MarkwonAdapter` to render TableBlocks: +```java +final MarkwonAdapter adapter = MarkwonAdapter.builder(R.layout.adapter_default_entry, R.id.text) + .include(TableBlock.class, TableEntry.create(builder -> builder + .tableLayout(R.layout.adapter_table_block, R.id.table_layout) + .textLayoutIsRoot(R.layout.view_table_entry_cell))) + .build(); +``` + +`TableEntry` requires at least 2 arguments: +* `tableLayout` - layout with `TableLayout` inside +* `textLayout` - layout with `TextView` inside (represents independent table cell) + +In case when required view is the root of layout specific builder methods can be used: +* `tableLayoutIsRoot(int)` +* `textLayoutIsRoot(int)` + +If your layouts have different structure (for example wrap a `TableView` inside a `HorizontalScrollView`) +then you should use methods that accept ID of required view inside layout: +* `tableLayout(int, int)` +* `textLayout(int, int)` + +--- + +To display `TableBlock` as a `TableLayout` specific `MarkwonPlugin` must be used: `TableEntryPlugin`. + +:::warning +Do not use `TablePlugin` if you wish to display markdown tables via `TableEntry`. Use **TableEntryPlugin** instead +::: + +`TableEntryPlugin` can reuse existing `TablePlugin` to make appearance of tables the same in both contexts: +when rendering _natively_ in a TextView and when rendering in RecyclerView with TableEntry. + +* `TableEntryPlugin.create(Context)` - creates plugin with default `TableTheme` +* `TableEntryPlugin.create(TableTheme)` - creates plugin with provided `TableTheme` +* `TableEntryPlugin.create(TablePlugin.ThemeConfigure)` - creates plugin with theme configured by `ThemeConfigure` +* `TableEntryPlugin.create(TablePlugin)` - creates plugin with `TableTheme` used in provided `TablePlugin` + +```java +final Markwon markwon = Markwon.builder(context) + .usePlugin(TableEntryPlugin.create(context)) + // other plugins + .build(); +``` + +```java +final Markwon markwon = Markwon.builder(context) + .usePlugin(TableEntryPlugin.create(builder -> builder + .tableBorderWidth(0) + .tableHeaderRowBackgroundColor(Color.RED))) + // other plugins + .build(); +``` + +## Table with scrollable content + +To stretch table columns to fit the width of screen or to make table scrollable when content exceeds screen width +this layout can be used: + +```xml + + + + + +``` \ No newline at end of file diff --git a/markwon-bundle.gradle b/markwon-bundle.gradle deleted file mode 100644 index 603404d6..00000000 --- a/markwon-bundle.gradle +++ /dev/null @@ -1,59 +0,0 @@ -// await project initialization and check for markwon object then -// (so we do not have to force users to put `apply from` block at the bottom -// of a build.gradle file) -project.afterEvaluate { - - if (!project.hasProperty('markwon')) { - throw new RuntimeException("No `markwon` property object is found. " + - "Define it with `ext.markwon = [prop: value]`") - } - - final def markwon = project.markwon - if (!(markwon instanceof Map)) { - throw new RuntimeException("`markwon` object property must be of type Map. " + - "Groovy short-hand to define: `[:]`.") - } - - final def version = markwon.version - final def includeAll = markwon.computeIfAbsent('includeAll', { false }) - final def artifacts - if (includeAll) { - - // cannot exclude core - final def exclude = markwon.computeIfAbsent('exclude', { [] }) \ - .unique() \ - .findAll { 'core' != it } - - artifacts = [ - 'core', - 'ext-latex', - 'ext-strikethrough', - 'ext-tables', - 'ext-tasklist', - 'html', - 'image-gif', - 'image-okhttp', - 'image-svg', - 'recycler', - 'syntax-highlight' - ].findAll { !exclude.contains(it) } - - } else { - artifacts = (markwon.containsKey('artifacts') ? markwon.artifacts : ['core']).with { - // add implicit core artifact - if (!it.contains('core')) { - it.add('core') - } - return it - } - } - - if (!version) { - throw new RuntimeException("Please specify version of Markwon, for example: " + - "`ext.markwon = [ 'version': '1.0.0']`") - } - - artifacts.forEach { - project.dependencies.add('implementation', "ru.noties.markwon:$it:$version") - } -} \ No newline at end of file diff --git a/markwon-core/src/main/java/ru/noties/markwon/Markwon.java b/markwon-core/src/main/java/ru/noties/markwon/Markwon.java index 6e7e1ba3..0ac110ff 100644 --- a/markwon-core/src/main/java/ru/noties/markwon/Markwon.java +++ b/markwon-core/src/main/java/ru/noties/markwon/Markwon.java @@ -2,6 +2,7 @@ package ru.noties.markwon; import android.content.Context; import android.support.annotation.NonNull; +import android.support.annotation.Nullable; import android.text.Spanned; import android.widget.TextView; @@ -101,6 +102,9 @@ public abstract class Markwon { */ public abstract boolean hasPlugin(@NonNull Class plugin); + @Nullable + public abstract

P getPlugin(@NonNull Class

type); + /** * Builder for {@link Markwon}. *

diff --git a/markwon-core/src/main/java/ru/noties/markwon/MarkwonImpl.java b/markwon-core/src/main/java/ru/noties/markwon/MarkwonImpl.java index a51bd62a..e90b7898 100644 --- a/markwon-core/src/main/java/ru/noties/markwon/MarkwonImpl.java +++ b/markwon-core/src/main/java/ru/noties/markwon/MarkwonImpl.java @@ -1,6 +1,7 @@ package ru.noties.markwon; import android.support.annotation.NonNull; +import android.support.annotation.Nullable; import android.text.Spanned; import android.widget.TextView; @@ -91,13 +92,19 @@ class MarkwonImpl extends Markwon { @Override public boolean hasPlugin(@NonNull Class type) { - boolean result = false; + return getPlugin(type) != null; + } + + @Nullable + @Override + public

P getPlugin(@NonNull Class

type) { + MarkwonPlugin out = null; for (MarkwonPlugin plugin : plugins) { if (type.isAssignableFrom(plugin.getClass())) { - result = true; - break; + out = plugin; } } - return result; + //noinspection unchecked + return (P) out; } } diff --git a/markwon-core/src/main/java/ru/noties/markwon/MarkwonReducer.java b/markwon-core/src/main/java/ru/noties/markwon/MarkwonReducer.java index 251cf1fe..a139e3f1 100644 --- a/markwon-core/src/main/java/ru/noties/markwon/MarkwonReducer.java +++ b/markwon-core/src/main/java/ru/noties/markwon/MarkwonReducer.java @@ -13,6 +13,10 @@ import java.util.List; */ public abstract class MarkwonReducer { + /** + * @return direct children of supplied Node. In the most usual case + * will return all BlockNodes of a Document + */ @NonNull public static MarkwonReducer directChildren() { return new DirectChildren(); diff --git a/markwon-core/src/main/java/ru/noties/markwon/MarkwonSpansFactory.java b/markwon-core/src/main/java/ru/noties/markwon/MarkwonSpansFactory.java index dbc6960d..8cf25a28 100644 --- a/markwon-core/src/main/java/ru/noties/markwon/MarkwonSpansFactory.java +++ b/markwon-core/src/main/java/ru/noties/markwon/MarkwonSpansFactory.java @@ -34,6 +34,12 @@ public interface MarkwonSpansFactory { @NonNull Builder setFactory(@NonNull Class node, @Nullable SpanFactory factory); + /** + * Can be useful when enhancing an already defined SpanFactory with another one. + */ + @Nullable + SpanFactory getFactory(@NonNull Class node); + @NonNull MarkwonSpansFactory build(); } diff --git a/markwon-core/src/main/java/ru/noties/markwon/MarkwonSpansFactoryImpl.java b/markwon-core/src/main/java/ru/noties/markwon/MarkwonSpansFactoryImpl.java index f3cd0dee..ef1906d8 100644 --- a/markwon-core/src/main/java/ru/noties/markwon/MarkwonSpansFactoryImpl.java +++ b/markwon-core/src/main/java/ru/noties/markwon/MarkwonSpansFactoryImpl.java @@ -52,6 +52,12 @@ class MarkwonSpansFactoryImpl implements MarkwonSpansFactory { return this; } + @Nullable + @Override + public SpanFactory getFactory(@NonNull Class node) { + return factories.get(node); + } + @NonNull @Override public MarkwonSpansFactory build() { diff --git a/markwon-core/src/main/java/ru/noties/markwon/utils/NoCopySpannableFactory.java b/markwon-core/src/main/java/ru/noties/markwon/utils/NoCopySpannableFactory.java new file mode 100644 index 00000000..f5eedc2a --- /dev/null +++ b/markwon-core/src/main/java/ru/noties/markwon/utils/NoCopySpannableFactory.java @@ -0,0 +1,30 @@ +package ru.noties.markwon.utils; + +import android.support.annotation.NonNull; +import android.text.Spannable; +import android.text.SpannableString; + +/** + * Utility SpannableFactory that re-uses Spannable instance between multiple + * `TextView#setText` calls. + * + * @since 3.0.0 + */ +public class NoCopySpannableFactory extends Spannable.Factory { + + @NonNull + public static NoCopySpannableFactory getInstance() { + return Holder.INSTANCE; + } + + @Override + public Spannable newSpannable(CharSequence source) { + return source instanceof Spannable + ? (Spannable) source + : new SpannableString(source); + } + + static class Holder { + private static final NoCopySpannableFactory INSTANCE = new NoCopySpannableFactory(); + } +} diff --git a/markwon-ext-tables/src/main/java/ru/noties/markwon/ext/tables/TablePlugin.java b/markwon-ext-tables/src/main/java/ru/noties/markwon/ext/tables/TablePlugin.java index ece065bf..f05a84fc 100644 --- a/markwon-ext-tables/src/main/java/ru/noties/markwon/ext/tables/TablePlugin.java +++ b/markwon-ext-tables/src/main/java/ru/noties/markwon/ext/tables/TablePlugin.java @@ -54,13 +54,20 @@ public class TablePlugin extends AbstractMarkwonPlugin { return new TablePlugin(builder.build()); } + private final TableTheme theme; private final TableVisitor visitor; @SuppressWarnings("WeakerAccess") TablePlugin(@NonNull TableTheme tableTheme) { + this.theme = tableTheme; this.visitor = new TableVisitor(tableTheme); } + @NonNull + public TableTheme theme() { + return theme; + } + @Override public void configureParser(@NonNull Parser.Builder builder) { builder.extensions(Collections.singleton(TablesExtension.create())); diff --git a/markwon-ext-tables/src/main/java/ru/noties/markwon/ext/tables/TableTheme.java b/markwon-ext-tables/src/main/java/ru/noties/markwon/ext/tables/TableTheme.java index 6752e729..e9b1bd47 100644 --- a/markwon-ext-tables/src/main/java/ru/noties/markwon/ext/tables/TableTheme.java +++ b/markwon-ext-tables/src/main/java/ru/noties/markwon/ext/tables/TableTheme.java @@ -13,15 +13,19 @@ public class TableTheme { @NonNull public static TableTheme create(@NonNull Context context) { - final Dip dip = Dip.create(context); - return builder() - .tableCellPadding(dip.toPx(4)) - .tableBorderWidth(dip.toPx(1)) - .build(); + return buildWithDefaults(context).build(); } @NonNull - public static Builder builder() { + public static Builder buildWithDefaults(@NonNull Context context) { + final Dip dip = Dip.create(context); + return emptyBuilder() + .tableCellPadding(dip.toPx(4)) + .tableBorderWidth(dip.toPx(1)); + } + + @NonNull + public static Builder emptyBuilder() { return new Builder(); } @@ -58,6 +62,20 @@ public class TableTheme { this.tableHeaderRowBackgroundColor = builder.tableHeaderRowBackgroundColor; } + /** + * @since 3.0.0 + */ + @NonNull + public Builder asBuilder() { + return new Builder() + .tableCellPadding(tableCellPadding) + .tableBorderColor(tableBorderColor) + .tableBorderWidth(tableBorderWidth) + .tableOddRowBackgroundColor(tableOddRowBackgroundColor) + .tableEvenRowBackgroundColor(tableEvenRowBackgroundColor) + .tableHeaderRowBackgroundColor(tableHeaderRowBackgroundColor); + } + public int tableCellPadding() { return tableCellPadding; } diff --git a/markwon-recycler-table/build.gradle b/markwon-recycler-table/build.gradle new file mode 100644 index 00000000..841c2b60 --- /dev/null +++ b/markwon-recycler-table/build.gradle @@ -0,0 +1,29 @@ +apply plugin: 'com.android.library' + +android { + + compileSdkVersion config['compile-sdk'] + buildToolsVersion config['build-tools'] + + defaultConfig { + minSdkVersion config['min-sdk'] + targetSdkVersion config['target-sdk'] + versionCode 1 + versionName version + } +} + +dependencies { + + api project(':markwon-core') + api project(':markwon-recycler') + api project(':markwon-ext-tables') + + deps['test'].with { + testImplementation it['junit'] + testImplementation it['robolectric'] + testImplementation it['mockito'] + } +} + +registerArtifact(this) \ No newline at end of file diff --git a/markwon-recycler-table/gradle.properties b/markwon-recycler-table/gradle.properties new file mode 100644 index 00000000..8e87be51 --- /dev/null +++ b/markwon-recycler-table/gradle.properties @@ -0,0 +1,4 @@ +POM_NAME=Recycler Table +POM_ARTIFACT_ID=recycler-table +POM_DESCRIPTION=Provides MarkwonAdapter.Entry to render TableBlocks inside Android-native TableLayout widget +POM_PACKAGING=aar \ No newline at end of file diff --git a/markwon-recycler-table/src/main/AndroidManifest.xml b/markwon-recycler-table/src/main/AndroidManifest.xml new file mode 100644 index 00000000..b215e1f6 --- /dev/null +++ b/markwon-recycler-table/src/main/AndroidManifest.xml @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/markwon-recycler-table/src/main/java/ru/noties/markwon/recycler/table/TableBorderDrawable.java b/markwon-recycler-table/src/main/java/ru/noties/markwon/recycler/table/TableBorderDrawable.java new file mode 100644 index 00000000..06a89f24 --- /dev/null +++ b/markwon-recycler-table/src/main/java/ru/noties/markwon/recycler/table/TableBorderDrawable.java @@ -0,0 +1,48 @@ +package ru.noties.markwon.recycler.table; + +import android.graphics.Canvas; +import android.graphics.ColorFilter; +import android.graphics.Paint; +import android.graphics.PixelFormat; +import android.graphics.drawable.Drawable; +import android.support.annotation.ColorInt; +import android.support.annotation.NonNull; +import android.support.annotation.Nullable; +import android.support.annotation.Px; + +class TableBorderDrawable extends Drawable { + + private final Paint paint = new Paint(Paint.ANTI_ALIAS_FLAG); + + TableBorderDrawable() { + paint.setStyle(Paint.Style.STROKE); + } + + @Override + public void draw(@NonNull Canvas canvas) { + if (paint.getStrokeWidth() > 0) { + canvas.drawRect(getBounds(), paint); + } + } + + @Override + public void setAlpha(int alpha) { + // no op + } + + @Override + public void setColorFilter(@Nullable ColorFilter colorFilter) { + // no op + } + + @Override + public int getOpacity() { + return PixelFormat.OPAQUE; + } + + void update(@Px int borderWidth, @ColorInt int color) { + paint.setStrokeWidth(borderWidth); + paint.setColor(color); + invalidateSelf(); + } +} diff --git a/markwon-recycler-table/src/main/java/ru/noties/markwon/recycler/table/TableEntry.java b/markwon-recycler-table/src/main/java/ru/noties/markwon/recycler/table/TableEntry.java new file mode 100644 index 00000000..5ff8b25e --- /dev/null +++ b/markwon-recycler-table/src/main/java/ru/noties/markwon/recycler/table/TableEntry.java @@ -0,0 +1,530 @@ +package ru.noties.markwon.recycler.table; + +import android.annotation.SuppressLint; +import android.content.Context; +import android.content.res.Resources; +import android.graphics.drawable.Drawable; +import android.support.annotation.ColorInt; +import android.support.annotation.IdRes; +import android.support.annotation.LayoutRes; +import android.support.annotation.NonNull; +import android.support.annotation.Px; +import android.support.annotation.VisibleForTesting; +import android.view.Gravity; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.TableLayout; +import android.widget.TableRow; +import android.widget.TextView; + +import org.commonmark.ext.gfm.tables.TableBlock; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import ru.noties.markwon.Markwon; +import ru.noties.markwon.ext.tables.Table; +import ru.noties.markwon.recycler.MarkwonAdapter; +import ru.noties.markwon.utils.NoCopySpannableFactory; + +/** + * @since 3.0.0 + */ +public class TableEntry extends MarkwonAdapter.Entry { + + public interface Builder { + + /** + * @param tableLayoutResId layout with TableLayout + * @param tableIdRes id of the TableLayout inside specified layout + * @see #tableLayoutIsRoot(int) + */ + @NonNull + Builder tableLayout(@LayoutRes int tableLayoutResId, @IdRes int tableIdRes); + + /** + * @param tableLayoutResId layout with TableLayout as the root view + * @see #tableLayout(int, int) + */ + @NonNull + Builder tableLayoutIsRoot(@LayoutRes int tableLayoutResId); + + /** + * @param textLayoutResId layout with TextView + * @param textIdRes id of the TextView inside specified layout + * @see #textLayoutIsRoot(int) + */ + @NonNull + Builder textLayout(@LayoutRes int textLayoutResId, @IdRes int textIdRes); + + /** + * @param textLayoutResId layout with TextView as the root view + * @see #textLayout(int, int) + */ + @NonNull + Builder textLayoutIsRoot(@LayoutRes int textLayoutResId); + + /** + * @param cellTextCenterVertical if text inside a table cell should centered + * vertically (by default `true`) + */ + @NonNull + Builder cellTextCenterVertical(boolean cellTextCenterVertical); + + /** + * @param isRecyclable flag to set on RecyclerView.ViewHolder (by default `true`) + */ + @NonNull + Builder isRecyclable(boolean isRecyclable); + + @NonNull + TableEntry build(); + } + + public interface BuilderConfigure { + void configure(@NonNull Builder builder); + } + + @NonNull + public static Builder builder() { + return new BuilderImpl(); + } + + @NonNull + public static TableEntry create(@NonNull BuilderConfigure configure) { + final Builder builder = builder(); + configure.configure(builder); + return builder.build(); + } + + private final int tableLayoutResId; + private final int tableIdRes; + + private final int textLayoutResId; + private final int textIdRes; + + private final boolean isRecyclable; + private final boolean cellTextCenterVertical; // by default true + + private LayoutInflater inflater; + + private final Map map = new HashMap<>(3); + + TableEntry( + @LayoutRes int tableLayoutResId, + @IdRes int tableIdRes, + @LayoutRes int textLayoutResId, + @IdRes int textIdRes, + boolean isRecyclable, + boolean cellTextCenterVertical) { + this.tableLayoutResId = tableLayoutResId; + this.tableIdRes = tableIdRes; + this.textLayoutResId = textLayoutResId; + this.textIdRes = textIdRes; + this.isRecyclable = isRecyclable; + this.cellTextCenterVertical = cellTextCenterVertical; + } + + @NonNull + @Override + public Holder createHolder(@NonNull LayoutInflater inflater, @NonNull ViewGroup parent) { + return new Holder( + isRecyclable, + tableIdRes, + inflater.inflate(tableLayoutResId, parent, false)); + } + + @Override + public void bindHolder(@NonNull Markwon markwon, @NonNull Holder holder, @NonNull TableBlock node) { + + Table table = map.get(node); + if (table == null) { + table = Table.parse(markwon, node); + map.put(node, table); + } + + // check if this exact TableBlock was already applied + // set tag of tableLayoutResId as it's 100% to be present (we still allow 0 as + // tableIdRes if tableLayoutResId has TableLayout as root view) + final TableLayout layout = holder.tableLayout; + if (table == null + || table == layout.getTag(tableLayoutResId)) { + return; + } + + // set this flag to indicate what table instance we current display + layout.setTag(tableLayoutResId, table); + + final TableEntryPlugin plugin = markwon.getPlugin(TableEntryPlugin.class); + if (plugin == null) { + throw new IllegalStateException("No TableEntryPlugin is found. Make sure that it " + + "is _used_ whilst configuring Markwon instance"); + } + + // we must remove unwanted ones (rows and columns) + + final TableEntryTheme theme = plugin.theme(); + final int borderWidth; + final int borderColor; + final int cellPadding; + { + final TextView textView = ensureTextView(layout, 0, 0); + borderWidth = theme.tableBorderWidth(textView.getPaint()); + borderColor = theme.tableBorderColor(textView.getPaint()); + cellPadding = theme.tableCellPadding(); + } + + ensureTableBorderBackground(layout, borderWidth, borderColor); + + //noinspection SuspiciousNameCombination +// layout.setPadding(borderWidth, borderWidth, borderWidth, borderWidth); +// layout.setClipToPadding(borderWidth == 0); + + final List rows = table.rows(); + + final int rowsSize = rows.size(); + + // all rows should have equal number of columns + final int columnsSize = rowsSize > 0 + ? rows.get(0).columns().size() + : 0; + + Table.Row row; + Table.Column column; + + TableRow tableRow; + + for (int y = 0; y < rowsSize; y++) { + + row = rows.get(y); + tableRow = ensureRow(layout, y); + + for (int x = 0; x < columnsSize; x++) { + + column = row.columns().get(x); + + final TextView textView = ensureTextView(layout, y, x); + textView.setGravity(textGravity(column.alignment(), cellTextCenterVertical)); + textView.getPaint().setFakeBoldText(row.header()); + + // apply padding only if not specified in theme (otherwise just use the value from layout) + if (cellPadding > 0) { + textView.setPadding(cellPadding, cellPadding, cellPadding, cellPadding); + } + + ensureTableBorderBackground(textView, borderWidth, borderColor); + markwon.setParsedMarkdown(textView, column.content()); + } + + // row appearance + if (row.header()) { + tableRow.setBackgroundColor(theme.tableHeaderRowBackgroundColor()); + } else { + // as we currently have no support for tables without head + // we shift even/odd calculation a bit (head should not be included in even/odd calculation) + final boolean isEven = (y % 2) == 1; + if (isEven) { + tableRow.setBackgroundColor(theme.tableEvenRowBackgroundColor()); + } else { + // just take first + final TextView textView = ensureTextView(layout, y, 0); + tableRow.setBackgroundColor( + theme.tableOddRowBackgroundColor(textView.getPaint())); + } + } + } + + // clean up here of un-used rows and columns + removeUnused(layout, rowsSize, columnsSize); + } + + @NonNull + private TableRow ensureRow(@NonNull TableLayout layout, int row) { + + final int count = layout.getChildCount(); + + // fill the requested views until we have added the `row` one + if (row >= count) { + + final Context context = layout.getContext(); + + int diff = row - count + 1; + while (diff > 0) { + layout.addView(new TableRow(context)); + diff -= 1; + } + } + + // return requested child (here it always should be the last one) + return (TableRow) layout.getChildAt(row); + } + + @NonNull + private TextView ensureTextView(@NonNull TableLayout layout, int row, int column) { + + final TableRow tableRow = ensureRow(layout, row); + final int count = tableRow.getChildCount(); + + if (column >= count) { + + final LayoutInflater inflater = ensureInflater(layout.getContext()); + + boolean textViewChecked = false; + + View view; + TextView textView; + ViewGroup.LayoutParams layoutParams; + + int diff = column - count + 1; + + while (diff > 0) { + + view = inflater.inflate(textLayoutResId, tableRow, false); + + // we should have `match_parent` as height (important for borders and text-vertical-align) + layoutParams = view.getLayoutParams(); + if (layoutParams.height != ViewGroup.LayoutParams.MATCH_PARENT) { + layoutParams.height = ViewGroup.LayoutParams.MATCH_PARENT; + } + + // it will be enough to check only once + if (!textViewChecked) { + + if (textIdRes == 0) { + if (!(view instanceof TextView)) { + final String name = layout.getContext().getResources().getResourceName(textLayoutResId); + throw new IllegalStateException(String.format("textLayoutResId(R.layout.%s) " + + "has other than TextView root view. Specify TextView ID explicitly", name)); + } + textView = (TextView) view; + } else { + textView = view.findViewById(textIdRes); + if (textView == null) { + final Resources r = layout.getContext().getResources(); + final String layoutName = r.getResourceName(textLayoutResId); + final String idName = r.getResourceName(textIdRes); + throw new NullPointerException(String.format("textLayoutResId(R.layout.%s) " + + "has no TextView found by id(R.id.%s): %s", layoutName, idName, view)); + } + } + // mark as checked + textViewChecked = true; + } else { + if (textIdRes == 0) { + textView = (TextView) view; + } else { + textView = view.findViewById(textIdRes); + } + } + + // we should set SpannableFactory during creation (to avoid another setText method) + textView.setSpannableFactory(NoCopySpannableFactory.getInstance()); + tableRow.addView(textView); + + diff -= 1; + } + } + + // we can skip all the validation here as we have validated our views whilst inflating them + final View last = tableRow.getChildAt(column); + if (textIdRes == 0) { + return (TextView) last; + } else { + return last.findViewById(textIdRes); + } + } + + private void ensureTableBorderBackground(@NonNull View view, @Px int borderWidth, @ColorInt int borderColor) { + if (borderWidth == 0) { + view.setBackground(null); + } else { + final Drawable drawable = view.getBackground(); + if (!(drawable instanceof TableBorderDrawable)) { + final TableBorderDrawable borderDrawable = new TableBorderDrawable(); + borderDrawable.update(borderWidth, borderColor); + view.setBackground(borderDrawable); + } else { + ((TableBorderDrawable) drawable).update(borderWidth, borderColor); + } + } + } + + @NonNull + private LayoutInflater ensureInflater(@NonNull Context context) { + if (inflater == null) { + inflater = LayoutInflater.from(context); + } + return inflater; + } + + @SuppressWarnings("WeakerAccess") + @VisibleForTesting + static void removeUnused(@NonNull TableLayout layout, int usedRows, int usedColumns) { + + // clean up rows + final int rowsCount = layout.getChildCount(); + if (rowsCount > usedRows) { + layout.removeViews(usedRows, (rowsCount - usedRows)); + } + + // validate columns + // here we can use usedRows as children count + + TableRow tableRow; + int columnCount; + + for (int i = 0; i < usedRows; i++) { + tableRow = (TableRow) layout.getChildAt(i); + columnCount = tableRow.getChildCount(); + if (columnCount > usedColumns) { + tableRow.removeViews(usedColumns, (columnCount - usedColumns)); + } + } + } + + @Override + public void clear() { + map.clear(); + } + + public static class Holder extends MarkwonAdapter.Holder { + + final TableLayout tableLayout; + + public Holder(boolean isRecyclable, @IdRes int tableLayoutIdRes, @NonNull View itemView) { + super(itemView); + + // we must call this method only once (it's somehow _paired_ inside, so + // any call in `onCreateViewHolder` or `onBindViewHolder` will log an error + // `isRecyclable decremented below 0` which make little sense here) + setIsRecyclable(isRecyclable); + + final TableLayout tableLayout; + if (tableLayoutIdRes == 0) { + // try to cast directly + if (!(itemView instanceof TableLayout)) { + throw new IllegalStateException("Root view is not TableLayout. Please provide " + + "TableLayout ID explicitly"); + } + tableLayout = (TableLayout) itemView; + } else { + tableLayout = requireView(tableLayoutIdRes); + } + this.tableLayout = tableLayout; + } + } + + // we will use gravity instead of textAlignment because min sdk is 16 (textAlignment starts at 17) + @SuppressWarnings("WeakerAccess") + @SuppressLint("RtlHardcoded") + @VisibleForTesting + static int textGravity(@NonNull Table.Alignment alignment, boolean cellTextCenterVertical) { + + final int gravity; + + switch (alignment) { + + case LEFT: + gravity = Gravity.LEFT; + break; + + case CENTER: + gravity = Gravity.CENTER_HORIZONTAL; + break; + + case RIGHT: + gravity = Gravity.RIGHT; + break; + + default: + throw new IllegalStateException("Unknown table alignment: " + alignment); + } + + if (cellTextCenterVertical) { + return gravity | Gravity.CENTER_VERTICAL; + } + + // do not center vertically + return gravity; + } + + static class BuilderImpl implements Builder { + + private int tableLayoutResId; + private int tableIdRes; + + private int textLayoutResId; + private int textIdRes; + + private boolean cellTextCenterVertical = true; + + private boolean isRecyclable = true; + + @NonNull + @Override + public Builder tableLayout(int tableLayoutResId, int tableIdRes) { + this.tableLayoutResId = tableLayoutResId; + this.tableIdRes = tableIdRes; + return this; + } + + @NonNull + @Override + public Builder tableLayoutIsRoot(int tableLayoutResId) { + this.tableLayoutResId = tableLayoutResId; + this.tableIdRes = 0; + return this; + } + + @NonNull + @Override + public Builder textLayout(int textLayoutResId, int textIdRes) { + this.textLayoutResId = textLayoutResId; + this.textIdRes = textIdRes; + return this; + } + + @NonNull + @Override + public Builder textLayoutIsRoot(int textLayoutResId) { + this.textLayoutResId = textLayoutResId; + this.textIdRes = 0; + return this; + } + + @NonNull + @Override + public Builder cellTextCenterVertical(boolean cellTextCenterVertical) { + this.cellTextCenterVertical = cellTextCenterVertical; + return this; + } + + @NonNull + @Override + public Builder isRecyclable(boolean isRecyclable) { + this.isRecyclable = isRecyclable; + return this; + } + + @NonNull + @Override + public TableEntry build() { + + if (tableLayoutResId == 0) { + throw new IllegalStateException("`tableLayoutResId` argument is required"); + } + + if (textLayoutResId == 0) { + throw new IllegalStateException("`textLayoutResId` argument is required"); + } + + return new TableEntry( + tableLayoutResId, tableIdRes, + textLayoutResId, textIdRes, + isRecyclable, cellTextCenterVertical + ); + } + } +} diff --git a/markwon-recycler-table/src/main/java/ru/noties/markwon/recycler/table/TableEntryPlugin.java b/markwon-recycler-table/src/main/java/ru/noties/markwon/recycler/table/TableEntryPlugin.java new file mode 100644 index 00000000..a92400ee --- /dev/null +++ b/markwon-recycler-table/src/main/java/ru/noties/markwon/recycler/table/TableEntryPlugin.java @@ -0,0 +1,65 @@ +package ru.noties.markwon.recycler.table; + +import android.content.Context; +import android.support.annotation.NonNull; + +import org.commonmark.ext.gfm.tables.TablesExtension; +import org.commonmark.parser.Parser; + +import java.util.Collections; + +import ru.noties.markwon.AbstractMarkwonPlugin; +import ru.noties.markwon.ext.tables.TablePlugin; +import ru.noties.markwon.ext.tables.TableTheme; + +/** + * This plugin must be used instead of {@link ru.noties.markwon.ext.tables.TablePlugin} when a markdown + * table is intended to be used in a RecyclerView via {@link TableEntry}. This is required + * because TablePlugin additionally processes markdown tables to be displayed in limited + * context of a TextView. If TablePlugin will be used, {@link TableEntry} will display table, + * but no content will be present + * + * @since 3.0.0 + */ +public class TableEntryPlugin extends AbstractMarkwonPlugin { + + @NonNull + public static TableEntryPlugin create(@NonNull Context context) { + final TableTheme tableTheme = TableTheme.create(context); + return create(tableTheme); + } + + @NonNull + public static TableEntryPlugin create(@NonNull TableTheme tableTheme) { + return new TableEntryPlugin(TableEntryTheme.create(tableTheme)); + } + + @NonNull + public static TableEntryPlugin create(@NonNull TablePlugin.ThemeConfigure themeConfigure) { + final TableTheme.Builder builder = new TableTheme.Builder(); + themeConfigure.configureTheme(builder); + return new TableEntryPlugin(new TableEntryTheme(builder)); + } + + @NonNull + public static TableEntryPlugin create(@NonNull TablePlugin plugin) { + return create(plugin.theme()); + } + + private final TableEntryTheme theme; + + @SuppressWarnings("WeakerAccess") + TableEntryPlugin(@NonNull TableEntryTheme tableTheme) { + this.theme = tableTheme; + } + + @NonNull + public TableEntryTheme theme() { + return theme; + } + + @Override + public void configureParser(@NonNull Parser.Builder builder) { + builder.extensions(Collections.singleton(TablesExtension.create())); + } +} diff --git a/markwon-recycler-table/src/main/java/ru/noties/markwon/recycler/table/TableEntryTheme.java b/markwon-recycler-table/src/main/java/ru/noties/markwon/recycler/table/TableEntryTheme.java new file mode 100644 index 00000000..e9b9b7ca --- /dev/null +++ b/markwon-recycler-table/src/main/java/ru/noties/markwon/recycler/table/TableEntryTheme.java @@ -0,0 +1,67 @@ +package ru.noties.markwon.recycler.table; + +import android.graphics.Paint; +import android.support.annotation.ColorInt; +import android.support.annotation.NonNull; +import android.support.annotation.Px; + +import ru.noties.markwon.ext.tables.TableTheme; +import ru.noties.markwon.utils.ColorUtils; + +/** + * Mimics TableTheme to allow uniform table customization + * + * @see #create(TableTheme) + * @see TableEntryPlugin + * @since 3.0.0 + */ +@SuppressWarnings("WeakerAccess") +public class TableEntryTheme extends TableTheme { + + @NonNull + public static TableEntryTheme create(@NonNull TableTheme tableTheme) { + return new TableEntryTheme(tableTheme.asBuilder()); + } + + protected TableEntryTheme(@NonNull Builder builder) { + super(builder); + } + + @Px + @Override + public int tableCellPadding() { + return tableCellPadding; + } + + @ColorInt + public int tableBorderColor(@NonNull Paint paint) { + return tableBorderColor == 0 + ? ColorUtils.applyAlpha(paint.getColor(), TABLE_BORDER_DEF_ALPHA) + : tableBorderColor; + } + + @Px + @Override + public int tableBorderWidth(@NonNull Paint paint) { + return tableBorderWidth < 0 + ? (int) (paint.getStrokeWidth() + .5F) + : tableBorderWidth; + } + + @ColorInt + public int tableOddRowBackgroundColor(@NonNull Paint paint) { + return tableOddRowBackgroundColor == 0 + ? ColorUtils.applyAlpha(paint.getColor(), TABLE_ODD_ROW_DEF_ALPHA) + : tableOddRowBackgroundColor; + } + + @ColorInt + public int tableEvenRowBackgroundColor() { + return tableEvenRowBackgroundColor; + } + + @ColorInt + public int tableHeaderRowBackgroundColor() { + return tableHeaderRowBackgroundColor; + } +} diff --git a/markwon-recycler-table/src/test/java/ru/noties/markwon/recycler/table/TableEntryTest.java b/markwon-recycler-table/src/test/java/ru/noties/markwon/recycler/table/TableEntryTest.java new file mode 100644 index 00000000..ae55f18e --- /dev/null +++ b/markwon-recycler-table/src/test/java/ru/noties/markwon/recycler/table/TableEntryTest.java @@ -0,0 +1,103 @@ +package ru.noties.markwon.recycler.table; + +import android.content.res.Resources; +import android.util.Pair; +import android.view.Gravity; +import android.view.View; +import android.widget.TableLayout; + +import org.junit.Test; +import org.junit.runner.RunWith; +import org.robolectric.RobolectricTestRunner; +import org.robolectric.annotation.Config; + +import java.util.Arrays; +import java.util.List; + +import ru.noties.markwon.ext.tables.Table; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +@RunWith(RobolectricTestRunner.class) +@Config(manifest = Config.NONE) +public class TableEntryTest { + + @Test + public void gravity() { + // test textGravity is calculated correctly + + final List> noVerticalAlign = Arrays.asList( + new Pair(Table.Alignment.LEFT, Gravity.LEFT), + new Pair(Table.Alignment.CENTER, Gravity.CENTER_HORIZONTAL), + new Pair(Table.Alignment.RIGHT, Gravity.RIGHT) + ); + + final List> withVerticalAlign = Arrays.asList( + new Pair(Table.Alignment.LEFT, Gravity.LEFT | Gravity.CENTER_VERTICAL), + new Pair(Table.Alignment.CENTER, Gravity.CENTER_HORIZONTAL | Gravity.CENTER_VERTICAL), + new Pair(Table.Alignment.RIGHT, Gravity.RIGHT | Gravity.CENTER_VERTICAL) + ); + + for (Pair pair : noVerticalAlign) { + assertEquals(pair.first.name(), pair.second.intValue(), TableEntry.textGravity(pair.first, false)); + } + + for (Pair pair : withVerticalAlign) { + assertEquals(pair.first.name(), pair.second.intValue(), TableEntry.textGravity(pair.first, true)); + } + } + + @Test + public void holder_no_table_layout_id() { + // validate that holder correctly obtains TableLayout instance casting root view + + // root is not TableLayout + try { + new TableEntry.Holder(false, 0, mock(View.class)); + fail(); + } catch (IllegalStateException e) { + assertTrue(e.getMessage().contains("Root view is not TableLayout")); + } + + // root is TableLayout + try { + final TableLayout tableLayout = mock(TableLayout.class); + final TableEntry.Holder h = new TableEntry.Holder(false, 0, tableLayout); + assertEquals(tableLayout, h.tableLayout); + } catch (IllegalStateException e) { + fail(e.getMessage()); + } + } + + @Test + public void holder_with_table_layout_id() { + + // not found + try { + + final View view = mock(View.class); + // resources are used to obtain id name for proper error message + when(view.getResources()).thenReturn(mock(Resources.class)); + new TableEntry.Holder(false, 1, view); + fail(); + } catch (NullPointerException e) { + assertTrue(e.getMessage(), e.getMessage().contains("No view with id")); + } + + // found + try { + final TableLayout tableLayout = mock(TableLayout.class); + final View view = mock(View.class); + when(view.findViewById(3)).thenReturn(tableLayout); + final TableEntry.Holder holder = new TableEntry.Holder(false, 3, view); + assertEquals(tableLayout, holder.tableLayout); + } catch (NullPointerException e) { + e.printStackTrace(); + fail(e.getMessage()); + } + } +} \ No newline at end of file diff --git a/markwon-recycler/src/main/java/ru/noties/markwon/recycler/MarkwonAdapter.java b/markwon-recycler/src/main/java/ru/noties/markwon/recycler/MarkwonAdapter.java index d288a44c..64241d55 100644 --- a/markwon-recycler/src/main/java/ru/noties/markwon/recycler/MarkwonAdapter.java +++ b/markwon-recycler/src/main/java/ru/noties/markwon/recycler/MarkwonAdapter.java @@ -18,17 +18,13 @@ import ru.noties.markwon.MarkwonReducer; /** * Adapter to display markdown in a RecyclerView. It is done by extracting root blocks from a - * parsed markdown document and rendering each block in a standalone RecyclerView entry. Provides + * parsed markdown document (via {@link MarkwonReducer} and rendering each block in a standalone RecyclerView entry. Provides * ability to customize rendering of blocks. For example display certain blocks in a horizontal * scrolling container or display tables in a specific widget designed for it ({@link Builder#include(Class, Entry)}). - *

- * By default each node will be rendered in a TextView provided by this artifact. It has no styling - * information and thus must be replaced with your own layout ({@link Builder#defaultEntry(int)} or - * {@link Builder#defaultEntry(Entry)}). * - * @see #builder() - * @see #create() - * @see #create(int) + * @see #builder(int, int) + * @see #builder(Entry) + * @see #create(int, int) * @see #create(Entry) * @see #setMarkdown(Markwon, String) * @see #setParsedMarkdown(Markwon, Node) @@ -37,14 +33,34 @@ import ru.noties.markwon.MarkwonReducer; */ public abstract class MarkwonAdapter extends RecyclerView.Adapter { + @NonNull + public static Builder builderTextViewIsRoot(@LayoutRes int defaultEntryLayoutResId) { + return builder(SimpleEntry.createTextViewIsRoot(defaultEntryLayoutResId)); + } + /** * Factory method to obtain {@link Builder} instance. * * @see Builder */ @NonNull - public static Builder builder() { - return new MarkwonAdapterImpl.BuilderImpl(); + public static Builder builder( + @LayoutRes int defaultEntryLayoutResId, + @IdRes int defaultEntryTextViewResId + ) { + return builder(SimpleEntry.create(defaultEntryLayoutResId, defaultEntryTextViewResId)); + } + + @NonNull + public static Builder builder(@NonNull Entry defaultEntry) { + //noinspection unchecked + return new MarkwonAdapterImpl.BuilderImpl((Entry) defaultEntry); + } + + @NonNull + public static MarkwonAdapter createTextViewIsRoot(@LayoutRes int defaultEntryLayoutResId) { + return builderTextViewIsRoot(defaultEntryLayoutResId) + .build(); } /** @@ -52,12 +68,16 @@ public abstract class MarkwonAdapter extends RecyclerView.Adapter defaultEntry) { - return new MarkwonAdapterImpl.BuilderImpl().defaultEntry(defaultEntry).build(); - } - - /** - * Factory method to create a {@link MarkwonAdapter} that will use supplied layoutResId view - * to display all nodes. - * - * Please note that supplied layout must have a TextView inside - * with {@code android:id="@+id/text"} - * - * @param layoutResId layout to be used to display all nodes - * @see SimpleEntry - */ - @NonNull - public static MarkwonAdapter create(@LayoutRes int layoutResId) { - return new MarkwonAdapterImpl.BuilderImpl().defaultEntry(layoutResId).build(); + return builder(defaultEntry).build(); } /** * Builder to create an instance of {@link MarkwonAdapter} * * @see #include(Class, Entry) - * @see #defaultEntry(int) - * @see #defaultEntry(Entry) * @see #reducer(MarkwonReducer) + * @see #build() */ public interface Builder { @@ -112,29 +116,6 @@ public abstract class MarkwonAdapter extends RecyclerView.Adapter node, @NonNull Entry entry); - /** - * Specify which {@link Entry} to use for all non-explicitly registered nodes - * - * @param defaultEntry {@link Entry} - * @return self - * @see SimpleEntry - */ - @NonNull - Builder defaultEntry(@NonNull Entry defaultEntry); - - /** - * Specify which layout {@link SimpleEntry} will use to render all non-explicitly - * registered nodes. - * - * Please note that supplied layout must have a TextView inside - * with {@code android:id="@+id/text"} - * - * @return self - * @see SimpleEntry - */ - @NonNull - Builder defaultEntry(@LayoutRes int layoutResId); - /** * Specify how root Node will be reduced to a list of nodes. There is a default * {@link MarkwonReducer} that will be used if not provided explicitly (there is no need to @@ -157,17 +138,27 @@ public abstract class MarkwonAdapter extends RecyclerView.Adapter { + public static abstract class Entry { @NonNull - H createHolder(@NonNull LayoutInflater inflater, @NonNull ViewGroup parent); + public abstract H createHolder(@NonNull LayoutInflater inflater, @NonNull ViewGroup parent); - void bindHolder(@NonNull Markwon markwon, @NonNull H holder, @NonNull N node); + public abstract void bindHolder(@NonNull Markwon markwon, @NonNull H holder, @NonNull N node); - long id(@NonNull N node); + /** + * Will be called when new content is available (clear internal cache if any) + */ + public void clear() { - // will be called when new content is available (clear internal cache if any) - void clear(); + } + + public long id(@NonNull N node) { + return node.hashCode(); + } + + public void onViewRecycled(@NonNull H holder) { + + } } public abstract void setMarkdown(@NonNull Markwon markwon, @NonNull String markdown); @@ -196,7 +187,15 @@ public abstract class MarkwonAdapter extends RecyclerView.Adapter V requireView(@IdRes int id) { final V v = itemView.findViewById(id); if (v == null) { - throw new NullPointerException(); + final String name; + if (id == 0 + || id == View.NO_ID) { + name = String.valueOf(id); + } else { + name = "R.id." + itemView.getResources().getResourceName(id); + } + throw new NullPointerException(String.format("No view with id(R.id.%s) is found " + + "in layout: %s", name, itemView)); } return v; } diff --git a/markwon-recycler/src/main/java/ru/noties/markwon/recycler/MarkwonAdapterImpl.java b/markwon-recycler/src/main/java/ru/noties/markwon/recycler/MarkwonAdapterImpl.java index 70406e78..30b093f5 100644 --- a/markwon-recycler/src/main/java/ru/noties/markwon/recycler/MarkwonAdapterImpl.java +++ b/markwon-recycler/src/main/java/ru/noties/markwon/recycler/MarkwonAdapterImpl.java @@ -32,6 +32,7 @@ class MarkwonAdapterImpl extends MarkwonAdapter { this.entries = entries; this.defaultEntry = defaultEntry; this.reducer = reducer; + setHasStableIds(true); } @@ -90,6 +91,14 @@ class MarkwonAdapterImpl extends MarkwonAdapter { : 0; } + @Override + public void onViewRecycled(@NonNull Holder holder) { + super.onViewRecycled(holder); + + final Entry entry = getEntry(holder.getItemViewType()); + entry.onViewRecycled(holder); + } + @SuppressWarnings("unused") @NonNull public List getItems() { @@ -132,9 +141,14 @@ class MarkwonAdapterImpl extends MarkwonAdapter { private final SparseArray> entries = new SparseArray<>(3); - private Entry defaultEntry; + private final Entry defaultEntry; + private MarkwonReducer reducer; + BuilderImpl(@NonNull Entry defaultEntry) { + this.defaultEntry = defaultEntry; + } + @NonNull @Override public Builder include( @@ -145,22 +159,6 @@ class MarkwonAdapterImpl extends MarkwonAdapter { return this; } - @NonNull - @Override - public Builder defaultEntry(@NonNull Entry defaultEntry) { - //noinspection unchecked - this.defaultEntry = (Entry) defaultEntry; - return this; - } - - @NonNull - @Override - public Builder defaultEntry(int layoutResId) { - //noinspection unchecked - this.defaultEntry = (Entry) (Entry) new SimpleEntry(layoutResId); - return this; - } - @NonNull @Override public Builder reducer(@NonNull MarkwonReducer reducer) { @@ -172,11 +170,6 @@ class MarkwonAdapterImpl extends MarkwonAdapter { @Override public MarkwonAdapter build() { - if (defaultEntry == null) { - //noinspection unchecked - defaultEntry = (Entry) (Entry) new SimpleEntry(); - } - if (reducer == null) { reducer = MarkwonReducer.directChildren(); } diff --git a/markwon-recycler/src/main/java/ru/noties/markwon/recycler/SimpleEntry.java b/markwon-recycler/src/main/java/ru/noties/markwon/recycler/SimpleEntry.java index 03ce5e56..93ef0d30 100644 --- a/markwon-recycler/src/main/java/ru/noties/markwon/recycler/SimpleEntry.java +++ b/markwon-recycler/src/main/java/ru/noties/markwon/recycler/SimpleEntry.java @@ -1,9 +1,8 @@ package ru.noties.markwon.recycler; +import android.support.annotation.IdRes; import android.support.annotation.LayoutRes; import android.support.annotation.NonNull; -import android.text.Spannable; -import android.text.SpannableString; import android.text.Spanned; import android.view.LayoutInflater; import android.view.View; @@ -16,32 +15,43 @@ import java.util.HashMap; import java.util.Map; import ru.noties.markwon.Markwon; +import ru.noties.markwon.utils.NoCopySpannableFactory; /** * @since 3.0.0 */ @SuppressWarnings("WeakerAccess") -public class SimpleEntry implements MarkwonAdapter.Entry { +public class SimpleEntry extends MarkwonAdapter.Entry { - public static final Spannable.Factory NO_COPY_SPANNABLE_FACTORY = new NoCopySpannableFactory(); + /** + * Create {@link SimpleEntry} that has TextView as the root view of + * specified layout. + */ + @NonNull + public static SimpleEntry createTextViewIsRoot(@LayoutRes int layoutResId) { + return new SimpleEntry(layoutResId, 0); + } + + @NonNull + public static SimpleEntry create(@LayoutRes int layoutResId, @IdRes int textViewIdRes) { + return new SimpleEntry(layoutResId, textViewIdRes); + } // small cache for already rendered nodes private final Map cache = new HashMap<>(); private final int layoutResId; + private final int textViewIdRes; - public SimpleEntry() { - this(R.layout.markwon_adapter_simple_entry); - } - - public SimpleEntry(@LayoutRes int layoutResId) { + public SimpleEntry(@LayoutRes int layoutResId, @IdRes int textViewIdRes) { this.layoutResId = layoutResId; + this.textViewIdRes = textViewIdRes; } @NonNull @Override public Holder createHolder(@NonNull LayoutInflater inflater, @NonNull ViewGroup parent) { - return new Holder(inflater.inflate(layoutResId, parent, false)); + return new Holder(textViewIdRes, inflater.inflate(layoutResId, parent, false)); } @Override @@ -54,11 +64,6 @@ public class SimpleEntry implements MarkwonAdapter.Entry - \ No newline at end of file diff --git a/sample/build.gradle b/sample/build.gradle index 35db7389..966d0747 100644 --- a/sample/build.gradle +++ b/sample/build.gradle @@ -29,14 +29,6 @@ android { targetCompatibility JavaVersion.VERSION_1_8 sourceCompatibility JavaVersion.VERSION_1_8 } - - sourceSets { - main { - // let's use different res directory so sample will have _isolated_ resources from others - res.srcDirs += [ './src/main/recycler/res' ] - assets.srcDirs += ['./src/main/recycler/assets'] - } - } } dependencies { @@ -51,6 +43,7 @@ dependencies { implementation project(':markwon-image-svg') implementation project(':markwon-syntax-highlight') implementation project(':markwon-recycler') + implementation project(':markwon-recycler-table') deps.with { implementation it['support-recycler-view'] diff --git a/sample/src/main/AndroidManifest.xml b/sample/src/main/AndroidManifest.xml index d1442bf0..376348ca 100644 --- a/sample/src/main/AndroidManifest.xml +++ b/sample/src/main/AndroidManifest.xml @@ -7,11 +7,9 @@ + tools:ignore="AllowBackup,GoogleAppIndexingWarning,MissingApplicationIcon"> diff --git a/sample/src/main/assets/README.md b/sample/src/main/assets/README.md new file mode 120000 index 00000000..ff5c7960 --- /dev/null +++ b/sample/src/main/assets/README.md @@ -0,0 +1 @@ +../../../../README.md \ No newline at end of file diff --git a/sample/src/main/java/ru/noties/markwon/sample/recycler/RecyclerActivity.java b/sample/src/main/java/ru/noties/markwon/sample/recycler/RecyclerActivity.java index c230aa6c..adeb6c4b 100644 --- a/sample/src/main/java/ru/noties/markwon/sample/recycler/RecyclerActivity.java +++ b/sample/src/main/java/ru/noties/markwon/sample/recycler/RecyclerActivity.java @@ -11,15 +11,12 @@ import android.support.v7.widget.RecyclerView; import android.text.TextUtils; import org.commonmark.ext.gfm.tables.TableBlock; -import org.commonmark.ext.gfm.tables.TablesExtension; import org.commonmark.node.FencedCodeBlock; -import org.commonmark.parser.Parser; import java.io.BufferedReader; import java.io.IOException; import java.io.InputStream; import java.io.InputStreamReader; -import java.util.Collections; import ru.noties.debug.AndroidLogDebugOutput; import ru.noties.debug.Debug; @@ -33,6 +30,8 @@ import ru.noties.markwon.image.ImagesPlugin; import ru.noties.markwon.image.svg.SvgPlugin; import ru.noties.markwon.recycler.MarkwonAdapter; import ru.noties.markwon.recycler.SimpleEntry; +import ru.noties.markwon.recycler.table.TableEntry; +import ru.noties.markwon.recycler.table.TableEntryPlugin; import ru.noties.markwon.sample.R; import ru.noties.markwon.urlprocessor.UrlProcessor; import ru.noties.markwon.urlprocessor.UrlProcessorRelativeToAbsolute; @@ -51,14 +50,12 @@ public class RecyclerActivity extends Activity { // create MarkwonAdapter and register two blocks that will be rendered differently // * fenced code block (can also specify the same Entry for indended code block) // * table block - final MarkwonAdapter adapter = MarkwonAdapter.builder() - // we can simply use bundled SimpleEntry, that will lookup a TextView - // with `@+id/text` id - .include(FencedCodeBlock.class, new SimpleEntry(R.layout.adapter_fenced_code_block)) - // create own implementation of entry for different rendering - .include(TableBlock.class, new TableEntry2()) - // specify default entry (for all other blocks) - .defaultEntry(new SimpleEntry(R.layout.adapter_default_entry)) + final MarkwonAdapter adapter = MarkwonAdapter.builder(R.layout.adapter_default_entry, R.id.text) + // we can simply use bundled SimpleEntry + .include(FencedCodeBlock.class, SimpleEntry.create(R.layout.adapter_fenced_code_block, R.id.text)) + .include(TableBlock.class, TableEntry.create(builder -> builder + .tableLayout(R.layout.adapter_table_block, R.id.table_layout) + .textLayoutIsRoot(R.layout.view_table_entry_cell))) .build(); final RecyclerView recyclerView = findViewById(R.id.recycler_view); @@ -71,10 +68,6 @@ public class RecyclerActivity extends Activity { // please note that we should notify updates (adapter doesn't do it implicitly) adapter.notifyDataSetChanged(); - - // NB, there is no currently available widget to render tables gracefully - // TableEntryView is here for demonstration purposes only (to show that rendering - // tables } @NonNull @@ -83,14 +76,8 @@ public class RecyclerActivity extends Activity { .usePlugin(CorePlugin.create()) .usePlugin(ImagesPlugin.createWithAssets(context)) .usePlugin(SvgPlugin.create(context.getResources())) - .usePlugin(new AbstractMarkwonPlugin() { - @Override - public void configureParser(@NonNull Parser.Builder builder) { - // it's important NOT to use TablePlugin - // the only thing we want from it is commonmark-java parser extension - builder.extensions(Collections.singleton(TablesExtension.create())); - } - }) + // important to use TableEntryPlugin instead of TablePlugin + .usePlugin(TableEntryPlugin.create(context)) .usePlugin(HtmlPlugin.create()) // .usePlugin(SyntaxHighlightPlugin.create()) .usePlugin(new AbstractMarkwonPlugin() { diff --git a/sample/src/main/java/ru/noties/markwon/sample/recycler/TableEntry.java b/sample/src/main/java/ru/noties/markwon/sample/recycler/TableEntry.java deleted file mode 100644 index 900ce719..00000000 --- a/sample/src/main/java/ru/noties/markwon/sample/recycler/TableEntry.java +++ /dev/null @@ -1,67 +0,0 @@ -package ru.noties.markwon.sample.recycler; - -import android.support.annotation.NonNull; -import android.view.LayoutInflater; -import android.view.View; -import android.view.ViewGroup; - -import org.commonmark.ext.gfm.tables.TableBlock; - -import java.util.HashMap; -import java.util.Map; - -import ru.noties.debug.Debug; -import ru.noties.markwon.Markwon; -import ru.noties.markwon.ext.tables.Table; -import ru.noties.markwon.recycler.MarkwonAdapter; -import ru.noties.markwon.sample.R; - -// do not use in real applications, this is just a showcase -public class TableEntry implements MarkwonAdapter.Entry { - - private final Map cache = new HashMap<>(2); - - @NonNull - @Override - public TableNodeHolder createHolder(@NonNull LayoutInflater inflater, @NonNull ViewGroup parent) { - return new TableNodeHolder(inflater.inflate(R.layout.adapter_table_block, parent, false)); - } - - @Override - public void bindHolder(@NonNull Markwon markwon, @NonNull TableNodeHolder holder, @NonNull TableBlock node) { - - Table table = cache.get(node); - if (table == null) { - table = Table.parse(markwon, node); - cache.put(node, table); - } - - Debug.i(table); - - if (table != null) { - holder.tableEntryView.setTable(table); - // render table - } // we need to do something with null table... - } - - @Override - public long id(@NonNull TableBlock node) { - return node.hashCode(); - } - - @Override - public void clear() { - cache.clear(); - } - - static class TableNodeHolder extends MarkwonAdapter.Holder { - - final TableEntryView tableEntryView; - - TableNodeHolder(@NonNull View itemView) { - super(itemView); - - this.tableEntryView = requireView(R.id.table_entry); - } - } -} diff --git a/sample/src/main/java/ru/noties/markwon/sample/recycler/TableEntry2.java b/sample/src/main/java/ru/noties/markwon/sample/recycler/TableEntry2.java deleted file mode 100644 index dcf8d061..00000000 --- a/sample/src/main/java/ru/noties/markwon/sample/recycler/TableEntry2.java +++ /dev/null @@ -1,121 +0,0 @@ -package ru.noties.markwon.sample.recycler; - -import android.annotation.SuppressLint; -import android.content.Context; -import android.support.annotation.NonNull; -import android.view.Gravity; -import android.view.LayoutInflater; -import android.view.View; -import android.view.ViewGroup; -import android.widget.TableLayout; -import android.widget.TableRow; -import android.widget.TextView; - -import org.commonmark.ext.gfm.tables.TableBlock; - -import java.util.HashMap; -import java.util.Map; - -import ru.noties.markwon.Markwon; -import ru.noties.markwon.ext.tables.Table; -import ru.noties.markwon.recycler.MarkwonAdapter; -import ru.noties.markwon.sample.R; - -public class TableEntry2 implements MarkwonAdapter.Entry { - - private final Map map = new HashMap<>(3); - - @NonNull - @Override - public TableHolder createHolder(@NonNull LayoutInflater inflater, @NonNull ViewGroup parent) { - return new TableHolder(inflater.inflate(R.layout.adapter_table_block_2, parent, false)); - } - - @Override - public void bindHolder(@NonNull Markwon markwon, @NonNull TableHolder holder, @NonNull TableBlock node) { - - Table table = map.get(node); - if (table == null) { - table = Table.parse(markwon, node); - map.put(node, table); - } - - // check if this exact TableBlock was already - final TableLayout layout = holder.layout; - if (table == null - || table == layout.getTag(R.id.table_layout)) { - return; - } - - layout.setTag(R.id.table_layout, table); - layout.removeAllViews(); - layout.setBackgroundResource(R.drawable.bg_table_cell); - - final Context context = layout.getContext(); - final LayoutInflater inflater = LayoutInflater.from(context); - - TableRow tableRow; - TextView textView; - - for (Table.Row row : table.rows()) { - tableRow = new TableRow(context); - for (Table.Column column : row.columns()) { - textView = (TextView) inflater.inflate(R.layout.view_table_entry_cell, tableRow, false); - textView.setGravity(textGravity(column.alignment())); - markwon.setParsedMarkdown(textView, column.content()); - textView.getPaint().setFakeBoldText(row.header()); - textView.setBackgroundResource(R.drawable.bg_table_cell); - tableRow.addView(textView); - } - layout.addView(tableRow); - } - } - - @Override - public long id(@NonNull TableBlock node) { - return node.hashCode(); - } - - @Override - public void clear() { - map.clear(); - } - - static class TableHolder extends MarkwonAdapter.Holder { - - final TableLayout layout; - - TableHolder(@NonNull View itemView) { - super(itemView); - - this.layout = requireView(R.id.table_layout); - } - } - - // we will use gravity instead of textAlignment because min sdk is 16 (textAlignment starts at 17) - @SuppressLint("RtlHardcoded") - private static int textGravity(@NonNull Table.Alignment alignment) { - - final int gravity; - - switch (alignment) { - - case LEFT: - gravity = Gravity.LEFT; - break; - - case CENTER: - gravity = Gravity.CENTER_HORIZONTAL; - break; - - case RIGHT: - gravity = Gravity.RIGHT; - break; - - default: - throw new IllegalStateException("Unknown table alignment: " + alignment); - } - - return gravity | Gravity.CENTER_VERTICAL; - } -} diff --git a/sample/src/main/java/ru/noties/markwon/sample/recycler/TableEntryView.java b/sample/src/main/java/ru/noties/markwon/sample/recycler/TableEntryView.java deleted file mode 100644 index df789728..00000000 --- a/sample/src/main/java/ru/noties/markwon/sample/recycler/TableEntryView.java +++ /dev/null @@ -1,219 +0,0 @@ -package ru.noties.markwon.sample.recycler; - -import android.annotation.SuppressLint; -import android.content.Context; -import android.content.res.TypedArray; -import android.graphics.Canvas; -import android.graphics.Color; -import android.graphics.Paint; -import android.graphics.Rect; -import android.support.annotation.NonNull; -import android.support.annotation.Nullable; -import android.text.SpannedString; -import android.util.AttributeSet; -import android.view.Gravity; -import android.view.LayoutInflater; -import android.view.View; -import android.view.ViewGroup; -import android.widget.LinearLayout; -import android.widget.TextView; - -import java.util.ArrayList; -import java.util.List; - -import ru.noties.markwon.ext.tables.Table; -import ru.noties.markwon.sample.R; - -public class TableEntryView extends LinearLayout { - - // paint and rect to draw borders - private final Paint paint = new Paint(Paint.ANTI_ALIAS_FLAG); - private final Rect rect = new Rect(); - - private LayoutInflater inflater; - - private int rowEvenBackgroundColor; - - public TableEntryView(Context context) { - super(context); - init(context, null); - } - - public TableEntryView(Context context, AttributeSet attrs) { - super(context, attrs); - init(context, attrs); - } - - private void init(Context context, @Nullable AttributeSet attrs) { - inflater = LayoutInflater.from(context); - setOrientation(VERTICAL); - - if (attrs != null) { - final TypedArray array = context.obtainStyledAttributes(attrs, R.styleable.TableEntryView); - try { - - rowEvenBackgroundColor = array.getColor(R.styleable.TableEntryView_tev_rowEvenBackgroundColor, 0); - - final int stroke = array.getDimensionPixelSize(R.styleable.TableEntryView_tev_borderWidth, 0); - - // half of requested - final float strokeWidth = stroke > 0 - ? stroke / 2.F - : context.getResources().getDisplayMetrics().density / 2.F; - - paint.setStyle(Paint.Style.STROKE); - paint.setStrokeWidth(strokeWidth); - paint.setColor(array.getColor(R.styleable.TableEntryView_tev_borderColor, Color.BLACK)); - - if (isInEditMode()) { - final String data = array.getString(R.styleable.TableEntryView_tev_debugData); - if (data != null) { - - boolean first = true; - - final List rows = new ArrayList<>(); - for (String row : data.split("\\|")) { - final List columns = new ArrayList<>(); - for (String column : row.split(",")) { - columns.add(new Table.Column(Table.Alignment.LEFT, new SpannedString(column))); - } - final boolean header = first; - first = false; - rows.add(new Table.Row(header, columns)); - } - final Table table = new Table(rows); - setTable(table); - } - } - } finally { - array.recycle(); - } - } - - setWillNotDraw(false); - } - - public void setTable(@NonNull Table table) { - final List rows = table.rows(); - for (int i = 0, size = rows.size(); i < size; i++) { - addRow(i, rows.get(i)); - } - requestLayout(); - } - - private void addRow(int index, @NonNull Table.Row row) { - - final ViewGroup group = ensureRow(index); - - final int backgroundColor = !row.header() && (index % 2) == 0 - ? rowEvenBackgroundColor - : 0; - - group.setBackgroundColor(backgroundColor); - - final List columns = row.columns(); - - TextView textView; - Table.Column column; - - for (int i = 0, size = columns.size(); i < size; i++) { - textView = ensureCell(group, i); - column = columns.get(i); - textView.setGravity(textGravity(column.alignment())); - textView.setText(column.content()); - textView.getPaint().setFakeBoldText(row.header()); - } - - group.requestLayout(); - } - - @NonNull - private ViewGroup ensureRow(int index) { - - final int count = getChildCount(); - if (index >= count) { - - // count=0,index=1, diff=2 - // count=0,index=5, diff=6 - // count=1,index=2, diff=2 - int diff = index - count + 1; - while (diff > 0) { - addView(inflater.inflate(R.layout.view_table_entry_row, this, false)); - diff -= 1; - } - } - - return (ViewGroup) getChildAt(index); - } - - @NonNull - private TextView ensureCell(@NonNull ViewGroup group, int index) { - - final int count = group.getChildCount(); - if (index >= count) { - int diff = index - count + 1; - while (diff > 0) { - group.addView(inflater.inflate(R.layout.view_table_entry_cell, group, false)); - diff -= 1; - } - } - - return (TextView) group.getChildAt(index); - } - - @Override - protected void onDraw(Canvas canvas) { - super.onDraw(canvas); - - final int rows = getChildCount(); - if (rows == 0) { - return; - } - - // first draw the whole border - rect.set(0, 0, getWidth(), getHeight()); - canvas.drawRect(rect, paint); - - ViewGroup group; - View view; - - int top; - - for (int row = 0; row < rows; row++) { - group = (ViewGroup) getChildAt(row); - top = group.getTop(); - for (int col = 0, cols = group.getChildCount(); col < cols; col++) { - view = group.getChildAt(col); - rect.set(view.getLeft(), top + view.getTop(), view.getRight(), top + view.getBottom()); - canvas.drawRect(rect, paint); - } - } - } - - // we will use gravity instead of textAlignment because min sdk is 16 (textAlignment starts at 17) - @SuppressLint("RtlHardcoded") - private static int textGravity(@NonNull Table.Alignment alignment) { - - final int gravity; - - switch (alignment) { - - case LEFT: - gravity = Gravity.LEFT; - break; - - case CENTER: - gravity = Gravity.CENTER_HORIZONTAL; - break; - - case RIGHT: - gravity = Gravity.RIGHT; - break; - - default: - throw new IllegalStateException("Unknown table alignment: " + alignment); - } - - return gravity; - } -} diff --git a/sample/src/main/recycler/assets/README.md b/sample/src/main/recycler/assets/README.md deleted file mode 120000 index 1dfab242..00000000 --- a/sample/src/main/recycler/assets/README.md +++ /dev/null @@ -1 +0,0 @@ -../../../../../README.md \ No newline at end of file diff --git a/sample/src/main/recycler/res/layout/adapter_table_block.xml b/sample/src/main/recycler/res/layout/adapter_table_block.xml deleted file mode 100644 index 3358ca5a..00000000 --- a/sample/src/main/recycler/res/layout/adapter_table_block.xml +++ /dev/null @@ -1,21 +0,0 @@ - - - - - - \ No newline at end of file diff --git a/sample/src/main/recycler/res/values/attrs.xml b/sample/src/main/recycler/res/values/attrs.xml deleted file mode 100644 index 1827819d..00000000 --- a/sample/src/main/recycler/res/values/attrs.xml +++ /dev/null @@ -1,9 +0,0 @@ - - - - - - - - - \ No newline at end of file diff --git a/sample/src/main/res/drawable-v26/ic_launcher_background.xml b/sample/src/main/res/drawable-v26/ic_launcher_background.xml deleted file mode 100644 index a197b896..00000000 --- a/sample/src/main/res/drawable-v26/ic_launcher_background.xml +++ /dev/null @@ -1,24 +0,0 @@ - - - - diff --git a/sample/src/main/recycler/res/layout/activity_recycler.xml b/sample/src/main/res/layout/activity_recycler.xml similarity index 100% rename from sample/src/main/recycler/res/layout/activity_recycler.xml rename to sample/src/main/res/layout/activity_recycler.xml diff --git a/sample/src/main/recycler/res/layout/adapter_default_entry.xml b/sample/src/main/res/layout/adapter_default_entry.xml similarity index 100% rename from sample/src/main/recycler/res/layout/adapter_default_entry.xml rename to sample/src/main/res/layout/adapter_default_entry.xml diff --git a/sample/src/main/recycler/res/layout/adapter_fenced_code_block.xml b/sample/src/main/res/layout/adapter_fenced_code_block.xml similarity index 100% rename from sample/src/main/recycler/res/layout/adapter_fenced_code_block.xml rename to sample/src/main/res/layout/adapter_fenced_code_block.xml diff --git a/sample/src/main/recycler/res/layout/adapter_table_block_2.xml b/sample/src/main/res/layout/adapter_table_block.xml similarity index 83% rename from sample/src/main/recycler/res/layout/adapter_table_block_2.xml rename to sample/src/main/res/layout/adapter_table_block.xml index 6cdb3be6..aaaaa369 100644 --- a/sample/src/main/recycler/res/layout/adapter_table_block_2.xml +++ b/sample/src/main/res/layout/adapter_table_block.xml @@ -13,6 +13,7 @@ + android:layout_height="wrap_content" + android:stretchColumns="*" /> \ No newline at end of file diff --git a/sample/src/main/recycler/res/layout/view_table_entry_cell.xml b/sample/src/main/res/layout/view_table_entry_cell.xml similarity index 69% rename from sample/src/main/recycler/res/layout/view_table_entry_cell.xml rename to sample/src/main/res/layout/view_table_entry_cell.xml index 6261bacb..6d544918 100644 --- a/sample/src/main/recycler/res/layout/view_table_entry_cell.xml +++ b/sample/src/main/res/layout/view_table_entry_cell.xml @@ -2,9 +2,7 @@ - - - - \ No newline at end of file diff --git a/sample/src/main/res/mipmap-hdpi-v26/ic_launcher_foreground.png b/sample/src/main/res/mipmap-hdpi-v26/ic_launcher_foreground.png deleted file mode 100644 index 01bae14798f5d5444355dfb093e5a42425916c02..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 3366 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 diff --git a/sample/src/main/res/mipmap-hdpi/ic_launcher.png b/sample/src/main/res/mipmap-hdpi/ic_launcher.png deleted file mode 100644 index 1ba57872a247eccfaf8945b7fee81b430c0c7ed8..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 2381 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 E5ARDejQ{`u diff --git a/sample/src/main/res/mipmap-xxhdpi/ic_launcher.png b/sample/src/main/res/mipmap-xxhdpi/ic_launcher.png deleted file mode 100644 index 11dcdbc94fc335ac51e8cbc7f2dc0467479d73a4..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 5093 zcmb7|^;?wB*T?T==~$NTu0=oul#oRb5Ku&xln!a66eJeVT~aAg1ZkE~Kopj4SgEDE zJ49NL4uS7}{)6Y4>zaFhn7Qt$^FHTw<{jdRCJiM!B>(_454F_w32pp;m7J9DUDH^u z0RV>Y57qA*`cLmz26!Y`r=WoHeg`p3I4J&K<(>6Dbj0_unig1f=0(~)ufeeWE-fO1 z=@%T;^2w^JESDE2w7XN~PFWsvbq`+c&NnI-zG9LMldv_ANt4r5Z9_F8kIXuR>s#XF zrb<5+hPp^7-=((NDwK3jo!-4)ITYxAB)h!($iraLx0X0Fo*6`|wwd6zxAmB57&Ewq z4e-97=es?Wi>H^mbx|MZqBKH!JiYP2@?LuRvow_9Bx98Wu8`r_@r{!llX~{-0bGd2 zSTn145n=nB6t?kL|b&s90J1JffPp9<-qQ?F_WH@v2OjqSE`Jf2fZ zN@{bKnGAY*6FAJ$9~ydoiY$0OQT!eYdc72S+vH4VeXhQG|KGuYm}K+6GIN%j7L&^d zdHMN)y`?W!G+uc`gS9G^R_I+lC-Qd3|LKkYN+() zszEyV{E#_orAs8sus=g_Ez{|2e-#bwNKYr5s`uHD9%hjwzG66^w0qzFXcq6nq-)10 z?bw&PD!aWglAkz~tH}|e=(#l5S=5Hdi{(GbcsY=en(BHt?g2bLArNy;_~rYxQ}P2+ z4EF8Yw@VGGE^L5 zxf-$3>EIP!&W`r>J9QCKQnJ5s7S2WPBh{YPNB0c$^~DoENC9I?`<(&|3jU8lVbMD& z^-8(A6%-K>i-d6LUzAwZ zk9%bvZA}!}aVq*geyFW&aDKS4vSf7pXRgRf`NeXWwAlF-Z3rrQ>Nt@3V?K1g(S5GYsL_uqfHw=o=ph6wjnGP>Mmguz zsVMz(`_km zePs7ZE8oJmP#uyTk8DF zw@Hjtd$!^LH8;hYtlK1}|6R-gYy{j@1@KTpAYneHq|gR%1G)g5?JOcssa3xIIX)f} z!OOtsIU<@6|=HPqOKlaczq z>nbDL)cXQ;flpn*w$6r$?>QB6tnm8bdIjol8GHC(94M8Z2@vNCOVm?)kIrdz)^?>$ ziG1igj|U_r{(oxMd``|$dr@BD4&;pv#88Vcet)4{gWWj(+4&h-e3P*)?cz4BdMS=KJz`@B)o3 zd-5^&aw^fyqk>Tm|r4N9JXPE?RJ{x4Bk5wf-hK0_MCsKH5iw^q-K~0_f)7PBUHnmOrkPB~dt!}p zD=J1y&YG^T0*|cPgSXt9L^HoKK${p{H@6{5FV6~w%T z>8se>e1Bx?%;_Ze#a2mk-9^y9zxW#<5n9GYAcWsQ8a(Kf75uxh;H`-4FKINu1%lz! zSQ%fj$^d+AUAm_0g`7>lju~4*02LFB_}2HC%X(bp_~Z}!(N%?zfA{Jn@2U?z% zTh#mZjSl+vTY3(Zr?;cMdKUiN=MyKFk4M0p9hf_sPM4!KXnB^kvt39Oy>mv|r&>c3t;(&ubxr}9G zKtQdSsA!=vxA*Vf6bSRUg(L7tSJ%&EqtV_hRsf9n(hRV70f&;~XWWYO4)_aWJyPB@ z$2;?d?lR7Un2Uv=1D(bcmS7+NCyD9@A_0m=yEU6L_SpU2eM3 z_^YZJ5BNlaU>^WM7d^cXb8ZZi)eEo92Tngu%ZiEh7@M1Kk!sr7W?L$9g9@ZjSJd{v zm%O}BY2i}#uHvIpi{UpdY7he9axCZcs1QyS0ut0-A5B=NMU)<_4LrrSwaq#$_4K`g zo)5~g0>l>vDeIykdUJYm$d@IEWEzxo2vKo9JvRhwrWI<-HN%~Yo(b zSm;yoYfpZX@yOrK3YswYY%~)1k+~I@5Fd~5J^5pqUlsp$@91zuH;oy%-zI6*U?Ro> zsSH=w`=oAuL$HdD#4lhaR?Zc(+jQZvLjV?z-Xx)$)=k^~mq!!NW8lMyVo%w_rQB8q zJ?5b1aICTK#;_1RmP3yHpQy$%!G*27;f0Jos`FY>O>17za;%=Q+}PNtvk;V%tJa^n z8hdK}LyAa}!O6R69ujqp1{iq(B0WdU-l7y68(WnP2?Cd}J~=u0-!#Ujxe5vjOsx?# zVt3dXQa;Yk!$Mn&JGQsCDc`J3+_)mUA^4FrkJN$d?I&O~Cu%j45W)>_72C3OmtmRL zxK{A*tGm2=(-q2x-8Kl0j+tmMf`)^0-!ti^2^Kd$e319$i&2-i+jK>j?JIy}HBP-z zb^v}5V|m`FzxGMgz)dH;2MfcONzV{cgA6wa%Wu;H-66?~_)7izssxhF!y`|QksHmvf4)bzHot=0-HNuez z3x8#dz`tTeY$?ck&TkWVPY+b(G?0l@S|PFzJo)p3MR}&t4j)%{dg@Obzdc=91epNk zt*<+u{M(c7Ns=gvI9X}l+>VKjl@Oqx(`cYw;TV%fGieZj(^#m=RVDoIXGv?T)>t!2 z4?%dsVcNk>t~j6T!u-r{Hk|o-GeX))lTyqJ6o}A+v$L}!fb+M`!-s{8nGeUme-{tL zepr_+BI7rMfCwe`*mN_W=MC(arSP>;rib z#*Rg7$6X%>xvQ$Go+U|`b0WQgikh!9p^PnNbn3uE4GoR4@gL?~{OJ@*1hhUF%vPg0 z<-pM`Jz6id!>0qREAmDHnsMS2f%}ZyBufx(5(kh!6B}o~Zk8{FWMpkrbaaN94S!IM z2Jy|j>K0wV6-=onAQ1Z5wBFZU0++jX$HqQ>s;y0(qfG9Nmc%r_n@;krpB<{tUfsWc zKQuERjVG$ree$Fj5E%vwzre_LN88vKMk)jx0d%;8loS^pC8UqS!s3|V18e_#F+%Wk zggVc*twC z*B2V0MYU*ViLy|Rr8^Dnb+5KuE8*mY3N_GazF+GOwu=uJ?>QIsbs+9SrxwqTwzxXW zD&XWN5#IHFyTz@+6-V*Rj6h4islADro%>8xpVYH9@#Iu|s=(}{(g_plVKcnP+Vhc`K`w}uTENS`^K7U>VKob%Az*7`erfw4i zk<%3R97ug2Ee*oVToJ+MU0vYEr#Oa3Ao?zQTrCc8UcE&(Tv?SLfV1HOFjgrOSC{S_(JN@(0Cm$n^_LM)wkO z3y-4{9iqYJyMBM*XfUjoT7}PfG+T{qyc&P)1FHrZ2f{23k_3W$60UDA|q zLD+2Ss^*Ex=M!%#JQrT`k|7G%P}5Su$r~%*4;&zv8RLZy zvD#^00>I2X-c`OjUAYE5_HwHr@_59NJMm*mLl>MEi!Is=NbYlgfA4&q&ISc8V8BO+qKIiJ*Q98B6_dVrZB3!B z)!}W6hoUql0|wo07X2;bG>!*vLU&Ri%O>O}9d9aZso!q(-%j1w8yp;bM@L78XBD`X zl>Mnoji^qU7pSa~Hs?2-i_l4NzfJ?K36>uTPNvT^DdJU=CFeDdRe47&laH4LZ`>3Y z7th>S-`m^U5SMk7wtVdBzB5_6Qoen9+l_Ljr2~7XIzTcGecXiPnW75!styMs_q`&} znkov(T442X%i3777ioS=tmJ?37whN(nGvlg-*b~8Ojhr1^WKg~ScQ44Xn7n6hiX#s zi(_6bhK3-Ne%jg57ArlJC~SE>SF2+?qgngodfsBMlb2qFa^>%y?Y)PZ#Ti=vb#v9| z%y@G|YWaFf(SfU0*j3pPH@2sjaeql+!#D)}|D}BV+H(ymbdKa(;`3f2Jl_EiA3RYn IR<#NFA5Kz!vH$=8 diff --git a/sample/src/main/res/mipmap-xxxhdpi-v26/ic_launcher_foreground.png b/sample/src/main/res/mipmap-xxxhdpi-v26/ic_launcher_foreground.png deleted file mode 100644 index b64bf44fe9cee02e463fa50ea44956311cf4535f..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 11195 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 diff --git a/settings.gradle b/settings.gradle index e3177a74..faf09343 100644 --- a/settings.gradle +++ b/settings.gradle @@ -10,5 +10,6 @@ include ':app', ':sample', ':markwon-image-okhttp', ':markwon-image-svg', ':markwon-recycler', + ':markwon-recycler-table', ':markwon-syntax-highlight', ':markwon-test-span' From db0936094fa94891adad4ff3b01e0783cd1a44b9 Mon Sep 17 00:00:00 2001 From: Dimitry Ivanov Date: Thu, 14 Mar 2019 17:22:14 +0300 Subject: [PATCH 095/103] Finishing documentation --- docs/.vuepress/config.js | 9 +- docs/docs/v3/core/html-renderer.md | 21 +- docs/docs/v3/html/README.md | 324 ++---------------- docs/docs/v3/html/custom-tag-handler.md | 1 - docs/docs/v3/image/gif.md | 14 +- docs/docs/v3/image/okhttp.md | 19 + docs/docs/v3/image/svg.md | 15 + docs/docs/v3/recycler-table/README.md | 2 + docs/docs/v3/recycler/README.md | 154 ++++++++- .../markwon/html/MarkwonHtmlRenderer.java | 3 + .../markwon/html/MarkwonHtmlRendererImpl.java | 6 + sample/build.gradle | 1 + .../res/layout/adapter_fenced_code_block.xml | 4 +- 13 files changed, 271 insertions(+), 302 deletions(-) delete mode 100644 docs/docs/v3/html/custom-tag-handler.md diff --git a/docs/.vuepress/config.js b/docs/.vuepress/config.js index 85ebf762..5a0cbe66 100644 --- a/docs/.vuepress/config.js +++ b/docs/.vuepress/config.js @@ -48,14 +48,7 @@ module.exports = { '/docs/v3/ext-strikethrough/', '/docs/v3/ext-tables/', '/docs/v3/ext-tasklist/', - { - title: 'HTML', - collapsable: false, - children: [ - '/docs/v3/html/', - '/docs/v3/html/custom-tag-handler.md' - ] - }, + '/docs/v3/html/', '/docs/v3/image/gif.md', '/docs/v3/image/okhttp.md', '/docs/v3/image/svg.md', diff --git a/docs/docs/v3/core/html-renderer.md b/docs/docs/v3/core/html-renderer.md index d0756b12..17ef63e3 100644 --- a/docs/docs/v3/core/html-renderer.md +++ b/docs/docs/v3/core/html-renderer.md @@ -79,4 +79,23 @@ builder.setHandler("a", new TagHandler() { } } }); -``` \ No newline at end of file +``` + +:::tip +Sometimes HTML content might include tags that are not closed (although +they are required to be by the spec, for example a `div`). +Markwon by default disallows such tags and ignores them. Still, +there is an option to allow them _explicitly_ via builder method: +```java +final Markwon markwon = Markwon.builder(context) + .usePlugin(new AbstractMarkwonPlugin() { + @Override + public void configureHtmlRenderer(@NonNull MarkwonHtmlRenderer.Builder builder) { + builder.allowNonClosedTags(true); + } + }) + .build(); +``` +Please note that if `allowNonClosedTags=true` then all non-closed tags will be closed +at the end of a document. +::: \ No newline at end of file diff --git a/docs/docs/v3/html/README.md b/docs/docs/v3/html/README.md index 96a75431..a8a12d1b 100644 --- a/docs/docs/v3/html/README.md +++ b/docs/docs/v3/html/README.md @@ -1,47 +1,21 @@ ---- -title: 'Overview' ---- +# HTML -# HTML +This artifact encapsulates HTML parsing from the core artifact and provides +few predefined `TagHandlers` - - -Starting with version `2.0.0` `Markwon` brings the whole HTML parsing/rendering -stack _on-site_. The main reason for this are _special_ definitions of HTML nodes -by . More specifically: -and . -These two are _a bit_ different from _native_ HTML understanding. -Well, they are _completely_ different and share only the same names as - and -elements. This leads to situations when for example an `` tag is considered -a block when it's used like this: - -```markdown - -Hello from italics tag - +```java +final Markwon markwon = Markwon.builder(context) + .usePlugin(HtmlPlugin.create()) + .build(); ``` -:::tip A bit of background -
- had brought attention to differences between HTML & commonmark implementations.

-::: +As this artifact brings modified [jsoup](https://github.com/jhy/jsoup) library +it was moved to a standalone module in order to minimize dependencies and unused code +in applications that does not require HTML render capabilities. -Let's modify code snippet above _a bit_: - -```markdown{3} - -Hello from italics tag - - -``` - -We have just added a `new-line` before closing `
` tag. And this -changes everything as now, according to the , -we have 2 HtmlBlocks: one before `new-line` (containing open `` tag and text content) -and one after (containing as little as closing `` tag). - -If we modify code snippet _a bit_ again: +Before `Markwon` used android `Html` class for parsing and +rendering. Unfortunately, according to markdown specification, markdown can contain +HTML in _unpredictable_ way if rendered _outside_ of browser. For example: ```markdown{4} @@ -50,260 +24,38 @@ Hello from italics tag bold> ``` -We will have 1 HtmlBlock (from previous snippet) and a bunch of HtmlInlines: +This snippet could be represented as: +* HtmlBlock (`\nHello from italics tag`) * HtmlInline (``) * HtmlInline (``) * Text (`bold`) * HtmlInline (``) -Those _little_ differences render `Html.fromHtml` (which was used in `1.x.x` versions) -useless. And actually it renders most of the HTML parsers implementations useless, -as most of them do not allow processing of HTML fragments in a raw fashion -without _fixing_ content on-the-fly. - -Both `TagSoup` and `Jsoup` HTML parsers (that were considered for this project) are built to deal with -_malicious_ HTML code (*all HTML code*? :no_mouth:). So, when supplied -with a `italic` fragment they will make it `italic`. -And it's a good thing, but consider these fragments for the sake of markdown: - -* `italic ` -* `bold italic` -* `` - -We will get: - -* `italic ` -* `bold italic` - -_* Or to be precise: `italic ` & -`bold italic`_ - -Which will be rendered in a final document: - - -|expected|actual| -|---|---| -|italic bold italic|italic bold italic| - -This might seem like a minor problem, but add more tags to a document, -introduce some deeply nested structures, spice openning and closing tags up -by adding markdown markup between them and finally write _malicious_ HTML code :laughing:! - -There is no such problem on the _frontend_ for which commonmark specification is mostly -aimed as _frontend_ runs in a web-browser environment. After all _parsed_ markdown -will become HTML tags (most common usage). And web-browser will know how to render final result. - -We, on the other hand, do not posess HTML heritage (*thank :robot:!*), but still -want to display some HTML to style resulting markdown a bit. That's why `Markwon` -incorporated own HTML parsing logic. It is based on the project. -And makes usage of the `Tokekiser` class that allows to _tokenise_ input HTML. -All other code that doesn't follow this purpose was removed. It's safe to use -in projects that already have `jsoup` dependency as `Markwon` repackaged **jsoup** source classes -(which could be found ) - -## Parser - -There are no additional steps to configure HTML parsing. It's enabled by default. -If you wish to _exclude_ it, please follow the [exclude](#exclude-html-parsing) section below. - -The key class here is: `MarkwonHtmlParser` that is defined in `markwon-html-parser-api` module. -`markwon-html-parser-api` is a simple module that defines HTML parsing contract and -does not provide implementation. - -To change what implementation `Markwon` should use, `SpannableConfiguration` can be used: - -```java{2} -SpannableConfiguration.builder(context) - .htmlParser(MarkwonHtmlParser) - .build(); -``` - -`markwon-html-parser-impl` on the other hand provides `MarkwonHtmlParser` implementation. -It's called `MarkwonHtmlParserImpl`. It can be created like this: - -```java -final MarkwonHtmlParser htmlParser = MarkwonHtmlParserImpl.create(); -// or -final MarkwonHtmlParser htmlParser = MarkwonHtmlParserImpl.create(HtmlEmptyTagReplacement); -``` - -### Empty tag replacement - -In order to append text content for self-closing, void or just _empty_ HTML tags, -`HtmlEmptyTagReplacement` can be used. As we cannot set Span for empty content, -we must represent empty tag with text during parsing stage (if we want it to be represented). - -Consider this: -* `` -* `
` -* `` - -By default (`HtmlEmptyTagReplacement.create()`) will handle `img` and `br` tags. -`img` will be replaced with `alt` property if it is present and `\uFFFC` if it is not. -And `br` will insert a new line. - -### Non-closed tags - -It's possible that your HTML can contain non-closed tags. By default `Markwon` will ignore them, -but if you wish to get a bit closer to a web-browser experience, you can allow this behaviour: - -```java{2} -SpannableConfiguration.builder(context) - .htmlAllowNonClosedTags(true) - .build(); -``` - -:::warning Note -If there is (for example) an `` tag at the start of a document and it's not closed -and `Markwon` is configured to **not** ignore non-closed tags (`.htmlAllowNonClosedTags(true)`), -it will make the whole document in italics +:::tip A bit of background +
+ had brought attention to differences between HTML & commonmark implementations.

::: -### Implementation note +Unfortunately Android `HTML` class cannot parse a _fragment_ of HTML to later +be included in a bigger set of content. This is why the decision was made to bring +HTML parsing _in-markwon-house_ -`MarkwonHtmlParserImpl` does not create a unified HTML node. Instead it creates -2 collections: inline tags and block tags. Inline tags are represented as a `List` -of inline tags (). And -block tags are structured in a tree. This helps to achieve _browser_-like behaviour, -when open inline tag is applied to all content (even if inside blocks) until closing tag. -All tags that are not _inline_ are considered to be _block_ ones. +## Predefined TagHandlers +* `` +* `
` +* `

` +* `` +* `` +* `, ` +* `, ` +* `, ` +* `
    ,
      ` +* `, , , ` +* `

      ,

      ,

      ,

      ,

      ,
      ` -## Renderer - -Unlike `MarkwonHtmlParser` `Markwon` comes with a `MarkwonHtmlRenderer` by default. - -Default implementation can be obtain like this: - -```java -MarkwonHtmlRenderer.create(); -``` - -Default instance have these tags _handled_: -* emphasis - * `i` - * `em` - * `cite` - * `dfn` -* strong emphasis - * `b` - * `strong` -* `sup` (super script) -* `sub` (sub script) -* underline - * `u` - * `ins` -* strike through - * `del` - * `s` - * `strike` -* `a` (link) -* `ul` (unordered list) -* `ol` (ordered list) -* `img` (image) -* `blockquote` (block quote) -* `h{1-6}` (heading) - -If you wish to _extend_ default handling (or override existing), -`#builderWithDefaults` factory method can be used: - -```java -MarkwonHtmlRenderer.builderWithDefaults(); -``` - -For a completely _clean_ configurable instance `#builder` method can be used: - -```java -MarkwonHtmlRenderer.builder(); -``` - -### Custom tag handler - -To configure `MarkwonHtmlRenderer` to handle tags differently or -create a new tag handler - `TagHandler` can be used - -```java -public abstract class TagHandler { - - public abstract void handle( - @NonNull SpannableConfiguration configuration, - @NonNull SpannableBuilder builder, - @NonNull HtmlTag tag - ); -} -``` - -For the most simple _inline_ tag handler a `SimpleTagHandler` can be used: - -```java -public abstract class SimpleTagHandler extends TagHandler { - - @Nullable - public abstract Object getSpans(@NonNull SpannableConfiguration configuration, @NonNull HtmlTag tag); -} -``` - -For example, `EmphasisHandler`: - -```java -public class EmphasisHandler extends SimpleTagHandler { - @Nullable - @Override - public Object getSpans(@NonNull SpannableConfiguration configuration, @NonNull HtmlTag tag) { - return configuration.factory().emphasis(); - } -} -``` - -If you wish to handle a _block_ HTML node (for example `
      • First
      • Second
      `) refer -to `ListHandler` source code for reference. - -:::warning -The most important thing when implementing custom `TagHandler` is to know -what type of `HtmlTag` we are dealing with. There are 2: inline & block. -Inline tag cannot contain children. Block _can_ contain children. And they -_most likely_ should also be visited and _handled_ by registered `TagHandler` (if any) -accordingly. See `TagHandler#visitChildren(configuration, builder, child);` +:::tip +All predefined tag handlers will use styling spans for native markdown content. +So, if your `Markwon` instance was configured to, for example, render Emphasis +nodes as a red text then HTML tag handler will +use the same span. This includes images, links, UrlResolver, LinkProcessor, etc ::: - -#### Css inline style parser - -When implementing own `TagHandler` you might want to inspect inline CSS styles -of a HTML element. `Markwon` provides an utility parser for that purpose: - -```java -final CssInlineStyleParser inlineStyleParser = CssInlineStyleParser.create(); -for (CssProperty property: inlineStyleParser.parse("width: 100%; height: 100%;")) { - // [0] = CssProperty({width=100%}), - // [1] = CssProperty({height=100%}) -} -``` - -## Exclude HTML parsing - -If you wish to exclude HTML parsing altogether, you can manually -exclude `markwon-html-parser-impl` artifact from your projects compile classpath. -This can be beneficial if you know that markdown input won't contain -HTML and/or you wish to ignore it. Excluding HTML parsing -can speed up `Markwon` parsing and will decrease final size of -`Markwon` dependency by around `100kb`. - - - -```groovy -dependencies { - implementation("ru.noties:markwon:${markwonVersion}") { - exclude module: 'markwon-html-parser-impl' - } -} -``` - -Excluding `markwon-html-parser-impl` this way will result in -`MarkwonHtmlParser#noOp` implementation. No further steps are -required. - -:::warning Note -Excluding `markwon-html-parser-impl` won't remove *all* the content between -HTML tags. It will if `commonmark` decides that a specific fragment is a -`HtmlBlock`, but it won't if fragment is considered a `HtmlInline` as `HtmlInline` -does not contain content (just a tag definition). -::: \ No newline at end of file diff --git a/docs/docs/v3/html/custom-tag-handler.md b/docs/docs/v3/html/custom-tag-handler.md deleted file mode 100644 index e5ed0ccc..00000000 --- a/docs/docs/v3/html/custom-tag-handler.md +++ /dev/null @@ -1 +0,0 @@ -# HTML custom tag handler \ No newline at end of file diff --git a/docs/docs/v3/image/gif.md b/docs/docs/v3/image/gif.md index 5966132e..56ce081c 100644 --- a/docs/docs/v3/image/gif.md +++ b/docs/docs/v3/image/gif.md @@ -1,3 +1,15 @@ # Image GIF - \ No newline at end of file + + +Adds support for GIF images inside markdown. +Relies on [android-gif-drawable library](https://github.com/koral--/android-gif-drawable) + +```java +final Markwon markwon = Markwon.builder(context) + // it's required to register ImagesPlugin + .usePlugin(ImagesPlugin.create(context)) + // add GIF support for images + .usePlugin(GifPlugin.create()) + .build(); +``` \ No newline at end of file diff --git a/docs/docs/v3/image/okhttp.md b/docs/docs/v3/image/okhttp.md index 9d193492..4a7c159d 100644 --- a/docs/docs/v3/image/okhttp.md +++ b/docs/docs/v3/image/okhttp.md @@ -2,6 +2,25 @@ +Uses [okhttp library](https://github.com/square/okhttp) as the network transport fro images. Since +`Markwon` uses a system-native `HttpUrlConnection` and does not rely on any +3rd-party tool to download resources from network. It can answer the most common needs, +but if you would like to have a custom redirect policy or add an explicit caching +of downloaded resources OkHttp might be a better option. + +```java +final Markwon markwon = Markwon.builder(context) + // it's required to register ImagesPlugin + .usePlugin(ImagesPlugin.create(context)) + + // will create default instance of OkHttpClient + .usePlugin(OkHttpImagesPlugin.create()) + + // or accept a configured client + .usePlugin(OkHttpImagesPlugin.create(new OkHttpClient())) + .build(); +``` + ## Proguard ```proguard -dontwarn okhttp3.** diff --git a/docs/docs/v3/image/svg.md b/docs/docs/v3/image/svg.md index 322eb6bb..538c524c 100644 --- a/docs/docs/v3/image/svg.md +++ b/docs/docs/v3/image/svg.md @@ -2,6 +2,21 @@ +Adds support for SVG images inside markdown. +Relies on [androidsvg library](https://github.com/BigBadaboom/androidsvg) + +```java +final Markwon markwon = Markwon.builder(context) + // it's required to register ImagesPlugin + .usePlugin(ImagesPlugin.create(context)) + .usePlugin(SvgPlugin.create(context.getResources())) + .build(); +``` + +:::tip +`SvgPlugin` requires `Resources` in order to scale SVG media based on display density +::: + ## Proguard ```proguard diff --git a/docs/docs/v3/recycler-table/README.md b/docs/docs/v3/recycler-table/README.md index 0cd90583..e7287649 100644 --- a/docs/docs/v3/recycler-table/README.md +++ b/docs/docs/v3/recycler-table/README.md @@ -1,5 +1,7 @@ # Recycler Table + + Artifact that provides [MarkwonAdapter.Entry](/docs/v3/recycler/) to render `TableBlock` inside Android-native `TableLayout` widget. diff --git a/docs/docs/v3/recycler/README.md b/docs/docs/v3/recycler/README.md index 1941f4ae..73b05826 100644 --- a/docs/docs/v3/recycler/README.md +++ b/docs/docs/v3/recycler/README.md @@ -1,3 +1,153 @@ -# Recycler +# Recycler - \ No newline at end of file + + +This artifact allows displaying markdown in a set of Android widgets +inside a RecyclerView. Can be useful when displaying lengthy markdown +content or **displaying certain markdown blocks inside specific widgets**. + +```java +// create an adapter that will use a TextView for each block of markdown +// `createTextViewIsRoot` accepts a layout in which TextView is the root view +final MarkwonAdapter adapter = + MarkwonAdapter.createTextViewIsRoot(R.layout.adapter_default_entry); +``` + +```java +// `create` method accepts a layout with TextView and ID of a TextView +// which allows wrapping a TextView inside another widget or combine with other widgets +final MarkwonAdapter adapter = + MarkwonAdapter.create(R.layout.adapter_default_entry, R.id.text_view); + +// initialize RecyclerView (LayoutManager, Decorations, etc) +final RecyclerView recyclerView = obtainRecyclerView(); + +// set adapter +recyclerView.setAdapter(adapter); + +// obtain an instance of Markwon (register all required plugins) +final Markwon markwon = obtainMarkwon(); + +// set markdown to be displayed +adapter.setMarkdown(markwon, "# This is markdown!"); + +// NB, adapter does not handle updates on its own, please use +// whatever method appropriate for you. +adapter.notifyDataSetChanged(); +``` + +Initialized adapter above will use a TextView for each markdown block. +In order to tell adapter to render certain blocks differently a `builder` can be used. +For example, let's render `FencedCodeBlock` inside a `HorizontalScrollView`: + +```java +// we still need to have a _default_ entry +final MarkwonAdapter adapter = + MarkwonAdapter.builderTextViewIsRoot(R.layout.adapter_default_entry) + .include(FencedCodeBlock.class, new FencedCodeBlockEntry()) + .build(); +``` + +where `FencedCodeBlockEntry` is: + +```java +public class FencedCodeBlockEntry extends MarkwonAdapter.Entry { + + @NonNull + @Override + public Holder createHolder(@NonNull LayoutInflater inflater, @NonNull ViewGroup parent) { + return new Holder(inflater.inflate(R.layout.adapter_fenced_code_block, parent, false)); + } + + @Override + public void bindHolder(@NonNull Markwon markwon, @NonNull Holder holder, @NonNull FencedCodeBlock node) { + markwon.setParsedMarkdown(holder.textView, markwon.render(node)); + } + + public static class Holder extends MarkwonAdapter.Holder { + + final TextView textView; + + public Holder(@NonNull View itemView) { + super(itemView); + + this.textView = requireView(R.id.text_view); + } + } +} +``` + +and its layout (`R.layout.adapter_fenced_code_block`): + +```xml + + + + + + +``` + +As we apply styling to `FencedCodeBlock` _manually_, we no longer need +`Markwon` to apply styling spans for us, so `Markwon` initialization could be: + +```java +final Markwon markwon = Markwon.builder(context) + // your other plugins + .usePlugin(new AbstractMarkwonPlugin() { + @Override + public void configureVisitor(@NonNull MarkwonVisitor.Builder builder) { + builder.on(FencedCodeBlock.class, (visitor, fencedCodeBlock) -> { + // we actually won't be applying code spans here, as our custom view will + // draw background and apply mono typeface + // + // NB the `trim` operation on literal (as code will have a new line at the end) + final CharSequence code = visitor.configuration() + .syntaxHighlight() + .highlight(fencedCodeBlock.getInfo(), fencedCodeBlock.getLiteral().trim()); + visitor.builder().append(code); + }); + } + }) + .build(); +``` + +Previously we have created a `FencedCodeBlockEntry` but all it does is apply markdown to a TextView. +For such a case there is a `SimpleEntry` that could be used instead: + +```java +final MarkwonAdapter adapter = + MarkwonAdapter.builderTextViewIsRoot(R.layout.adapter_default_entry) + .include(FencedCodeBlock.class, SimpleEntry.create(R.layout.adapter_fenced_code_block, R.id.text_view)) + .build(); +``` + +:::tip +`SimpleEntry` also takes care of _caching_ parsed markdown. So each node will be +parsed only once and each subsequent adapter binding call will reuse previously cached markdown. +::: + +:::tip Tables +There is a standalone artifact that adds support for displaying markdown tables +natively via `TableLayout`. Please refer to its [documentation](/docs/v3/recycler-table/) +::: \ No newline at end of file diff --git a/markwon-core/src/main/java/ru/noties/markwon/html/MarkwonHtmlRenderer.java b/markwon-core/src/main/java/ru/noties/markwon/html/MarkwonHtmlRenderer.java index 19d355f0..93d0411d 100644 --- a/markwon-core/src/main/java/ru/noties/markwon/html/MarkwonHtmlRenderer.java +++ b/markwon-core/src/main/java/ru/noties/markwon/html/MarkwonHtmlRenderer.java @@ -47,6 +47,9 @@ public abstract class MarkwonHtmlRenderer { @NonNull Builder setHandler(@NonNull Collection tagNames, @Nullable TagHandler tagHandler); + @Nullable + TagHandler getHandler(@NonNull String tagName); + @NonNull MarkwonHtmlRenderer build(); } diff --git a/markwon-core/src/main/java/ru/noties/markwon/html/MarkwonHtmlRendererImpl.java b/markwon-core/src/main/java/ru/noties/markwon/html/MarkwonHtmlRendererImpl.java index 973342e2..0f4c97e6 100644 --- a/markwon-core/src/main/java/ru/noties/markwon/html/MarkwonHtmlRendererImpl.java +++ b/markwon-core/src/main/java/ru/noties/markwon/html/MarkwonHtmlRendererImpl.java @@ -124,6 +124,12 @@ class MarkwonHtmlRendererImpl extends MarkwonHtmlRenderer { return this; } + @Nullable + @Override + public TagHandler getHandler(@NonNull String tagName) { + return tagHandlers.get(tagName); + } + @NonNull @Override public MarkwonHtmlRenderer build() { diff --git a/sample/build.gradle b/sample/build.gradle index 966d0747..dbe4bdaa 100644 --- a/sample/build.gradle +++ b/sample/build.gradle @@ -40,6 +40,7 @@ dependencies { implementation project(':markwon-ext-tasklist') implementation project(':markwon-html') implementation project(':markwon-image-gif') + implementation project(':markwon-image-okhttp') implementation project(':markwon-image-svg') implementation project(':markwon-syntax-highlight') implementation project(':markwon-recycler') diff --git a/sample/src/main/res/layout/adapter_fenced_code_block.xml b/sample/src/main/res/layout/adapter_fenced_code_block.xml index e823fb09..053d59cc 100644 --- a/sample/src/main/res/layout/adapter_fenced_code_block.xml +++ b/sample/src/main/res/layout/adapter_fenced_code_block.xml @@ -1,6 +1,5 @@ + android:textSize="14sp" /> \ No newline at end of file From 2a100244c82f9ce80b20f2cb94bc8eaa69d8d0ed Mon Sep 17 00:00:00 2001 From: Dimitry Ivanov Date: Thu, 14 Mar 2019 17:40:40 +0300 Subject: [PATCH 096/103] Fix CorePluginTest --- .../test/java/ru/noties/markwon/core/CorePluginTest.java | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/markwon-core/src/test/java/ru/noties/markwon/core/CorePluginTest.java b/markwon-core/src/test/java/ru/noties/markwon/core/CorePluginTest.java index 681e1f0a..1cbf908b 100644 --- a/markwon-core/src/test/java/ru/noties/markwon/core/CorePluginTest.java +++ b/markwon-core/src/test/java/ru/noties/markwon/core/CorePluginTest.java @@ -156,6 +156,12 @@ public class CorePluginTest { return this; } + @Nullable + @Override + public SpanFactory getFactory(@NonNull Class node) { + throw new RuntimeException(); + } + @NonNull @Override public MarkwonSpansFactory build() { From 519c5c98e5d84dff644b6079864ae7e2b491937a Mon Sep 17 00:00:00 2001 From: Dimitry Ivanov Date: Fri, 15 Mar 2019 13:39:14 +0300 Subject: [PATCH 097/103] Update build script dependencies --- .travis.yml | 2 +- build.gradle | 10 ++++------ 2 files changed, 5 insertions(+), 7 deletions(-) diff --git a/.travis.yml b/.travis.yml index bd4791e7..ec7e3e53 100644 --- a/.travis.yml +++ b/.travis.yml @@ -10,7 +10,7 @@ android: - tools - build-tools-28.0.3 - - android-27 + - android-28 branches: except: diff --git a/build.gradle b/build.gradle index 08aeb0b6..63782269 100644 --- a/build.gradle +++ b/build.gradle @@ -4,7 +4,7 @@ buildscript { jcenter() } dependencies { - classpath 'com.android.tools.build:gradle:3.3.1' + classpath 'com.android.tools.build:gradle:3.3.2' classpath 'com.github.ben-manes:gradle-versions-plugin:0.20.0' } } @@ -47,15 +47,13 @@ ext { // NB, updating build-tools or compile-sdk will require updating Travis config (.travis.yml) config = [ 'build-tools' : '28.0.3', - 'compile-sdk' : 27, - 'target-sdk' : 27, + 'compile-sdk' : 28, + 'target-sdk' : 28, 'min-sdk' : 16, 'push-aar-gradle': 'https://raw.githubusercontent.com/noties/gradle-mvn-push/master/gradle-mvn-push-aar.gradle' ] - // for now 27.1.1 is used because it's the last one distributed with source files - // next version with sources is androidx one (we wait until migration) - final def supportVersion = '27.1.1' + final def supportVersion = '28.0.0' final def commonMarkVersion = '0.12.1' final def daggerVersion = '2.10' From 50bf5b341a7de4321a1033fb7c7959a94d0f14d5 Mon Sep 17 00:00:00 2001 From: Dimitry Ivanov Date: Fri, 15 Mar 2019 14:39:15 +0300 Subject: [PATCH 098/103] Add placeholder drawable for asyncDrawableLoader --- .../noties/markwon/image/AsyncDrawable.java | 29 +++++++++++++++++++ .../markwon/image/AsyncDrawableLoader.java | 29 +++++++++++++++++-- .../image/AsyncDrawableLoaderImpl.java | 19 ++++++++++-- .../image/AsyncDrawableLoaderNoOp.java | 8 +++++ 4 files changed, 79 insertions(+), 6 deletions(-) diff --git a/markwon-core/src/main/java/ru/noties/markwon/image/AsyncDrawable.java b/markwon-core/src/main/java/ru/noties/markwon/image/AsyncDrawable.java index 9d38b915..696f6529 100644 --- a/markwon-core/src/main/java/ru/noties/markwon/image/AsyncDrawable.java +++ b/markwon-core/src/main/java/ru/noties/markwon/image/AsyncDrawable.java @@ -39,6 +39,26 @@ public class AsyncDrawable extends Drawable { this.loader = loader; this.imageSizeResolver = imageSizeResolver; this.imageSize = imageSize; + + final Drawable placeholder = loader.placeholder(); + if (placeholder != null) { + + // process placeholder bounds + final Rect bounds = placeholder.getBounds(); + if (bounds.isEmpty()) { + // set intrinsic bounds + final Rect rect = new Rect( + 0, + 0, + placeholder.getIntrinsicWidth(), + placeholder.getIntrinsicHeight()); + placeholder.setBounds(rect); + setBounds(rect); + } + + // apply placeholder immediately if we have one + setResult(placeholder); + } } @NonNull @@ -66,6 +86,15 @@ public class AsyncDrawable extends Drawable { // if not null -> means we are attached if (callback != null) { + + // as we have a placeholder now, it's important to check it our placeholder + // has a proper callback at this point. This is not required in most cases, + // as placeholder should be static, but if it's not -> it can operate as usual + if (result != null + && result.getCallback() == null) { + result.setCallback(callback); + } + loader.load(destination, this); } else { if (result != null) { diff --git a/markwon-core/src/main/java/ru/noties/markwon/image/AsyncDrawableLoader.java b/markwon-core/src/main/java/ru/noties/markwon/image/AsyncDrawableLoader.java index f036aa7b..fe6506ee 100644 --- a/markwon-core/src/main/java/ru/noties/markwon/image/AsyncDrawableLoader.java +++ b/markwon-core/src/main/java/ru/noties/markwon/image/AsyncDrawableLoader.java @@ -12,6 +12,14 @@ import java.util.concurrent.Executors; public abstract class AsyncDrawableLoader { + /** + * @since 3.0.0 + */ + public interface DrawableProvider { + @Nullable + Drawable provide(); + } + /** * @since 3.0.0 */ @@ -33,6 +41,8 @@ public abstract class AsyncDrawableLoader { public abstract void cancel(@NonNull String destination); + @Nullable + public abstract Drawable placeholder(); public static class Builder { @@ -40,7 +50,8 @@ public abstract class AsyncDrawableLoader { final Map schemeHandlers = new HashMap<>(3); final Map mediaDecoders = new HashMap<>(3); MediaDecoder defaultMediaDecoder; - Drawable errorDrawable; + DrawableProvider placeholderDrawableProvider; + DrawableProvider errorDrawableProvider; @NonNull public Builder executorService(@NonNull ExecutorService executorService) { @@ -94,9 +105,21 @@ public abstract class AsyncDrawableLoader { return this; } + /** + * @since 3.0.0 + */ @NonNull - public Builder errorDrawable(Drawable errorDrawable) { - this.errorDrawable = errorDrawable; + public Builder placeholderDrawableProvider(@NonNull DrawableProvider placeholderDrawableProvider) { + this.placeholderDrawableProvider = placeholderDrawableProvider; + return this; + } + + /** + * @since 3.0.0 + */ + @NonNull + public Builder errorDrawableProvider(@NonNull DrawableProvider errorDrawableProvider) { + this.errorDrawableProvider = errorDrawableProvider; return this; } diff --git a/markwon-core/src/main/java/ru/noties/markwon/image/AsyncDrawableLoaderImpl.java b/markwon-core/src/main/java/ru/noties/markwon/image/AsyncDrawableLoaderImpl.java index 281d61fc..fd7c3ad1 100644 --- a/markwon-core/src/main/java/ru/noties/markwon/image/AsyncDrawableLoaderImpl.java +++ b/markwon-core/src/main/java/ru/noties/markwon/image/AsyncDrawableLoaderImpl.java @@ -5,6 +5,7 @@ import android.net.Uri; import android.os.Handler; import android.os.Looper; import android.support.annotation.NonNull; +import android.support.annotation.Nullable; import java.io.IOException; import java.io.InputStream; @@ -20,7 +21,8 @@ class AsyncDrawableLoaderImpl extends AsyncDrawableLoader { private final Map schemeHandlers; private final Map mediaDecoders; private final MediaDecoder defaultMediaDecoder; - private final Drawable errorDrawable; + private final DrawableProvider placeholderDrawableProvider; + private final DrawableProvider errorDrawableProvider; private final Handler mainThread; @@ -31,7 +33,8 @@ class AsyncDrawableLoaderImpl extends AsyncDrawableLoader { this.schemeHandlers = builder.schemeHandlers; this.mediaDecoders = builder.mediaDecoders; this.defaultMediaDecoder = builder.defaultMediaDecoder; - this.errorDrawable = builder.errorDrawable; + this.placeholderDrawableProvider = builder.placeholderDrawableProvider; + this.errorDrawableProvider = builder.errorDrawableProvider; this.mainThread = new Handler(Looper.getMainLooper()); } @@ -49,6 +52,14 @@ class AsyncDrawableLoaderImpl extends AsyncDrawableLoader { } } + @Nullable + @Override + public Drawable placeholder() { + return placeholderDrawableProvider != null + ? placeholderDrawableProvider.provide() + : null; + } + private Future execute(@NonNull final String destination, @NonNull AsyncDrawable drawable) { final WeakReference reference = new WeakReference(drawable); @@ -109,7 +120,9 @@ class AsyncDrawableLoaderImpl extends AsyncDrawableLoader { // if result is null, we assume it's an error if (result == null) { - result = errorDrawable; + result = errorDrawableProvider != null + ? errorDrawableProvider.provide() + : null; } if (result != null) { diff --git a/markwon-core/src/main/java/ru/noties/markwon/image/AsyncDrawableLoaderNoOp.java b/markwon-core/src/main/java/ru/noties/markwon/image/AsyncDrawableLoaderNoOp.java index cef634a4..74520fb2 100644 --- a/markwon-core/src/main/java/ru/noties/markwon/image/AsyncDrawableLoaderNoOp.java +++ b/markwon-core/src/main/java/ru/noties/markwon/image/AsyncDrawableLoaderNoOp.java @@ -1,6 +1,8 @@ package ru.noties.markwon.image; +import android.graphics.drawable.Drawable; import android.support.annotation.NonNull; +import android.support.annotation.Nullable; class AsyncDrawableLoaderNoOp extends AsyncDrawableLoader { @Override @@ -12,4 +14,10 @@ class AsyncDrawableLoaderNoOp extends AsyncDrawableLoader { public void cancel(@NonNull String destination) { } + + @Nullable + @Override + public Drawable placeholder() { + return null; + } } From 4514afc5940f4ef74aa845aa761cd5f1bbbd817d Mon Sep 17 00:00:00 2001 From: Dimitry Ivanov Date: Fri, 15 Mar 2019 14:53:07 +0300 Subject: [PATCH 099/103] Update docs to include placeholder image functionality --- docs/docs/v3/README.md | 2 +- docs/docs/v3/core/images.md | 52 +++++++++++++++++++++++++++++++++-- docs/docs/v3/migration-2-3.md | 4 ++- 3 files changed, 53 insertions(+), 5 deletions(-) diff --git a/docs/docs/v3/README.md b/docs/docs/v3/README.md index ef82ddba..b8e28b4c 100644 --- a/docs/docs/v3/README.md +++ b/docs/docs/v3/README.md @@ -50,7 +50,7 @@ listed in are supported (including support for * * Blockquote (`blockquote`) * Heading (`h1`, `h2`, `h3`, `h4`, `h5`, `h6`) * there is support to render any HTML tag, but it will require to create a special `TagHandler`, - more information can be found in [HTML section](/docs/v3/html/custom-tag-handler.md) + more information can be found in [HTML section](/docs/v3/core/html-renderer.md) * [Task lists](/docs/v3/ext-tasklist/): - [ ] Not _done_ - [X] **Done** with `X` diff --git a/docs/docs/v3/core/images.md b/docs/docs/v3/core/images.md index 01ca81ce..4aa675e2 100644 --- a/docs/docs/v3/core/images.md +++ b/docs/docs/v3/core/images.md @@ -145,8 +145,6 @@ If you want to display GIF or SVG images also, you can use [image-gif](/docs/v3/ and [image-svg](/docs/v3/image/svg.md) modules. ::: -## - :::tip If you are using [html](/docs/v3/html/) you do not have to additionally setup images displayed via `` tag, as `HtmlPlugin` automatically uses configured @@ -156,4 +154,52 @@ sizes, which is not supported natively by markdown, allowing absolute or relativ ```html ``` -::: \ No newline at end of file +::: + +## Placeholder drawable + +It's possible to provide a custom placeholder for an image (whilst it's loading). + +```java +final Markwon markwon = Markwon.builder(context) + .usePlugin(ImagesPlugin.create(context)) + .usePlugin(new AbstractMarkwonPlugin() { + @Override + public void configureImages(@NonNull AsyncDrawableLoader.Builder builder) { + builder.placeholderDrawableProvider(new AsyncDrawableLoader.DrawableProvider() { + @Override + public Drawable provide() { + // your custom placeholder drawable + return new PlaceholderDrawable(); + } + }); + } + }); +``` + +## Error drawable + +To fallback in case of error whilst loading an image, an `error drawable` can be used: + + +```java +final Markwon markwon = Markwon.builder(context) + .usePlugin(ImagesPlugin.create(context)) + .usePlugin(new AbstractMarkwonPlugin() { + @Override + public void configureImages(@NonNull AsyncDrawableLoader.Builder builder) { + builder.errorDrawableProvider(new AsyncDrawableLoader.DrawableProvider() { + @Override + public Drawable provide() { + // your custom error drawable + return new MyErrorDrawable(); + } + }); + } + }); +``` + +:::warning +Before `3.0.0` `AsyncDrawableLoader` accepted a simple `Drawable` as error drawable +argument. Starting `3.0.0` it accepts a `DrawableProvider` instead. +::: diff --git a/docs/docs/v3/migration-2-3.md b/docs/docs/v3/migration-2-3.md index fbaacba7..aeb045c6 100644 --- a/docs/docs/v3/migration-2-3.md +++ b/docs/docs/v3/migration-2-3.md @@ -7,4 +7,6 @@ * * OkHttpClient to download images moved to standalone module * HTML no longer _implicitly_ added to core functionality, it must be specified __explicitly__ (as an artifact) * removed `markwon-view` module -* changed Maven artifacts group to `ru.noties.markwon` \ No newline at end of file +* changed Maven artifacts group to `ru.noties.markwon` +* removed `errorDrawable` in AsyncDrawableLoader in favor of a drawable provider +* added placeholder for AsyncDrawableProvider \ No newline at end of file From 66fdbfeb31d6d5cd2c0b6ea5125e6ee7b833cc1f Mon Sep 17 00:00:00 2001 From: Dimitry Ivanov Date: Fri, 15 Mar 2019 15:09:28 +0300 Subject: [PATCH 100/103] Remove deprecated builder method in MarkwonTheme --- .../ru/noties/markwon/core/MarkwonTheme.java | 20 +------------------ 1 file changed, 1 insertion(+), 19 deletions(-) diff --git a/markwon-core/src/main/java/ru/noties/markwon/core/MarkwonTheme.java b/markwon-core/src/main/java/ru/noties/markwon/core/MarkwonTheme.java index 43848d75..8186957f 100644 --- a/markwon-core/src/main/java/ru/noties/markwon/core/MarkwonTheme.java +++ b/markwon-core/src/main/java/ru/noties/markwon/core/MarkwonTheme.java @@ -47,25 +47,7 @@ public class MarkwonTheme { public static MarkwonTheme create(@NonNull Context context) { return builderWithDefaults(context).build(); } - - /** - * Factory method to obtain an instance of {@link Builder}. Please note, that no default - * values are set. This might be useful if you require a lot of special styling that differs - * a lot with default one - * - * @return {@link Builder instance} - * @see #builderWithDefaults(Context) - * @see #builder(MarkwonTheme) - * @see #emptyBuilder() - * @since 1.0.0 - * @deprecated 3.0.0 - */ - @NonNull - @Deprecated - public static Builder builder() { - return new Builder(); - } - + /** * Create an empty instance of {@link Builder} with no default values applied *

      From 6098bcc65249f5a439671f8242c7142a6e0a8c5c Mon Sep 17 00:00:00 2001 From: Dimitry Ivanov Date: Fri, 15 Mar 2019 17:00:09 +0300 Subject: [PATCH 101/103] Update documentation to point to v3 as primary target --- README.md | 13 ++--- .../ru/noties/markwon/gif/GifProcessor.java | 3 +- docs/.vuepress/config.js | 29 ++++++------ docs/CHANGELOG.md | 40 +++++++++++++++- docs/README.md | 47 ++++++++++++++----- docs/docs/{v3 => v2}/README.md | 43 ++++------------- docs/docs/{ => v2}/configure.md | 14 +++--- docs/docs/{ => v2}/factory.md | 0 docs/docs/{ => v2}/getting-started.md | 2 +- docs/docs/{ => v2}/html.md | 0 docs/docs/{ => v2}/image-loader.md | 2 +- docs/docs/{ => v2}/install.md | 6 +-- docs/docs/{ => v2}/syntax-highlight.md | 0 docs/docs/{ => v2}/theme.md | 2 +- docs/docs/{ => v2}/view.md | 0 docs/docs/v3/ext-tables/README.md | 2 +- docs/docs/v3/html/README.md | 2 + docs/docs/v3/install.md | 2 +- 18 files changed, 123 insertions(+), 84 deletions(-) rename docs/docs/{v3 => v2}/README.md (59%) rename docs/docs/{ => v2}/configure.md (94%) rename docs/docs/{ => v2}/factory.md (100%) rename docs/docs/{ => v2}/getting-started.md (98%) rename docs/docs/{ => v2}/html.md (100%) rename docs/docs/{ => v2}/image-loader.md (98%) rename docs/docs/{ => v2}/install.md (91%) rename docs/docs/{ => v2}/syntax-highlight.md (100%) rename docs/docs/{ => v2}/theme.md (98%) rename docs/docs/{ => v2}/view.md (100%) diff --git a/README.md b/README.md index be6976a9..6e15de8a 100644 --- a/README.md +++ b/README.md @@ -2,10 +2,8 @@ # Markwon -[![markwon](https://img.shields.io/maven-central/v/ru.noties/markwon.svg?label=markwon)](http://search.maven.org/#search|ga|1|g%3A%22ru.noties%22%20AND%20a%3A%22markwon%22) -[![markwon-image-loader](https://img.shields.io/maven-central/v/ru.noties/markwon-image-loader.svg?label=markwon-image-loader)](http://search.maven.org/#search|ga|1|g%3A%22ru.noties%22%20AND%20a%3A%22markwon-image-loader%22) -[![markwon-syntax-highlight](https://img.shields.io/maven-central/v/ru.noties/markwon-syntax-highlight.svg?label=markwon-syntax-highlight)](http://search.maven.org/#search|ga|1|g%3A%22ru.noties%22%20AND%20a%3A%22markwon-syntax-highlight%22) -[![markwon-view](https://img.shields.io/maven-central/v/ru.noties/markwon-view.svg?label=markwon-view)](http://search.maven.org/#search|ga|1|g%3A%22ru.noties%22%20AND%20a%3A%22markwon-view%22) +![stable](https://img.shields.io/maven-central/v/ru.noties.markwon/core.svg?label=stable) +![snapshot](https://img.shields.io/nexus/s/https/oss.sonatype.org/ru.noties.markwon/core.svg?label=snapshot) [![Build Status](https://travis-ci.org/noties/Markwon.svg?branch=master)](https://travis-ci.org/noties/Markwon) @@ -33,14 +31,12 @@ features listed in [commonmark-spec] are supported ## Installation ```groovy -implementation "ru.noties:markwon:${markwonVersion}" -implementation "ru.noties:markwon-image-loader:${markwonVersion}" // optional -implementation "ru.noties:markwon-syntax-highlight:${markwonVersion}" // optional -implementation "ru.noties:markwon-view:${markwonVersion}" // optional +implementation "ru.noties.markwon:core:${markwonVersion}" ``` Please visit [documentation] web-site for further reference + ## Supported markdown features: * Emphasis (`*`, `_`) * Strong emphasis (`**`, `__`) @@ -55,6 +51,7 @@ Please visit [documentation] web-site for further reference * Code blocks * Tables (*with limitations*) * Syntax highlight +* LaTeX formulas * HTML * Emphasis (``, ``, ``, ``) * Strong emphasis (``, ``) diff --git a/app/src/main/java/ru/noties/markwon/gif/GifProcessor.java b/app/src/main/java/ru/noties/markwon/gif/GifProcessor.java index fcec2e17..8cdb1da5 100644 --- a/app/src/main/java/ru/noties/markwon/gif/GifProcessor.java +++ b/app/src/main/java/ru/noties/markwon/gif/GifProcessor.java @@ -115,12 +115,13 @@ public abstract class GifProcessor { } @Override - public void onClick(View widget) { + public void onClick(@NonNull View widget) { if (gifDrawable.isPlaying()) { gifDrawable.pause(); } else { gifDrawable.start(); } + widget.invalidate(); } } } diff --git a/docs/.vuepress/config.js b/docs/.vuepress/config.js index 5a0cbe66..dc157e44 100644 --- a/docs/.vuepress/config.js +++ b/docs/.vuepress/config.js @@ -12,20 +12,30 @@ module.exports = { ], themeConfig: { nav: [ - { text: 'Install', link: '/docs/install.md' }, + { text: 'Install', link: '/docs/v3/install.md' }, { text: 'Changelog', link: '/CHANGELOG.md' }, { text: 'API Version', items: [ - { text: 'Current (2.x.x)', link: '/' }, - { text: 'BETA (3.x.x)', link: '/docs/v3/' } + { text: 'Current (3.x.x)', link: '/' }, + { text: 'Legacy (2.x.x)', link: '/docs/v2/' } ] }, { text: 'Sandbox', link: '/sandbox.md' }, { text: 'Github', link: 'https://github.com/noties/Markwon' } ], sidebar: { - '/docs/v3/': [ + '/docs/v2': [ + '/docs/v2/getting-started.md', + '/docs/v2/configure.md', + '/docs/v2/theme.md', + '/docs/v2/factory.md', + '/docs/v2/image-loader.md', + '/docs/v2/syntax-highlight.md', + '/docs/v2/html.md', + '/docs/v2/view.md' + ], + '/': [ '', { title: 'Core', @@ -56,17 +66,6 @@ module.exports = { '/docs/v3/recycler-table/', '/docs/v3/syntax-highlight/', '/docs/v3/migration-2-3.md' - ], - '/': [ - '', - '/docs/getting-started.md', - '/docs/configure.md', - '/docs/theme.md', - '/docs/factory.md', - '/docs/image-loader.md', - '/docs/syntax-highlight.md', - '/docs/html.md', - '/docs/view.md' ] }, sidebarDepth: 2, diff --git a/docs/CHANGELOG.md b/docs/CHANGELOG.md index cfb1a790..fd3e24e2 100644 --- a/docs/CHANGELOG.md +++ b/docs/CHANGELOG.md @@ -1,6 +1,44 @@ # Changelog -# 2.0.1 +# 3.0.0 +* Plugins, plugins, plugins +* Split basic functionality blocks into standalone modules +* Maven artifacts group changed to `ru.noties.markwon` (previously had been `ru.noties`) +* removed `markwon`, `markwon-image-loader`, `markwon-html-pareser-api`, `markwon-html-parser-impl`, `markwon-view` modules +* new module system: `core`, `ext-latex`, `ext-strikethrough`, `ext-tables`, `ext-tasklist`, `html`, `image-gif`, `image-okhttp`, `image-svg`, `recycler`, `recycler-table`, `syntax-highlight` +* Add BufferType option for Markwon configuration +* Fix typo in AsyncDrawable waitingForDimensions +* New tests format +* `Markwon.render` returns `Spanned` instance of generic `CharSequence` +* LinkMovementMethod is applied implicitly if not set on a TextView explicitly +* Split code and codeBlock spans and factories +* Add CustomTypefaceSpan +* Add NoCopySpansFactory +* Add placeholder to image loading + +Generally speaking there are a lot of changes. Most of them are not backwards-compatible. +The main point of this release is the `Plugin` system that allows more fluent configuration +and opens the possibility of extending `Markwon` with 3rd party functionality in a simple +and intuitive fashion. Please refer to the [documentation web-site](https://noties.github.io/Markwon) +that has information on how to start migration. + +The shortest excerpt of this release can be expressed like this: + +```java +// previous v2.x.x way +Markwon.setMarkdown(textView, "**Hello there!**"); +``` + +```java +// 3.x.x +Markwon.create(context) + .setMarkdown(textView, "**Hello there!**"); +``` + +But there is much more to it, please visit documentation web-site +to get the full picture of latest changes. + +## 2.0.1 * `SpannableMarkdownVisitor` Rename blockQuoteIndent to blockIndent * Fixed block new lines logic for block quote and paragraph () * AsyncDrawable fix no dimensions bug () diff --git a/docs/README.md b/docs/README.md index 0dce1917..521bb1d5 100644 --- a/docs/README.md +++ b/docs/README.md @@ -1,11 +1,12 @@ --- -title: 'Overview' +title: 'Introduction' --- -Markwon Logo +Markwon Logo

      - +[![markwon](https://img.shields.io/maven-central/v/ru.noties.markwon/core.svg?label=markwon)](http://search.maven.org/#search|ga|1|g%3A%22ru.noties.markwon%22%20) +[![Build Status](https://travis-ci.org/noties/Markwon.svg?branch=master)](https://travis-ci.org/noties/Markwon) **Markwon** is a markdown library for Android. It parses markdown following with the help of amazing library @@ -20,22 +21,23 @@ but also gives all the means to tweak the appearance if desired. All markdown fe listed in are supported (including support for **inlined/block HTML code**, **markdown tables**, **images** and **syntax highlight**). -## Supported markdown features: +## Supported markdown features * Emphasis (`*`, `_`) * Strong emphasis (`**`, `__`) -* Strike-through (`~~`) * Headers (`#{1,6}`) * Links (`[]()` && `[][]`) -* [Images](/docs/image-loader.md) +* [Images](/docs/v3/core/images.md) * Thematic break (`---`, `***`, `___`) * Quotes & nested quotes (`>{1,}`) * Ordered & non-ordered lists & nested ones * Inline code * Code blocks -* Tables (*with limitations*) -* [Syntax highlight](/docs/syntax-highlight.md) -* [HTML](/docs/html.md) +* [Strike-through](/docs/v3/ext-strikethrough/) (`~~`) +* [Tables](/docs/v3/ext-tables/) (*with limitations*) +* [Syntax highlight](/docs/v3/syntax-highlight/) +* [LaTeX](/docs/v3/ext-latex/) formulas +* [HTML](/docs/v3/html/) * Emphasis (``, ``, ``, ``) * Strong emphasis (``, ``) * SuperScript (``) @@ -48,12 +50,14 @@ listed in are supported (including support for * * Blockquote (`blockquote`) * Heading (`h1`, `h2`, `h3`, `h4`, `h5`, `h6`) * there is support to render any HTML tag, but it will require to create a special `TagHandler`, - more information can be found in [HTML section](/docs/html.md#custom-tag-handler) -* Task lists: + more information can be found in [HTML section](/docs/v3/core/html-renderer.md) +* [Task lists](/docs/v3/ext-tasklist/): - [ ] Not _done_ - [X] **Done** with `X` - [x] ~~and~~ **or** small `x` +* Task-lists are not properly displayed on this web-site, library renders them correctly + ## Screenshots screenshot light #1 @@ -68,3 +72,24 @@ Screenshots are taken from sample application. It is a generic markdown viewer with support to display markdown content via `http`, `https` & `file` schemes and 2 themes included: Light & Dark. It can be downloaded from [releases](https://github.com/noties/Markwon/releases) ::: + + +## Awesome Markwon + +Applications using Markwon: + +* [Partico](https://partiko.app/) - Partiko is a censorship free social network. +* [FairNote](https://play.google.com/store/apps/details?id=com.rgiskard.fairnote) - simple and intuitive notepad app. It gives you speed and efficiency when you write notes, to-do lists, e-mails, or jot down quick ideas. + + +Extension/plugins: + +* [MarkwonCodeEx](https://github.com/kingideayou/MarkwonCodeEx) - Markwon extension support elegant code background. + +--- + +[Help to improve][awesome_link] this section by submitting your application or library +that is using `Markwon` + + +[awesome_link]: https://github.com/noties/Markwon/issues/new?labels=awesome&body=Please%20provide%20the%20following%3A%0A*%20Project%20name%0A*%20Project%20URL%20(repository%2C%20store%20listing%2C%20web%20page)%0A*%20Optionally%20_brand_%20image%20URL%0A%0APlease%20make%20sure%20that%20there%20is%20the%20**awesome**%20label%20selected%20for%20this%20issue.%0A%0A%F0%9F%99%8C%20 diff --git a/docs/docs/v3/README.md b/docs/docs/v2/README.md similarity index 59% rename from docs/docs/v3/README.md rename to docs/docs/v2/README.md index b8e28b4c..20275911 100644 --- a/docs/docs/v3/README.md +++ b/docs/docs/v2/README.md @@ -1,12 +1,11 @@ --- -title: 'Introduction' +title: 'Overview' --- Markwon Logo

      -[![markwon](https://img.shields.io/maven-central/v/ru.noties.markwon/core.svg?label=markwon)](http://search.maven.org/#search|ga|1|g%3A%22ru.noties.markwon%22%20) -[![Build Status](https://travis-ci.org/noties/Markwon.svg?branch=master)](https://travis-ci.org/noties/Markwon) + **Markwon** is a markdown library for Android. It parses markdown following with the help of amazing library @@ -21,23 +20,22 @@ but also gives all the means to tweak the appearance if desired. All markdown fe listed in are supported (including support for **inlined/block HTML code**, **markdown tables**, **images** and **syntax highlight**). -## Supported markdown features +## Supported markdown features: * Emphasis (`*`, `_`) * Strong emphasis (`**`, `__`) +* Strike-through (`~~`) * Headers (`#{1,6}`) * Links (`[]()` && `[][]`) -* [Images](/docs/v3/core/images.md) +* [Images](/docs/v2/image-loader.md) * Thematic break (`---`, `***`, `___`) * Quotes & nested quotes (`>{1,}`) * Ordered & non-ordered lists & nested ones * Inline code * Code blocks -* [Strike-through](/docs/v3/ext-strikethrough/) (`~~`) -* [Tables](/docs/v3/ext-tables/) (*with limitations*) -* [Syntax highlight](/docs/v3/syntax-highlight/) -* [LaTeX](/docs/v3/ext-latex/) formulas -* [HTML](/docs/v3/html/) +* Tables (*with limitations*) +* [Syntax highlight](/docs/v2/syntax-highlight.md) +* [HTML](/docs/v2/html.md) * Emphasis (``, ``, ``, ``) * Strong emphasis (``, ``) * SuperScript (``) @@ -50,8 +48,8 @@ listed in are supported (including support for * * Blockquote (`blockquote`) * Heading (`h1`, `h2`, `h3`, `h4`, `h5`, `h6`) * there is support to render any HTML tag, but it will require to create a special `TagHandler`, - more information can be found in [HTML section](/docs/v3/core/html-renderer.md) -* [Task lists](/docs/v3/ext-tasklist/): + more information can be found in [HTML section](/docs/v2/html.md#custom-tag-handler) +* Task lists: - [ ] Not _done_ - [X] **Done** with `X` - [x] ~~and~~ **or** small `x` @@ -70,24 +68,3 @@ Screenshots are taken from sample application. It is a generic markdown viewer with support to display markdown content via `http`, `https` & `file` schemes and 2 themes included: Light & Dark. It can be downloaded from [releases](https://github.com/noties/Markwon/releases) ::: - - -## Awesome Markwon - -Applications using Markwon: - -* [Partico](https://partiko.app/) - Partiko is a censorship free social network. -* [FairNote](https://play.google.com/store/apps/details?id=com.rgiskard.fairnote) - simple and intuitive notepad app. It gives you speed and efficiency when you write notes, to-do lists, e-mails, or jot down quick ideas. - - -Extension/plugins: - -* [MarkwonCodeEx](https://github.com/kingideayou/MarkwonCodeEx) - Markwon extension support elegant code background. - ---- - -[Help to improve][awesome_link] this section by submitting your application or library -that is using `Markwon` - - -[awesome_link]: https://github.com/noties/Markwon/issues/new?labels=awesome&body=Please%20provide%20the%20following%3A%0A*%20Project%20name%0A*%20Project%20URL%20(repository%2C%20store%20listing%2C%20web%20page)%0A*%20Optionally%20_brand_%20image%20URL%0A%0APlease%20make%20sure%20that%20there%20is%20the%20**awesome**%20label%20selected%20for%20this%20issue.%0A%0A%F0%9F%99%8C%20 diff --git a/docs/docs/configure.md b/docs/docs/v2/configure.md similarity index 94% rename from docs/docs/configure.md rename to docs/docs/v2/configure.md index 1d09e5fc..4ba81749 100644 --- a/docs/docs/configure.md +++ b/docs/docs/v2/configure.md @@ -24,13 +24,13 @@ values as they will be applied automatically If you plan on using images inside your markdown/HTML, you will have to **explicitly** register an implementation of `AsyncDrawable.Loader` via `#asyncDrawableLoader` builder method. `Markwon` comes with ready implementation for that and it can be found in -`markwon-image-loader` module. Refer to module [documentation](/docs/image-loader.md) +`markwon-image-loader` module. Refer to module [documentation](/docs/v2/image-loader.md) ::: ## Theme `SpannableTheme` controls how markdown is rendered. It has pretty extensive number of -options that can be found [here](/docs/theme.md) +options that can be found [here](/docs/v2/theme.md) ```java SpannableConfiguration.builder(context) @@ -56,7 +56,7 @@ If `AsyncDrawable.Loader` is not provided explicitly, default **no-op** implemen :::tip Implementation There are no restrictions on what implementation to use, but `Markwon` has artifact that can -answer the most common needs of displaying SVG, GIF and other image formats. It can be found [here](/docs/image-loader.md) +answer the most common needs of displaying SVG, GIF and other image formats. It can be found [here](/docs/v2/image-loader.md) ::: ### Size resolver @@ -107,7 +107,7 @@ If not provided explicitly, default **no-op** implementation will be used. Although `SyntaxHighlight` interface was included with the very first version of `Markwon` there were no ready-to-use implementations. But starting with `Markwon` provides one. It can be found in `markwon-syntax-highlight` artifact. Refer -to module [documentation](/docs/syntax-highlight.md) +to module [documentation](/docs/v2/syntax-highlight.md) ::: ## Link resolver @@ -166,7 +166,7 @@ SpannableConfiguration.builder(context) ``` If not provided explicitly, default `SpannableFactoryDef` implementation will be used. It is documented -in [this section](/docs/factory.md) +in [this section](/docs/v2/factory.md) ## Soft line break @@ -197,7 +197,7 @@ SpannableConfiguration.builder(context) if not provided explicitly, default `MarkwonHtmlParserImpl` will be used **if** it can be found in classpath, otherwise default **no-op** implementation -wiil be used. Refer to [HTML](/docs/html.md#parser) document for more information about this behavior. +wiil be used. Refer to [HTML](/docs/v2/html.md#parser) document for more information about this behavior. ### Renderer @@ -210,7 +210,7 @@ SpannableConfiguration.builder(context) ``` If not provided explicitly, default `MarkwonHtmlRenderer` implementation will be used. -It is documented [here](/docs/html.md#renderer) +It is documented [here](/docs/v2/html.md#renderer) ### HTML allow non-closed tags diff --git a/docs/docs/factory.md b/docs/docs/v2/factory.md similarity index 100% rename from docs/docs/factory.md rename to docs/docs/v2/factory.md diff --git a/docs/docs/getting-started.md b/docs/docs/v2/getting-started.md similarity index 98% rename from docs/docs/getting-started.md rename to docs/docs/v2/getting-started.md index bed30085..3361767a 100644 --- a/docs/docs/getting-started.md +++ b/docs/docs/v2/getting-started.md @@ -20,7 +20,7 @@ Toast.makeText(context, markdown, Toast.LENGTH_LONG).show(); ## Longer one -When you need to customize markdown parsing/rendering you can use [SpannableConfiguration](/docs/configure.md): +When you need to customize markdown parsing/rendering you can use [SpannableConfiguration](/docs/v2/configure.md): ```java final SpannableConfiguration configuration = SpannableConfiguration.builder(context) diff --git a/docs/docs/html.md b/docs/docs/v2/html.md similarity index 100% rename from docs/docs/html.md rename to docs/docs/v2/html.md diff --git a/docs/docs/image-loader.md b/docs/docs/v2/image-loader.md similarity index 98% rename from docs/docs/image-loader.md rename to docs/docs/v2/image-loader.md index ca79337e..6dca5991 100644 --- a/docs/docs/image-loader.md +++ b/docs/docs/v2/image-loader.md @@ -21,7 +21,7 @@ public interface Loader { `AsyncDrawableLoader` from `markwon-image-loader` artifact can be used. :::tip Install -[Learn how to add](/docs/install.md#image-loader) `markwon-image-loader` to your project +[Learn how to add](/docs/v2/install.md#image-loader) `markwon-image-loader` to your project ::: Default instance of `AsyncDrawableLoader` can be obtain like this: diff --git a/docs/docs/install.md b/docs/docs/v2/install.md similarity index 91% rename from docs/docs/install.md rename to docs/docs/v2/install.md index 3b4c735c..b46816dd 100644 --- a/docs/docs/install.md +++ b/docs/docs/v2/install.md @@ -39,7 +39,7 @@ Provides implementation of `AsyncDrawable.Loader` and comes with support for: * GIF * Other image formats -Please refer to documentation for [image loader](/docs/image-loader.md) module +Please refer to documentation for [image loader](/docs/v2/image-loader.md) module ### Syntax highlight @@ -49,7 +49,7 @@ implementation "ru.noties:markwon-syntax-highlight:${markwonVersion}" Provides implementation of `SyntaxHighlight` and allows various syntax highlighting in your markdown based Android applications. Comes with 2 ready-to-be-used themes: `light` and `dark`. -Please refer to documentation for [syntax highlight](/docs/syntax-highlight.md) module +Please refer to documentation for [syntax highlight](/docs/v2/syntax-highlight.md) module ### View @@ -59,7 +59,7 @@ implementation "ru.noties:markwon-view:${markwonVersion}" Provides 2 widgets to display markdown: `MarkwonView` and `MarkwonViewCompat` (subclasses of `TextView` and `AppCompatTextView` respectively). -Please refer to documentation for [view](/docs/view.md) module +Please refer to documentation for [view](/docs/v2/view.md) module ## Proguard diff --git a/docs/docs/syntax-highlight.md b/docs/docs/v2/syntax-highlight.md similarity index 100% rename from docs/docs/syntax-highlight.md rename to docs/docs/v2/syntax-highlight.md diff --git a/docs/docs/theme.md b/docs/docs/v2/theme.md similarity index 98% rename from docs/docs/theme.md rename to docs/docs/v2/theme.md index 4baf954c..292e3e39 100644 --- a/docs/docs/theme.md +++ b/docs/docs/v2/theme.md @@ -1,7 +1,7 @@ # Theme Here is the list of properties that can be configured via `SpannableTheme`. If you wish to control what -is out of this list, you can use [SpannableFactory](/docs/factory.md) +is out of this list, you can use [SpannableFactory](/docs/v2/factory.md) abstraction which lets you to gather full control of Spans that are used to display markdown. * `SpannableTheme#create(Context)` - creates a **default** instance of `SpannableBuilder (with _defaults_ registered) diff --git a/docs/docs/view.md b/docs/docs/v2/view.md similarity index 100% rename from docs/docs/view.md rename to docs/docs/v2/view.md diff --git a/docs/docs/v3/ext-tables/README.md b/docs/docs/v3/ext-tables/README.md index fe149ed2..8c75fc21 100644 --- a/docs/docs/v3/ext-tables/README.md +++ b/docs/docs/v3/ext-tables/README.md @@ -56,7 +56,7 @@ myTableWidget.setTable(table); :::tip To take advantage of this functionality and render tables without limitations (including -horizontally scrollable layout when its contents exceed screen width), refer to [recycler-table](/docs/v3/recycler-table) +horizontally scrollable layout when its contents exceed screen width), refer to [recycler-table](/docs/v3/recycler-table/) module documentation that adds support for rendering `TableBlock` markdown node inside Android-native `TableLayout` widget. ::: diff --git a/docs/docs/v3/html/README.md b/docs/docs/v3/html/README.md index a8a12d1b..12f66400 100644 --- a/docs/docs/v3/html/README.md +++ b/docs/docs/v3/html/README.md @@ -59,3 +59,5 @@ So, if your `Markwon` instance was configured to, for example, render Emphasis nodes as a red text then HTML tag handler will use the same span. This includes images, links, UrlResolver, LinkProcessor, etc ::: + +To learn more about defining own TagHandlers, please refer to [html-renderer docs](/docs/v3/core/html-renderer.md) diff --git a/docs/docs/v3/install.md b/docs/docs/v3/install.md index 7f7184b7..19c28cba 100644 --- a/docs/docs/v3/install.md +++ b/docs/docs/v3/install.md @@ -5,7 +5,7 @@ next: /docs/v3/core/getting-started.md # Installation -![release](https://img.shields.io/maven-central/v/ru.noties.markwon/core.svg?label=release) +![stable](https://img.shields.io/maven-central/v/ru.noties.markwon/core.svg?label=stable) ![snapshot](https://img.shields.io/nexus/s/https/oss.sonatype.org/ru.noties.markwon/core.svg?label=snapshot) From ab74222c54db87e572c0e3af144ad7dcc33a293f Mon Sep 17 00:00:00 2001 From: Dimitry Ivanov Date: Fri, 15 Mar 2019 19:29:52 +0300 Subject: [PATCH 102/103] Polishing documentation --- README.md | 7 +- docs/.vuepress/components/ArtifactPicker.vue | 71 +++++++------------- docs/.vuepress/config.js | 5 -- docs/README.md | 10 +-- docs/package-lock.json | 5 -- docs/package.json | 1 - 6 files changed, 35 insertions(+), 64 deletions(-) diff --git a/README.md b/README.md index 6e15de8a..87311b0f 100644 --- a/README.md +++ b/README.md @@ -2,9 +2,6 @@ # Markwon -![stable](https://img.shields.io/maven-central/v/ru.noties.markwon/core.svg?label=stable) -![snapshot](https://img.shields.io/nexus/s/https/oss.sonatype.org/ru.noties.markwon/core.svg?label=snapshot) - [![Build Status](https://travis-ci.org/noties/Markwon.svg?branch=master)](https://travis-ci.org/noties/Markwon) **Markwon** is a markdown library for Android. It parses markdown @@ -30,6 +27,10 @@ features listed in [commonmark-spec] are supported [sample-apk]: https://github.com/noties/Markwon/releases ## Installation + +![stable](https://img.shields.io/maven-central/v/ru.noties.markwon/core.svg?label=stable) +![snapshot](https://img.shields.io/nexus/s/https/oss.sonatype.org/ru.noties.markwon/core.svg?label=snapshot) + ```groovy implementation "ru.noties.markwon:core:${markwonVersion}" ``` diff --git a/docs/.vuepress/components/ArtifactPicker.vue b/docs/.vuepress/components/ArtifactPicker.vue index 463d230a..453b1cd2 100644 --- a/docs/.vuepress/components/ArtifactPicker.vue +++ b/docs/.vuepress/components/ArtifactPicker.vue @@ -3,38 +3,36 @@

      - - + + + +
      {{artifact.description}}
      -
      +
      - final def markwon_version = '{{latestVersion}}' + final def +  markwon_version =  + '{{latestVersion}}'

      - implementation "{{artifact.group}}:{{artifact.id}}:$markwon_version" + implementation  + "{{artifact.group}}:{{artifact.id}}: + $markwon_version + "