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/README.md b/README.md index be6976a9..9cc72be9 100644 --- a/README.md +++ b/README.md @@ -2,11 +2,6 @@ # 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 @@ -32,15 +27,20 @@ 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:${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 + +> You can find previous version of Markwon in [2.x.x](https://github.com/noties/Markwon/tree/2.x.x) branch + + ## Supported markdown features: * Emphasis (`*`, `_`) * Strong emphasis (`**`, `__`) @@ -55,6 +55,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/build.gradle b/app/build.gradle index 0869e5e5..c6ad59c1 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 { @@ -28,8 +28,13 @@ android { dependencies { - implementation project(':markwon') - implementation project(':markwon-image-loader') + implementation project(':markwon-core') + 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 { @@ -43,4 +48,4 @@ dependencies { annotationProcessor it['prism4j-bundler'] annotationProcessor it['dagger-compiler'] } -} +} \ No newline at end of file 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..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.spans.TaskListDrawable; +import ru.noties.markwon.ext.tasklist.TaskListDrawable; public class DebugCheckboxDrawableView extends View { diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 1cf83a05..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"> @@ -38,21 +41,6 @@ android:host="*" android:scheme="https" /> - - - - - - - - - - - - - - - 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/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/GifAwareSpannableFactory.java b/app/src/main/java/ru/noties/markwon/GifAwareSpannableFactory.java deleted file mode 100644 index f070e9fc..00000000 --- a/app/src/main/java/ru/noties/markwon/GifAwareSpannableFactory.java +++ /dev/null @@ -1,36 +0,0 @@ -package ru.noties.markwon; - -import android.support.annotation.NonNull; -import android.support.annotation.Nullable; - -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; - -public class GifAwareSpannableFactory extends SpannableFactoryDef { - - private final GifPlaceholder gifPlaceholder; - - public GifAwareSpannableFactory(@NonNull GifPlaceholder gifPlaceholder) { - this.gifPlaceholder = gifPlaceholder; - } - - @Nullable - @Override - public Object image(@NonNull SpannableTheme theme, @NonNull String destination, @NonNull AsyncDrawable.Loader 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/MainActivity.java b/app/src/main/java/ru/noties/markwon/MainActivity.java index 3bf49109..fd3965b6 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.view.View; import android.widget.TextView; @@ -27,9 +28,6 @@ public class MainActivity extends Activity { @Inject UriProcessor uriProcessor; - @Inject - GifProcessor gifProcessor; - @Override protected void onCreate(final Bundle savedInstanceState) { super.onCreate(savedInstanceState); @@ -40,9 +38,6 @@ public class MainActivity extends Activity { themes.apply(this); - // how can we obtain SpannableConfiguration 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 @@ -58,7 +53,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()); @@ -68,11 +63,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(CharSequence markdown) { + public void onMarkdownReady(@NonNull Markwon markwon, Spanned markdown) { - Markwon.setText(textView, markdown); - - gifProcessor.process(textView); + 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 23ff268b..cf2ab04c 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; @@ -13,24 +14,30 @@ 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.SpannableTheme; -import ru.noties.markwon.syntax.Prism4jSyntaxHighlight; +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.HtmlPlugin; +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.markwon.urlprocessor.UrlProcessor; +import ru.noties.markwon.urlprocessor.UrlProcessorRelativeToAbsolute; import ru.noties.prism4j.Prism4j; @ActivityScope public class MarkdownRenderer { interface MarkdownReadyListener { - void onMarkdownReady(CharSequence markdown); + void onMarkdownReady(@NonNull Markwon markwon, Spanned markdown); } - @Inject - AsyncDrawable.Loader loader; - @Inject ExecutorService service; @@ -64,9 +71,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(); @@ -78,29 +93,28 @@ public class MarkdownRenderer { ? prism4jThemeDefault : prism4JThemeDarkula; - 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 SpannableConfiguration configuration = SpannableConfiguration.builder(context) - .asyncDrawableLoader(loader) - .urlProcessor(urlProcessor) - .syntaxHighlight(Prism4jSyntaxHighlight.create(prism4j, prism4jTheme)) - .theme(SpannableTheme.builderWithDefaults(context) - .codeBackgroundColor(background) - .codeTextColor(prism4jTheme.textColor()) - .build()) - .factory(new GifAwareSpannableFactory(gifPlaceholder)) + final Markwon markwon = Markwon.builder(context) + .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); + } + }) .build(); final long start = SystemClock.uptimeMillis(); - final CharSequence text = Markwon.markdown(configuration, markdown); + final Spanned text = markwon.toMarkdown(markdown); final long end = SystemClock.uptimeMillis(); @@ -111,7 +125,7 @@ public class MarkdownRenderer { @Override public void run() { if (!isCancelled()) { - listener.onMarkdownReady(text); + listener.onMarkdownReady(markwon, text); task = null; } } 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/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/app/src/main/java/ru/noties/markwon/GifAwareAsyncDrawable.java b/app/src/main/java/ru/noties/markwon/gif/GifAwareAsyncDrawable.java similarity index 85% rename from app/src/main/java/ru/noties/markwon/GifAwareAsyncDrawable.java rename to app/src/main/java/ru/noties/markwon/gif/GifAwareAsyncDrawable.java index 78d286af..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; @@ -6,9 +6,10 @@ import android.support.annotation.NonNull; import android.support.annotation.Nullable; import pl.droidsonroids.gif.GifDrawable; -import ru.noties.markwon.renderer.ImageSize; -import ru.noties.markwon.renderer.ImageSizeResolver; -import ru.noties.markwon.spans.AsyncDrawable; +import ru.noties.markwon.image.AsyncDrawableLoader; +import ru.noties.markwon.image.ImageSize; +import ru.noties.markwon.image.ImageSizeResolver; +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/gif/GifAwarePlugin.java b/app/src/main/java/ru/noties/markwon/gif/GifAwarePlugin.java new file mode 100644 index 00000000..89e49384 --- /dev/null +++ b/app/src/main/java/ru/noties/markwon/gif/GifAwarePlugin.java @@ -0,0 +1,72 @@ +package ru.noties.markwon.gif; + +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; +import ru.noties.markwon.image.ImagesPlugin; +import ru.noties.markwon.priority.Priority; + +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; + + GifAwarePlugin(@NonNull Context context) { + this.context = context; + this.processor = GifProcessor.create(); + } + + @Override + 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.setFactory(Image.class, new SpanFactory() { + @Override + public Object getSpans(@NonNull MarkwonConfiguration configuration, @NonNull RenderProps props) { + return new AsyncDrawableSpan( + configuration.theme(), + new GifAwareAsyncDrawable( + gifPlaceholder, + ImageProps.DESTINATION.require(props), + configuration.asyncDrawableLoader(), + configuration.imageSizeResolver(), + ImageProps.IMAGE_SIZE.get(props) + ), + AsyncDrawableSpan.ALIGN_BOTTOM, + ImageProps.REPLACEMENT_TEXT_IS_LINK.get(props, false) + ); + } + }); + } + + @NonNull + @Override + public Priority priority() { + return Priority.after(ImagesPlugin.class); + } + + @Override + public void afterSetText(@NonNull TextView textView) { + processor.process(textView); + } +} 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 95% rename from app/src/main/java/ru/noties/markwon/GifProcessor.java rename to app/src/main/java/ru/noties/markwon/gif/GifProcessor.java index 7d2cd7c6..8cdb1da5 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; @@ -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.image.AsyncDrawableSpan; public abstract class GifProcessor { @@ -31,6 +31,7 @@ public abstract class GifProcessor { // if not we apply onGifListener final Spannable spannable = spannable(textView); + if (spannable == null) { return; } @@ -89,6 +90,7 @@ public abstract class GifProcessor { // 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; } @@ -113,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/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/drawable/ic_launcher_background.xml b/app/src/main/res/drawable/ic_launcher_background.xml new file mode 100644 index 00000000..49c86ecb --- /dev/null +++ b/app/src/main/res/drawable/ic_launcher_background.xml @@ -0,0 +1,24 @@ + + + + diff --git a/sample-custom-extension/src/main/res/mipmap-anydpi-v26/ic_launcher.xml b/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml similarity index 65% rename from sample-custom-extension/src/main/res/mipmap-anydpi-v26/ic_launcher.xml rename to app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml index eca70cfe..c4a603d4 100644 --- a/sample-custom-extension/src/main/res/mipmap-anydpi-v26/ic_launcher.xml +++ b/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml @@ -1,5 +1,5 @@ - - + + \ No newline at end of file diff --git a/app/src/main/res/mipmap-hdpi-v26/ic_launcher_foreground.png b/app/src/main/res/mipmap-hdpi-v26/ic_launcher_foreground.png new file mode 100644 index 00000000..89979b2e Binary files /dev/null and b/app/src/main/res/mipmap-hdpi-v26/ic_launcher_foreground.png differ diff --git a/app/src/main/res/mipmap-hdpi/ic_launcher.png b/app/src/main/res/mipmap-hdpi/ic_launcher.png index cac0e405..61bb0e66 100644 Binary files a/app/src/main/res/mipmap-hdpi/ic_launcher.png and b/app/src/main/res/mipmap-hdpi/ic_launcher.png differ diff --git a/app/src/main/res/mipmap-mdpi/ic_launcher.png b/app/src/main/res/mipmap-mdpi/ic_launcher.png deleted file mode 100644 index 25b79b68..00000000 Binary files a/app/src/main/res/mipmap-mdpi/ic_launcher.png and /dev/null differ diff --git a/app/src/main/res/mipmap-xhdpi-v26/ic_launcher_foreground.png b/app/src/main/res/mipmap-xhdpi-v26/ic_launcher_foreground.png new file mode 100644 index 00000000..c61b1584 Binary files /dev/null and b/app/src/main/res/mipmap-xhdpi-v26/ic_launcher_foreground.png differ diff --git a/app/src/main/res/mipmap-xhdpi/ic_launcher.png b/app/src/main/res/mipmap-xhdpi/ic_launcher.png index c7191395..80952f3d 100644 Binary files a/app/src/main/res/mipmap-xhdpi/ic_launcher.png and b/app/src/main/res/mipmap-xhdpi/ic_launcher.png differ diff --git a/app/src/main/res/mipmap-xxhdpi-v26/ic_launcher_foreground.png b/app/src/main/res/mipmap-xxhdpi-v26/ic_launcher_foreground.png new file mode 100644 index 00000000..f5213a49 Binary files /dev/null and b/app/src/main/res/mipmap-xxhdpi-v26/ic_launcher_foreground.png differ diff --git a/app/src/main/res/mipmap-xxhdpi/ic_launcher.png b/app/src/main/res/mipmap-xxhdpi/ic_launcher.png index 5fda70fe..d0b015ba 100644 Binary files a/app/src/main/res/mipmap-xxhdpi/ic_launcher.png and b/app/src/main/res/mipmap-xxhdpi/ic_launcher.png differ diff --git a/app/src/main/res/mipmap-xxxhdpi-v26/ic_launcher_foreground.png b/app/src/main/res/mipmap-xxxhdpi-v26/ic_launcher_foreground.png new file mode 100644 index 00000000..37333547 Binary files /dev/null and b/app/src/main/res/mipmap-xxxhdpi-v26/ic_launcher_foreground.png differ diff --git a/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png b/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png index 510044e8..df7e0fc6 100644 Binary files a/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png and b/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png differ diff --git a/art/markwon-icon-foreground.svg b/art/markwon-icon-foreground.svg new file mode 100644 index 00000000..64cf269b --- /dev/null +++ b/art/markwon-icon-foreground.svg @@ -0,0 +1,97 @@ + + + + + + + + + + image/svg+xml + + + + + + + M + ** + ** + + diff --git a/art/sample-icon-foreground.svg b/art/sample-icon-foreground.svg new file mode 100644 index 00000000..ae284cf4 --- /dev/null +++ b/art/sample-icon-foreground.svg @@ -0,0 +1,97 @@ + + + + + + + + + + image/svg+xml + + + + + + + M + ** + ** + + diff --git a/build.gradle b/build.gradle index 6ba06545..63782269 100644 --- a/build.gradle +++ b/build.gradle @@ -4,7 +4,7 @@ buildscript { jcenter() } dependencies { - classpath 'com.android.tools.build:gradle:3.2.1' + classpath 'com.android.tools.build:gradle:3.3.2' classpath 'com.github.ben-manes:gradle-versions-plugin:0.20.0' } } @@ -19,6 +19,10 @@ allprojects { } version = VERSION_NAME group = GROUP + + tasks.withType(Javadoc) { + options.addStringOption('Xdoclint:none', '-quiet') + } } task clean(type: Delete) { @@ -43,27 +47,30 @@ 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' ] - final def supportVersion = '27.1.1' + final def supportVersion = '28.0.0' final def commonMarkVersion = '0.12.1' final def daggerVersion = '2.10' 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", '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', + 'adapt' : 'ru.noties:adapt:1.1.0', 'dagger' : "com.google.dagger:dagger:$daggerVersion" ] @@ -73,14 +80,11 @@ 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', + 'commons-io' : 'commons-io:commons-io:2.6', + 'mockito' : 'org.mockito:mockito-core:2.21.0' ] registerArtifact = this.®isterArtifact diff --git a/docs/.vuepress/.artifacts.js b/docs/.vuepress/.artifacts.js new file mode 100644 index 00000000..2a1c3f43 --- /dev/null +++ b/docs/.vuepress/.artifacts.js @@ -0,0 +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":"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/components/ArtifactPicker.vue b/docs/.vuepress/components/ArtifactPicker.vue new file mode 100644 index 00000000..453b1cd2 --- /dev/null +++ b/docs/.vuepress/components/ArtifactPicker.vue @@ -0,0 +1,105 @@ + + + + + \ No newline at end of file diff --git a/docs/.vuepress/components/CommonmarkSandbox.vue b/docs/.vuepress/components/CommonmarkSandbox.vue new file mode 100644 index 00000000..0ddca312 --- /dev/null +++ b/docs/.vuepress/components/CommonmarkSandbox.vue @@ -0,0 +1,105 @@ + + + + + + + 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/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 @@ + 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 3e7cdaf0..a29ca520 100644 --- a/docs/.vuepress/config.js +++ b/docs/.vuepress/config.js @@ -1,37 +1,74 @@ module.exports = { base: '/Markwon/', title: 'Markwon', - description: 'Android markdown library based on commonmark specification', + description: 'Android markdown library based on commonmark specification that renders markdown as system-native Spannables (no WebView)', 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' }], + ['meta', { name: 'keywords', content: 'android,markdown,library,spannable,markwon,commonmark' }] ], 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 (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/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' - ], + sidebar: { + '/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', + collapsable: false, + children: [ + '/docs/v3/core/getting-started.md', + '/docs/v3/core/plugins.md', + '/docs/v3/core/theme.md', + '/docs/v3/core/images.md', + '/docs/v3/core/configuration.md', + '/docs/v3/core/visitor.md', + '/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/render-props.md' + ] + }, + '/docs/v3/ext-latex/', + '/docs/v3/ext-strikethrough/', + '/docs/v3/ext-tables/', + '/docs/v3/ext-tasklist/', + '/docs/v3/html/', + '/docs/v3/image/gif.md', + '/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' + ] + }, sidebarDepth: 2, lastUpdated: true - }, - markdown: { - config: md => { - md.use(require('markdown-it-task-lists')); - } } } \ No newline at end of file diff --git a/docs/.vuepress/override.styl b/docs/.vuepress/override.styl index ed8161b4..1e96c201 100644 --- a/docs/.vuepress/override.styl +++ b/docs/.vuepress/override.styl @@ -1,2 +1,23 @@ $textColor = #000000 -$accentColor = #4CAF50 \ No newline at end of file +$accentColor = #4CAF50 + +a.sidebar-link { + font-weight: 500; +} + +.sidebar-sub-headers a.sidebar-link { + font-weight: normal; +} + +.sidebar-group a.sidebar-link { + font-weight: normal; +} + +.sidebar-heading { + color: $textColor; + font-weight: 600; +} + +.sidebar-heading.open, .sidebar-heading:hover { + color: $accentColor; +} \ No newline at end of file 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 00000000..f609c65f Binary files /dev/null and b/docs/.vuepress/public/assets/recycler-table-screenshot.png differ diff --git a/docs/.vuepress/style.styl b/docs/.vuepress/style.styl index e69de29b..6ec59aba 100644 --- a/docs/.vuepress/style.styl +++ b/docs/.vuepress/style.styl @@ -0,0 +1,71 @@ +div[class~=language-gradle]:before { + content:"gradle" +} + +div[class~=language-proguard]:before { + content:"proguard" +} + +div[class~=language-groovy]:before { + content:"gradle" +} + +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/CHANGELOG.md b/docs/CHANGELOG.md index 05ff885e..fd3e24e2 100644 --- a/docs/CHANGELOG.md +++ b/docs/CHANGELOG.md @@ -1,17 +1,54 @@ # 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 (#82) -* AsyncDrawable fix no dimensions bug (#81) +* Fixed block new lines logic for block quote and paragraph () +* AsyncDrawable fix no dimensions bug () * Update SpannableTheme to use Px instead of Dimension annotation * Allow TaskListSpan isDone mutation * Updated commonmark-java to 0.12.1 -* Add OrderedListItemSpan measure utility method (#78) +* Add OrderedListItemSpan measure utility method () * Add SpannableBuilder#getSpans method -* Fix DataUri scheme handler in image-loader (#74) -* Introduced a "copy" builder for SpannableThem - Thanks @c-b-h 🙌 +* Fix DataUri scheme handler in image-loader () +* Introduced a "copy" builder for SpannableThem
Thanks ## 2.0.0 * Add `html-parser-api` and `html-parser-impl` modules diff --git a/docs/README.md b/docs/README.md index 0a8e8ba5..caa8ea4f 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,11 +50,13 @@ 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: -- [ ] Not _done_ - - [X] **Done** with `X` - - [x] ~~and~~ **or** small `x` + more information can be found in [HTML section](/docs/v3/core/html-renderer.md) +* [Task lists](/docs/v3/ext-tasklist/): +
    +
  • Not done
  • +
  • Done with X
  • +
  • and or small x
  • +
## Screenshots @@ -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/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/docs/v2/README.md b/docs/docs/v2/README.md new file mode 100644 index 00000000..20275911 --- /dev/null +++ b/docs/docs/v2/README.md @@ -0,0 +1,70 @@ +--- +title: 'Overview' +--- + +Markwon Logo + +

+ + +**Markwon** is a markdown library for Android. It parses markdown following + with the help of amazing 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 are supported (including support for **inlined/block HTML code**, +**markdown tables**, **images** and **syntax highlight**). + +## Supported markdown features: + +* Emphasis (`*`, `_`) +* Strong emphasis (`**`, `__`) +* Strike-through (`~~`) +* Headers (`#{1,6}`) +* Links (`[]()` && `[][]`) +* [Images](/docs/v2/image-loader.md) +* Thematic break (`---`, `***`, `___`) +* Quotes & nested quotes (`>{1,}`) +* Ordered & non-ordered lists & nested ones +* Inline code +* Code blocks +* Tables (*with limitations*) +* [Syntax highlight](/docs/v2/syntax-highlight.md) +* [HTML](/docs/v2/html.md) + * 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, but it will require to create a special `TagHandler`, + 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` + +## Screenshots + +screenshot light #1 +screenshot light #2 +screenshot light #3 +screenshot dark #2 + +By default configuration uses TextView textColor for styling, so changing textColor changes style + +:::tip Sample application +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) +::: 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 95% rename from docs/docs/getting-started.md rename to docs/docs/v2/getting-started.md index 60524932..3361767a 100644 --- a/docs/docs/getting-started.md +++ b/docs/docs/v2/getting-started.md @@ -1,10 +1,5 @@ # 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: @@ -25,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 97% rename from docs/docs/image-loader.md rename to docs/docs/v2/image-loader.md index f9642215..6dca5991 100644 --- a/docs/docs/image-loader.md +++ b/docs/docs/v2/image-loader.md @@ -16,12 +16,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/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 730a6511..b46816dd 100644 --- a/docs/docs/install.md +++ b/docs/docs/v2/install.md @@ -5,7 +5,7 @@ next: /docs/getting-started.md # Installation - + In order to start using `Markwon` add this to your dependencies block in your projects `build.gradle`: @@ -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 97% rename from docs/docs/syntax-highlight.md rename to docs/docs/v2/syntax-highlight.md index 64454903..854b82b0 100644 --- a/docs/docs/syntax-highlight.md +++ b/docs/docs/v2/syntax-highlight.md @@ -1,6 +1,6 @@ # 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 87% rename from docs/docs/theme.md rename to docs/docs/v2/theme.md index b01f7f31..292e3e39 100644 --- a/docs/docs/theme.md +++ b/docs/docs/v2/theme.md @@ -1,10 +1,19 @@ # Theme -Here is the list of properties that can be configured via `SpannableTheme#builder` factory -method. If you wish to control what is out of this list, you can use [SpannableFactory](/docs/factory.md) +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/v2/factory.md) abstraction which lets you to gather full control of Spans that are used to display markdown. -* factory methods +* `SpannableTheme#create(Context)` - creates a **default** instance of `SpannableBuilder (with _defaults_ registered) +* `SpannableTheme#builder` - creates **empty** builder with **no defaults registered** +* `SpannableTheme#builderWithDefaults(Context)` - create a **default** instance of builder (with default values registered) + +:::warning +`SpannbleTheme#builder` method has an unfortunate naming. It should've been `emptyBuilder` +or `builderNoDefaults` because `#builder` method returns a builder with no default +theme values registered. To create a builder **with** default values registered +use `SpannableBuilder#builderWithDefaults(Context)` +::: ## Link color @@ -107,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/view.md b/docs/docs/v2/view.md similarity index 96% rename from docs/docs/view.md rename to docs/docs/v2/view.md index c43b4e9d..bd610344 100644 --- a/docs/docs/view.md +++ b/docs/docs/v2/view.md @@ -1,6 +1,6 @@ # MarkwonView - + This is simple library containing 2 views that are able to display markdown: * MarkwonView - extends `android.view.TextView` @@ -27,7 +27,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/docs/docs/v3/core/configuration.md b/docs/docs/v3/core/configuration.md new file mode 100644 index 00000000..245b3310 --- /dev/null +++ b/docs/docs/v3/core/configuration.md @@ -0,0 +1,181 @@ +# 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 new file mode 100644 index 00000000..3a63c2c5 --- /dev/null +++ b/docs/docs/v3/core/core-plugin.md @@ -0,0 +1,105 @@ +# Core plugin + +Since with introduction of _plugins_, Markwon +**core** functionality was moved to a dedicated plugin. + +```java +CorePlugin.create(); +``` + +## Node visitors + +`CorePlugin` registers these `commonmark-java` node visitors: +* `Text` +* `StrongEmphasis` +* `Emphasis` +* `BlockQuote` +* `Code` +* `FencedCodeBlock` +* `IndentedCodeBlock` +* `BulletList` +* `OrderedList` +* `ListItem` +* `ThematicBreak` +* `Heading` +* `SoftLineBreak` +* `HardLineBreak` +* `Paragraph` +* `Link` + +## Span factories + +`CorePlugin` adds these `SpanFactory`s: +* `StrongEmphasis` +* `Emphasis` +* `BlockQuote` +* `Code` +* `FencedCodeBlock` +* `IndentedCodeBlock` +* `ListItem` +* `Heading` +* `Link` +* `ThematicBreak` + + +:::tip +By default `CorePlugin` does not register a `Paragraph` `SpanFactory` but +this can be done in your custom plugin: + +```java +Markwon.builder(context) + .usePlugin(new AbstractMarkwonPlugin() { + @Override + public void configureSpansFactory(@NonNull MarkwonSpansFactory.Builder builder) { + builder.setFactory(Paragraph.class, (configuration, props) -> + new ForegroundColorSpan(Color.RED)); + } + }) +``` +::: + +## Props +These props are exported by `CorePlugin` and can be found in `CoreProps`: +* `Prop LIST_ITEM_TYPE` (BULLET | ORDERED) +* `Prop BULLET_LIST_ITEM_LEVEL` +* `Prop ORDERED_LIST_ITEM_NUMBER` +* `Prop HEADING_LEVEL` +* `Prop LINK_DESTINATION` +* `Prop PARAGRAPH_IS_IN_TIGHT_LIST` + +:::warning List item type +Before `Markwon` had 2 distinct lists (bullet and ordered). +Since a single `SpanFactory` is used, which internally checks +for `Prop LIST_ITEM_TYPE`. +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 +Since Markwon core does not give an option to +insert a new line when there is a soft line break in markdown. Instead a +custom plugin can be used: + +```java +final Markwon markwon = Markwon.builder(this) + .usePlugin(new AbstractMarkwonPlugin() { + @Override + public void configureVisitor(@NonNull MarkwonVisitor.Builder builder) { + builder.on(SoftLineBreak.class, (visitor, softLineBreak) -> + visitor.forceNewLine()); + } + }) + .build(); +``` +::: + +:::warning +Please note that `CorePlugin` will implicitly set a `LinkMovementMethod` on a TextView +if one is not present. If you wish to customize a MovementMethod that is used, apply +one manually to a TextView (before applying markdown) or use the [MovementMethodPlugin](/docs/v3/core/movement-method-plugin.md) +which accepts a MovementMethod as an argument. +::: \ No newline at end of file diff --git a/docs/docs/v3/core/getting-started.md b/docs/docs/v3/core/getting-started.md new file mode 100644 index 00000000..652cfb7c --- /dev/null +++ b/docs/docs/v3/core/getting-started.md @@ -0,0 +1,65 @@ +# Getting started + +:::tip Installation +Please follow [installation](/docs/v3/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 +// obtain an instance of Markwon +final Markwon markwon = Markwon.create(context); + +// set markdown +markwon.setMarkdown(textView, "**Hello there!**"); +``` + +The most simple way to obtain markdown to be applied _somewhere_ else: + +```java +// obtain an instance of Markwon +final Markwon markwon = Markwon.create(context); + +// parse markdown and create styled text +final Spanned markdown = markwon.toMarkdown("**Hello there!**"); + +// use it +Toast.makeText(context, markdown, Toast.LENGTH_LONG).show(); +``` + +:::warning 3.x.x migration +Starting with version Markwon no longer relies on static +utility methods. To learn more about migrating existing applications +refer to [migration](/docs/v3/migration-2-3.md) section. +::: + +## Longer one + +With explicit `parse` and `render` methods: + +```java +// obtain an instance of Markwon +final Markwon markwon = Markwon.create(context); + +// parse markdown to commonmark-java Node +final Node node = markwon.parse("Are **you** still there?"); + +// create styled text from parsed Node +final Spanned markdown = markwon.render(node); + +// use it on a TextView +markwon.setParsedMarkdown(textView, markdown); + +// or a Toast +Toast.makeText(context, markdown, Toast.LENGTH_LONG).show(); +``` + +## No magic one + +This section is kept due to historical reasons. Starting with version +the amount of magic is reduced. To leverage your `Markwon` usage a concept of `Plugin` +is introduced which helps to extend default behavior in a simple and _no-breaking-the-flow_ manner. +Head to the [next section](/docs/v3/core/plugins.md) to know more. diff --git a/docs/docs/v3/core/html-renderer.md b/docs/docs/v3/core/html-renderer.md new file mode 100644 index 00000000..17ef63e3 --- /dev/null +++ b/docs/docs/v3/core/html-renderer.md @@ -0,0 +1,101 @@ +# 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 (includes HtmlParser) +to your 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()); + } + } +}); +``` + +:::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/core/images.md b/docs/docs/v3/core/images.md new file mode 100644 index 00000000..4aa675e2 --- /dev/null +++ b/docs/docs/v3/core/images.md @@ -0,0 +1,205 @@ +# Images + +Starting with `Markwon` comes with `ImagesPlugin` +which supports `http(s)`, `file` and `data` schemes and default media +decoder (for simple images, no [SVG](/docs/v3/image/svg.md) or [GIF](/docs/v3/image/gif.md) which +are defined in standalone modules). + +## ImagesPlugin + +`ImagePlugin` takes care of _obtaining_ image resource, decoding it and displaying it in a `TextView`. + +:::warning +Although `core` artifact contains `ImagesPlugin` one must +still **explicitly** register the `ImagesPlugin` on resulting `Markwon` +instance. +```java +final Markwon markwon = Markwon.builder(context) + .usePlugin(ImagesPlugin.create()) +``` +::: + +There are 2 factory methods to obtain `ImagesPlugin`: +* `ImagesPlugin#create(Context)` +* `ImagesPlugin#createWithAssets(Context)` + +The first one `#create(Context)` configures: +* `FileSchemeHandler` that allows obtaining images from `file://` uris +* `DataUriSchemeHandler` that allows _inlining_ images with `data:` + scheme (`data:image/svg+xml;base64,MTIz`) +* `NetworkSchemeHandler` that allows obtaining images from `http://` and `https://` uris + (internally it uses `HttpURLConnection`) +* `ImageMediaDecoder` which _tries_ to decode all encountered images as regular ones (png, jpg, etc) + +The second one `#createWithAssets(Context)` does the same but also adds support +for images that reside in `assets` folder of your application and +referenced by `file:///android_asset/{path}` uri. + +`ImagesPlugin` also _prepares_ a TextView to display images. Due to asynchronous +nature of image loading, there must be a way to invalidate resulting Spanned +content after an image is loaded. + +:::warning +Images come with few limitations. For of all, they work with a **TextView only**. +This is due to the fact that there is no way to invalidate a `Spanned` content +by itself (without context in which it is displayed). So, if `Markwon` is used, +for example, to display a `Toast` with an image: + +```java +final Spanned spanned = markwon.toMarkdown("Hello ![alt](https://my.image/1.JPG)"); +Toast.makeText(context, spanned, Toast.LENGTH_LONG).show(); +``` + +Image _probably_ won't be displayed. As a workaround for `Toast` a custom `View` +can be used: + +```java +final Spanned spanned = markwon.toMarkdown("Hello ![alt](https://my.image/1.JPG)"); + +final View view = createToastView(); +final TextView textView = view.findViewById(R.id.text_view); +markwon.setParsedMarkdown(textView, spanned); + +final Toast toast = new Toast(context); +toast.setView(view); +// other Toast configurations +toast.show(); +``` +::: + +## SchemeHandler + +To add support for different schemes (or customize provided) a `SchemeHandler` must be used. + +```java +final Markwon markwon = Markwon.builder(context) + .usePlugin(ImagesPlugin.create(context)) + .usePlugin(new AbstractMarkwonPlugin() { + @Override + public void configureImages(@NonNull AsyncDrawableLoader.Builder builder) { + // example only, Markwon doesn't come with a ftp scheme handler + builder.addSchemeHandler("ftp", new FtpSchemeHandler()); + } + }) + .build(); +``` + +It's a class to _convert_ an URI into an `InputStream`: + +```java +public abstract class SchemeHandler { + + @Nullable + public abstract ImageItem handle(@NonNull String raw, @NonNull Uri uri); +} +``` + +`ImageItem` is a holder class for resulting `InputStream` and (optional) +content type: + +```java +public class ImageItem { + + private final String contentType; + private final InputStream inputStream; + + /* rest omitted */ +} +``` + +Based on `contentType` returned a corresponding `MediaDecoder` will be matched. +If no `MediaDecoder` can handle given `contentType` then a default media decoder will +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 +image loader. But images referenced in HTML come with additional support for +sizes, which is not supported natively by markdown, allowing absolute or relative sizes: + +```html + +``` +::: + +## 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/core/movement-method-plugin.md b/docs/docs/v3/core/movement-method-plugin.md new file mode 100644 index 00000000..6bb50c87 --- /dev/null +++ b/docs/docs/v3/core/movement-method-plugin.md @@ -0,0 +1,17 @@ +# Movement method plugin + +`MovementMethodPlugin` can be used to apply a `MovementMethod` to a TextView +(important if you have links inside your markdown). By default `CorePlugin` +will set a `LinkMovementMethod` on a TextView if one is missing. If you have +specific needs for a `MovementMethod` and `LinkMovementMethod` doesn't answer +your needs use `MovementMethodPlugin`: + +```java +Markwon.builder(context) + .usePlugin(MovementMethodPlugin.create(ScrollingMovementMethod.getInstance())) +``` + +:::tip +If you are having trouble with system `LinkMovementMethod` as an alternative +[BetterLinkMovementMethod](https://github.com/saket/Better-Link-Movement-Method) library can be used. +::: diff --git a/docs/docs/v3/core/plugins.md b/docs/docs/v3/core/plugins.md new file mode 100644 index 00000000..dd83ab76 --- /dev/null +++ b/docs/docs/v3/core/plugins.md @@ -0,0 +1,467 @@ +# Plugins + +Since `MarkwonPlugin` takes the key role in +processing and rendering markdown. Even **core** functionaly is abstracted +into a `CorePlugin`. So it's still possible to use `Markwon` with a completely +own set of plugins. + +To register a plugin `Markwon.Builder` must be used: + +```java +Markwon.builder(context) + .usePlugin(CorePlugin.create()) + .build(); +``` + +All the process of transforming _raw_ markdown into a styled text (Spanned) +will go through plugins. A plugin can: + +* [configure commonmark-java `Parser`](#parser) +* [configure `MarkwonTheme`](#markwontheme) +* [configure `AsyncDrawableLoader` (used to display images in markdown)](#images) +* [configure `MarkwonConfiguration`](#configuration) +* [configure `MarkwonVisitor` (extensible commonmark-java Node visitor)](#visitor) +* [configure `MarkwonSpansFactory` (factory to hold spans information for each Node)](#spans-factory) +* [configure `MarkwonHtmlRenderer` (utility to properly display HTML in markdown)](#html-renderer) + +--- + +* [declare a dependency on another plugin (will be used as a runtime validator)](#priority) + +--- + +* [process raw input markdown before parsing it](#process-markdown) +* [inspect/modify commonmark-java Node after it's been parsed, but before rendering](#inspect-modify-node) +* [inspect commonmark-java Node after it's been rendered](#inspect-node-after-render) +* [prepare TextView to display markdown _before_ markdown is applied to a TextView](#prepare-textview) +* [post-process TextView _after_ markdown was applied](#textview-after-markdown-applied) + +:::tip +if you need to override only few methods of `MarkwonPlugin` (since it is an interface), +`AbstractMarkwonPlugin` can be used. +::: + +## Parser + +For example, let's register a new commonmark-java Parser extension: + +```java +final Markwon markwon = Markwon.builder(context) + .usePlugin(CorePlugin.create()) + .usePlugin(new AbstractMarkwonPlugin() { + @Override + public void configureParser(@NonNull Parser.Builder builder) { + // no need to call `super.configureParser(builder)` + builder.extensions(Collections.singleton(StrikethroughExtension.create())); + } + }) + .build(); +``` + +There are no limitations on what to do with commonmark-java Parser. For more info +_what_ can be done please refer to . + +## MarkwonTheme + +Starting `MarkwonTheme` represents _core_ theme. Aka theme for +things core module knows of. For example it doesn't know anything about `strikethrough` +or `tables` (as they belong to different modules). + +```java +final Markwon markwon = Markwon.builder(context) + .usePlugin(new AbstractMarkwonPlugin() { + @Override + public void configureTheme(@NonNull MarkwonTheme.Builder builder) { + builder + .codeTextColor(Color.BLACK) + .codeBackgroundColor(Color.GREEN); + } + }) + .build(); +``` + +:::warning +`CorePlugin` has special handling - it will be **implicitly** added +if a plugin declares dependency on it. This is why in previous example we haven't +added CorePlugin _explicitly_ as `AbstractMarkwonPlugin` declares a dependency on it. +If it's not desireable override `AbstractMarkwonPlugin#priority` method to specify own rules. +::: + +More information about `MarkwonTheme` can be found [here](/docs/v3/core/theme.md). + + +## Images + +Since core images functionality moved to the `core` module. +Now `Markwon` comes bundled with support for regular images (no `SVG` or `GIF`, they +defined in standalone modules now). And 3(4) schemes supported by default: +* http (+https; using system built-in `HttpURLConnection`) +* file (including Android assets) +* data (image inline, `data:image/svg+xml;base64,!@#$%^&*(`) + +```java +final Markwon markwon = Markwon.builder(context) + .usePlugin(ImagesPlugin.create()) + .usePlugin(new AbstractMarkwonPlugin() { + @Override + public void configureImages(@NonNull AsyncDrawableLoader.Builder builder) { + // sorry, these are not bundled with the library + builder + .addSchemeHandler("ftp", new FtpSchemeHandler("root", "")) + .addMediaDecoder("text/plain", new AnsiiMediaDecoder()); + } + }) + .build(); +``` + +:::warning +Although `ImagesPlugin` is bundled with the `core` artifact, it is **not** used by default +and one must **explicitly** add it: + +```java +Markwon.builder(context) + .usePlugin(ImagesPlugin.create(context)); +``` + +Without explicit usage of `ImagesPlugin` all image configuration will be ignored (no-op'ed) +::: + +More information about dealing with images can be found [here](/docs/v3/core/images.md) + + +## Configuration + +`MarkwonConfiguration` is a set of common tools that are used by different parts +of `Markwon`. It allows configurations of these: + +* `SyntaxHighlight` (highlighting code blocks) +* `LinkResolver` (opens links in markdown) +* `UrlProcessor` (process URLs in markdown for both links and images) +* `MarkwonHtmlParser` (HTML parser) +* `ImageSizeResolver` (resolve image sizes, like `fit-to-canvas`, etc) + +```java +final Markwon markwon = Markwon.builder(context) + .usePlugin(new AbstractMarkwonPlugin() { + @Override + public void configureConfiguration(@NonNull MarkwonConfiguration.Builder builder) { + // MarkwonHtmlParserImpl is defined in `markwon-html` artifact + builder.htmlParser(MarkwonHtmlParserImpl.create()); + } + }) + .build(); +``` + +More information about `MarkwonConfiguration` can be found [here](/docs/v3/core/configuration.md) + + +## Visitor + +`MarkwonVisitor` is commonmark-java Visitor that allows +configuration of how each Node is visited. There is no longer need to create +own subclass of Visitor and override required methods (like in `2.x.x` versions). +`MarkwonVisitor` also allows registration of Nodes, that `core` module knows +nothing about (instead of relying on `visit(CustomNode)` method)). + +For example, let's add `strikethrough` Node visitor: + +```java +final Markwon markwon = Markwon.builder(context) + .usePlugin(new AbstractMarkwonPlugin() { + @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.setSpansForNodeOptional(strikethrough, length); + } + }); + } + }) + .build(); +``` + +:::tip +`MarkwonVisitor` also allows _overriding_ already registered nodes. For example, +we can disable `Heading` Node rendering: + +```java +builder.on(Heading.class, null); +``` + +Please note that `Priority` plays nicely here to ensure that your +custom Node override/disable happens _after_ some plugin defines it. +::: + +More information about `MarkwonVisitor` can be found [here](/docs/v3/core/visitor.md) + + +## Spans Factory + +`MarkwonSpansFactory` is an abstract factory (factory that produces other factories) +for spans that `Markwon` uses. It controls what spans to use for certain Nodes. + +```java +final Markwon markwon = Markwon.builder(context) + .usePlugin(new AbstractMarkwonPlugin() { + @Override + public void configureSpansFactory(@NonNull MarkwonSpansFactory.Builder builder) { + // override emphasis factory to make all emphasis nodes underlined + builder.setFactory(Emphasis.class, new SpanFactory() { + @Override + public Object getSpans(@NonNull MarkwonConfiguration configuration, @NonNull RenderProps props) { + return new UnderlineSpan(); + } + }); + } + }) + .build(); +``` + +:::tip +`SpanFactory` allows to return an _array_ of spans to apply multiple spans +for a Node: + +```java +@Override +public Object getSpans(@NonNull MarkwonConfiguration configuration, @NonNull RenderProps props) { + // make underlined and set text color to red + return new Object[]{ + new UnderlineSpan(), + new ForegroundColorSpan(Color.RED) + }; +} +``` +::: + +More information about spans factory can be found [here](/docs/v3/core/spans-factory.md) + + +## HTML Renderer + +`MarkwonHtmlRenderer` controls how HTML is rendered in markdown. + +```java +final Markwon markwon = Markwon.builder(context) + .usePlugin(HtmlPlugin.create()) + .usePlugin(new AbstractMarkwonPlugin() { + @Override + public void configureHtmlRenderer(@NonNull MarkwonHtmlRenderer.Builder builder) { + //
tag handling (deprecated but valid in our case) + // can be any tag name, there is no connection with _real_ HTML tags, + // + builder.addHandler("center", new SimpleTagHandler() { + @Override + public Object getSpans(@NonNull MarkwonConfiguration configuration, @NonNull RenderProps renderProps, @NonNull HtmlTag tag) { + return new AlignmentSpan() { + @Override + public Layout.Alignment getAlignment() { + return Layout.Alignment.ALIGN_CENTER; + } + }; + } + }); + } + }) + .build(); +``` + +:::danger +Although `MarkwonHtmlRenderer` is bundled with `core` artifact, actual +HTML parser is placed in a standalone artifact and must be added to your +project **explicitly** and then registered via `Markwon.Builder#usePlugin(HtmlPlugin.create())`. +If not done so, no HTML will be parsed nor rendered. +::: + +More information about HTML rendering can be found [here](/docs/v3/core/html-renderer.md) + + +## Priority + +`Priority` is an abstraction to _state_ dependency connection between plugins. It is +also used as a runtime graph validator. If a plugin defines a dependency on other, but +_other_ is not in resulting `Markwon` instance, then a runtime exception will be thrown. +`Priority` is also defines the order in which plugins will be placed. So, if a plugin `A` +states a plugin `B` as a dependency, then plugin `A` will come **after** plugin `B`. + +```java +final Markwon markwon = Markwon.builder(context) + .usePlugin(new AbstractMarkwonPlugin() { + @NonNull + @Override + public Priority priority() { + return Priority.after(CorePlugin.class); + } + }) + .build(); +``` + +:::warning +Please note that `AbstractMarkwonPlugin` _implicitly_ defines `CorePlugin` +as a dependency (`return Priority.after(CorePlugin.class);`). This will +also add `CorePlugin` to a `Markwon` instance, because it will be added +_implicitly_ if a plugin defines it as a dependency. +::: + +Use one of the factory methods to create a `Priority` instance: + +```java +// none +Priority.none(); + +// single dependency +Priority.after(CorePlugin.class); + +// 2 dependencies +Priority.after(CorePlugin.class, ImagesPlugin.class); + +// for a number >2, use #builder +Priority.builder() + .after(CorePlugin.class) + .after(ImagesPlugin.class) + .after(StrikethroughPlugin.class) + .build(); +``` + +## Process markdown + +A plugin can be used to _pre-process_ input markdown (this will be called before _parsing_): + +```java +final Markwon markwon = Markwon.builder(context) + .usePlugin(new AbstractMarkwonPlugin() { + @NonNull + @Override + public String processMarkdown(@NonNull String markdown) { + return markdown.replaceAll("foo", "bar"); + } + }) + .build(); +``` + +## Inspect/modify Node + +A plugin can inspect/modify commonmark-java Node _before_ it's being rendered. + +```java +final Markwon markwon = Markwon.builder(context) + .usePlugin(new AbstractMarkwonPlugin() { + @Override + public void beforeRender(@NonNull Node node) { + + // for example inspect it with custom visitor + node.accept(new MyVisitor()); + + // or modify (you know what you are doing, right?) + node.appendChild(new Text("Appended")); + } + }) + .build(); +``` + +## Inspect Node after render + +A plugin can inspect commonmark-java Node after it's been rendered. +Modifying Node at this point makes not much sense (it's already been +rendered and all modifications won't change anything). But this method can be used, +for example, to clean-up some internal state (after rendering). Generally +speaking, a plugin must be stateless, but if it cannot, then this method is +the best place to clean-up. + +```java +final Markwon markwon = Markwon.builder(context) + .usePlugin(new AbstractMarkwonPlugin() { + @Override + public void afterRender(@NonNull Node node, @NonNull MarkwonVisitor visitor) { + cleanUp(); + } + }) + .build(); +``` + +## Prepare TextView + +A plugin can _prepare_ a TextView before markdown is applied. For example `images` +unschedules all previously scheduled `AsyncDrawableSpans` (if any) here. This way +when new markdown (and set of Spannables) arrives, previous set won't be kept in +memory and could be garbage-collected. + +```java +final Markwon markwon = Markwon.builder(context) + .usePlugin(new AbstractMarkwonPlugin() { + @Override + public void beforeSetText(@NonNull TextView textView, @NonNull Spanned markdown) { + // clean-up previous + AsyncDrawableScheduler.unschedule(textView); + } + }) + .build(); +``` + +## TextView after markdown applied + +A plugin will receive a callback _after_ markdown is applied to a TextView. +For example `images` uses this callback to schedule new set of Spannables. + +```java +final Markwon markwon = Markwon.builder(context) + .usePlugin(new AbstractMarkwonPlugin() { + @Override + public void afterSetText(@NonNull TextView textView) { + AsyncDrawableScheduler.schedule(textView); + } + }) + .build(); +``` + +:::tip +Please note that unlike `#beforeSetText`, `#afterSetText` won't receive +`Spanned` markdown. This happens because at this point spans must be +queried directly from a TextView. +::: + +## What happens underneath + +Here is what happens inside `Markwon` when `setMarkdown` method is called: + +```java +// `Markwon#create` implicitly uses CorePlugin +final Markwon markwon = Markwon.builder(context) + .usePlugin(CorePlugin.create()) + .build(); + +// warning: pseudo-code + +// 0. each plugin will be called to _pre-process_ raw input markdown +rawInput = plugins.reduce(rawInput, (input, plugin) -> plugin.processMarkdown(input)); + +// 1. after input is processed it's being parsed to a Node +node = parser.parse(rawInput); + +// 2. each plugin will be able to inspect or manipulate resulting Node +// before rendering +plugins.forEach(plugin -> plugin.beforeRender(node)); + +// 3. node is being visited by a visitor +node.accept(visitor); + +// 4. each plugin will be called after node is being visited (aka rendered) +plugins.forEach(plugin -> plugin.afterRender(node, visitor)); + +// 5. styled markdown ready at this point +final Spanned markdown = visitor.markdown(); + +// NB, points 6-8 are applied **only** if markdown is set to a TextView + +// 6. each plugin will be called before styled markdown is applied to a TextView +plugins.forEach(plugin -> plugin.beforeSetText(textView, markdown)); + +// 7. markdown is applied to a TextView +textView.setText(markdown); + +// 8. each plugin will be called after markdown is applied to a TextView +plugins.forEach(plugin -> plugin.afterSetText(textView)); +``` \ No newline at end of file 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/spans-factory.md b/docs/docs/v3/core/spans-factory.md new file mode 100644 index 00000000..6044de9a --- /dev/null +++ b/docs/docs/v3/core/spans-factory.md @@ -0,0 +1,61 @@ +# 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 new file mode 100644 index 00000000..672babc6 --- /dev/null +++ b/docs/docs/v3/core/theme.md @@ -0,0 +1,187 @@ +# Theme + +Here is the list of properties that can be configured via `MarkwonTheme.Builder` class. + +:::tip +Starting with there is no need to manually construct a `MarkwonTheme`. +Instead a `Plugin` should be used: +```java +final Markwon markwon = Markwon.builder(context) + .usePlugin(new AbstractMarkwonPlugin() { + @Override + public void configureTheme(@NonNull MarkwonTheme.Builder builder) { + builder + .codeTextColor(Color.BLACK) + .codeBackgroundColor(Color.GREEN); + } + }) + .build(); +``` +::: + +## 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 + + + +### 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 + +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/v3/core/visitor.md b/docs/docs/v3/core/visitor.md new file mode 100644 index 00000000..f0cce0f2 --- /dev/null +++ b/docs/docs/v3/core/visitor.md @@ -0,0 +1,73 @@ +# 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/docs/docs/v3/ext-latex/README.md b/docs/docs/v3/ext-latex/README.md new file mode 100644 index 00000000..ea1256f0 --- /dev/null +++ b/docs/docs/v3/ext-latex/README.md @@ -0,0 +1,46 @@ +# LaTeX extension + + + +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(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 +final Markwon markwon = Markwon.builder(context) + .usePlugin(ImagesPlugin.create(context)) + .usePlugin(JLatexMathPlugin.create(textSize, new BuilderConfigure() { + @Override + public void configureBuilder(@NonNull Builder builder) { + builder + .background(backgroundDrawable) + .align(JLatexMathDrawable.ALIGN_CENTER) + .fitCanvas(true) + .padding(paddingPx); + } + })) + .build(); +``` diff --git a/docs/docs/v3/ext-strikethrough/README.md b/docs/docs/v3/ext-strikethrough/README.md new file mode 100644 index 00000000..9bbfc0e5 --- /dev/null +++ b/docs/docs/v3/ext-strikethrough/README.md @@ -0,0 +1,29 @@ +# Strikethrough extension + + + +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/v3/ext-tables/README.md b/docs/docs/v3/ext-tables/README.md new file mode 100644 index 00000000..8c75fc21 --- /dev/null +++ b/docs/docs/v3/ext-tables/README.md @@ -0,0 +1,99 @@ +# Tables extension + + + +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)) +``` + +```java +Markwon.builder(context) + .usePlugin(TablePlugin.create(builder -> + builder + .tableBorderColor(Color.RED) + .tableBorderWidth(0) + .tableCellPadding(0) + .tableHeaderRowBackgroundColor(Color.BLACK) + .tableEvenRowBackgroundColor(Color.GREEN) + .tableOddRowBackgroundColor(Color.YELLOW) +)) +``` + +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); +``` + +:::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 + +### 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/v3/ext-tasklist/README.md b/docs/docs/v3/ext-tasklist/README.md new file mode 100644 index 00000000..3fb332f4 --- /dev/null +++ b/docs/docs/v3/ext-tasklist/README.md @@ -0,0 +1,146 @@ +# Task list extension + + + +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/html/README.md b/docs/docs/v3/html/README.md new file mode 100644 index 00000000..12f66400 --- /dev/null +++ b/docs/docs/v3/html/README.md @@ -0,0 +1,63 @@ +# HTML + +This artifact encapsulates HTML parsing from the core artifact and provides +few predefined `TagHandlers` + +```java +final Markwon markwon = Markwon.builder(context) + .usePlugin(HtmlPlugin.create()) + .build(); +``` + +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. + +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} + +Hello from italics tag + +bold> +``` + +This snippet could be represented as: +* HtmlBlock (`\nHello from italics tag`) +* HtmlInline (``) +* HtmlInline (``) +* Text (`bold`) +* HtmlInline (``) + +:::tip A bit of background +
+ had brought attention to differences between HTML & commonmark implementations.

+::: + +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_ + +## Predefined TagHandlers +* `` +* `
` +* `
` +* `` +* `` +* `, ` +* `, ` +* `, ` +* `
    ,
      ` +* `, , , ` +* `

      ,

      ,

      ,

      ,

      ,
      ` + +:::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 +::: + +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/image/gif.md b/docs/docs/v3/image/gif.md new file mode 100644 index 00000000..56ce081c --- /dev/null +++ b/docs/docs/v3/image/gif.md @@ -0,0 +1,15 @@ +# Image GIF + + + +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 new file mode 100644 index 00000000..4a7c159d --- /dev/null +++ b/docs/docs/v3/image/okhttp.md @@ -0,0 +1,28 @@ +# Image OkHttp + + + +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.** +-dontwarn okio.** +``` \ No newline at end of file diff --git a/docs/docs/v3/image/svg.md b/docs/docs/v3/image/svg.md new file mode 100644 index 00000000..538c524c --- /dev/null +++ b/docs/docs/v3/image/svg.md @@ -0,0 +1,25 @@ +# Image SVG + + + +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 +-keep class com.caverock.androidsvg.** { *; } +-dontwarn com.caverock.androidsvg.** +``` \ No newline at end of file diff --git a/docs/docs/v3/install.md b/docs/docs/v3/install.md new file mode 100644 index 00000000..19c28cba --- /dev/null +++ b/docs/docs/v3/install.md @@ -0,0 +1,34 @@ +--- +prev: false +next: /docs/v3/core/getting-started.md +--- + +# 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) + + + +## Snapshot + +In order to use latest `SNAPSHOT` version add snapshot repository +to your root project's `build.gradle` file: + +```groovy +allprojects { + repositories { + jcenter() + google() + // this one 👇 + maven { url 'https://oss.sonatype.org/content/repositories/snapshots/' } // 👈 this one + // this one 👆 + } +} +``` + +:::tip Info +All official artifacts share the same version number and all +are uploaded to **release** and **snapshot** repositories +::: + diff --git a/docs/docs/v3/migration-2-3.md b/docs/docs/v3/migration-2-3.md new file mode 100644 index 00000000..aeb045c6 --- /dev/null +++ b/docs/docs/v3/migration-2-3.md @@ -0,0 +1,12 @@ +# Migration 2.x.x -> 3.x.x + +* strikethrough moved to standalone module +* tables moved to standalone module +* core functionality of `AsyncDrawableLoader` moved to `core` module +* * Handling of GIF and SVG media moved to standalone modules (`gif` and `svg` respectively) +* * 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` +* removed `errorDrawable` in AsyncDrawableLoader in favor of a drawable provider +* added placeholder for AsyncDrawableProvider \ No newline at end of file diff --git a/docs/docs/v3/recycler-table/README.md b/docs/docs/v3/recycler-table/README.md new file mode 100644 index 00000000..e7287649 --- /dev/null +++ b/docs/docs/v3/recycler-table/README.md @@ -0,0 +1,92 @@ +# 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/docs/docs/v3/recycler/README.md b/docs/docs/v3/recycler/README.md new file mode 100644 index 00000000..73b05826 --- /dev/null +++ b/docs/docs/v3/recycler/README.md @@ -0,0 +1,153 @@ +# Recycler + + + +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/docs/docs/v3/syntax-highlight/README.md b/docs/docs/v3/syntax-highlight/README.md new file mode 100644 index 00000000..a5203cf6 --- /dev/null +++ b/docs/docs/v3/syntax-highlight/README.md @@ -0,0 +1,69 @@ +# 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. + +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/package-lock.json b/docs/package-lock.json index 5cc23227..0e3376df 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", @@ -5465,9 +5483,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" } @@ -5700,11 +5718,6 @@ "resolved": "https://registry.npmjs.org/markdown-it-table-of-contents/-/markdown-it-table-of-contents-0.4.3.tgz", "integrity": "sha512-x/OdaRzLYxAjmB+jIVlXuE3nX7tZTLDQxm58RkgjTLyQ+I290jYQvPS9cJjVN6SM3U6K6CHKYNgUtPNZmLblYQ==" }, - "markdown-it-task-lists": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/markdown-it-task-lists/-/markdown-it-task-lists-2.1.1.tgz", - "integrity": "sha512-TxFAc76Jnhb2OUu+n3yz9RMu4CwGfaT788br6HhEDlvWfdeJcLUsxk1Hgw2yJio0OXsxv7pyIPmvECY7bMbluA==" - }, "math-expression-evaluator": { "version": "1.2.17", "resolved": "https://registry.npmjs.org/math-expression-evaluator/-/math-expression-evaluator-1.2.17.tgz", @@ -9060,6 +9073,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 d469413c..9265fd10 100644 --- a/docs/package.json +++ b/docs/package.json @@ -1,9 +1,9 @@ { "scripts": { - "docs:build": "vuepress build" + "docs:build": "node ./collectArtifacts.js && vuepress build" }, "dependencies": { - "markdown-it-task-lists": "^2.1.1", + "commonmark": "^0.28.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 diff --git a/gradle.properties b/gradle.properties index 7865eb58..99e26be7 100644 --- a/gradle.properties +++ b/gradle.properties @@ -6,10 +6,10 @@ org.gradle.configureondemand=true android.enableBuildCache=true android.buildCacheDir=build/pre-dex-cache -VERSION_NAME=2.0.1 +VERSION_NAME=3.0.0 -GROUP=ru.noties -POM_DESCRIPTION=Markwon +GROUP=ru.noties.markwon +POM_DESCRIPTION=Markwon markdown for Android POM_URL=https://github.com/noties/Markwon POM_SCM_URL=https://github.com/noties/Markwon POM_SCM_CONNECTION=scm:git:git://github.com/noties/Markwon.git diff --git a/markwon-html-parser-impl/build.gradle b/markwon-core/build.gradle similarity index 72% rename from markwon-html-parser-impl/build.gradle rename to markwon-core/build.gradle index 2af46373..0f6f15db 100644 --- a/markwon-html-parser-impl/build.gradle +++ b/markwon-core/build.gradle @@ -15,17 +15,21 @@ android { dependencies { - api project(':markwon-html-parser-api') - deps.with { api it['support-annotations'] api it['commonmark'] } - deps.test.with { + deps['test'].with { + + testImplementation project(':markwon-test-span') + testImplementation it['junit'] testImplementation it['robolectric'] + testImplementation it['mockito'] + + testImplementation it['commons-io'] } } -registerArtifact(this) +registerArtifact(this) \ No newline at end of file diff --git a/markwon-core/gradle.properties b/markwon-core/gradle.properties new file mode 100644 index 00000000..e5c664e2 --- /dev/null +++ b/markwon-core/gradle.properties @@ -0,0 +1,4 @@ +POM_NAME=Core +POM_ARTIFACT_ID=core +POM_DESCRIPTION=Core Markwon artifact that includes basic markdown parsing and rendering +POM_PACKAGING=aar diff --git a/markwon/src/main/AndroidManifest.xml b/markwon-core/src/main/AndroidManifest.xml similarity index 100% rename from markwon/src/main/AndroidManifest.xml rename to markwon-core/src/main/AndroidManifest.xml diff --git a/markwon-core/src/main/java/ru/noties/markwon/AbstractMarkwonPlugin.java b/markwon-core/src/main/java/ru/noties/markwon/AbstractMarkwonPlugin.java new file mode 100644 index 00000000..5492d4d0 --- /dev/null +++ b/markwon-core/src/main/java/ru/noties/markwon/AbstractMarkwonPlugin.java @@ -0,0 +1,92 @@ +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 ru.noties.markwon.core.CorePlugin; +import ru.noties.markwon.core.MarkwonTheme; +import ru.noties.markwon.html.MarkwonHtmlRenderer; +import ru.noties.markwon.image.AsyncDrawableLoader; +import ru.noties.markwon.priority.Priority; + +/** + * 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 { + + @Override + public void configureParser(@NonNull Parser.Builder builder) { + + } + + @Override + public void configureTheme(@NonNull MarkwonTheme.Builder builder) { + + } + + @Override + public void configureImages(@NonNull AsyncDrawableLoader.Builder builder) { + + } + + @Override + public void configureConfiguration(@NonNull MarkwonConfiguration.Builder builder) { + + } + + @Override + public void configureVisitor(@NonNull MarkwonVisitor.Builder builder) { + + } + + @Override + public void configureSpansFactory(@NonNull MarkwonSpansFactory.Builder builder) { + + } + + @Override + public void configureHtmlRenderer(@NonNull MarkwonHtmlRenderer.Builder builder) { + + } + + @NonNull + @Override + public Priority priority() { + // by default all come after CorePlugin + return Priority.after(CorePlugin.class); + } + + @NonNull + @Override + public String processMarkdown(@NonNull String markdown) { + 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 Spanned markdown) { + + } + + @Override + public void afterSetText(@NonNull TextView textView) { + + } +} diff --git a/markwon/src/main/java/ru/noties/markwon/LinkResolverDef.java b/markwon-core/src/main/java/ru/noties/markwon/LinkResolverDef.java similarity index 95% rename from markwon/src/main/java/ru/noties/markwon/LinkResolverDef.java rename to markwon-core/src/main/java/ru/noties/markwon/LinkResolverDef.java index 109af717..4f893761 100644 --- a/markwon/src/main/java/ru/noties/markwon/LinkResolverDef.java +++ b/markwon-core/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-core/src/main/java/ru/noties/markwon/Markwon.java b/markwon-core/src/main/java/ru/noties/markwon/Markwon.java new file mode 100644 index 00000000..0ac110ff --- /dev/null +++ b/markwon-core/src/main/java/ru/noties/markwon/Markwon.java @@ -0,0 +1,136 @@ +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; + +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)} + * method. + * + * @see #create(Context) + * @see #builder(Context) + * @see Builder + */ +public abstract class Markwon { + + /** + * 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 + */ + @NonNull + public static Builder builder(@NonNull Context context) { + return new MarkwonBuilderImpl(context); + } + + /** + * Method to 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); + + /** + * 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); + + /** + * 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); + + public abstract void setMarkdown(@NonNull TextView textView, @NonNull String markdown); + + 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); + + @Nullable + public abstract

      P getPlugin(@NonNull Class

      type); + + /** + * 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 { + + /** + * 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 usePlugin(@NonNull MarkwonPlugin plugin); + + @NonNull + Builder usePlugins(@NonNull Iterable plugins); + + @NonNull + Markwon build(); + } +} diff --git a/markwon-core/src/main/java/ru/noties/markwon/MarkwonBuilderImpl.java b/markwon-core/src/main/java/ru/noties/markwon/MarkwonBuilderImpl.java new file mode 100644 index 00000000..a390c46e --- /dev/null +++ b/markwon-core/src/main/java/ru/noties/markwon/MarkwonBuilderImpl.java @@ -0,0 +1,194 @@ +package ru.noties.markwon; + +import android.content.Context; +import android.support.annotation.NonNull; +import android.support.annotation.VisibleForTesting; +import android.widget.TextView; + +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.CorePlugin; +import ru.noties.markwon.core.MarkwonTheme; +import ru.noties.markwon.html.MarkwonHtmlRenderer; +import ru.noties.markwon.image.AsyncDrawableLoader; +import ru.noties.markwon.priority.PriorityProcessor; + +/** + * @since 3.0.0 + */ +@SuppressWarnings("WeakerAccess") +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; + } + + @NonNull + @Override + public Markwon.Builder bufferType(@NonNull TextView.BufferType bufferType) { + this.bufferType = bufferType; + return this; + } + + @NonNull + @Override + 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; + } + + @SuppressWarnings("UnusedReturnValue") + @NonNull + public MarkwonBuilderImpl priorityProcessor(@NonNull PriorityProcessor priorityProcessor) { + this.priorityProcessor = priorityProcessor; + return this; + } + + @NonNull + @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(); + final MarkwonConfiguration.Builder configurationBuilder = new MarkwonConfiguration.Builder(); + final MarkwonVisitor.Builder visitorBuilder = new MarkwonVisitorImpl.BuilderImpl(); + final MarkwonSpansFactory.Builder spanFactoryBuilder = new MarkwonSpansFactoryImpl.BuilderImpl(); + final MarkwonHtmlRenderer.Builder htmlRendererBuilder = MarkwonHtmlRenderer.builder(); + + for (MarkwonPlugin plugin : plugins) { + plugin.configureParser(parserBuilder); + plugin.configureTheme(themeBuilder); + plugin.configureImages(asyncDrawableLoaderBuilder); + plugin.configureConfiguration(configurationBuilder); + plugin.configureVisitor(visitorBuilder); + plugin.configureSpansFactory(spanFactoryBuilder); + plugin.configureHtmlRenderer(htmlRendererBuilder); + } + + final MarkwonConfiguration configuration = configurationBuilder.build( + themeBuilder.build(), + asyncDrawableLoaderBuilder.build(), + htmlRendererBuilder.build(), + spanFactoryBuilder.build()); + + final RenderProps renderProps = new RenderPropsImpl(); + + return new MarkwonImpl( + bufferType, + parserBuilder.build(), + visitorBuilder.build(configuration, renderProps), + 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 core is present already we do not need to iterate anymore -> as nothing + // will be changed (and we actually do not care if there are any dependents of Core + // as it's present anyway) + 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; + } + } + } + + // important thing here is to check if corePlugin is added + // add it _only_ if it's not present + 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-core/src/main/java/ru/noties/markwon/MarkwonConfiguration.java b/markwon-core/src/main/java/ru/noties/markwon/MarkwonConfiguration.java new file mode 100644 index 00000000..af481ee8 --- /dev/null +++ b/markwon-core/src/main/java/ru/noties/markwon/MarkwonConfiguration.java @@ -0,0 +1,185 @@ +package ru.noties.markwon; + +import android.support.annotation.NonNull; + +import ru.noties.markwon.core.MarkwonTheme; +import ru.noties.markwon.core.spans.LinkSpan; +import ru.noties.markwon.html.MarkwonHtmlParser; +import ru.noties.markwon.html.MarkwonHtmlRenderer; +import ru.noties.markwon.image.AsyncDrawableLoader; +import ru.noties.markwon.image.ImageSizeResolver; +import ru.noties.markwon.image.ImageSizeResolverDef; +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` + */ +@SuppressWarnings("WeakerAccess") +public class MarkwonConfiguration { + + @NonNull + public static Builder builder() { + return new Builder(); + } + + private final MarkwonTheme theme; + private final AsyncDrawableLoader asyncDrawableLoader; + private final SyntaxHighlight syntaxHighlight; + private final LinkSpan.Resolver linkResolver; + private final UrlProcessor urlProcessor; + private final ImageSizeResolver imageSizeResolver; + private final MarkwonHtmlParser htmlParser; + private final MarkwonHtmlRenderer htmlRenderer; + + // @since 3.0.0 + private final MarkwonSpansFactory spansFactory; + + private MarkwonConfiguration(@NonNull Builder builder) { + this.theme = builder.theme; + this.asyncDrawableLoader = builder.asyncDrawableLoader; + this.syntaxHighlight = builder.syntaxHighlight; + this.linkResolver = builder.linkResolver; + this.urlProcessor = builder.urlProcessor; + this.imageSizeResolver = builder.imageSizeResolver; + this.spansFactory = builder.spansFactory; + this.htmlParser = builder.htmlParser; + this.htmlRenderer = builder.htmlRenderer; + } + + @NonNull + public MarkwonTheme theme() { + return theme; + } + + @NonNull + public AsyncDrawableLoader asyncDrawableLoader() { + return asyncDrawableLoader; + } + + @NonNull + public SyntaxHighlight syntaxHighlight() { + return syntaxHighlight; + } + + @NonNull + public LinkSpan.Resolver linkResolver() { + return linkResolver; + } + + @NonNull + public UrlProcessor urlProcessor() { + return urlProcessor; + } + + @NonNull + public ImageSizeResolver imageSizeResolver() { + return imageSizeResolver; + } + + @NonNull + public MarkwonHtmlParser htmlParser() { + return htmlParser; + } + + @NonNull + public MarkwonHtmlRenderer htmlRenderer() { + return htmlRenderer; + } + + /** + * @since 3.0.0 + */ + @NonNull + public MarkwonSpansFactory spansFactory() { + return spansFactory; + } + + @SuppressWarnings({"unused", "UnusedReturnValue"}) + public static class Builder { + + private MarkwonTheme theme; + private AsyncDrawableLoader asyncDrawableLoader; + private SyntaxHighlight syntaxHighlight; + private LinkSpan.Resolver linkResolver; + private UrlProcessor urlProcessor; + private ImageSizeResolver imageSizeResolver; + private MarkwonHtmlParser htmlParser; + private MarkwonHtmlRenderer htmlRenderer; + private MarkwonSpansFactory spansFactory; + + Builder() { + } + + @NonNull + public Builder syntaxHighlight(@NonNull SyntaxHighlight syntaxHighlight) { + this.syntaxHighlight = syntaxHighlight; + return this; + } + + @NonNull + public Builder linkResolver(@NonNull LinkSpan.Resolver linkResolver) { + this.linkResolver = linkResolver; + return this; + } + + @NonNull + public Builder urlProcessor(@NonNull UrlProcessor urlProcessor) { + this.urlProcessor = urlProcessor; + return this; + } + + @NonNull + public Builder htmlParser(@NonNull MarkwonHtmlParser htmlParser) { + this.htmlParser = htmlParser; + return this; + } + + /** + * @since 1.0.1 + */ + @NonNull + public Builder imageSizeResolver(@NonNull ImageSizeResolver imageSizeResolver) { + this.imageSizeResolver = imageSizeResolver; + return this; + } + + @NonNull + public MarkwonConfiguration build( + @NonNull MarkwonTheme theme, + @NonNull AsyncDrawableLoader asyncDrawableLoader, + @NonNull MarkwonHtmlRenderer htmlRenderer, + @NonNull MarkwonSpansFactory spansFactory) { + + this.theme = theme; + this.asyncDrawableLoader = asyncDrawableLoader; + this.htmlRenderer = htmlRenderer; + this.spansFactory = spansFactory; + + if (syntaxHighlight == null) { + syntaxHighlight = new SyntaxHighlightNoOp(); + } + + if (linkResolver == null) { + linkResolver = new LinkResolverDef(); + } + + if (urlProcessor == null) { + urlProcessor = new UrlProcessorNoOp(); + } + + if (imageSizeResolver == null) { + imageSizeResolver = new ImageSizeResolverDef(); + } + + if (htmlParser == null) { + htmlParser = MarkwonHtmlParser.noOp(); + } + + return new MarkwonConfiguration(this); + } + } + +} diff --git a/markwon-core/src/main/java/ru/noties/markwon/MarkwonImpl.java b/markwon-core/src/main/java/ru/noties/markwon/MarkwonImpl.java new file mode 100644 index 00000000..e90b7898 --- /dev/null +++ b/markwon-core/src/main/java/ru/noties/markwon/MarkwonImpl.java @@ -0,0 +1,110 @@ +package ru.noties.markwon; + +import android.support.annotation.NonNull; +import android.support.annotation.Nullable; +import android.text.Spanned; +import android.widget.TextView; + +import org.commonmark.node.Node; +import org.commonmark.parser.Parser; + +import java.util.List; + +/** + * @since 3.0.0 + */ +class MarkwonImpl extends Markwon { + + 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; + } + + @NonNull + @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); + } + + return parser.parse(input); + } + + @NonNull + @Override + public Spanned render(@NonNull Node node) { + + for (MarkwonPlugin plugin : plugins) { + plugin.beforeRender(node); + } + + node.accept(visitor); + + for (MarkwonPlugin plugin : plugins) { + plugin.afterRender(node, visitor); + } + + final Spanned spanned = visitor.builder().spannableStringBuilder(); + + // clear render props and builder after rendering + visitor.clear(); + + return spanned; + } + + @NonNull + @Override + public Spanned 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 Spanned markdown) { + + for (MarkwonPlugin plugin : plugins) { + plugin.beforeSetText(textView, markdown); + } + + textView.setText(markdown, bufferType); + + for (MarkwonPlugin plugin : plugins) { + plugin.afterSetText(textView); + } + } + + @Override + public boolean hasPlugin(@NonNull Class type) { + return getPlugin(type) != null; + } + + @Nullable + @Override + public

      P getPlugin(@NonNull Class

      type) { + MarkwonPlugin out = null; + for (MarkwonPlugin plugin : plugins) { + if (type.isAssignableFrom(plugin.getClass())) { + out = plugin; + } + } + //noinspection unchecked + return (P) out; + } +} 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/MarkwonPlugin.java b/markwon-core/src/main/java/ru/noties/markwon/MarkwonPlugin.java new file mode 100644 index 00000000..0804848b --- /dev/null +++ b/markwon-core/src/main/java/ru/noties/markwon/MarkwonPlugin.java @@ -0,0 +1,143 @@ +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 ru.noties.markwon.core.MarkwonTheme; +import ru.noties.markwon.html.MarkwonHtmlRenderer; +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 + * 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); + + /** + * Configure {@link MarkwonHtmlRenderer} to add or remove HTML {@link ru.noties.markwon.html.TagHandler}s + * + * @see MarkwonHtmlRenderer + * @see MarkwonHtmlRenderer.Builder + */ + void configureHtmlRenderer(@NonNull MarkwonHtmlRenderer.Builder builder); + + @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. + * + * @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); + + /** + * 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 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-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..a139e3f1 --- /dev/null +++ b/markwon-core/src/main/java/ru/noties/markwon/MarkwonReducer.java @@ -0,0 +1,61 @@ +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 { + + /** + * @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(); + } + + @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/MarkwonSpansFactory.java b/markwon-core/src/main/java/ru/noties/markwon/MarkwonSpansFactory.java new file mode 100644 index 00000000..8cf25a28 --- /dev/null +++ b/markwon-core/src/main/java/ru/noties/markwon/MarkwonSpansFactory.java @@ -0,0 +1,46 @@ +package ru.noties.markwon; + +import android.support.annotation.NonNull; +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 + SpanFactory get(@NonNull Class node); + + @NonNull + SpanFactory require(@NonNull Class node); + + + interface Builder { + + @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 new file mode 100644 index 00000000..ef1906d8 --- /dev/null +++ b/markwon-core/src/main/java/ru/noties/markwon/MarkwonSpansFactoryImpl.java @@ -0,0 +1,67 @@ +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; + + MarkwonSpansFactoryImpl(@NonNull Map, SpanFactory> factories) { + this.factories = factories; + } + + @Nullable + @Override + public SpanFactory get(@NonNull Class node) { + return factories.get(node); + } + + @NonNull + @Override + public SpanFactory require(@NonNull Class node) { + final SpanFactory f = get(node); + if (f == null) { + throw new NullPointerException(node.getName()); + } + return f; + } + + static class BuilderImpl implements Builder { + + private final Map, SpanFactory> factories = + new HashMap<>(3); + + @NonNull + @Override + public Builder setFactory(@NonNull Class node, @Nullable SpanFactory factory) { + if (factory == null) { + factories.remove(node); + } else { + factories.put(node, factory); + } + return this; + } + + @Nullable + @Override + public SpanFactory getFactory(@NonNull Class node) { + return factories.get(node); + } + + @NonNull + @Override + public MarkwonSpansFactory build() { + return new MarkwonSpansFactoryImpl(Collections.unmodifiableMap(factories)); + } + } +} diff --git a/markwon-core/src/main/java/ru/noties/markwon/MarkwonVisitor.java b/markwon-core/src/main/java/ru/noties/markwon/MarkwonVisitor.java new file mode 100644 index 00000000..e7a83db0 --- /dev/null +++ b/markwon-core/src/main/java/ru/noties/markwon/MarkwonVisitor.java @@ -0,0 +1,136 @@ +package ru.noties.markwon; + +import android.support.annotation.NonNull; +import android.support.annotation.Nullable; + +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); + + @NonNull + MarkwonVisitor build(@NonNull MarkwonConfiguration configuration, @NonNull RenderProps renderProps); + } + + @NonNull + MarkwonConfiguration configuration(); + + @NonNull + RenderProps renderProps(); + + @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(); + + /** + * 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()}. + * + * @param start start position of spans + * @param spans to apply + */ + void setSpans(int start, @Nullable Object spans); + + /** + * 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 + * 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(Class)}. This method uses {@link MarkwonSpansFactory#get(Class)}. + * + * @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-core/src/main/java/ru/noties/markwon/MarkwonVisitorImpl.java b/markwon-core/src/main/java/ru/noties/markwon/MarkwonVisitorImpl.java new file mode 100644 index 00000000..d4f0bce6 --- /dev/null +++ b/markwon-core/src/main/java/ru/noties/markwon/MarkwonVisitorImpl.java @@ -0,0 +1,292 @@ +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; + +/** + * @since 3.0.0 + */ +class MarkwonVisitorImpl implements MarkwonVisitor { + + private final MarkwonConfiguration configuration; + + private final RenderProps renderProps; + + private final SpannableBuilder builder; + + private final Map, NodeVisitor> nodes; + + MarkwonVisitorImpl( + @NonNull MarkwonConfiguration configuration, + @NonNull RenderProps renderProps, + @NonNull SpannableBuilder builder, + @NonNull Map, NodeVisitor> nodes) { + this.configuration = configuration; + this.renderProps = renderProps; + this.builder = builder; + 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 MarkwonConfiguration configuration() { + return configuration; + } + + @NonNull + @Override + public RenderProps renderProps() { + return renderProps; + } + + @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 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()); + } + + @Override + public void clear() { + renderProps.clearAll(); + builder.clear(); + } + + @Override + public void setSpansForNode(@NonNull N node, int start) { + setSpansForNode(node.getClass(), start); + } + + @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) { + setSpansForNodeOptional(node.getClass(), start); + } + + @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<>(); + + @NonNull + @Override + 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; + } + + @NonNull + @Override + public MarkwonVisitor build(@NonNull MarkwonConfiguration configuration, @NonNull RenderProps renderProps) { + return new MarkwonVisitorImpl( + configuration, + renderProps, + new SpannableBuilder(), + Collections.unmodifiableMap(nodes)); + } + } +} diff --git a/markwon-core/src/main/java/ru/noties/markwon/Prop.java b/markwon-core/src/main/java/ru/noties/markwon/Prop.java new file mode 100644 index 00000000..91b4515b --- /dev/null +++ b/markwon-core/src/main/java/ru/noties/markwon/Prop.java @@ -0,0 +1,86 @@ +package ru.noties.markwon; + +import android.support.annotation.NonNull; +import android.support.annotation.Nullable; + +/** + * 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 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; + + Prop(@NonNull String name) { + this.name = name; + } + + @NonNull + public String name() { + return name; + } + + @Nullable + public T get(@NonNull RenderProps props) { + return props.get(this); + } + + @NonNull + public T get(@NonNull RenderProps props, @NonNull T defValue) { + return props.get(this, defValue); + } + + @NonNull + public T require(@NonNull RenderProps props) { + final T t = get(props); + if (t == null) { + throw new NullPointerException(name); + } + return t; + } + + public void set(@NonNull RenderProps props, @Nullable T value) { + props.set(this, value); + } + + public void clear(@NonNull RenderProps props) { + props.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-core/src/main/java/ru/noties/markwon/RenderProps.java b/markwon-core/src/main/java/ru/noties/markwon/RenderProps.java new file mode 100644 index 00000000..e28edbaf --- /dev/null +++ b/markwon-core/src/main/java/ru/noties/markwon/RenderProps.java @@ -0,0 +1,22 @@ +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); + + void clearAll(); +} diff --git a/markwon-core/src/main/java/ru/noties/markwon/RenderPropsImpl.java b/markwon-core/src/main/java/ru/noties/markwon/RenderPropsImpl.java new file mode 100644 index 00000000..24557975 --- /dev/null +++ b/markwon-core/src/main/java/ru/noties/markwon/RenderPropsImpl.java @@ -0,0 +1,52 @@ +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 + */ +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); + } + + @Override + public void clearAll() { + values.clear(); + } +} diff --git a/markwon-core/src/main/java/ru/noties/markwon/SpanFactory.java b/markwon-core/src/main/java/ru/noties/markwon/SpanFactory.java new file mode 100644 index 00000000..76e251e7 --- /dev/null +++ b/markwon-core/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 props); +} diff --git a/markwon/src/main/java/ru/noties/markwon/SpannableBuilder.java b/markwon-core/src/main/java/ru/noties/markwon/SpannableBuilder.java similarity index 96% rename from markwon/src/main/java/ru/noties/markwon/SpannableBuilder.java rename to markwon-core/src/main/java/ru/noties/markwon/SpannableBuilder.java index 07e2bb85..0b99d05c 100644 --- a/markwon/src/main/java/ru/noties/markwon/SpannableBuilder.java +++ b/markwon-core/src/main/java/ru/noties/markwon/SpannableBuilder.java @@ -208,7 +208,11 @@ 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. + * <<<<<<< HEAD:markwon-core/src/main/java/ru/noties/markwon/SpannableBuilder.java + * NB spans are returned in reversed order (not in order that we store them internally) + * ======= * NB spans are returned in reversed order (no in order that we store them internally) + * >>>>>>> master:markwon/src/main/java/ru/noties/markwon/SpannableBuilder.java * * @since 2.0.1 */ @@ -325,6 +329,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... diff --git a/markwon-core/src/main/java/ru/noties/markwon/core/CorePlugin.java b/markwon-core/src/main/java/ru/noties/markwon/core/CorePlugin.java new file mode 100644 index 00000000..77fa3653 --- /dev/null +++ b/markwon-core/src/main/java/ru/noties/markwon/core/CorePlugin.java @@ -0,0 +1,410 @@ +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.text.method.LinkMovementMethod; +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.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; +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.MarkwonSpansFactory; +import ru.noties.markwon.MarkwonVisitor; +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; +import ru.noties.markwon.priority.Priority; + +/** + * @see CoreProps + * @since 3.0.0 + */ +public class CorePlugin extends AbstractMarkwonPlugin { + + @NonNull + public static CorePlugin create() { + return new CorePlugin(); + } + + protected CorePlugin() { + } + + @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); + heading(builder); + softLineBreak(builder); + 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()); + } + + @NonNull + @Override + public Priority priority() { + return Priority.none(); + } + + @Override + public void beforeSetText(@NonNull TextView textView, @NonNull Spanned markdown) { + OrderedListItemSpan.measure(textView, markdown); + } + + @Override + public void afterSetText(@NonNull TextView textView) { + // let's ensure that there is a movement method applied + // we do it `afterSetText` so any user-defined movement method won't be + // replaced (it should be done in `beforeSetText` or manually on a TextView) + if (textView.getMovementMethod() == null) { + textView.setMovementMethod(LinkMovementMethod.getInstance()); + } + } + + 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()); + } + }); + } + + 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.setSpansForNodeOptional(strongEmphasis, length); + } + }); + } + + 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.setSpansForNodeOptional(emphasis, length); + } + }); + } + + 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) { + + visitor.ensureNewLine(); + + final int length = visitor.length(); + + visitor.visitChildren(blockQuote); + visitor.setSpansForNodeOptional(blockQuote, length); + + if (visitor.hasNext(blockQuote)) { + visitor.ensureNewLine(); + visitor.forceNewLine(); + } + } + }); + } + + 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.setSpansForNodeOptional(code, length); + } + }); + } + + 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); + } + }); + } + + 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); + } + }); + } + + @VisibleForTesting + 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.setSpansForNodeOptional(node, length); + + if (visitor.hasNext(node)) { + visitor.ensureNewLine(); + visitor.forceNewLine(); + } + } + + private static void bulletList(@NonNull MarkwonVisitor.Builder builder) { + builder.on(BulletList.class, new SimpleBlockNodeVisitor()); + } + + private static void orderedList(@NonNull MarkwonVisitor.Builder builder) { + builder.on(OrderedList.class, new SimpleBlockNodeVisitor()); + } + + 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(); + + // 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) { + + 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.setSpansForNodeOptional(listItem, length); + + 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; + } + + 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.setSpansForNodeOptional(thematicBreak, length); + + if (visitor.hasNext(thematicBreak)) { + visitor.ensureNewLine(); + visitor.forceNewLine(); + } + } + }); + } + + 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.setSpansForNodeOptional(heading, length); + + if (visitor.hasNext(heading)) { + visitor.ensureNewLine(); + visitor.forceNewLine(); + } + } + }); + } + + private static void softLineBreak(@NonNull MarkwonVisitor.Builder builder) { + builder.on(SoftLineBreak.class, new MarkwonVisitor.NodeVisitor() { + @Override + public void visit(@NonNull MarkwonVisitor visitor, @NonNull SoftLineBreak softLineBreak) { + visitor.builder().append(' '); + } + }); + } + + 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.setSpansForNodeOptional(link, length); + } + }); + } +} diff --git a/markwon-core/src/main/java/ru/noties/markwon/core/CoreProps.java b/markwon-core/src/main/java/ru/noties/markwon/core/CoreProps.java new file mode 100644 index 00000000..4a722b0f --- /dev/null +++ b/markwon-core/src/main/java/ru/noties/markwon/core/CoreProps.java @@ -0,0 +1,29 @@ +package ru.noties.markwon.core; + +import ru.noties.markwon.Prop; + +/** + * @since 3.0.0 + */ +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/spans/SpannableTheme.java b/markwon-core/src/main/java/ru/noties/markwon/core/MarkwonTheme.java similarity index 60% rename from markwon/src/main/java/ru/noties/markwon/spans/SpannableTheme.java rename to markwon-core/src/main/java/ru/noties/markwon/core/MarkwonTheme.java index a3ba8c55..f01df083 100644 --- a/markwon/src/main/java/ru/noties/markwon/spans/SpannableTheme.java +++ b/markwon-core/src/main/java/ru/noties/markwon/core/MarkwonTheme.java @@ -1,67 +1,79 @@ -package ru.noties.markwon.spans; +package ru.noties.markwon.core; 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.Dimension; -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.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 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(); } /** - * 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 + * 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 * - * @return {@link Builder instance} - * @see #builderWithDefaults(Context) - * @see #builder(SpannableTheme) - * @since 1.0.0 + * @since 3.0.0 */ + @SuppressWarnings("unused") @NonNull - public static Builder builder() { + public static Builder emptyBuilder() { return new Builder(); } /** * 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); } @@ -76,36 +88,14 @@ public class SpannableTheme { @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)) + .codeBlockMargin(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; @@ -121,14 +111,8 @@ public class SpannableTheme { 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 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 @@ -163,15 +147,19 @@ public class SpannableTheme { // 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; @@ -187,39 +175,13 @@ public class SpannableTheme { // @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; // 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 - 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; @@ -231,23 +193,17 @@ public class SpannableTheme { 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; this.headingTextSizeMultipliers = builder.headingTextSizeMultipliers; - 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; } /** @@ -333,66 +289,117 @@ public class SpannableTheme { } /** - * 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); @@ -428,28 +435,6 @@ public class SpannableTheme { } } - 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) { @@ -466,70 +451,6 @@ public class SpannableTheme { } } - 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 - public Drawable getTaskListDrawable() { - return taskListDrawable; - } - @SuppressWarnings("unused") public static class Builder { @@ -544,28 +465,22 @@ public class SpannableTheme { 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; private float[] headingTextSizeMultipliers; - 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() { } - Builder(@NonNull SpannableTheme theme) { + Builder(@NonNull MarkwonTheme theme) { this.linkColor = theme.linkColor; this.blockMargin = theme.blockMargin; this.blockQuoteWidth = theme.blockQuoteWidth; @@ -577,21 +492,15 @@ public class SpannableTheme { 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; this.headingBreakColor = theme.headingBreakColor; this.headingTypeface = theme.headingTypeface; this.headingTextSizeMultipliers = theme.headingTextSizeMultipliers; - 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 @@ -652,7 +561,7 @@ public class SpannableTheme { return this; } - @SuppressWarnings("SameParameterValue") + @SuppressWarnings({"SameParameterValue", "UnusedReturnValue"}) @NonNull public Builder codeBackgroundColor(@ColorInt int codeBackgroundColor) { this.codeBackgroundColor = codeBackgroundColor; @@ -669,8 +578,8 @@ public class SpannableTheme { } @NonNull - public Builder codeMultilineMargin(@Px int codeMultilineMargin) { - this.codeMultilineMargin = codeMultilineMargin; + public Builder codeBlockMargin(@Px int codeBlockMargin) { + this.codeBlockMargin = codeBlockMargin; return this; } @@ -680,12 +589,30 @@ public class SpannableTheme { 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; @@ -715,18 +642,13 @@ public class SpannableTheme { * @return self * @since 1.1.0 */ + @SuppressWarnings("UnusedReturnValue") @NonNull public Builder headingTextSizeMultipliers(@Size(6) @NonNull float[] headingTextSizeMultipliers) { this.headingTextSizeMultipliers = headingTextSizeMultipliers; 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; @@ -740,79 +662,9 @@ public class SpannableTheme { } @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 - 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); } } - 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-core/src/main/java/ru/noties/markwon/core/SimpleBlockNodeVisitor.java b/markwon-core/src/main/java/ru/noties/markwon/core/SimpleBlockNodeVisitor.java new file mode 100644 index 00000000..1cc2fa02 --- /dev/null +++ b/markwon-core/src/main/java/ru/noties/markwon/core/SimpleBlockNodeVisitor.java @@ -0,0 +1,29 @@ +package ru.noties.markwon.core; + +import android.support.annotation.NonNull; + +import org.commonmark.node.Node; + +import ru.noties.markwon.MarkwonVisitor; + +/** + * 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) { + + visitor.ensureNewLine(); + + visitor.visitChildren(node); + + if (visitor.hasNext(node)) { + visitor.ensureNewLine(); + visitor.forceNewLine(); + } + } +} diff --git a/markwon-core/src/main/java/ru/noties/markwon/core/factory/BlockQuoteSpanFactory.java b/markwon-core/src/main/java/ru/noties/markwon/core/factory/BlockQuoteSpanFactory.java new file mode 100644 index 00000000..cbe86db6 --- /dev/null +++ b/markwon-core/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 props) { + return new BlockQuoteSpan(configuration.theme()); + } +} 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 new file mode 100644 index 00000000..2bf9383e --- /dev/null +++ b/markwon-core/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.CodeBlockSpan; + +public class CodeBlockSpanFactory implements SpanFactory { + @Nullable + @Override + public Object getSpans(@NonNull MarkwonConfiguration configuration, @NonNull RenderProps props) { + 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 new file mode 100644 index 00000000..944556fb --- /dev/null +++ b/markwon-core/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 props) { + return new CodeSpan(configuration.theme()); + } +} diff --git a/markwon-core/src/main/java/ru/noties/markwon/core/factory/EmphasisSpanFactory.java b/markwon-core/src/main/java/ru/noties/markwon/core/factory/EmphasisSpanFactory.java new file mode 100644 index 00000000..d9d8331d --- /dev/null +++ b/markwon-core/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 props) { + return new EmphasisSpan(); + } +} diff --git a/markwon-core/src/main/java/ru/noties/markwon/core/factory/HeadingSpanFactory.java b/markwon-core/src/main/java/ru/noties/markwon/core/factory/HeadingSpanFactory.java new file mode 100644 index 00000000..bd675edd --- /dev/null +++ b/markwon-core/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 props) { + return new HeadingSpan( + configuration.theme(), + CoreProps.HEADING_LEVEL.require(props) + ); + } +} diff --git a/markwon-core/src/main/java/ru/noties/markwon/core/factory/LinkSpanFactory.java b/markwon-core/src/main/java/ru/noties/markwon/core/factory/LinkSpanFactory.java new file mode 100644 index 00000000..ee97ba34 --- /dev/null +++ b/markwon-core/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 props) { + return new LinkSpan( + configuration.theme(), + CoreProps.LINK_DESTINATION.require(props), + configuration.linkResolver() + ); + } +} diff --git a/markwon-core/src/main/java/ru/noties/markwon/core/factory/ListItemSpanFactory.java b/markwon-core/src/main/java/ru/noties/markwon/core/factory/ListItemSpanFactory.java new file mode 100644 index 00000000..06d73d2e --- /dev/null +++ b/markwon-core/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 props) { + + // type of list item + // bullet : level + // ordered: number + final Object spans; + + if (CoreProps.ListItemType.BULLET == CoreProps.LIST_ITEM_TYPE.require(props)) { + spans = new BulletListItemSpan( + configuration.theme(), + 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(props)) + + "." + '\u00a0'; + + spans = new OrderedListItemSpan( + configuration.theme(), + number + ); + } + + return spans; + } +} diff --git a/markwon-core/src/main/java/ru/noties/markwon/core/factory/StrongEmphasisSpanFactory.java b/markwon-core/src/main/java/ru/noties/markwon/core/factory/StrongEmphasisSpanFactory.java new file mode 100644 index 00000000..c81d6121 --- /dev/null +++ b/markwon-core/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 props) { + return new StrongEmphasisSpan(); + } +} diff --git a/markwon-core/src/main/java/ru/noties/markwon/core/factory/ThematicBreakSpanFactory.java b/markwon-core/src/main/java/ru/noties/markwon/core/factory/ThematicBreakSpanFactory.java new file mode 100644 index 00000000..ecd20f32 --- /dev/null +++ b/markwon-core/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 props) { + return new ThematicBreakSpan(configuration.theme()); + } +} diff --git a/markwon/src/main/java/ru/noties/markwon/spans/BlockQuoteSpan.java b/markwon-core/src/main/java/ru/noties/markwon/core/spans/BlockQuoteSpan.java similarity index 87% rename from markwon/src/main/java/ru/noties/markwon/spans/BlockQuoteSpan.java rename to markwon-core/src/main/java/ru/noties/markwon/core/spans/BlockQuoteSpan.java index aa85c33c..ea4e353c 100644 --- a/markwon/src/main/java/ru/noties/markwon/spans/BlockQuoteSpan.java +++ b/markwon-core/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,13 +7,15 @@ 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 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-core/src/main/java/ru/noties/markwon/core/spans/BulletListItemSpan.java similarity index 73% rename from markwon/src/main/java/ru/noties/markwon/spans/BulletListItemSpan.java rename to markwon-core/src/main/java/ru/noties/markwon/core/spans/BulletListItemSpan.java index 368f13a9..86a6c81b 100644 --- a/markwon/src/main/java/ru/noties/markwon/spans/BulletListItemSpan.java +++ b/markwon-core/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,9 +9,12 @@ 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 { - private SpannableTheme theme; + private MarkwonTheme theme; private final Paint paint = ObjectsPool.paint(); private final RectF circle = ObjectsPool.rectF(); @@ -20,7 +23,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; @@ -58,14 +61,28 @@ public class BulletListItemSpan implements LeadingMarginSpan { final int marginLeft = (width - side) / 2; + // @since 2.0.2 + // There is a bug in Android Nougat, when this span receives an `x` that + // doesn't correspond to what it should be (text is placed correctly though). + // Let's make this a general rule -> manually calculate difference between expected/actual + // and add this difference to resulting left/right values. If everything goes well + // we do not encounter a bug -> this `diff` value will be 0 + final int diff; + if (dir < 0) { + // rtl + diff = x - (layout.getWidth() - (width * level)); + } else { + diff = (width * level) - x; + } + // in order to support RTL final int l; final int r; { final int left = x + (dir * marginLeft); final int right = left + (dir * side); - l = Math.min(left, right); - r = Math.max(left, right); + l = Math.min(left, right) + (dir * diff); + r = Math.max(left, right) + (dir * diff); } final int t = baseline + (int) ((paint.descent() + paint.ascent()) / 2.F + .5F) - (side / 2); 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 new file mode 100644 index 00000000..856d5807 --- /dev/null +++ b/markwon-core/src/main/java/ru/noties/markwon/core/spans/CodeSpan.java @@ -0,0 +1,34 @@ +package ru.noties.markwon.core.spans; + +import android.support.annotation.NonNull; +import android.text.TextPaint; +import android.text.style.MetricAffectingSpan; + +import ru.noties.markwon.core.MarkwonTheme; + +/** + * @since 3.0.0 split inline and block spans + */ +public class CodeSpan extends MetricAffectingSpan { + + private final MarkwonTheme theme; + + public CodeSpan(@NonNull MarkwonTheme theme) { + this.theme = theme; + } + + @Override + public void updateMeasureState(TextPaint p) { + apply(p); + } + + @Override + public void updateDrawState(TextPaint ds) { + apply(ds); + ds.bgColor = theme.getCodeBackgroundColor(ds); + } + + private void apply(TextPaint p) { + theme.applyCodeTextStyle(p); + } +} 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); + } +} diff --git a/markwon/src/main/java/ru/noties/markwon/spans/EmphasisSpan.java b/markwon-core/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-core/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-core/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-core/src/main/java/ru/noties/markwon/core/spans/HeadingSpan.java similarity index 88% rename from markwon/src/main/java/ru/noties/markwon/spans/HeadingSpan.java rename to markwon-core/src/main/java/ru/noties/markwon/core/spans/HeadingSpan.java index 6293d554..a942cc64 100644 --- a/markwon/src/main/java/ru/noties/markwon/spans/HeadingSpan.java +++ b/markwon-core/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,14 +10,17 @@ 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 { - 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-core/src/main/java/ru/noties/markwon/core/spans/LinkSpan.java similarity index 74% rename from markwon/src/main/java/ru/noties/markwon/spans/LinkSpan.java rename to markwon-core/src/main/java/ru/noties/markwon/core/spans/LinkSpan.java index 2359f9a4..e8f7d8f7 100644 --- a/markwon/src/main/java/ru/noties/markwon/spans/LinkSpan.java +++ b/markwon-core/src/main/java/ru/noties/markwon/core/spans/LinkSpan.java @@ -1,22 +1,23 @@ -package ru.noties.markwon.spans; +package ru.noties.markwon.core.spans; import android.support.annotation.NonNull; import android.text.TextPaint; -import android.text.style.ClickableSpan; import android.text.style.URLSpan; import android.view.View; +import ru.noties.markwon.core.MarkwonTheme; + public class LinkSpan extends URLSpan { public interface Resolver { 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/ObjectsPool.java b/markwon-core/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-core/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-core/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-core/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-core/src/main/java/ru/noties/markwon/core/spans/OrderedListItemSpan.java index 1db29e1a..6be46fd5 100644 --- a/markwon/src/main/java/ru/noties/markwon/spans/OrderedListItemSpan.java +++ b/markwon-core/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,9 @@ 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 { /** @@ -42,7 +45,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 +55,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/StrongEmphasisSpan.java b/markwon-core/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-core/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-core/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-core/src/main/java/ru/noties/markwon/core/spans/ThematicBreakSpan.java similarity index 87% rename from markwon/src/main/java/ru/noties/markwon/spans/ThematicBreakSpan.java rename to markwon-core/src/main/java/ru/noties/markwon/core/spans/ThematicBreakSpan.java index 316e4312..0e06537b 100644 --- a/markwon/src/main/java/ru/noties/markwon/spans/ThematicBreakSpan.java +++ b/markwon-core/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,13 +7,15 @@ 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 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-html-parser-api/src/main/java/ru/noties/markwon/html/api/HtmlTag.java b/markwon-core/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-core/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-core/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-core/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-core/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-core/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-core/src/main/java/ru/noties/markwon/html/MarkwonHtmlParserNoOp.java similarity index 88% rename from markwon-html-parser-api/src/main/java/ru/noties/markwon/html/api/MarkwonHtmlParserNoOp.java rename to markwon-core/src/main/java/ru/noties/markwon/html/MarkwonHtmlParserNoOp.java index 0a024865..ecf6e423 100644 --- a/markwon-html-parser-api/src/main/java/ru/noties/markwon/html/api/MarkwonHtmlParserNoOp.java +++ b/markwon-core/src/main/java/ru/noties/markwon/html/MarkwonHtmlParserNoOp.java @@ -1,18 +1,13 @@ -package ru.noties.markwon.html.api; +package ru.noties.markwon.html; import android.support.annotation.NonNull; import java.util.Collections; -/** - * @see MarkwonHtmlParser - * @since 2.0.0 - */ class MarkwonHtmlParserNoOp extends MarkwonHtmlParser { - @Override public void processFragment(@NonNull T output, @NonNull String htmlFragment) { - + // no op } @Override @@ -27,6 +22,6 @@ class MarkwonHtmlParserNoOp extends MarkwonHtmlParser { @Override public void reset() { - + // no op } } 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 new file mode 100644 index 00000000..93d0411d --- /dev/null +++ b/markwon-core/src/main/java/ru/noties/markwon/html/MarkwonHtmlRenderer.java @@ -0,0 +1,56 @@ +package ru.noties.markwon.html; + +import android.support.annotation.NonNull; +import android.support.annotation.Nullable; + +import java.util.Collection; + +import ru.noties.markwon.MarkwonVisitor; + +/** + * @since 2.0.0 + */ +public abstract class MarkwonHtmlRenderer { + + @NonNull + public static Builder builder() { + return new MarkwonHtmlRendererImpl.BuilderImpl(); + } + + public abstract void render( + @NonNull MarkwonVisitor visitor, + @NonNull MarkwonHtmlParser parser + ); + + @Nullable + public abstract TagHandler tagHandler(@NonNull String tagName); + + + /** + * @since 3.0.0 + */ + public interface Builder { + + /** + * @param allowNonClosedTags parameter to indicate that all non-closed HTML tags should be + * closed at the end of a document. if {@code true} all non-closed + * tags will be force-closed at the end. Otherwise these tags will be + * ignored and thus not rendered. + * @return self + */ + @NonNull + Builder allowNonClosedTags(boolean allowNonClosedTags); + + @NonNull + Builder setHandler(@NonNull String tagName, @Nullable TagHandler tagHandler); + + @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 new file mode 100644 index 00000000..0f4c97e6 --- /dev/null +++ b/markwon-core/src/main/java/ru/noties/markwon/html/MarkwonHtmlRendererImpl.java @@ -0,0 +1,143 @@ +package ru.noties.markwon.html; + +import android.support.annotation.NonNull; +import android.support.annotation.Nullable; + +import java.util.Collection; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import ru.noties.markwon.MarkwonVisitor; + +class MarkwonHtmlRendererImpl extends MarkwonHtmlRenderer { + + private final boolean allowNonClosedTags; + private final Map tagHandlers; + + MarkwonHtmlRendererImpl(boolean allowNonClosedTags, @NonNull Map tagHandlers) { + this.allowNonClosedTags = allowNonClosedTags; + this.tagHandlers = tagHandlers; + } + + @Override + public void render( + @NonNull final MarkwonVisitor visitor, + @NonNull MarkwonHtmlParser parser) { + + final int end; + if (!allowNonClosedTags) { + end = HtmlTag.NO_END; + } else { + end = visitor.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(visitor, MarkwonHtmlRendererImpl.this, 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(visitor, MarkwonHtmlRendererImpl.this, 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); + } + + static class BuilderImpl implements Builder { + + private final Map tagHandlers = new HashMap<>(2); + private boolean allowNonClosedTags; + + @NonNull + @Override + public Builder allowNonClosedTags(boolean allowNonClosedTags) { + this.allowNonClosedTags = allowNonClosedTags; + return this; + } + + @NonNull + @Override + 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 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; + } + + @Nullable + @Override + public TagHandler getHandler(@NonNull String tagName) { + return tagHandlers.get(tagName); + } + + @NonNull + @Override + public MarkwonHtmlRenderer build() { + // okay, let's validate that we have at least one tagHandler registered + // if we have none -> return no-op implementation + return tagHandlers.size() > 0 + ? new MarkwonHtmlRendererImpl(allowNonClosedTags, Collections.unmodifiableMap(tagHandlers)) + : new MarkwonHtmlRendererNoOp(); + } + } +} diff --git a/markwon-core/src/main/java/ru/noties/markwon/html/MarkwonHtmlRendererNoOp.java b/markwon-core/src/main/java/ru/noties/markwon/html/MarkwonHtmlRendererNoOp.java new file mode 100644 index 00000000..1a6ac51f --- /dev/null +++ b/markwon-core/src/main/java/ru/noties/markwon/html/MarkwonHtmlRendererNoOp.java @@ -0,0 +1,20 @@ +package ru.noties.markwon.html; + +import android.support.annotation.NonNull; +import android.support.annotation.Nullable; + +import ru.noties.markwon.MarkwonVisitor; + +class MarkwonHtmlRendererNoOp extends MarkwonHtmlRenderer { + + @Override + public void render(@NonNull MarkwonVisitor visitor, @NonNull MarkwonHtmlParser parser) { + parser.reset(); + } + + @Nullable + @Override + public TagHandler tagHandler(@NonNull String tagName) { + return null; + } +} diff --git a/markwon-core/src/main/java/ru/noties/markwon/html/TagHandler.java b/markwon-core/src/main/java/ru/noties/markwon/html/TagHandler.java new file mode 100644 index 00000000..84122946 --- /dev/null +++ b/markwon-core/src/main/java/ru/noties/markwon/html/TagHandler.java @@ -0,0 +1,36 @@ +package ru.noties.markwon.html; + +import android.support.annotation.NonNull; + +import ru.noties.markwon.MarkwonVisitor; + +public abstract class TagHandler { + + public abstract void handle( + @NonNull MarkwonVisitor visitor, + @NonNull MarkwonHtmlRenderer renderer, + @NonNull HtmlTag tag + ); + + protected static void visitChildren( + @NonNull MarkwonVisitor visitor, + @NonNull MarkwonHtmlRenderer renderer, + @NonNull HtmlTag.Block block) { + + TagHandler handler; + + for (HtmlTag.Block child : block.children()) { + + if (!child.isClosed()) { + continue; + } + + handler = renderer.tagHandler(child.name()); + if (handler != null) { + handler.handle(visitor, renderer, child); + } else { + visitChildren(visitor, renderer, child); + } + } + } +} diff --git a/markwon/src/main/java/ru/noties/markwon/spans/AsyncDrawable.java b/markwon-core/src/main/java/ru/noties/markwon/image/AsyncDrawable.java similarity index 78% rename from markwon/src/main/java/ru/noties/markwon/spans/AsyncDrawable.java rename to markwon-core/src/main/java/ru/noties/markwon/image/AsyncDrawable.java index dbd0fe0b..696f6529 100644 --- a/markwon/src/main/java/ru/noties/markwon/spans/AsyncDrawable.java +++ b/markwon-core/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; @@ -10,20 +10,10 @@ 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 { - 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; @@ -41,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 ) { @@ -49,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 @@ -76,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 new file mode 100644 index 00000000..fe6506ee --- /dev/null +++ b/markwon-core/src/main/java/ru/noties/markwon/image/AsyncDrawableLoader.java @@ -0,0 +1,143 @@ +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 { + + /** + * @since 3.0.0 + */ + public interface DrawableProvider { + @Nullable + Drawable provide(); + } + + /** + * @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); + + public abstract void cancel(@NonNull String destination); + + @Nullable + public abstract Drawable placeholder(); + + public static class Builder { + + ExecutorService executorService; + final Map schemeHandlers = new HashMap<>(3); + final Map mediaDecoders = new HashMap<>(3); + MediaDecoder defaultMediaDecoder; + DrawableProvider placeholderDrawableProvider; + DrawableProvider errorDrawableProvider; + + @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; + } + + /** + * @since 3.0.0 + */ + @NonNull + 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; + } + + @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-core/src/main/java/ru/noties/markwon/image/AsyncDrawableLoaderImpl.java b/markwon-core/src/main/java/ru/noties/markwon/image/AsyncDrawableLoaderImpl.java new file mode 100644 index 00000000..fd7c3ad1 --- /dev/null +++ b/markwon-core/src/main/java/ru/noties/markwon/image/AsyncDrawableLoaderImpl.java @@ -0,0 +1,148 @@ +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 android.support.annotation.Nullable; + +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 DrawableProvider placeholderDrawableProvider; + private final DrawableProvider errorDrawableProvider; + + 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.placeholderDrawableProvider = builder.placeholderDrawableProvider; + this.errorDrawableProvider = builder.errorDrawableProvider; + 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); + } + } + + @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); + + // 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 = errorDrawableProvider != null + ? errorDrawableProvider.provide() + : null; + } + + 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-core/src/main/java/ru/noties/markwon/image/AsyncDrawableLoaderNoOp.java b/markwon-core/src/main/java/ru/noties/markwon/image/AsyncDrawableLoaderNoOp.java new file mode 100644 index 00000000..74520fb2 --- /dev/null +++ b/markwon-core/src/main/java/ru/noties/markwon/image/AsyncDrawableLoaderNoOp.java @@ -0,0 +1,23 @@ +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 + public void load(@NonNull String destination, @NonNull AsyncDrawable drawable) { + + } + + @Override + public void cancel(@NonNull String destination) { + + } + + @Nullable + @Override + public Drawable placeholder() { + return null; + } +} diff --git a/markwon/src/main/java/ru/noties/markwon/DrawablesScheduler.java b/markwon-core/src/main/java/ru/noties/markwon/image/AsyncDrawableScheduler.java similarity index 94% rename from markwon/src/main/java/ru/noties/markwon/DrawablesScheduler.java rename to markwon-core/src/main/java/ru/noties/markwon/image/AsyncDrawableScheduler.java index fd64d9d5..0093abed 100644 --- a/markwon/src/main/java/ru/noties/markwon/DrawablesScheduler.java +++ b/markwon-core/src/main/java/ru/noties/markwon/image/AsyncDrawableScheduler.java @@ -1,4 +1,4 @@ -package ru.noties.markwon; +package ru.noties.markwon.image; import android.graphics.Rect; import android.graphics.drawable.Drawable; @@ -15,12 +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.spans.AsyncDrawableSpan; -abstract class DrawablesScheduler { +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 +48,7 @@ abstract class DrawablesScheduler { } // 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); } @@ -104,7 +102,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/spans/AsyncDrawableSpan.java b/markwon-core/src/main/java/ru/noties/markwon/image/AsyncDrawableSpan.java similarity index 88% rename from markwon/src/main/java/ru/noties/markwon/spans/AsyncDrawableSpan.java rename to markwon-core/src/main/java/ru/noties/markwon/image/AsyncDrawableSpan.java index f9bd8f33..a268ef19 100644 --- a/markwon/src/main/java/ru/noties/markwon/spans/AsyncDrawableSpan.java +++ b/markwon-core/src/main/java/ru/noties/markwon/image/AsyncDrawableSpan.java @@ -1,4 +1,4 @@ -package ru.noties.markwon.spans; +package ru.noties.markwon.image; import android.graphics.Canvas; import android.graphics.Paint; @@ -12,6 +12,8 @@ import android.text.style.ReplacementSpan; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; +import ru.noties.markwon.core.MarkwonTheme; + @SuppressWarnings("WeakerAccess") public class AsyncDrawableSpan extends ReplacementSpan { @@ -24,24 +26,13 @@ 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) { - this(theme, drawable, ALIGN_BOTTOM); - } - public AsyncDrawableSpan( - @NonNull SpannableTheme 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) { @@ -137,7 +128,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); } @@ -147,7 +138,13 @@ public class AsyncDrawableSpan extends ReplacementSpan { } } + @NonNull 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-image-loader/src/main/java/ru/noties/markwon/il/ImageItem.java b/markwon-core/src/main/java/ru/noties/markwon/image/ImageItem.java similarity index 66% rename from markwon-image-loader/src/main/java/ru/noties/markwon/il/ImageItem.java rename to markwon-core/src/main/java/ru/noties/markwon/image/ImageItem.java index 3ac9e9ec..2dc4b729 100644 --- a/markwon-image-loader/src/main/java/ru/noties/markwon/il/ImageItem.java +++ b/markwon-core/src/main/java/ru/noties/markwon/image/ImageItem.java @@ -1,4 +1,4 @@ -package ru.noties.markwon.il; +package ru.noties.markwon.image; import android.support.annotation.Nullable; @@ -11,15 +11,12 @@ 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) { + @Nullable InputStream inputStream) { this.contentType = contentType; this.inputStream = inputStream; - this.fileName = fileName; } @Nullable @@ -31,9 +28,4 @@ public class ImageItem { 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-core/src/main/java/ru/noties/markwon/image/ImageMediaDecoder.java similarity index 84% rename from markwon-image-loader/src/main/java/ru/noties/markwon/il/ImageMediaDecoder.java rename to markwon-core/src/main/java/ru/noties/markwon/image/ImageMediaDecoder.java index c36545ea..796d016e 100644 --- a/markwon-image-loader/src/main/java/ru/noties/markwon/il/ImageMediaDecoder.java +++ b/markwon-core/src/main/java/ru/noties/markwon/image/ImageMediaDecoder.java @@ -1,4 +1,4 @@ -package ru.noties.markwon.il; +package ru.noties.markwon.image; import android.content.res.Resources; import android.graphics.Bitmap; @@ -10,6 +10,8 @@ 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. @@ -30,16 +32,6 @@ public class ImageMediaDecoder extends MediaDecoder { 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) { diff --git a/markwon-core/src/main/java/ru/noties/markwon/image/ImageProps.java b/markwon-core/src/main/java/ru/noties/markwon/image/ImageProps.java new file mode 100644 index 00000000..7bb9bfd1 --- /dev/null +++ b/markwon-core/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/renderer/ImageSize.java b/markwon-core/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-core/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-core/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-core/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-core/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-core/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-core/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-core/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-core/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-core/src/main/java/ru/noties/markwon/image/ImageSpanFactory.java b/markwon-core/src/main/java/ru/noties/markwon/image/ImageSpanFactory.java new file mode 100644 index 00000000..5af5b0ec --- /dev/null +++ b/markwon-core/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 props) { + return new AsyncDrawableSpan( + configuration.theme(), + new AsyncDrawable( + ImageProps.DESTINATION.require(props), + configuration.asyncDrawableLoader(), + configuration.imageSizeResolver(), + ImageProps.IMAGE_SIZE.get(props) + ), + AsyncDrawableSpan.ALIGN_BOTTOM, + ImageProps.REPLACEMENT_TEXT_IS_LINK.get(props, false) + ); + } +} diff --git a/markwon-core/src/main/java/ru/noties/markwon/image/ImagesPlugin.java b/markwon-core/src/main/java/ru/noties/markwon/image/ImagesPlugin.java new file mode 100644 index 00000000..2f6e8117 --- /dev/null +++ b/markwon-core/src/main/java/ru/noties/markwon/image/ImagesPlugin.java @@ -0,0 +1,130 @@ +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; +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.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; + +/** + * @since 3.0.0 + */ +public class ImagesPlugin extends AbstractMarkwonPlugin { + + @NonNull + public static ImagesPlugin create(@NonNull Context context) { + return new ImagesPlugin(context, false); + } + + /** + * Special scheme that is used {@code file:///android_asset/} + * + * @param context + * @return + */ + @NonNull + public static ImagesPlugin createWithAssets(@NonNull Context context) { + return new ImagesPlugin(context, true); + } + + private final Context context; + private final boolean useAssets; + + protected 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 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() { + @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.class); + if (spanFactory == null) { + visitor.visitChildren(image); + return; + } + + 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 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(props, destination); + ImageProps.REPLACEMENT_TEXT_IS_LINK.set(props, link); + ImageProps.IMAGE_SIZE.set(props, null); + + visitor.setSpans(length, spanFactory.getSpans(configuration, props)); + } + }); + } + + @Override + public void beforeSetText(@NonNull TextView textView, @NonNull Spanned markdown) { + AsyncDrawableScheduler.unschedule(textView); + } + + @Override + public void afterSetText(@NonNull TextView textView) { + AsyncDrawableScheduler.schedule(textView); + } +} diff --git a/markwon-image-loader/src/main/java/ru/noties/markwon/il/MediaDecoder.java b/markwon-core/src/main/java/ru/noties/markwon/image/MediaDecoder.java similarity index 58% rename from markwon-image-loader/src/main/java/ru/noties/markwon/il/MediaDecoder.java rename to markwon-core/src/main/java/ru/noties/markwon/image/MediaDecoder.java index 294b716b..68d0ff33 100644 --- a/markwon-image-loader/src/main/java/ru/noties/markwon/il/MediaDecoder.java +++ b/markwon-core/src/main/java/ru/noties/markwon/image/MediaDecoder.java @@ -1,4 +1,4 @@ -package ru.noties.markwon.il; +package ru.noties.markwon.image; import android.graphics.drawable.Drawable; import android.support.annotation.NonNull; @@ -7,14 +7,10 @@ import android.support.annotation.Nullable; import java.io.InputStream; /** - * @since 1.1.0 + * @since 3.0.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-core/src/main/java/ru/noties/markwon/image/SchemeHandler.java b/markwon-core/src/main/java/ru/noties/markwon/image/SchemeHandler.java new file mode 100644 index 00000000..cac1c801 --- /dev/null +++ b/markwon-core/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-image-loader/src/main/java/ru/noties/markwon/il/DataUri.java b/markwon-core/src/main/java/ru/noties/markwon/image/data/DataUri.java similarity index 97% rename from markwon-image-loader/src/main/java/ru/noties/markwon/il/DataUri.java rename to markwon-core/src/main/java/ru/noties/markwon/image/data/DataUri.java index 697b7b2e..6e812c92 100644 --- a/markwon-image-loader/src/main/java/ru/noties/markwon/il/DataUri.java +++ b/markwon-core/src/main/java/ru/noties/markwon/image/data/DataUri.java @@ -1,4 +1,4 @@ -package ru.noties.markwon.il; +package ru.noties.markwon.image.data; import android.support.annotation.Nullable; diff --git a/markwon-image-loader/src/main/java/ru/noties/markwon/il/DataUriDecoder.java b/markwon-core/src/main/java/ru/noties/markwon/image/data/DataUriDecoder.java similarity index 96% rename from markwon-image-loader/src/main/java/ru/noties/markwon/il/DataUriDecoder.java rename to markwon-core/src/main/java/ru/noties/markwon/image/data/DataUriDecoder.java index ffb0d840..7e3d4f73 100644 --- a/markwon-image-loader/src/main/java/ru/noties/markwon/il/DataUriDecoder.java +++ b/markwon-core/src/main/java/ru/noties/markwon/image/data/DataUriDecoder.java @@ -1,4 +1,4 @@ -package ru.noties.markwon.il; +package ru.noties.markwon.image.data; import android.support.annotation.NonNull; import android.support.annotation.Nullable; diff --git a/markwon-image-loader/src/main/java/ru/noties/markwon/il/DataUriParser.java b/markwon-core/src/main/java/ru/noties/markwon/image/data/DataUriParser.java similarity index 98% rename from markwon-image-loader/src/main/java/ru/noties/markwon/il/DataUriParser.java rename to markwon-core/src/main/java/ru/noties/markwon/image/data/DataUriParser.java index 63744a42..0768ee4a 100644 --- a/markwon-image-loader/src/main/java/ru/noties/markwon/il/DataUriParser.java +++ b/markwon-core/src/main/java/ru/noties/markwon/image/data/DataUriParser.java @@ -1,4 +1,4 @@ -package ru.noties.markwon.il; +package ru.noties.markwon.image.data; import android.support.annotation.NonNull; import android.support.annotation.Nullable; diff --git a/markwon-image-loader/src/main/java/ru/noties/markwon/il/DataUriSchemeHandler.java b/markwon-core/src/main/java/ru/noties/markwon/image/data/DataUriSchemeHandler.java similarity index 80% rename from markwon-image-loader/src/main/java/ru/noties/markwon/il/DataUriSchemeHandler.java rename to markwon-core/src/main/java/ru/noties/markwon/image/data/DataUriSchemeHandler.java index c70ea863..f4c87ed4 100644 --- a/markwon-image-loader/src/main/java/ru/noties/markwon/il/DataUriSchemeHandler.java +++ b/markwon-core/src/main/java/ru/noties/markwon/image/data/DataUriSchemeHandler.java @@ -1,18 +1,21 @@ -package ru.noties.markwon.il; +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 java.util.Collection; -import java.util.Collections; + +import ru.noties.markwon.image.ImageItem; +import ru.noties.markwon.image.SchemeHandler; /** * @since 2.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()); @@ -56,19 +59,7 @@ public class DataUriSchemeHandler extends SchemeHandler { return new ImageItem( dataUri.contentType(), - new ByteArrayInputStream(bytes), - null + new ByteArrayInputStream(bytes) ); } - - @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/FileSchemeHandler.java b/markwon-core/src/main/java/ru/noties/markwon/image/file/FileSchemeHandler.java similarity index 84% rename from markwon-image-loader/src/main/java/ru/noties/markwon/il/FileSchemeHandler.java rename to markwon-core/src/main/java/ru/noties/markwon/image/file/FileSchemeHandler.java index 437bbf7a..712899aa 100644 --- a/markwon-image-loader/src/main/java/ru/noties/markwon/il/FileSchemeHandler.java +++ b/markwon-core/src/main/java/ru/noties/markwon/image/file/FileSchemeHandler.java @@ -1,9 +1,10 @@ -package ru.noties.markwon.il; +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; @@ -11,15 +12,18 @@ 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; +import ru.noties.markwon.image.ImageItem; +import ru.noties.markwon.image.SchemeHandler; + /** - * @since 2.0.0 + * @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); @@ -88,22 +92,14 @@ public class FileSchemeHandler extends SchemeHandler { } if (inputStream != null) { - out = new ImageItem(fileName, inputStream, fileName); + final String contentType = MimeTypeMap + .getSingleton() + .getMimeTypeFromExtension(MimeTypeMap.getFileExtensionFromUrl(fileName)); + out = new ImageItem(contentType, inputStream); } 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-core/src/main/java/ru/noties/markwon/image/network/NetworkSchemeHandler.java b/markwon-core/src/main/java/ru/noties/markwon/image/network/NetworkSchemeHandler.java new file mode 100644 index 00000000..ebaaf803 --- /dev/null +++ b/markwon-core/src/main/java/ru/noties/markwon/image/network/NetworkSchemeHandler.java @@ -0,0 +1,70 @@ +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; + +/** + * 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"; + 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-core/src/main/java/ru/noties/markwon/movement/MovementMethodPlugin.java b/markwon-core/src/main/java/ru/noties/markwon/movement/MovementMethodPlugin.java new file mode 100644 index 00000000..59dd04ab --- /dev/null +++ b/markwon-core/src/main/java/ru/noties/markwon/movement/MovementMethodPlugin.java @@ -0,0 +1,43 @@ +package ru.noties.markwon.movement; + +import android.support.annotation.NonNull; +import android.text.Spanned; +import android.text.method.LinkMovementMethod; +import android.text.method.MovementMethod; +import android.widget.TextView; + +import ru.noties.markwon.AbstractMarkwonPlugin; + +/** + * @since 3.0.0 + */ +public class MovementMethodPlugin extends AbstractMarkwonPlugin { + + /** + * Creates plugin that will ensure that there is movement method registered on a TextView. + * Uses Android system LinkMovementMethod as default + * + * @see #create(MovementMethod) + */ + @NonNull + public static MovementMethodPlugin create() { + return create(LinkMovementMethod.getInstance()); + } + + @NonNull + public static MovementMethodPlugin create(@NonNull MovementMethod movementMethod) { + return new MovementMethodPlugin(movementMethod); + } + + private final MovementMethod movementMethod; + + @SuppressWarnings("WeakerAccess") + MovementMethodPlugin(@NonNull MovementMethod movementMethod) { + this.movementMethod = movementMethod; + } + + @Override + public void beforeSetText(@NonNull TextView textView, @NonNull Spanned markdown) { + textView.setMovementMethod(movementMethod); + } +} diff --git a/markwon-core/src/main/java/ru/noties/markwon/priority/Priority.java b/markwon-core/src/main/java/ru/noties/markwon/priority/Priority.java new file mode 100644 index 00000000..5582ff72 --- /dev/null +++ b/markwon-core/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-core/src/main/java/ru/noties/markwon/priority/PriorityProcessor.java b/markwon-core/src/main/java/ru/noties/markwon/priority/PriorityProcessor.java new file mode 100644 index 00000000..1ba1353d --- /dev/null +++ b/markwon-core/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-core/src/main/java/ru/noties/markwon/priority/PriorityProcessorImpl.java b/markwon-core/src/main/java/ru/noties/markwon/priority/PriorityProcessorImpl.java new file mode 100644 index 00000000..e676a9c9 --- /dev/null +++ b/markwon-core/src/main/java/ru/noties/markwon/priority/PriorityProcessorImpl.java @@ -0,0 +1,132 @@ +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; +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 in) { + + // create new collection based on supplied argument + final List plugins = new ArrayList<>(in); + + 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)); + } + } + + 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)); + } + } +} diff --git a/markwon/src/main/java/ru/noties/markwon/SyntaxHighlight.java b/markwon-core/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-core/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-core/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-core/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-core/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-core/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-core/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-core/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-core/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-core/src/main/java/ru/noties/markwon/urlprocessor/UrlProcessorAndroidAssets.java similarity index 82% rename from markwon/src/main/java/ru/noties/markwon/UrlProcessorAndroidAssets.java rename to markwon-core/src/main/java/ru/noties/markwon/urlprocessor/UrlProcessorAndroidAssets.java index 2a1544a9..439d7f12 100644 --- a/markwon/src/main/java/ru/noties/markwon/UrlProcessorAndroidAssets.java +++ b/markwon-core/src/main/java/ru/noties/markwon/urlprocessor/UrlProcessorAndroidAssets.java @@ -1,10 +1,14 @@ -package ru.noties.markwon; +package ru.noties.markwon.urlprocessor; import android.net.Uri; import android.support.annotation.NonNull; import android.support.annotation.Nullable; import android.text.TextUtils; +/** + * Processor that will assume that an URL without scheme points to android assets folder. + * URL with a scheme will be processed by {@link #processor} (if it is specified) or returned `as-is`. + */ @SuppressWarnings({"unused", "WeakerAccess"}) public class UrlProcessorAndroidAssets implements UrlProcessor { diff --git a/markwon/src/main/java/ru/noties/markwon/UrlProcessorNoOp.java b/markwon-core/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-core/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-core/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-core/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-core/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-core/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-core/src/main/java/ru/noties/markwon/utils/ColorUtils.java b/markwon-core/src/main/java/ru/noties/markwon/utils/ColorUtils.java new file mode 100644 index 00000000..d6305132 --- /dev/null +++ b/markwon-core/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-core/src/main/java/ru/noties/markwon/utils/Dip.java b/markwon-core/src/main/java/ru/noties/markwon/utils/Dip.java new file mode 100644 index 00000000..51d098e0 --- /dev/null +++ b/markwon-core/src/main/java/ru/noties/markwon/utils/Dip.java @@ -0,0 +1,27 @@ +package ru.noties.markwon.utils; + +import android.content.Context; +import android.support.annotation.NonNull; + +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-image-loader/src/main/java/ru/noties/markwon/il/DrawableUtils.java b/markwon-core/src/main/java/ru/noties/markwon/utils/DrawableUtils.java similarity index 61% rename from markwon-image-loader/src/main/java/ru/noties/markwon/il/DrawableUtils.java rename to markwon-core/src/main/java/ru/noties/markwon/utils/DrawableUtils.java index f2aef636..34342093 100644 --- a/markwon-image-loader/src/main/java/ru/noties/markwon/il/DrawableUtils.java +++ b/markwon-core/src/main/java/ru/noties/markwon/utils/DrawableUtils.java @@ -1,11 +1,11 @@ -package ru.noties.markwon.il; +package ru.noties.markwon.utils; import android.graphics.drawable.Drawable; import android.support.annotation.NonNull; -abstract class DrawableUtils { +public abstract class DrawableUtils { - static void intrinsicBounds(@NonNull Drawable drawable) { + public static void intrinsicBounds(@NonNull Drawable drawable) { drawable.setBounds(0, 0, drawable.getIntrinsicWidth(), drawable.getIntrinsicHeight()); } 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/markwon-core/src/main/java/ru/noties/markwon/utils/LeadingMarginUtils.java b/markwon-core/src/main/java/ru/noties/markwon/utils/LeadingMarginUtils.java new file mode 100644 index 00000000..bc18a140 --- /dev/null +++ b/markwon-core/src/main/java/ru/noties/markwon/utils/LeadingMarginUtils.java @@ -0,0 +1,18 @@ +package ru.noties.markwon.utils; + +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; + } + + public static boolean selfEnd(int end, CharSequence text, Object span) { + return text instanceof Spanned && ((Spanned) text).getSpanEnd(span) == end; + } + + private LeadingMarginUtils() { + } +} 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/src/main/res/values/ids.xml b/markwon-core/src/main/res/values/ids.xml similarity index 62% rename from markwon/src/main/res/values/ids.xml rename to markwon-core/src/main/res/values/ids.xml index de911baf..90fb8322 100644 --- a/markwon/src/main/res/values/ids.xml +++ b/markwon-core/src/main/res/values/ids.xml @@ -2,6 +2,5 @@ - \ No newline at end of file diff --git a/markwon-core/src/test/java/ru/noties/markwon/AbstractMarkwonPluginTest.java b/markwon-core/src/test/java/ru/noties/markwon/AbstractMarkwonPluginTest.java new file mode 100644 index 00000000..1af2b513 --- /dev/null +++ b/markwon-core/src/test/java/ru/noties/markwon/AbstractMarkwonPluginTest.java @@ -0,0 +1,44 @@ +package ru.noties.markwon; + +import org.junit.Test; +import org.junit.runner.RunWith; +import org.robolectric.RobolectricTestRunner; +import org.robolectric.annotation.Config; + +import java.util.List; + +import ru.noties.markwon.core.CorePlugin; +import ru.noties.markwon.priority.Priority; + +import static org.junit.Assert.assertEquals; + +@RunWith(RobolectricTestRunner.class) +@Config(manifest = Config.NONE) +public class AbstractMarkwonPluginTest { + + @Test + public void priority() { + // returns CorePlugin dependency + + final Priority priority = new AbstractMarkwonPlugin() { + }.priority(); + final List> after = priority.after(); + assertEquals(1, after.size()); + assertEquals(CorePlugin.class, after.get(0)); + } + + @Test + public void process_markdown() { + // returns supplied argument (no-op) + + final String[] input = { + "hello", + "!\nworld___-976" + }; + + for (String s : input) { + assertEquals(s, new AbstractMarkwonPlugin() { + }.processMarkdown(s)); + } + } +} \ No newline at end of file diff --git a/markwon-core/src/test/java/ru/noties/markwon/AbstractMarkwonVisitorImpl.java b/markwon-core/src/test/java/ru/noties/markwon/AbstractMarkwonVisitorImpl.java new file mode 100644 index 00000000..ea099248 --- /dev/null +++ b/markwon-core/src/test/java/ru/noties/markwon/AbstractMarkwonVisitorImpl.java @@ -0,0 +1,18 @@ +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 RenderProps renderProps, + @NonNull SpannableBuilder spannableBuilder, + @NonNull Map, NodeVisitor> nodes) { + super(configuration, renderProps, spannableBuilder, nodes); + } +} diff --git a/markwon-core/src/test/java/ru/noties/markwon/MarkwonBuilderImplTest.java b/markwon-core/src/test/java/ru/noties/markwon/MarkwonBuilderImplTest.java new file mode 100644 index 00000000..af3d00b1 --- /dev/null +++ b/markwon-core/src/test/java/ru/noties/markwon/MarkwonBuilderImplTest.java @@ -0,0 +1,232 @@ +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.html.MarkwonHtmlRenderer; +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() { + + }; + + 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)); + verify(plugin, times(1)).configureHtmlRenderer(any(MarkwonHtmlRenderer.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)).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-core/src/test/java/ru/noties/markwon/MarkwonImplTest.java b/markwon-core/src/test/java/ru/noties/markwon/MarkwonImplTest.java new file mode 100644 index 00000000..d80cb765 --- /dev/null +++ b/markwon-core/src/test/java/ru/noties/markwon/MarkwonImplTest.java @@ -0,0 +1,256 @@ +package ru.noties.markwon; + +import android.text.Spanned; +import android.widget.TextView; + +import org.commonmark.node.Node; +import org.commonmark.node.Visitor; +import org.commonmark.parser.Parser; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.ArgumentCaptor; +import org.mockito.invocation.InvocationOnMock; +import org.mockito.stubbing.Answer; +import org.robolectric.RobolectricTestRunner; +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; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.RETURNS_MOCKS; +import static org.mockito.Mockito.doAnswer; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +@RunWith(RobolectricTestRunner.class) +@Config(manifest = Config.NONE) +public class MarkwonImplTest { + + @Test + public void parse_calls_plugin_process_markdown() { + + final MarkwonPlugin plugin = mock(MarkwonPlugin.class); + final MarkwonImpl impl = new MarkwonImpl( + TextView.BufferType.SPANNABLE, + mock(Parser.class), + mock(MarkwonVisitor.class), + Collections.singletonList(plugin)); + + impl.parse("whatever"); + + verify(plugin, times(1)).processMarkdown(eq("whatever")); + } + + @Test + public void parse_markwon_processed() { + + final Parser parser = mock(Parser.class); + + final MarkwonPlugin first = mock(MarkwonPlugin.class); + final MarkwonPlugin second = mock(MarkwonPlugin.class); + + when(first.processMarkdown(anyString())).thenReturn("first"); + when(second.processMarkdown(anyString())).thenReturn("second"); + + final MarkwonImpl impl = new MarkwonImpl( + TextView.BufferType.SPANNABLE, + parser, + mock(MarkwonVisitor.class), + Arrays.asList(first, second)); + + impl.parse("zero"); + + verify(first, times(1)).processMarkdown(eq("zero")); + verify(second, times(1)).processMarkdown(eq("first")); + + // verify parser has `second` as input + verify(parser, times(1)).parse(eq("second")); + } + + @Test + public void render_calls_plugins() { + // before parsing each plugin is called `configureRenderProps` and `beforeRender` + // after parsing each plugin is called `afterRender` + + final MarkwonPlugin plugin = mock(MarkwonPlugin.class); + + final MarkwonVisitor visitor = mock(MarkwonVisitor.class); + final SpannableBuilder builder = mock(SpannableBuilder.class); + + final MarkwonImpl impl = new MarkwonImpl( + TextView.BufferType.SPANNABLE, + mock(Parser.class), + visitor, + Collections.singletonList(plugin)); + + when(visitor.builder()).thenReturn(builder); + + final Node node = mock(Node.class); + + final AtomicBoolean flag = new AtomicBoolean(false); + + // we will validate _before_ part here + doAnswer(new Answer() { + @Override + public Object answer(InvocationOnMock invocation) { + + // mark this flag (we must ensure that this method body is executed) + flag.set(true); + + verify(plugin, times(1)).beforeRender(eq(node)); + verify(plugin, times(0)).afterRender(any(Node.class), any(MarkwonVisitor.class)); + + return null; + } + }).when(node).accept(any(Visitor.class)); + + impl.render(node); + + // validate that Answer was called (it has assertions about _before_ part + assertTrue(flag.get()); + + verify(plugin, times(1)).afterRender(eq(node), eq(visitor)); + } + + @Test + public void render_clears_visitor() { + // each render call should have empty-state visitor (no previous rendering info) + + final MarkwonVisitor visitor = mock(MarkwonVisitor.class, RETURNS_MOCKS); + + final MarkwonImpl impl = new MarkwonImpl( + TextView.BufferType.SPANNABLE, + mock(Parser.class), + visitor, + Collections.emptyList()); + + impl.render(mock(Node.class)); + + verify(visitor, times(1)).clear(); + } + + @Test + public void render_props() { + // render props are configured properly and cleared after render function + + final MarkwonVisitor visitor = mock(MarkwonVisitor.class, RETURNS_MOCKS); + + final RenderProps renderProps = mock(RenderProps.class); + doAnswer(new Answer() { + @Override + public Object answer(InvocationOnMock invocation) { + renderProps.clearAll(); + return null; + } + }).when(visitor).clear(); + + when(visitor.renderProps()).thenReturn(renderProps); + + final MarkwonPlugin plugin = mock(MarkwonPlugin.class); + + final MarkwonImpl impl = new MarkwonImpl( + TextView.BufferType.SPANNABLE, + mock(Parser.class), + visitor, + Collections.singletonList(plugin)); + + final AtomicBoolean flag = new AtomicBoolean(false); + final Node node = mock(Node.class); + + doAnswer(new Answer() { + @Override + public Object answer(InvocationOnMock invocation) { + + flag.set(true); + + verify(renderProps, times(0)).clearAll(); + + return null; + } + }).when(node).accept(any(Visitor.class)); + + impl.render(node); + + assertTrue(flag.get()); + + verify(renderProps, times(1)).clearAll(); + } + + @Test + public void set_parsed_markdown() { + // calls `beforeSetText` on plugins + // calls `TextView#setText(text, BUFFER_TYPE)` + // calls `afterSetText` on plugins + + final MarkwonPlugin plugin = mock(MarkwonPlugin.class); + final MarkwonImpl impl = new MarkwonImpl( + TextView.BufferType.EDITABLE, + mock(Parser.class), + mock(MarkwonVisitor.class, RETURNS_MOCKS), + Collections.singletonList(plugin)); + + final TextView textView = mock(TextView.class); + final AtomicBoolean flag = new AtomicBoolean(false); + + doAnswer(new Answer() { + @Override + public Object answer(InvocationOnMock invocation) { + + flag.set(true); + + verify(plugin, times(1)).beforeSetText(eq(textView), any(Spanned.class)); + verify(plugin, times(0)).afterSetText(any(TextView.class)); + + final ArgumentCaptor captor = + ArgumentCaptor.forClass(TextView.BufferType.class); + + verify(textView).setText(any(CharSequence.class), captor.capture()); + assertEquals(TextView.BufferType.EDITABLE, captor.getValue()); + + return null; + } + }).when(textView).setText(any(CharSequence.class), any(TextView.BufferType.class)); + + impl.setParsedMarkdown(textView, mock(Spanned.class)); + + assertTrue(flag.get()); + + 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 diff --git a/markwon-core/src/test/java/ru/noties/markwon/MarkwonSpansFactoryImplTest.java b/markwon-core/src/test/java/ru/noties/markwon/MarkwonSpansFactoryImplTest.java new file mode 100644 index 00000000..6245df0e --- /dev/null +++ b/markwon-core/src/test/java/ru/noties/markwon/MarkwonSpansFactoryImplTest.java @@ -0,0 +1,90 @@ +package ru.noties.markwon; + +import org.commonmark.node.Block; +import org.commonmark.node.Emphasis; +import org.commonmark.node.Image; +import org.commonmark.node.Link; +import org.commonmark.node.ListItem; +import org.commonmark.node.Node; +import org.commonmark.node.Paragraph; +import org.commonmark.node.Text; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.robolectric.RobolectricTestRunner; +import org.robolectric.annotation.Config; + +import java.util.Collections; + +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; +import static org.mockito.Mockito.mock; + +@RunWith(RobolectricTestRunner.class) +@Config(manifest = Config.NONE) +public class MarkwonSpansFactoryImplTest { + + @Test + public void get_class() { + + // register one TextNode + final MarkwonSpansFactoryImpl impl = new MarkwonSpansFactoryImpl( + Collections., SpanFactory>singletonMap(Text.class, mock(SpanFactory.class))); + + // text must be present + assertNotNull(impl.get(Text.class)); + + // we haven't registered ListItem, so null here + assertNull(impl.get(ListItem.class)); + } + + @Test + public void require_class() { + + // register one TextNode + final MarkwonSpansFactoryImpl impl = new MarkwonSpansFactoryImpl( + Collections., SpanFactory>singletonMap(Text.class, mock(SpanFactory.class))); + + // text must be present + assertNotNull(impl.require(Text.class)); + + // we haven't registered ListItem, so null here + try { + impl.require(ListItem.class); + fail(); + } catch (NullPointerException e) { + assertTrue(true); + } + } + + @Test + public void builder() { + // all passed to builder will be in factory + + final SpanFactory text = mock(SpanFactory.class); + final SpanFactory link = mock(SpanFactory.class); + + final MarkwonSpansFactory factory = new MarkwonSpansFactoryImpl.BuilderImpl() + .setFactory(Text.class, text) + .setFactory(Link.class, link) + .build(); + + assertNotNull(factory.get(Text.class)); + + assertNotNull(factory.get(Link.class)); + + // a bunch of non-present factories + //noinspection unchecked + final Class[] types = new Class[]{ + Image.class, + Block.class, + Emphasis.class, + Paragraph.class + }; + + for (Class type : types) { + assertNull(factory.get(type)); + } + } +} \ No newline at end of file diff --git a/markwon-core/src/test/java/ru/noties/markwon/MarkwonVisitorImplTest.java b/markwon-core/src/test/java/ru/noties/markwon/MarkwonVisitorImplTest.java new file mode 100644 index 00000000..1a444d70 --- /dev/null +++ b/markwon-core/src/test/java/ru/noties/markwon/MarkwonVisitorImplTest.java @@ -0,0 +1,266 @@ +package ru.noties.markwon; + +import org.commonmark.node.BlockQuote; +import org.commonmark.node.Node; +import org.commonmark.node.Text; +import org.commonmark.node.Visitor; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.robolectric.RobolectricTestRunner; +import org.robolectric.annotation.Config; + +import java.lang.reflect.Method; +import java.util.Collections; +import java.util.List; + +import ix.Ix; +import ix.IxPredicate; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.RETURNS_MOCKS; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +@RunWith(RobolectricTestRunner.class) +@Config(manifest = Config.NONE) +public class MarkwonVisitorImplTest { + + @Test + public void clear() { + // clear method will clear renderProps and spannableBuilder + + final RenderProps renderProps = mock(RenderProps.class); + final SpannableBuilder spannableBuilder = mock(SpannableBuilder.class); + + final MarkwonVisitorImpl impl = new MarkwonVisitorImpl( + mock(MarkwonConfiguration.class), + renderProps, + spannableBuilder, + Collections., MarkwonVisitor.NodeVisitor>emptyMap()); + + impl.clear(); + + verify(renderProps, times(1)).clearAll(); + verify(spannableBuilder, times(1)).clear(); + } + + @Test + public void ensure_new_line() { + // new line will be inserted if length > 0 && last character is not a new line + + final SpannableBuilder builder = new SpannableBuilder(); + + final MarkwonVisitorImpl impl = new MarkwonVisitorImpl( + mock(MarkwonConfiguration.class), + mock(RenderProps.class), + builder, + Collections., MarkwonVisitor.NodeVisitor>emptyMap()); + + // at the start - won't add anything + impl.ensureNewLine(); + assertEquals(0, builder.length()); + + // last char is new line -> won't add anything + builder.append('\n'); + assertEquals(1, builder.length()); + impl.ensureNewLine(); + assertEquals(1, builder.length()); + + // not-empty and last char is not new-line -> add new line + builder.clear(); + assertEquals(0, builder.length()); + builder.append('a'); + assertEquals(1, builder.length()); + impl.ensureNewLine(); + assertEquals(2, builder.length()); + assertEquals('\n', builder.lastChar()); + } + + @Test + public void force_new_line() { + // force new line always add new-line + + final SpannableBuilder builder = new SpannableBuilder(); + final MarkwonVisitorImpl impl = new MarkwonVisitorImpl( + mock(MarkwonConfiguration.class), + mock(RenderProps.class), + builder, + Collections., MarkwonVisitor.NodeVisitor>emptyMap()); + + assertEquals(0, builder.length()); + + for (int i = 0; i < 9; i++) { + impl.forceNewLine(); + } + + assertEquals(9, builder.length()); + + // all characters are new lines + for (int i = 0; i < builder.length(); i++) { + assertEquals('\n', builder.charAt(i)); + } + } + + @Test + public void all_known_nodes_visit_methods_are_overridden() { + // checks that all methods from Visitor (commonmark-java) interface are implemented + + final List methods = Ix.fromArray(Visitor.class.getDeclaredMethods()) + .filter(new IxPredicate() { + + @Override + public boolean test(Method method) { + + // if it's present in our impl -> remove + // else keep (to report) + + try { + MarkwonVisitorImpl.class + .getDeclaredMethod(method.getName(), method.getParameterTypes()); + return false; + } catch (NoSuchMethodException e) { + return true; + } + } + }) + .toList(); + + assertEquals(methods.toString(), 0, methods.size()); + } + + @Test + public void non_registered_nodes_children_visited() { + // if a node is encountered, but we have no registered visitor -> just visit children + // (node.firstChild.accept) + + final MarkwonVisitorImpl impl = new MarkwonVisitorImpl( + mock(MarkwonConfiguration.class), + mock(RenderProps.class), + mock(SpannableBuilder.class), + Collections., MarkwonVisitor.NodeVisitor>emptyMap()); + + final BlockQuote node = mock(BlockQuote.class); + final Node child = mock(Node.class); + when(node.getFirstChild()).thenReturn(child); + + impl.visit(node); + + verify(node, times(1)).getFirstChild(); + verify(child, times(1)).accept(eq(impl)); + } + + @Test + public void has_next() { + + final MarkwonVisitorImpl impl = new MarkwonVisitorImpl( + mock(MarkwonConfiguration.class), + mock(RenderProps.class), + mock(SpannableBuilder.class), + Collections., MarkwonVisitor.NodeVisitor>emptyMap()); + + final Node noNext = mock(Node.class); + assertFalse(impl.hasNext(noNext)); + + final Node hasNext = mock(Node.class, RETURNS_MOCKS); + assertTrue(impl.hasNext(hasNext)); + } + + @Test + public void length() { + // redirects call to SpannableBuilder (no internal caching) + + final class BuilderImpl extends SpannableBuilder { + + private int length; + + private void setLength(int length) { + this.length = length; + } + + @Override + public int length() { + return length; + } + } + final BuilderImpl builder = new BuilderImpl(); + + final MarkwonVisitorImpl impl = new MarkwonVisitorImpl( + mock(MarkwonConfiguration.class), + mock(RenderProps.class), + builder, + Collections., MarkwonVisitor.NodeVisitor>emptyMap()); + + for (int i = 0; i < 13; i++) { + builder.setLength(i); + assertEquals(i, builder.length()); + assertEquals(builder.length(), impl.length()); + } + } + + @Test + public void set_spans_for_node() { + // internally requests spanFactory via `require` call (thus throwing exception) + // configuration.spansFactory().require(node).getSpans(configuration, renderProps) + + final MarkwonConfiguration configuration = mock(MarkwonConfiguration.class); + final MarkwonSpansFactory spansFactory = mock(MarkwonSpansFactory.class); + final SpanFactory factory = mock(SpanFactory.class); + + when(configuration.spansFactory()).thenReturn(spansFactory); + when(spansFactory.require(eq(Node.class))).thenReturn(factory); + when(spansFactory.require(eq(Text.class))).thenThrow(new NullPointerException()); + + final MarkwonVisitorImpl impl = new MarkwonVisitorImpl( + configuration, + mock(RenderProps.class), + mock(SpannableBuilder.class), + Collections., MarkwonVisitor.NodeVisitor>emptyMap()); + + impl.setSpansForNode(Node.class, 0); + + verify(configuration, times(1)).spansFactory(); + verify(spansFactory, times(1)).require(eq(Node.class)); + verify(factory, times(1)).getSpans(eq(configuration), any(RenderProps.class)); + + try { + impl.setSpansForNode(Text.class, 0); + fail(); + } catch (NullPointerException e) { + assertTrue(true); + } + } + + @Test + public void set_spans_for_node_optional() { + // if spanFactory is not found -> nothing will happen (no spans will be applied) + + final MarkwonConfiguration configuration = mock(MarkwonConfiguration.class); + final MarkwonSpansFactory spansFactory = mock(MarkwonSpansFactory.class); + + when(configuration.spansFactory()).thenReturn(spansFactory); + + final SpannableBuilder builder = new SpannableBuilder(); + + final MarkwonVisitorImpl impl = new MarkwonVisitorImpl( + configuration, + mock(RenderProps.class), + builder, + Collections., MarkwonVisitor.NodeVisitor>emptyMap()); + + // append something + builder.append("no-spans-test"); + + assertEquals(0, builder.getSpans(0, builder.length()).size()); + + impl.setSpansForNodeOptional(Node.class, 0); + + assertEquals(0, builder.getSpans(0, builder.length()).size()); + } +} \ No newline at end of file diff --git a/markwon-core/src/test/java/ru/noties/markwon/PropTest.java b/markwon-core/src/test/java/ru/noties/markwon/PropTest.java new file mode 100644 index 00000000..85a81fe1 --- /dev/null +++ b/markwon-core/src/test/java/ru/noties/markwon/PropTest.java @@ -0,0 +1,100 @@ +package ru.noties.markwon; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.robolectric.RobolectricTestRunner; +import org.robolectric.annotation.Config; + +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; +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 org.mockito.Mockito.when; + +@RunWith(RobolectricTestRunner.class) +@Config(manifest = Config.NONE) +public class PropTest { + + // get + // get with default + // require + // set + // clear + + private RenderProps props; + private Prop prop; + + @Before + public void before() { + props = mock(RenderProps.class); + prop = new Prop<>("a prop"); + } + + @Test + public void methods_redirected_get() { + + prop.get(props); + + verify(props, times(1)).get(eq(prop)); + } + + @Test + public void methods_redirected_get_with_default() { + + prop.get(props, false); + + verify(props, times(1)).get(eq(prop), eq(false)); + } + + @Test + public void methods_redirected_require() { + // require is a bit different as `require` has no place in renderProps + // instead a Prop will throw an exception if requested prop is not in props + + when(props.get(eq(prop))).thenReturn(true); + + prop.require(props); + + verify(props, times(1)).get(eq(prop)); + } + + @Test + public void methods_redirected_set() { + + prop.set(props, true); + + verify(props, times(1)).set(eq(prop), eq(true)); + } + + @Test + public void methods_redirected_clear() { + + prop.clear(props); + + verify(props, times(1)).clear(eq(prop)); + } + + @Test + public void require() { + + try { + prop.require(props); + fail(); + } catch (NullPointerException e) { + assertTrue(true); + } + } + + @Test + public void has_hashcode_and_equals() { + try { + Prop.class.getDeclaredMethod("hashCode"); + Prop.class.getDeclaredMethod("equals", Object.class); + } catch (NoSuchMethodException e) { + throw new RuntimeException(e); + } + } +} \ No newline at end of file diff --git a/markwon-core/src/test/java/ru/noties/markwon/RenderPropsImplTest.java b/markwon-core/src/test/java/ru/noties/markwon/RenderPropsImplTest.java new file mode 100644 index 00000000..36a6315b --- /dev/null +++ b/markwon-core/src/test/java/ru/noties/markwon/RenderPropsImplTest.java @@ -0,0 +1,119 @@ +package ru.noties.markwon; + +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 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 RenderPropsImplTest { + + private RenderPropsImpl props; + private Prop prop; + + @Before + public void before() { + props = new RenderPropsImpl(); + prop = Prop.of("a prop of byte"); + } + + @Test + public void get() { + + // initial + assertNull(props.get(prop)); + + // update value + props.set(prop, "get-value"); + + assertEquals("get-value", props.get(prop)); + } + + @Test + public void get_with_default() { + + // validate that it's null + assertNull(props.get(prop)); + + assertEquals("a-default", props.get(prop, "a-default")); + + // update value (so, no default will be returned) + props.set(prop, "get-with-default-value"); + + assertEquals("get-with-default-value", props.get(prop, "not-used")); + } + + @Test + public void set() { + + assertNull(props.get(prop)); + + props.set(prop, "set-value"); + assertEquals("set-value", props.get(prop)); + + // update (aka delete) with null value + props.set(prop, null); + assertNull(props.get(prop)); + + // multiple set's (last one will be used, each one replaces previous) + props.set(prop, "value-1"); + props.set(prop, "value-2"); + props.set(prop, "value-3"); + + assertEquals("value-3", props.get(prop)); + } + + @Test + public void clear() { + + props.set(prop, "clear-value"); + + assertEquals("clear-value", props.get(prop)); + + props.clear(prop); + + assertNull(props.get(prop)); + } + + @Test + public void clear_all() { + + final List> list = Arrays.asList( + Prop.of("#1"), + Prop.of("#2"), + Prop.of("#3"), + Prop.of("#4"), + Prop.of("#5")); + + // validate that all nulls + for (Prop prop : list) { + assertNull(props.get(prop)); + } + + // set each + for (Prop prop : list) { + props.set(prop, prop.name()); + } + + // validate that all are not-null + for (Prop prop : list) { + assertNotNull(props.get(prop)); + } + + props.clearAll(); + + // validate that all are nulls + for (Prop prop : list) { + assertNull(props.get(prop)); + } + } +} \ No newline at end of file diff --git a/markwon/src/test/java/ru/noties/markwon/SpannableBuilderTest.java b/markwon-core/src/test/java/ru/noties/markwon/SpannableBuilderTest.java similarity index 100% rename from markwon/src/test/java/ru/noties/markwon/SpannableBuilderTest.java rename to markwon-core/src/test/java/ru/noties/markwon/SpannableBuilderTest.java diff --git a/markwon-core/src/test/java/ru/noties/markwon/core/CorePluginBridge.java b/markwon-core/src/test/java/ru/noties/markwon/core/CorePluginBridge.java new file mode 100644 index 00000000..6517873b --- /dev/null +++ b/markwon-core/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-core/src/test/java/ru/noties/markwon/core/CorePluginTest.java b/markwon-core/src/test/java/ru/noties/markwon/core/CorePluginTest.java new file mode 100644 index 00000000..1cbf908b --- /dev/null +++ b/markwon-core/src/test/java/ru/noties/markwon/core/CorePluginTest.java @@ -0,0 +1,294 @@ +package ru.noties.markwon.core; + +import android.support.annotation.NonNull; +import android.support.annotation.Nullable; +import android.text.method.MovementMethod; +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.HardLineBreak; +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.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 org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.ArgumentCaptor; +import org.robolectric.RobolectricTestRunner; +import org.robolectric.annotation.Config; + +import java.lang.reflect.Method; +import java.lang.reflect.Modifier; +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 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.SpannableBuilder; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertTrue; +import static org.mockito.ArgumentMatchers.any; +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 org.mockito.Mockito.when; + +@RunWith(RobolectricTestRunner.class) +@Config(manifest = Config.NONE) +public class CorePluginTest { + + @Test + public void visitors_registered() { + + // only these must be registered (everything else is an error) + // Paragraph has registered visitor but no Span by default + + //noinspection unchecked + final Class[] expected = new Class[]{ + BlockQuote.class, + BulletList.class, + Code.class, + Emphasis.class, + FencedCodeBlock.class, + HardLineBreak.class, + Heading.class, + IndentedCodeBlock.class, + Link.class, + ListItem.class, + OrderedList.class, + Paragraph.class, + SoftLineBreak.class, + StrongEmphasis.class, + Text.class, + ThematicBreak.class + }; + + final CorePlugin plugin = CorePlugin.create(); + + final class BuilderImpl implements MarkwonVisitor.Builder { + + private final Map, MarkwonVisitor.NodeVisitor> map = + new HashMap<>(); + + @NonNull + @Override + public MarkwonVisitor.Builder on(@NonNull Class node, @Nullable MarkwonVisitor.NodeVisitor nodeVisitor) { + if (map.put(node, nodeVisitor) != null) { + throw new RuntimeException("Multiple visitors registered for the node: " + node.getClass().getName()); + } + return this; + } + + @NonNull + @Override + public MarkwonVisitor build(@NonNull MarkwonConfiguration configuration, @NonNull RenderProps renderProps) { + throw new RuntimeException(); + } + } + final BuilderImpl impl = new BuilderImpl(); + + plugin.configureVisitor(impl); + + for (Class node : expected) { + assertNotNull("Node visitor registered: " + node.getName(), impl.map.remove(node)); + } + + // all other nodes (that could've been registered is an error) + assertEquals(impl.map.toString(), 0, impl.map.size()); + } + + @Test + public void spans_registered() { + + // paragraph has visitor registered, but no span associated by default + + //noinspection unchecked + final Class[] expected = new Class[]{ + BlockQuote.class, + Code.class, + Emphasis.class, + FencedCodeBlock.class, + Heading.class, + IndentedCodeBlock.class, + Link.class, + ListItem.class, + StrongEmphasis.class, + ThematicBreak.class + }; + + final CorePlugin plugin = CorePlugin.create(); + + final class BuilderImpl implements MarkwonSpansFactory.Builder { + + private final Map, SpanFactory> map = + new HashMap<>(); + + @NonNull + @Override + public MarkwonSpansFactory.Builder setFactory(@NonNull Class node, @NonNull SpanFactory factory) { + if (map.put(node, factory) != null) { + throw new RuntimeException("Multiple SpanFactories registered for the node: " + node.getName()); + } + return this; + } + + @Nullable + @Override + public SpanFactory getFactory(@NonNull Class node) { + throw new RuntimeException(); + } + + @NonNull + @Override + public MarkwonSpansFactory build() { + throw new RuntimeException(); + } + } + final BuilderImpl impl = new BuilderImpl(); + + plugin.configureSpansFactory(impl); + + for (Class node : expected) { + assertNotNull("SpanFactory registered: " + node.getName(), impl.map.remove(node)); + } + + assertEquals(impl.map.toString(), 0, impl.map.size()); + } + + @Test + public void priority_none() { + // CorePlugin returns none as priority (thus 0) + + assertEquals(0, CorePlugin.create().priority().after().size()); + } + + @Test + public void plugin_methods() { + // checks that only expected plugin methods are overridden + + // these represent actual methods that are present (we expect them to be present) + final Set usedMethods = new HashSet() {{ + add("configureVisitor"); + add("configureSpansFactory"); + add("beforeSetText"); + add("afterSetText"); + add("priority"); + }}; + + // we will use declaredMethods because it won't return inherited ones + final Method[] declaredMethods = CorePlugin.class.getDeclaredMethods(); + assertNotNull(declaredMethods); + assertTrue(declaredMethods.length > 0); + + final List methods = Ix.fromArray(declaredMethods) + .filter(new IxPredicate() { + @Override + public boolean test(Method method) { + // ignore private, static + final int modifiers = method.getModifiers(); + return !Modifier.isStatic(modifiers) + && !Modifier.isPrivate(modifiers); + } + }) + .map(new IxFunction() { + @Override + public String apply(Method method) { + return method.getName(); + } + }) + .filter(new IxPredicate() { + @Override + public boolean test(String s) { + return !usedMethods.contains(s); + } + }) + .toList(); + + assertEquals(methods.toString(), 0, methods.size()); + } + + @Test + public void softbreak() { + + final CorePlugin plugin = CorePlugin.create(); + + final MarkwonVisitor.Builder builder = mock(MarkwonVisitor.Builder.class); + when(builder.on(any(Class.class), any(MarkwonVisitor.NodeVisitor.class))).thenReturn(builder); + + final ArgumentCaptor captor = + ArgumentCaptor.forClass(MarkwonVisitor.NodeVisitor.class); + + plugin.configureVisitor(builder); + + //noinspection unchecked + verify(builder).on(eq(SoftLineBreak.class), captor.capture()); + + //noinspection unchecked + final MarkwonVisitor.NodeVisitor nodeVisitor = captor.getValue(); + final MarkwonVisitor visitor = mock(MarkwonVisitor.class); + + // we must mock SpannableBuilder and verify that it has a space character appended + final SpannableBuilder spannableBuilder = mock(SpannableBuilder.class); + when(visitor.builder()).thenReturn(spannableBuilder); + nodeVisitor.visit(visitor, mock(SoftLineBreak.class)); + + verify(visitor, times(1)).builder(); + verify(spannableBuilder, times(1)).append(eq(' ')); + } + + @Test + public void implicit_movement_method_after_set_text_added() { + // validate that CorePlugin will implicitly add LinkMovementMethod if one is missing + final TextView textView = mock(TextView.class); + when(textView.getMovementMethod()).thenReturn(null); + + final CorePlugin plugin = CorePlugin.create(); + + assertNull(textView.getMovementMethod()); + + plugin.afterSetText(textView); + + final ArgumentCaptor captor = ArgumentCaptor.forClass(MovementMethod.class); + verify(textView, times(1)).setMovementMethod(captor.capture()); + + assertNotNull(captor.getValue()); + } + + @Test + public void implicit_movement_method_after_set_text_no_op() { + // validate that CorePlugin won't change movement method if one is present on a TextView + + final TextView textView = mock(TextView.class); + when(textView.getMovementMethod()).thenReturn(mock(MovementMethod.class)); + + final CorePlugin plugin = CorePlugin.create(); + + plugin.afterSetText(textView); + + verify(textView, times(0)).setMovementMethod(any(MovementMethod.class)); + } +} \ No newline at end of file diff --git a/markwon-core/src/test/java/ru/noties/markwon/core/CoreTest.java b/markwon-core/src/test/java/ru/noties/markwon/core/CoreTest.java new file mode 100644 index 00000000..de7c0dd0 --- /dev/null +++ b/markwon-core/src/test/java/ru/noties/markwon/core/CoreTest.java @@ -0,0 +1,64 @@ +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; +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.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 = Markwon.builder(RuntimeEnvironment.application) + .usePlugin(CorePlugin.create()) + .usePlugin(new AbstractMarkwonPlugin() { + @Override + 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() + .toMarkdown(input); + + TestSpanMatcher.matches(spanned, document); + } +} diff --git a/markwon-core/src/test/java/ru/noties/markwon/core/suite/.editorconfig b/markwon-core/src/test/java/ru/noties/markwon/core/suite/.editorconfig new file mode 100644 index 00000000..be598039 --- /dev/null +++ b/markwon-core/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-core/src/test/java/ru/noties/markwon/core/suite/BaseSuiteTest.java b/markwon-core/src/test/java/ru/noties/markwon/core/suite/BaseSuiteTest.java new file mode 100644 index 00000000..d434ed99 --- /dev/null +++ b/markwon-core/src/test/java/ru/noties/markwon/core/suite/BaseSuiteTest.java @@ -0,0 +1,167 @@ +package ru.noties.markwon.core.suite; + +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; +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.io.IOException; +import java.nio.charset.StandardCharsets; +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 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); + } + + void matchInput(@NonNull String name, @NonNull TestSpan.Document document) { + final Spanned spanned = markwon().toMarkdown(read(name)); + TestSpanMatcher.matches(spanned, document); + } + + @NonNull + private String read(@NonNull String name) { + try { + return IOUtils.resourceToString("tests/" + name, StandardCharsets.UTF_8, getClass().getClassLoader()); + } catch (IOException e) { + throw new RuntimeException(e); + } + } + + @NonNull + Markwon markwon() { + return Markwon.builder(RuntimeEnvironment.application) + .usePlugin(CorePlugin.create()) + .usePlugin(new AbstractMarkwonPlugin() { + @Override + 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(); + } + + boolean useParagraphs() { + 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); + } + } +} diff --git a/markwon-core/src/test/java/ru/noties/markwon/core/suite/BlockquoteTest.java b/markwon-core/src/test/java/ru/noties/markwon/core/suite/BlockquoteTest.java new file mode 100644 index 00000000..21d2c852 --- /dev/null +++ b/markwon-core/src/test/java/ru/noties/markwon/core/suite/BlockquoteTest.java @@ -0,0 +1,47 @@ +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.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"))); + + match("> blockquote", document); + } +} diff --git a/markwon-core/src/test/java/ru/noties/markwon/core/suite/BoldItalicTest.java b/markwon-core/src/test/java/ru/noties/markwon/core/suite/BoldItalicTest.java new file mode 100644 index 00000000..1315136b --- /dev/null +++ b/markwon-core/src/test/java/ru/noties/markwon/core/suite/BoldItalicTest.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; + +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 BoldItalicTest extends BaseSuiteTest { + + @Test + public void test() { + + final TestSpan.Document document = document( + span(BOLD, + span(ITALIC, text("bold italic")))); + + match("**_bold italic_**", document); + } +} diff --git a/markwon-core/src/test/java/ru/noties/markwon/core/suite/CodeTest.java b/markwon-core/src/test/java/ru/noties/markwon/core/suite/CodeTest.java new file mode 100644 index 00000000..52a8f614 --- /dev/null +++ b/markwon-core/src/test/java/ru/noties/markwon/core/suite/CodeTest.java @@ -0,0 +1,70 @@ +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.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 CodeTest extends BaseSuiteTest { + + /* + ```java + final String s = null; + ``` + ```html + + ``` + ``` + nothing here + ``` + */ + + @Test + public void multiple_blocks() { + + final 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); + } + + @Test + public void single() { + + final Document document = document( + span(CODE, args("multiline", false), text("\u00a0code\u00a0")) + ); + + match("`code`", 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-core/src/test/java/ru/noties/markwon/core/suite/DeeplyNestedTest.java b/markwon-core/src/test/java/ru/noties/markwon/core/suite/DeeplyNestedTest.java new file mode 100644 index 00000000..e0dcda38 --- /dev/null +++ b/markwon-core/src/test/java/ru/noties/markwon/core/suite/DeeplyNestedTest.java @@ -0,0 +1,41 @@ +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.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 DeeplyNestedTest 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-core/src/test/java/ru/noties/markwon/core/suite/EmphasisTest.java b/markwon-core/src/test/java/ru/noties/markwon/core/suite/EmphasisTest.java new file mode 100644 index 00000000..057acc18 --- /dev/null +++ b/markwon-core/src/test/java/ru/noties/markwon/core/suite/EmphasisTest.java @@ -0,0 +1,26 @@ +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.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-core/src/test/java/ru/noties/markwon/core/suite/FirstTest.java b/markwon-core/src/test/java/ru/noties/markwon/core/suite/FirstTest.java new file mode 100644 index 00000000..f67a5062 --- /dev/null +++ b/markwon-core/src/test/java/ru/noties/markwon/core/suite/FirstTest.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.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 FirstTest 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-core/src/test/java/ru/noties/markwon/core/suite/HeadingTest.java b/markwon-core/src/test/java/ru/noties/markwon/core/suite/HeadingTest.java new file mode 100644 index 00000000..10fa5f9b --- /dev/null +++ b/markwon-core/src/test/java/ru/noties/markwon/core/suite/HeadingTest.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.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-core/src/test/java/ru/noties/markwon/core/suite/LinkTest.java b/markwon-core/src/test/java/ru/noties/markwon/core/suite/LinkTest.java new file mode 100644 index 00000000..ff70f82c --- /dev/null +++ b/markwon-core/src/test/java/ru/noties/markwon/core/suite/LinkTest.java @@ -0,0 +1,28 @@ +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.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-core/src/test/java/ru/noties/markwon/core/suite/NoParagraphsTest.java b/markwon-core/src/test/java/ru/noties/markwon/core/suite/NoParagraphsTest.java new file mode 100644 index 00000000..32ea306b --- /dev/null +++ b/markwon-core/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-core/src/test/java/ru/noties/markwon/core/suite/OrderedListTest.java b/markwon-core/src/test/java/ru/noties/markwon/core/suite/OrderedListTest.java new file mode 100644 index 00000000..9efb46b8 --- /dev/null +++ b/markwon-core/src/test/java/ru/noties/markwon/core/suite/OrderedListTest.java @@ -0,0 +1,107 @@ +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.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")) + ); + + match("1. ol", document); + } +} diff --git a/markwon-core/src/test/java/ru/noties/markwon/core/suite/ParagraphTest.java b/markwon-core/src/test/java/ru/noties/markwon/core/suite/ParagraphTest.java new file mode 100644 index 00000000..877a8818 --- /dev/null +++ b/markwon-core/src/test/java/ru/noties/markwon/core/suite/ParagraphTest.java @@ -0,0 +1,42 @@ +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.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-core/src/test/java/ru/noties/markwon/core/suite/SecondTest.java b/markwon-core/src/test/java/ru/noties/markwon/core/suite/SecondTest.java new file mode 100644 index 00000000..340df08d --- /dev/null +++ b/markwon-core/src/test/java/ru/noties/markwon/core/suite/SecondTest.java @@ -0,0 +1,58 @@ +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.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-core/src/test/java/ru/noties/markwon/core/suite/SoftBreakTest.java b/markwon-core/src/test/java/ru/noties/markwon/core/suite/SoftBreakTest.java new file mode 100644 index 00000000..b94ab45a --- /dev/null +++ b/markwon-core/src/test/java/ru/noties/markwon/core/suite/SoftBreakTest.java @@ -0,0 +1,28 @@ +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); + } +} diff --git a/markwon-core/src/test/java/ru/noties/markwon/core/suite/StrongEmphasisTest.java b/markwon-core/src/test/java/ru/noties/markwon/core/suite/StrongEmphasisTest.java new file mode 100644 index 00000000..d08377a1 --- /dev/null +++ b/markwon-core/src/test/java/ru/noties/markwon/core/suite/StrongEmphasisTest.java @@ -0,0 +1,28 @@ +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.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-core/src/test/java/ru/noties/markwon/core/suite/ThematicBreakTest.java b/markwon-core/src/test/java/ru/noties/markwon/core/suite/ThematicBreakTest.java new file mode 100644 index 00000000..1e0159bc --- /dev/null +++ b/markwon-core/src/test/java/ru/noties/markwon/core/suite/ThematicBreakTest.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.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-core/src/test/java/ru/noties/markwon/core/suite/UnOrderedListTest.java b/markwon-core/src/test/java/ru/noties/markwon/core/suite/UnOrderedListTest.java new file mode 100644 index 00000000..9b1dfb85 --- /dev/null +++ b/markwon-core/src/test/java/ru/noties/markwon/core/suite/UnOrderedListTest.java @@ -0,0 +1,83 @@ +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.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/java/ru/noties/markwon/spans/AsyncDrawableTest.java b/markwon-core/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-core/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-core/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-core/src/test/java/ru/noties/markwon/image/ImageSizeResolverDefTest.java similarity index 92% rename from markwon/src/test/java/ru/noties/markwon/renderer/ImageSizeResolverDefTest.java rename to markwon-core/src/test/java/ru/noties/markwon/image/ImageSizeResolverDefTest.java index f0945554..f8649603 100644 --- a/markwon/src/test/java/ru/noties/markwon/renderer/ImageSizeResolverDefTest.java +++ b/markwon-core/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; @@ -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-core/src/test/java/ru/noties/markwon/image/ImageTest.java b/markwon-core/src/test/java/ru/noties/markwon/image/ImageTest.java new file mode 100644 index 00000000..69391899 --- /dev/null +++ b/markwon-core/src/test/java/ru/noties/markwon/image/ImageTest.java @@ -0,0 +1,62 @@ +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; +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)) + .usePlugin(new AbstractMarkwonPlugin() { + @Override + 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(); + + final Document document = document( + span("image", args("href", "#href"), text("alt")) + ); + + TestSpanMatcher.matches(markwon.toMarkdown(markdown), document); + } +} diff --git a/markwon-image-loader/src/test/java/ru/noties/markwon/il/DataUriParserTest.java b/markwon-core/src/test/java/ru/noties/markwon/image/data/DataUriParserTest.java similarity index 98% rename from markwon-image-loader/src/test/java/ru/noties/markwon/il/DataUriParserTest.java rename to markwon-core/src/test/java/ru/noties/markwon/image/data/DataUriParserTest.java index 6de01af5..b361a0d3 100644 --- a/markwon-image-loader/src/test/java/ru/noties/markwon/il/DataUriParserTest.java +++ b/markwon-core/src/test/java/ru/noties/markwon/image/data/DataUriParserTest.java @@ -1,4 +1,4 @@ -package ru.noties.markwon.il; +package ru.noties.markwon.image.data; import org.junit.Before; import org.junit.Test; diff --git a/markwon-image-loader/src/test/java/ru/noties/markwon/il/DataUriSchemeHandlerTest.java b/markwon-core/src/test/java/ru/noties/markwon/image/data/DataUriSchemeHandlerTest.java similarity index 97% rename from markwon-image-loader/src/test/java/ru/noties/markwon/il/DataUriSchemeHandlerTest.java rename to markwon-core/src/test/java/ru/noties/markwon/image/data/DataUriSchemeHandlerTest.java index 1473744a..16dc73b5 100644 --- a/markwon-image-loader/src/test/java/ru/noties/markwon/il/DataUriSchemeHandlerTest.java +++ b/markwon-core/src/test/java/ru/noties/markwon/image/data/DataUriSchemeHandlerTest.java @@ -1,4 +1,4 @@ -package ru.noties.markwon.il; +package ru.noties.markwon.image.data; import android.net.Uri; import android.support.annotation.NonNull; @@ -14,6 +14,8 @@ 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; diff --git a/markwon-core/src/test/java/ru/noties/markwon/priority/PriorityProcessorTest.java b/markwon-core/src/test/java/ru/noties/markwon/priority/PriorityProcessorTest.java new file mode 100644 index 00000000..2a6063ed --- /dev/null +++ b/markwon-core/src/test/java/ru/noties/markwon/priority/PriorityProcessorTest.java @@ -0,0 +1,495 @@ +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() { + }; + 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 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() { + + 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 diff --git a/markwon/src/test/java/ru/noties/markwon/renderer/SyntaxHighlightTest.java b/markwon-core/src/test/java/ru/noties/markwon/syntax/SyntaxHighlightTest.java similarity index 61% rename from markwon/src/test/java/ru/noties/markwon/renderer/SyntaxHighlightTest.java rename to markwon-core/src/test/java/ru/noties/markwon/syntax/SyntaxHighlightTest.java index 1f5d88d4..9c0262bb 100644 --- a/markwon/src/test/java/ru/noties/markwon/renderer/SyntaxHighlightTest.java +++ b/markwon-core/src/test/java/ru/noties/markwon/syntax/SyntaxHighlightTest.java @@ -1,6 +1,5 @@ -package ru.noties.markwon.renderer; +package ru.noties.markwon.syntax; -import android.content.Context; import android.os.Build; import android.support.annotation.NonNull; import android.support.annotation.Nullable; @@ -8,21 +7,31 @@ 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.Arrays; +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.SpanFactory; 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.core.CorePluginBridge; +import ru.noties.markwon.core.MarkwonTheme; +import ru.noties.markwon.html.MarkwonHtmlRenderer; +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; @@ -32,6 +41,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) @@ -45,6 +56,7 @@ public class SyntaxHighlightTest { @Test public void test() { + // code span must be first in the list, then should go highlight spans class Highlight { } @@ -62,16 +74,27 @@ public class SyntaxHighlightTest { } }; - final SpannableFactory factory = mock(SpannableFactory.class); - when(factory.code(any(SpannableTheme.class), anyBoolean())).thenReturn(codeSpan); + final MarkwonSpansFactory spansFactory = mock(MarkwonSpansFactory.class); + when(spansFactory.get(any(Class.class))).thenReturn(new SpanFactory() { + @Override + public Object getSpans(@NonNull MarkwonConfiguration configuration, @NonNull RenderProps props) { + return codeSpan; + } + }); - final SpannableConfiguration configuration = SpannableConfiguration.builder(mock(Context.class)) + final MarkwonConfiguration configuration = MarkwonConfiguration.builder() .syntaxHighlight(highlight) - .factory(factory) - .theme(mock(SpannableTheme.class)) - .build(); + .build(mock(MarkwonTheme.class), mock(AsyncDrawableLoader.class), mock(MarkwonHtmlRenderer.class), spansFactory); - final SpannableBuilder builder = new SpannableBuilder(); + final Map, MarkwonVisitor.NodeVisitor> visitorMap = Collections.emptyMap(); + + final MarkwonVisitor visitor = new AbstractMarkwonVisitorImpl( + configuration, + mock(RenderProps.class), + new SpannableBuilder(), + visitorMap); + + final SpannableBuilder builder = visitor.builder(); append(builder, "# Header 1\n", new Object()); append(builder, "## Header 2\n", new Object()); @@ -79,11 +102,10 @@ 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); + CorePluginBridge.visitCodeBlock(visitor, null, fencedCodeBlock.getLiteral(), fencedCodeBlock); final int end = builder.length(); @@ -95,9 +117,10 @@ 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 for (int i = 1; i < length; i++) { assertTrue(spans[i] instanceof Highlight); } diff --git a/markwon/src/test/java/ru/noties/markwon/UrlProcessorAndroidAssetsTest.java b/markwon-core/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-core/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-core/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-core/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-core/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-core/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-core/src/test/resources/tests/code-blocks.md b/markwon-core/src/test/resources/tests/code-blocks.md new file mode 100644 index 00000000..3b24ed05 --- /dev/null +++ b/markwon-core/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-core/src/test/resources/tests/deeply-nested.md b/markwon-core/src/test/resources/tests/deeply-nested.md new file mode 100644 index 00000000..3f8f18ab --- /dev/null +++ b/markwon-core/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-core/src/test/resources/tests/first.md b/markwon-core/src/test/resources/tests/first.md new file mode 100644 index 00000000..6f075385 --- /dev/null +++ b/markwon-core/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-core/src/test/resources/tests/nested-blockquotes.md b/markwon-core/src/test/resources/tests/nested-blockquotes.md new file mode 100644 index 00000000..08b25416 --- /dev/null +++ b/markwon-core/src/test/resources/tests/nested-blockquotes.md @@ -0,0 +1,3 @@ +> First +> > Second +> > > Third \ No newline at end of file diff --git a/markwon-core/src/test/resources/tests/no-paragraphs.md b/markwon-core/src/test/resources/tests/no-paragraphs.md new file mode 100644 index 00000000..75fa2e29 --- /dev/null +++ b/markwon-core/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-core/src/test/resources/tests/ol-2-spaces.md b/markwon-core/src/test/resources/tests/ol-2-spaces.md new file mode 100644 index 00000000..08087952 --- /dev/null +++ b/markwon-core/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-core/src/test/resources/tests/ol-starts-with-5.md b/markwon-core/src/test/resources/tests/ol-starts-with-5.md new file mode 100644 index 00000000..384dba30 --- /dev/null +++ b/markwon-core/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-core/src/test/resources/tests/ol.md b/markwon-core/src/test/resources/tests/ol.md new file mode 100644 index 00000000..d4e5b9db --- /dev/null +++ b/markwon-core/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-core/src/test/resources/tests/paragraph.md b/markwon-core/src/test/resources/tests/paragraph.md new file mode 100644 index 00000000..98fce9a0 --- /dev/null +++ b/markwon-core/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-core/src/test/resources/tests/second.md b/markwon-core/src/test/resources/tests/second.md new file mode 100644 index 00000000..44410d75 --- /dev/null +++ b/markwon-core/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-core/src/test/resources/tests/single-code-block.md b/markwon-core/src/test/resources/tests/single-code-block.md new file mode 100644 index 00000000..fc057a09 --- /dev/null +++ b/markwon-core/src/test/resources/tests/single-code-block.md @@ -0,0 +1,3 @@ +``` +code block +``` \ No newline at end of file diff --git a/markwon-core/src/test/resources/tests/soft-break-adds-new-line.md b/markwon-core/src/test/resources/tests/soft-break-adds-new-line.md new file mode 100644 index 00000000..5d81a53e --- /dev/null +++ b/markwon-core/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-core/src/test/resources/tests/soft-break.md b/markwon-core/src/test/resources/tests/soft-break.md new file mode 100644 index 00000000..8e768a71 --- /dev/null +++ b/markwon-core/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-core/src/test/resources/tests/ul-levels.md b/markwon-core/src/test/resources/tests/ul-levels.md new file mode 100644 index 00000000..767d3e7c --- /dev/null +++ b/markwon-core/src/test/resources/tests/ul-levels.md @@ -0,0 +1,3 @@ +* First +* * Second +* * * Third \ No newline at end of file diff --git a/markwon-core/src/test/resources/tests/ul.md b/markwon-core/src/test/resources/tests/ul.md new file mode 100644 index 00000000..0dd60b3e --- /dev/null +++ b/markwon-core/src/test/resources/tests/ul.md @@ -0,0 +1,3 @@ +* First + * Second + * Third \ No newline at end of file diff --git a/markwon-ext-latex/README.md b/markwon-ext-latex/README.md new file mode 100644 index 00000000..620b05fa --- /dev/null +++ b/markwon-ext-latex/README.md @@ -0,0 +1,3 @@ +# LaTeX + +[Documentation](https://noties.github.io/Markwon/docs/ext-latex) diff --git a/sample-latex-math/build.gradle b/markwon-ext-latex/build.gradle similarity index 53% rename from sample-latex-math/build.gradle rename to markwon-ext-latex/build.gradle index 81732d95..7cd28127 100644 --- a/sample-latex-math/build.gradle +++ b/markwon-ext-latex/build.gradle @@ -1,4 +1,4 @@ -apply plugin: 'com.android.application' +apply plugin: 'com.android.library' android { @@ -6,9 +6,6 @@ android { buildToolsVersion config['build-tools'] defaultConfig { - - applicationId "ru.noties.markwon.sample.jlatexmath" - minSdkVersion config['min-sdk'] targetSdkVersion config['target-sdk'] versionCode 1 @@ -17,7 +14,9 @@ android { } dependencies { - implementation project(':markwon') - implementation project(':markwon-image-loader') - implementation 'ru.noties:jlatexmath-android:0.1.0' + + api project(':markwon-core') + api deps['jlatexmath-android'] } + +registerArtifact(this) \ No newline at end of file diff --git a/markwon-ext-latex/gradle.properties b/markwon-ext-latex/gradle.properties new file mode 100644 index 00000000..b7726044 --- /dev/null +++ b/markwon-ext-latex/gradle.properties @@ -0,0 +1,4 @@ +POM_NAME=LaTeX +POM_ARTIFACT_ID=ext-latex +POM_DESCRIPTION=Extension to add LaTeX formulas to Markwon markdown +POM_PACKAGING=aar 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..c4761d0c --- /dev/null +++ b/markwon-ext-latex/src/main/java/ru/noties/markwon/ext/latex/JLatexMathPlugin.java @@ -0,0 +1,223 @@ +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 android.support.annotation.Px; + +import org.commonmark.node.Image; +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.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.ImagesPlugin; +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 { + + private final float textSize; + + private final Drawable background; + + @JLatexMathDrawable.Align + private final int align; + + private final boolean fitCanvas; + + private final int padding; + + Config(@NonNull Builder builder) { + this.textSize = builder.textSize; + this.background = builder.background; + this.align = builder.align; + this.fitCanvas = builder.fitCanvas; + this.padding = builder.padding; + } + } + + @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 RenderProps renderProps = visitor.renderProps(); + + 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); + } + }); + } + + @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(); + } + }); + } + + @NonNull + @Override + 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-strikethrough/README.md b/markwon-ext-strikethrough/README.md new file mode 100644 index 00000000..acd8b513 --- /dev/null +++ b/markwon-ext-strikethrough/README.md @@ -0,0 +1,29 @@ +# Strikethrough + +[![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`: + +```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(); + } + }); + } + }) +``` \ No newline at end of file diff --git a/markwon/build.gradle b/markwon-ext-strikethrough/build.gradle similarity index 57% rename from markwon/build.gradle rename to markwon-ext-strikethrough/build.gradle index fc3dc8ff..da9acfb0 100644 --- a/markwon/build.gradle +++ b/markwon-ext-strikethrough/build.gradle @@ -15,25 +15,18 @@ android { dependencies { - api project(':markwon-html-parser-api') - api project(':markwon-html-parser-impl') + api project(':markwon-core') deps.with { - api it['support-annotations'] - api it['commonmark'] api it['commonmark-strikethrough'] - api it['commonmark-table'] } - deps['test'].with { + deps.test.with { + testImplementation project(':markwon-test-span') testImplementation it['junit'] + testImplementation it['mockito'] testImplementation it['robolectric'] 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-ext-strikethrough/gradle.properties b/markwon-ext-strikethrough/gradle.properties new file mode 100644 index 00000000..120b6928 --- /dev/null +++ b/markwon-ext-strikethrough/gradle.properties @@ -0,0 +1,4 @@ +POM_NAME=Strikethrough +POM_ARTIFACT_ID=ext-strikethrough +POM_DESCRIPTION=Extension to add strikethrough markup to Markwon markdown +POM_PACKAGING=aar \ 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..f73ccc71 --- /dev/null +++ b/markwon-ext-strikethrough/src/main/java/ru/noties/markwon/ext/strikethrough/StrikethroughPlugin.java @@ -0,0 +1,60 @@ +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.MarkwonConfiguration; +import ru.noties.markwon.MarkwonSpansFactory; +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 + public static StrikethroughPlugin create() { + return new StrikethroughPlugin(); + } + + @Override + public void configureParser(@NonNull Parser.Builder builder) { + 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 props) { + return new StrikethroughSpan(); + } + }); + } + + @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.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-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-view/build.gradle b/markwon-ext-tables/build.gradle similarity index 78% rename from markwon-view/build.gradle rename to markwon-ext-tables/build.gradle index 5e4e72ab..ee753b1a 100644 --- a/markwon-view/build.gradle +++ b/markwon-ext-tables/build.gradle @@ -15,11 +15,11 @@ android { dependencies { - api project(':markwon') + api project(':markwon-core') deps.with { - compileOnly it['support-app-compat'] + api it['commonmark-table'] } } -registerArtifact(this) +registerArtifact(this) \ No newline at end of file diff --git a/markwon-ext-tables/gradle.properties b/markwon-ext-tables/gradle.properties new file mode 100644 index 00000000..7f35aa68 --- /dev/null +++ b/markwon-ext-tables/gradle.properties @@ -0,0 +1,4 @@ +POM_NAME=Tables +POM_ARTIFACT_ID=ext-tables +POM_DESCRIPTION=Extension to add tables markup (GFM) to Markwon markdown +POM_PACKAGING=aar \ 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-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..12fee872 --- /dev/null +++ b/markwon-ext-tables/src/main/java/ru/noties/markwon/ext/tables/Table.java @@ -0,0 +1,209 @@ +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 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; + + if (pendingRow == null) { + pendingRow = new ArrayList<>(2); + } + + pendingRow.add(new Table.Column(alignment(cell.getAlignment()), markwon.render(cell))); + 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 new file mode 100644 index 00000000..f05a84fc --- /dev/null +++ b/markwon-ext-tables/src/main/java/ru/noties/markwon/ext/tables/TablePlugin.java @@ -0,0 +1,230 @@ +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; +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; + +/** + * @since 3.0.0 + */ +public class TablePlugin extends AbstractMarkwonPlugin { + + public interface ThemeConfigure { + void configureTheme(@NonNull TableTheme.Builder builder); + } + + /** + * Factory method to create a {@link TablePlugin} with default {@link TableTheme} instance + * (obtained via {@link TableTheme#create(Context)} method) + * + * @see #create(TableTheme) + * @see #create(ThemeConfigure) + */ + @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); + } + + @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 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())); + } + + @Override + public void configureVisitor(@NonNull MarkwonVisitor.Builder builder) { + visitor.configure(builder); + } + + @Override + public void beforeRender(@NonNull Node node) { + // clear before rendering (as visitor has some internal mutable state) + visitor.clear(); + } + + @Override + public void beforeSetText(@NonNull TextView textView, @NonNull Spanned markdown) { + TableRowsScheduler.unschedule(textView); + } + + @Override + public void afterSetText(@NonNull TextView textView) { + TableRowsScheduler.schedule(textView); + } + + private static class TableVisitor { + + private final TableTheme tableTheme; + + private List pendingTableRow; + private boolean tableRowIsHeader; + private int tableRows; + + 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 + 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-ext-tables/src/main/java/ru/noties/markwon/ext/tables/TableRowSpan.java similarity index 96% rename from markwon/src/main/java/ru/noties/markwon/spans/TableRowSpan.java rename to markwon-ext-tables/src/main/java/ru/noties/markwon/ext/tables/TableRowSpan.java index 4d3a35fb..3f9bbace 100644 --- a/markwon/src/main/java/ru/noties/markwon/spans/TableRowSpan.java +++ b/markwon-ext-tables/src/main/java/ru/noties/markwon/ext/tables/TableRowSpan.java @@ -1,4 +1,4 @@ -package ru.noties.markwon.spans; +package ru.noties.markwon.ext.tables; import android.annotation.SuppressLint; import android.graphics.Canvas; @@ -61,22 +61,22 @@ public class TableRowSpan extends ReplacementSpan { } } - private final SpannableTheme 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 SpannableTheme 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-ext-tables/src/main/java/ru/noties/markwon/ext/tables/TableRowsScheduler.java similarity index 95% rename from markwon/src/main/java/ru/noties/markwon/TableRowsScheduler.java rename to markwon-ext-tables/src/main/java/ru/noties/markwon/ext/tables/TableRowsScheduler.java index 6fc9a584..b3b4c773 100644 --- a/markwon/src/main/java/ru/noties/markwon/TableRowsScheduler.java +++ b/markwon-ext-tables/src/main/java/ru/noties/markwon/ext/tables/TableRowsScheduler.java @@ -1,14 +1,12 @@ -package ru.noties.markwon; +package ru.noties.markwon.ext.tables; 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 { static void schedule(@NonNull final TextView view) { @@ -57,6 +55,7 @@ abstract class TableRowsScheduler { } } + @Nullable private static Object[] extract(@NonNull TextView view) { final Object[] out; final CharSequence text = view.getText(); 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 new file mode 100644 index 00000000..e9b1bd47 --- /dev/null +++ b/markwon-ext-tables/src/main/java/ru/noties/markwon/ext/tables/TableTheme.java @@ -0,0 +1,184 @@ +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; + +public class TableTheme { + + @NonNull + public static TableTheme create(@NonNull Context context) { + return buildWithDefaults(context).build(); + } + + @NonNull + 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(); + } + + + 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; + } + + /** + * @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; + } + + 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(@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; + } + + @NonNull + public Builder tableEvenRowBackgroundColor(@ColorInt int tableEvenRowBackgroundColor) { + this.tableEvenRowBackgroundColor = tableEvenRowBackgroundColor; + return this; + } + + @NonNull + public Builder tableHeaderRowBackgroundColor(@ColorInt int tableHeaderRowBackgroundColor) { + this.tableHeaderRowBackgroundColor = tableHeaderRowBackgroundColor; + return this; + } + + @NonNull + public TableTheme build() { + return new TableTheme(this); + } + } +} 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-image-loader/build.gradle b/markwon-ext-tasklist/build.gradle similarity index 64% rename from markwon-image-loader/build.gradle rename to markwon-ext-tasklist/build.gradle index fd4293c8..43a236df 100644 --- a/markwon-image-loader/build.gradle +++ b/markwon-ext-tasklist/build.gradle @@ -11,27 +11,19 @@ android { 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'] - } + api project(':markwon-core') deps['test'].with { + + testImplementation project(':markwon-test-span') + testImplementation it['junit'] testImplementation it['robolectric'] + testImplementation it['commons-io'] } } -registerArtifact(this) +registerArtifact(this) \ No newline at end of file diff --git a/markwon-ext-tasklist/gradle.properties b/markwon-ext-tasklist/gradle.properties new file mode 100644 index 00000000..7353aa98 --- /dev/null +++ b/markwon-ext-tasklist/gradle.properties @@ -0,0 +1,4 @@ +POM_NAME=Task List +POM_ARTIFACT_ID=ext-tasklist +POM_DESCRIPTION=Extension to add task lists (GFM) to Markwon markdown +POM_PACKAGING=aar \ 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 95% 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..417d92a7 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; @@ -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/main/java/ru/noties/markwon/spans/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/spans/TaskListDrawable.java rename to markwon-ext-tasklist/src/main/java/ru/noties/markwon/ext/tasklist/TaskListDrawable.java index 5119deb4..4a02c6c4 100644 --- a/markwon/src/main/java/ru/noties/markwon/spans/TaskListDrawable.java +++ b/markwon-ext-tasklist/src/main/java/ru/noties/markwon/ext/tasklist/TaskListDrawable.java @@ -1,4 +1,4 @@ -package ru.noties.markwon.spans; +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-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 new file mode 100644 index 00000000..9f74c206 --- /dev/null +++ b/markwon-ext-tasklist/src/main/java/ru/noties/markwon/ext/tasklist/TaskListPlugin.java @@ -0,0 +1,125 @@ +package ru.noties.markwon.ext.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.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; +import ru.noties.markwon.core.SimpleBlockNodeVisitor; + +/** + * @since 3.0.0 + */ +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 + public static TaskListPlugin create(@NonNull Drawable drawable) { + return new TaskListPlugin(drawable); + } + + @NonNull + public static TaskListPlugin create(@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); + + return new TaskListPlugin(new TaskListDrawable(linkColor, linkColor, backgroundColor)); + } + + @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 configureSpansFactory(@NonNull MarkwonSpansFactory.Builder builder) { + builder.setFactory(TaskListItem.class, new TaskListSpanFactory(drawable)); + } + + @Override + public void configureVisitor(@NonNull MarkwonVisitor.Builder builder) { + builder + .on(TaskListBlock.class, new SimpleBlockNodeVisitor()) + .on(TaskListItem.class, new MarkwonVisitor.NodeVisitor() { + @Override + public void visit(@NonNull MarkwonVisitor visitor, @NonNull TaskListItem taskListItem) { + + final int length = visitor.length(); + + visitor.visitChildren(taskListItem); + + 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(); + } + } + }); + } + + 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); + try { + return typedArray.getColor(0, 0); + } finally { + 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-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/src/main/java/ru/noties/markwon/spans/TaskListSpan.java b/markwon-ext-tasklist/src/main/java/ru/noties/markwon/ext/tasklist/TaskListSpan.java similarity index 87% rename from markwon/src/main/java/ru/noties/markwon/spans/TaskListSpan.java rename to markwon-ext-tasklist/src/main/java/ru/noties/markwon/ext/tasklist/TaskListSpan.java index 25bc6a41..b851f382 100644 --- a/markwon/src/main/java/ru/noties/markwon/spans/TaskListSpan.java +++ b/markwon-ext-tasklist/src/main/java/ru/noties/markwon/ext/tasklist/TaskListSpan.java @@ -1,4 +1,4 @@ -package ru.noties.markwon.spans; +package ru.noties.markwon.ext.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.core.MarkwonTheme; +import ru.noties.markwon.utils.LeadingMarginUtils; + /** * @since 1.0.1 */ @@ -16,14 +19,17 @@ 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) { + + 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,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..bd68ede3 --- /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 props) { + return new TaskListSpan( + configuration.theme(), + drawable, + TaskListProps.BLOCK_INDENT.get(props, 0), + TaskListProps.DONE.get(props, false) + ); + } +} diff --git a/markwon-ext-tasklist/src/test/java/ru/noties/markwon/ext/tasklist/TaskListTest.java b/markwon-ext-tasklist/src/test/java/ru/noties/markwon/ext/tasklist/TaskListTest.java new file mode 100644 index 00000000..6097312a --- /dev/null +++ b/markwon-ext-tasklist/src/test/java/ru/noties/markwon/ext/tasklist/TaskListTest.java @@ -0,0 +1,101 @@ +package ru.noties.markwon.ext.tasklist; + +import android.support.annotation.NonNull; + +import org.apache.commons.io.IOUtils; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.robolectric.RobolectricTestRunner; +import org.robolectric.RuntimeEnvironment; +import org.robolectric.annotation.Config; + +import java.io.IOException; +import java.nio.charset.StandardCharsets; + +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; + +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 TaskListTest { + + private static final String SPAN = "task-list"; + private static final String IS_DONE = "is-done"; + + @Test + public void test() { + + final TestSpan.Document document = document( + span(SPAN, args(IS_DONE, false), text("First")), + newLine(), + span(SPAN, args(IS_DONE, true), text("Second")), + newLine(), + span(SPAN, args(IS_DONE, true), text("Third")), + newLine(), + span(SPAN, args(IS_DONE, false), text("First star")), + newLine(), + span(SPAN, args(IS_DONE, true), text("Second star")), + newLine(), + span(SPAN, args(IS_DONE, true), text("Third star")), + newLine(), + span(SPAN, args(IS_DONE, false), text("First plus")), + newLine(), + span(SPAN, args(IS_DONE, true), text("Second plus")), + newLine(), + span(SPAN, args(IS_DONE, true), text("Third plus")), + newLine(), + span(SPAN, args(IS_DONE, true), text("Number with dot")), + newLine(), + span(SPAN, args(IS_DONE, false), text("Number")) + ); + + TestSpanMatcher.matches( + markwon().toMarkdown(read("task-lists.md")), + document + ); + } + + @NonNull + private static Markwon markwon() { + return Markwon.builder(RuntimeEnvironment.application) + .usePlugin(TaskListPlugin.create(RuntimeEnvironment.application)) + .usePlugin(new AbstractMarkwonPlugin() { + @Override + public void configureSpansFactory(@NonNull MarkwonSpansFactory.Builder builder) { + builder.setFactory(TaskListItem.class, new SpanFactory() { + @Override + public Object getSpans(@NonNull MarkwonConfiguration configuration, @NonNull RenderProps props) { + return span(SPAN, args(IS_DONE, TaskListProps.DONE.require(props))); + } + }); + } + }) + .build(); + } + + @SuppressWarnings("SameParameterValue") + @NonNull + private static String read(@NonNull String name) { + try { + return IOUtils.resourceToString("tests/" + name, StandardCharsets.UTF_8, TaskListDrawable.class.getClassLoader()); + } catch (IOException e) { + throw new RuntimeException(e); + } + } + + @NonNull + private static TestSpan.Text newLine() { + return text("\n"); + } +} diff --git a/markwon-ext-tasklist/src/test/resources/tests/task-lists.md b/markwon-ext-tasklist/src/test/resources/tests/task-lists.md new file mode 100644 index 00000000..b06f8215 --- /dev/null +++ b/markwon-ext-tasklist/src/test/resources/tests/task-lists.md @@ -0,0 +1,11 @@ +- [ ] 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 \ No newline at end of file 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/gradle.properties b/markwon-html-parser-impl/gradle.properties deleted file mode 100644 index 5db86376..00000000 --- a/markwon-html-parser-impl/gradle.properties +++ /dev/null @@ -1,3 +0,0 @@ -POM_NAME=Markwon -POM_ARTIFACT_ID=markwon-html-parser-impl -POM_PACKAGING=aar \ No newline at end of file diff --git a/markwon-html-parser-impl/src/main/AndroidManifest.xml b/markwon-html-parser-impl/src/main/AndroidManifest.xml deleted file mode 100644 index 14bee867..00000000 --- a/markwon-html-parser-impl/src/main/AndroidManifest.xml +++ /dev/null @@ -1 +0,0 @@ - diff --git a/markwon-html/build.gradle b/markwon-html/build.gradle new file mode 100644 index 00000000..c23a7b4f --- /dev/null +++ b/markwon-html/build.gradle @@ -0,0 +1,38 @@ +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') + + 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 { + testImplementation it['junit'] + testImplementation it['robolectric'] + testImplementation it['ix-java'] + } +} + +registerArtifact(this) diff --git a/markwon-html/gradle.properties b/markwon-html/gradle.properties new file mode 100644 index 00000000..1006b3db --- /dev/null +++ b/markwon-html/gradle.properties @@ -0,0 +1,4 @@ +POM_NAME=HTML +POM_ARTIFACT_ID=html +POM_DESCRIPTION=Provides HTML parsing functionality +POM_PACKAGING=aar \ No newline at end of file diff --git a/markwon-html/src/main/AndroidManifest.xml b/markwon-html/src/main/AndroidManifest.xml new file mode 100644 index 00000000..6d886e0e --- /dev/null +++ b/markwon-html/src/main/AndroidManifest.xml @@ -0,0 +1 @@ + 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/AppendableUtils.java similarity index 95% 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/AppendableUtils.java index 39b6bf73..5e890174 100644 --- 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/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/src/main/java/ru/noties/markwon/renderer/html2/CssInlineStyleParser.java b/markwon-html/src/main/java/ru/noties/markwon/html/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/CssInlineStyleParser.java index 9670d018..c9d7c1fc 100644 --- a/markwon/src/main/java/ru/noties/markwon/renderer/html2/CssInlineStyleParser.java +++ b/markwon-html/src/main/java/ru/noties/markwon/html/CssInlineStyleParser.java @@ -1,4 +1,4 @@ -package ru.noties.markwon.renderer.html2; +package ru.noties.markwon.html; 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/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/CssProperty.java index aa490361..70bc6d88 100644 --- a/markwon/src/main/java/ru/noties/markwon/renderer/html2/CssProperty.java +++ b/markwon-html/src/main/java/ru/noties/markwon/html/CssProperty.java @@ -1,4 +1,4 @@ -package ru.noties.markwon.renderer.html2; +package ru.noties.markwon.html; 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/HtmlEmptyTagReplacement.java similarity index 95% 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/HtmlEmptyTagReplacement.java index c0d304dc..c191b59d 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/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.api.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/HtmlPlugin.java b/markwon-html/src/main/java/ru/noties/markwon/html/HtmlPlugin.java new file mode 100644 index 00000000..d087058e --- /dev/null +++ b/markwon-html/src/main/java/ru/noties/markwon/html/HtmlPlugin.java @@ -0,0 +1,111 @@ +package ru.noties.markwon.html; + +import android.support.annotation.NonNull; +import android.support.annotation.Nullable; + +import org.commonmark.node.HtmlBlock; +import org.commonmark.node.HtmlInline; +import org.commonmark.node.Node; + +import ru.noties.markwon.AbstractMarkwonPlugin; +import ru.noties.markwon.MarkwonConfiguration; +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; +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; + +import static java.util.Arrays.asList; + +/** + * @since 3.0.0 + */ +public class HtmlPlugin extends AbstractMarkwonPlugin { + + @NonNull + public static HtmlPlugin create() { + return new HtmlPlugin(); + } + + public static final float SCRIPT_DEF_TEXT_SIZE_RATIO = .75F; + + @Override + public void configureConfiguration(@NonNull MarkwonConfiguration.Builder builder) { + builder.htmlParser(MarkwonHtmlParserImpl.create()); + } + + @Override + public void configureHtmlRenderer(@NonNull MarkwonHtmlRenderer.Builder builder) { + + builder + .setHandler( + "img", + ImageHandler.create()) + .setHandler( + "a", + new LinkHandler()) + .setHandler( + "blockquote", + new BlockquoteHandler()) + .setHandler( + "sub", + new SubScriptHandler()) + .setHandler( + "sup", + new SuperScriptHandler()) + .setHandler( + asList("b", "strong"), + new StrongEmphasisHandler()) + .setHandler( + asList("s", "del"), + new StrikeHandler()) + .setHandler( + asList("u", "ins"), + new UnderlineHandler()) + .setHandler( + asList("ul", "ol"), + new ListHandler()) + .setHandler( + asList("i", "em", "cite", "dfn"), + new EmphasisHandler()) + .setHandler( + asList("h1", "h2", "h3", "h4", "h5", "h6"), + new HeadingHandler()); + } + + @Override + public void afterRender(@NonNull Node node, @NonNull MarkwonVisitor visitor) { + final MarkwonConfiguration configuration = visitor.configuration(); + configuration.htmlRenderer().render(visitor, configuration.htmlParser()); + } + + @Override + public void configureVisitor(@NonNull MarkwonVisitor.Builder builder) { + builder + .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-parser-impl/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-parser-impl/src/main/java/ru/noties/markwon/html/impl/HtmlTagImpl.java rename to markwon-html/src/main/java/ru/noties/markwon/html/HtmlTagImpl.java index 01466106..7c07360a 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/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.api.HtmlTag; - abstract class HtmlTagImpl implements HtmlTag { final String name; 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/MarkwonHtmlParserImpl.java similarity index 95% 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/MarkwonHtmlParserImpl.java index 86f985a2..6d715510 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/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.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.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.HtmlTag.Block; +import ru.noties.markwon.html.HtmlTag.Inline; +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-html-parser-impl/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-parser-impl/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-parser-impl/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-parser-impl/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-parser-impl/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-parser-impl/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-parser-impl/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-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/jsoup/helper/Normalizer.java index 024e5350..a0df7dd4 100644 --- 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/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-parser-impl/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-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/jsoup/helper/Validate.java index e8effd8c..0d00249b 100644 --- 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/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-parser-impl/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-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/jsoup/nodes/Attribute.java index 3ece793d..934dc364 100644 --- 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/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-parser-impl/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-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/jsoup/nodes/Attributes.java index 71494812..ced993b0 100644 --- 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/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-parser-impl/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-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/jsoup/nodes/CommonMarkEntities.java index abb49575..ec362312 100644 --- 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/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-parser-impl/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-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/jsoup/nodes/DocumentType.java index 0b1240df..dc11e537 100644 --- 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/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-parser-impl/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-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/jsoup/parser/CharacterReader.java index f7b7d892..1839cc2b 100644 --- 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/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-parser-impl/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-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/jsoup/parser/ParseError.java index f43c3007..533f9aee 100644 --- 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/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-parser-impl/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-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/jsoup/parser/ParseErrorList.java index 032b7571..a3e42a08 100644 --- 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/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-parser-impl/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-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/jsoup/parser/Token.java index a70da0e7..887cc61c 100644 --- 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/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-parser-impl/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-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/jsoup/parser/Tokeniser.java index 77df2b7c..3b871776 100644 --- 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/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-parser-impl/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-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/jsoup/parser/TokeniserState.java index 4c3e4405..01a98958 100644 --- 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/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/src/main/java/ru/noties/markwon/spans/SubScriptSpan.java b/markwon-html/src/main/java/ru/noties/markwon/html/span/SubScriptSpan.java similarity index 55% rename from markwon/src/main/java/ru/noties/markwon/spans/SubScriptSpan.java rename to markwon-html/src/main/java/ru/noties/markwon/html/span/SubScriptSpan.java index 4386613d..5ab51160 100644 --- a/markwon/src/main/java/ru/noties/markwon/spans/SubScriptSpan.java +++ b/markwon-html/src/main/java/ru/noties/markwon/html/span/SubScriptSpan.java @@ -1,28 +1,25 @@ -package ru.noties.markwon.spans; +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.HtmlPlugin; + public class SubScriptSpan extends MetricAffectingSpan { - private final SpannableTheme theme; - - public SubScriptSpan(@NonNull SpannableTheme theme) { - this.theme = theme; - } - @Override public void updateDrawState(TextPaint tp) { apply(tp); } @Override - public void updateMeasureState(TextPaint tp) { + public void updateMeasureState(@NonNull TextPaint tp) { apply(tp); } private void apply(TextPaint paint) { - theme.applySubScriptStyle(paint); + paint.setTextSize(paint.getTextSize() * HtmlPlugin.SCRIPT_DEF_TEXT_SIZE_RATIO); + paint.baselineShift -= (int) (paint.ascent() / 2); } } diff --git a/markwon/src/main/java/ru/noties/markwon/spans/SuperScriptSpan.java b/markwon-html/src/main/java/ru/noties/markwon/html/span/SuperScriptSpan.java similarity index 55% rename from markwon/src/main/java/ru/noties/markwon/spans/SuperScriptSpan.java rename to markwon-html/src/main/java/ru/noties/markwon/html/span/SuperScriptSpan.java index 4b8151ec..7375309a 100644 --- a/markwon/src/main/java/ru/noties/markwon/spans/SuperScriptSpan.java +++ b/markwon-html/src/main/java/ru/noties/markwon/html/span/SuperScriptSpan.java @@ -1,28 +1,25 @@ -package ru.noties.markwon.spans; +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.HtmlPlugin; + public class SuperScriptSpan extends MetricAffectingSpan { - private final SpannableTheme theme; - - public SuperScriptSpan(@NonNull SpannableTheme theme) { - this.theme = theme; - } - @Override public void updateDrawState(TextPaint tp) { apply(tp); } @Override - public void updateMeasureState(TextPaint tp) { + public void updateMeasureState(@NonNull TextPaint tp) { apply(tp); } private void apply(TextPaint paint) { - theme.applySuperScriptStyle(paint); + paint.setTextSize(paint.getTextSize() * HtmlPlugin.SCRIPT_DEF_TEXT_SIZE_RATIO); + paint.baselineShift += (int) (paint.ascent() / 2); } } 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 new file mode 100644 index 00000000..930a8dda --- /dev/null +++ b/markwon-html/src/main/java/ru/noties/markwon/html/tag/BlockquoteHandler.java @@ -0,0 +1,38 @@ +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; +import ru.noties.markwon.html.TagHandler; + +public class BlockquoteHandler extends TagHandler { + + @Override + public void handle( + @NonNull MarkwonVisitor visitor, + @NonNull MarkwonHtmlRenderer renderer, + @NonNull HtmlTag tag) { + + if (tag.isBlock()) { + visitChildren(visitor, renderer, tag.getAsBlock()); + } + + 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 new file mode 100644 index 00000000..fe546ee0 --- /dev/null +++ b/markwon-html/src/main/java/ru/noties/markwon/html/tag/EmphasisHandler.java @@ -0,0 +1,26 @@ +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 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 new file mode 100644 index 00000000..a7de3a47 --- /dev/null +++ b/markwon-html/src/main/java/ru/noties/markwon/html/tag/HeadingHandler.java @@ -0,0 +1,44 @@ +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 { + + @Nullable + @Override + 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; + } + + int level; + try { + level = Integer.parseInt(tag.name().substring(1)); + } catch (NumberFormatException e) { + e.printStackTrace(); + level = 0; + } + + if (level < 1 || level > 6) { + return null; + } + + CoreProps.HEADING_LEVEL.set(renderProps, level); + + return factory.getSpans(configuration, renderProps); + } +} 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/tag/ImageHandler.java similarity index 55% 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/tag/ImageHandler.java index ed5f7f3f..4e7ffa54 100644 --- a/markwon/src/main/java/ru/noties/markwon/renderer/html2/tag/ImageHandler.java +++ b/markwon-html/src/main/java/ru/noties/markwon/html/tag/ImageHandler.java @@ -1,15 +1,20 @@ -package ru.noties.markwon.renderer.html2.tag; +package ru.noties.markwon.html.tag; 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.SpannableConfiguration; -import ru.noties.markwon.html.api.HtmlTag; -import ru.noties.markwon.renderer.ImageSize; -import ru.noties.markwon.renderer.html2.CssInlineStyleParser; +import ru.noties.markwon.MarkwonConfiguration; +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 SpannableConfiguration 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,13 +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 configuration.factory().image( - configuration.theme(), - destination, - configuration.asyncDrawableLoader(), - configuration.imageSizeResolver(), - imageSizeParser.parse(tag.attributes()), - false - ); + ImageProps.DESTINATION.set(renderProps, destination); + ImageProps.IMAGE_SIZE.set(renderProps, imageSize); + ImageProps.REPLACEMENT_TEXT_IS_LINK.set(renderProps, false); + + return spanFactory.getSpans(configuration, renderProps); } } 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/tag/ImageSizeParserImpl.java similarity index 93% 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/tag/ImageSizeParserImpl.java index 56ad13c0..236f53be 100644 --- a/markwon/src/main/java/ru/noties/markwon/renderer/html2/tag/ImageSizeParserImpl.java +++ b/markwon-html/src/main/java/ru/noties/markwon/html/tag/ImageSizeParserImpl.java @@ -1,4 +1,4 @@ -package ru.noties.markwon.renderer.html2.tag; +package ru.noties.markwon.html.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.renderer.ImageSize; -import ru.noties.markwon.renderer.html2.CssInlineStyleParser; -import ru.noties.markwon.renderer.html2.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/tag/LinkHandler.java b/markwon-html/src/main/java/ru/noties/markwon/html/tag/LinkHandler.java new file mode 100644 index 00000000..04e768e9 --- /dev/null +++ b/markwon-html/src/main/java/ru/noties/markwon/html/tag/LinkHandler.java @@ -0,0 +1,33 @@ +package ru.noties.markwon.html.tag; + +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 RenderProps renderProps, @NonNull HtmlTag tag) { + final String destination = tag.attributes().get("href"); + if (!TextUtils.isEmpty(destination)) { + 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 new file mode 100644 index 00000000..66f76cc0 --- /dev/null +++ b/markwon-html/src/main/java/ru/noties/markwon/html/tag/ListHandler.java @@ -0,0 +1,78 @@ +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; + +public class ListHandler extends TagHandler { + + @Override + public void handle( + @NonNull MarkwonVisitor visitor, + @NonNull MarkwonHtmlRenderer renderer, + @NonNull HtmlTag tag) { + + if (!tag.isBlock()) { + return; + } + + final HtmlTag.Block block = tag.getAsBlock(); + final boolean ol = "ol".equals(block.name()); + final boolean ul = "ul".equals(block.name()); + + if (!ol && !ul) { + 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); + + for (HtmlTag.Block child : block.children()) { + + visitChildren(visitor, renderer, child); + + if (spanFactory != null && "li".equals(child.name())) { + + // insert list item here + if (ol) { + CoreProps.LIST_ITEM_TYPE.set(renderProps, CoreProps.ListItemType.ORDERED); + CoreProps.ORDERED_LIST_ITEM_NUMBER.set(renderProps, number++); + } else { + CoreProps.LIST_ITEM_TYPE.set(renderProps, CoreProps.ListItemType.BULLET); + CoreProps.BULLET_LIST_ITEM_LEVEL.set(renderProps, bulletLevel); + } + + SpannableBuilder.setSpans( + visitor.builder(), + spanFactory.getSpans(configuration, renderProps), + child.start(), + child.end()); + } + } + } + + private static int currentBulletListLevel(@NonNull HtmlTag.Block block) { + int level = 0; + while ((block = block.parent()) != null) { + if ("ul".equals(block.name()) + || "ol".equals(block.name())) { + level += 1; + } + } + return level; + } +} 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 new file mode 100644 index 00000000..d5717c20 --- /dev/null +++ b/markwon-html/src/main/java/ru/noties/markwon/html/tag/SimpleTagHandler.java @@ -0,0 +1,29 @@ +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.MarkwonVisitor; +import ru.noties.markwon.RenderProps; +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 { + + @Nullable + public abstract Object getSpans( + @NonNull MarkwonConfiguration configuration, + @NonNull RenderProps renderProps, + @NonNull HtmlTag tag); + + @Override + 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(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 new file mode 100644 index 00000000..2771501e --- /dev/null +++ b/markwon-html/src/main/java/ru/noties/markwon/html/tag/StrikeHandler.java @@ -0,0 +1,60 @@ +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; +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 MarkwonVisitor visitor, + @NonNull MarkwonHtmlRenderer renderer, + @NonNull HtmlTag tag) { + + if (tag.isBlock()) { + visitChildren(visitor, renderer, tag.getAsBlock()); + } + + SpannableBuilder.setSpans( + 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 new file mode 100644 index 00000000..8e604b7f --- /dev/null +++ b/markwon-html/src/main/java/ru/noties/markwon/html/tag/StrongEmphasisHandler.java @@ -0,0 +1,26 @@ +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 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 new file mode 100644 index 00000000..5ddc5697 --- /dev/null +++ b/markwon-html/src/main/java/ru/noties/markwon/html/tag/SubScriptHandler.java @@ -0,0 +1,17 @@ +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.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 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 new file mode 100644 index 00000000..77147c99 --- /dev/null +++ b/markwon-html/src/main/java/ru/noties/markwon/html/tag/SuperScriptHandler.java @@ -0,0 +1,17 @@ +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.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 RenderProps renderProps, @NonNull HtmlTag tag) { + 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/tag/UnderlineHandler.java similarity index 50% 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/tag/UnderlineHandler.java index ff870ef6..eaa397a6 100644 --- a/markwon/src/main/java/ru/noties/markwon/renderer/html2/tag/UnderlineHandler.java +++ b/markwon-html/src/main/java/ru/noties/markwon/html/tag/UnderlineHandler.java @@ -1,29 +1,32 @@ -package ru.noties.markwon.renderer.html2.tag; +package ru.noties.markwon.html.tag; import android.support.annotation.NonNull; +import android.text.style.UnderlineSpan; +import ru.noties.markwon.MarkwonVisitor; import ru.noties.markwon.SpannableBuilder; -import ru.noties.markwon.SpannableConfiguration; -import ru.noties.markwon.html.api.HtmlTag; +import ru.noties.markwon.html.HtmlTag; +import ru.noties.markwon.html.MarkwonHtmlRenderer; +import ru.noties.markwon.html.TagHandler; public class UnderlineHandler extends TagHandler { @Override public void handle( - @NonNull SpannableConfiguration configuration, - @NonNull SpannableBuilder builder, + @NonNull MarkwonVisitor visitor, + @NonNull MarkwonHtmlRenderer renderer, @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, builder, tag.getAsBlock()); + visitChildren(visitor, renderer, tag.getAsBlock()); } SpannableBuilder.setSpans( - builder, - configuration.factory().underline(), + visitor.builder(), + new UnderlineSpan(), tag.start(), tag.end() ); diff --git a/markwon/src/test/java/ru/noties/markwon/renderer/html2/CssInlineStyleParserTest.java b/markwon-html/src/test/java/ru/noties/markwon/html/CssInlineStyleParserTest.java similarity index 88% rename from markwon/src/test/java/ru/noties/markwon/renderer/html2/CssInlineStyleParserTest.java rename to markwon-html/src/test/java/ru/noties/markwon/html/CssInlineStyleParserTest.java index 4ba3fffb..cc105137 100644 --- a/markwon/src/test/java/ru/noties/markwon/renderer/html2/CssInlineStyleParserTest.java +++ b/markwon-html/src/test/java/ru/noties/markwon/html/CssInlineStyleParserTest.java @@ -1,4 +1,4 @@ -package ru.noties.markwon.renderer.html2; +package ru.noties.markwon.html; import android.support.annotation.NonNull; @@ -14,11 +14,9 @@ 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) @@ -40,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()); @@ -58,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()); @@ -66,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()); @@ -82,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()); @@ -98,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()); @@ -114,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()); @@ -122,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()); @@ -147,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()); @@ -172,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()); @@ -180,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()); @@ -209,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()); @@ -236,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-parser-impl/src/test/java/ru/noties/markwon/html/impl/HtmlEmptyTagReplacementTest.java b/markwon-html/src/test/java/ru/noties/markwon/html/HtmlEmptyTagReplacementTest.java similarity index 88% 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/HtmlEmptyTagReplacementTest.java index eab3a9f1..c54fab18 100644 --- 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/HtmlEmptyTagReplacementTest.java @@ -1,12 +1,11 @@ -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.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-parser-impl/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-parser-impl/src/test/java/ru/noties/markwon/html/impl/MarkwonHtmlParserImplTest.java rename to markwon-html/src/test/java/ru/noties/markwon/html/MarkwonHtmlParserImplTest.java index ad0669b3..c614d36c 100644 --- 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/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,9 +15,6 @@ import java.util.List; import java.util.Map; import java.util.Set; -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; @@ -845,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-parser-impl/src/test/java/ru/noties/markwon/html/impl/TrimmingAppenderTest.java b/markwon-html/src/test/java/ru/noties/markwon/html/TrimmingAppenderTest.java similarity index 93% 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/TrimmingAppenderTest.java index 87923c33..ef82795e 100644 --- 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/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; @@ -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-parser-impl/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 62% 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/jsoup/nodes/CommonMarkEntitiesTest.java index ab013578..3472d8d5 100644 --- 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/jsoup/nodes/CommonMarkEntitiesTest.java @@ -1,22 +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 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/src/test/java/ru/noties/markwon/renderer/html2/tag/ImageSizeParserImplTest.java b/markwon-html/src/test/java/ru/noties/markwon/html/tag/ImageSizeParserImplTest.java similarity index 86% rename from markwon/src/test/java/ru/noties/markwon/renderer/html2/tag/ImageSizeParserImplTest.java rename to markwon-html/src/test/java/ru/noties/markwon/html/tag/ImageSizeParserImplTest.java index 23d7eb93..ffa9fdc6 100644 --- a/markwon/src/test/java/ru/noties/markwon/renderer/html2/tag/ImageSizeParserImplTest.java +++ b/markwon-html/src/test/java/ru/noties/markwon/html/tag/ImageSizeParserImplTest.java @@ -1,8 +1,9 @@ -package ru.noties.markwon.renderer.html2.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,12 +14,8 @@ 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; +import ru.noties.markwon.html.CssInlineStyleParser; +import ru.noties.markwon.image.ImageSize; @RunWith(RobolectricTestRunner.class) @Config(manifest = Config.NONE) @@ -35,7 +32,7 @@ public class ImageSizeParserImplTest { @Test public void nothing() { - assertNull(impl.parse(Collections.emptyMap())); + Assert.assertNull(impl.parse(Collections.emptyMap())); } @Test @@ -151,16 +148,16 @@ public class ImageSizeParserImplTest { "!@#$%^&*(%" }; - for (String dimension: dimensions) { - assertNull(dimension, impl.dimension(dimension)); + for (String dimension : dimensions) { + 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); } @@ -171,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-image-gif/build.gradle b/markwon-image-gif/build.gradle new file mode 100644 index 00000000..bcb3fa77 --- /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-core') + + deps.with { + api it['android-gif'] + } +} + +registerArtifact(this) \ No newline at end of file diff --git a/markwon-image-gif/gradle.properties b/markwon-image-gif/gradle.properties new file mode 100644 index 00000000..2630a2f3 --- /dev/null +++ b/markwon-image-gif/gradle.properties @@ -0,0 +1,4 @@ +POM_NAME=Image GIF +POM_ARTIFACT_ID=image-gif +POM_DESCRIPTION=Adds GIF media support to Markwon markdown +POM_PACKAGING=aar \ 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-loader/src/main/java/ru/noties/markwon/il/GifMediaDecoder.java b/markwon-image-gif/src/main/java/ru/noties/markwon/image/gif/GifMediaDecoder.java similarity index 81% rename from markwon-image-loader/src/main/java/ru/noties/markwon/il/GifMediaDecoder.java rename to markwon-image-gif/src/main/java/ru/noties/markwon/image/gif/GifMediaDecoder.java index 8342e7d5..7a6600b8 100644 --- a/markwon-image-loader/src/main/java/ru/noties/markwon/il/GifMediaDecoder.java +++ b/markwon-image-gif/src/main/java/ru/noties/markwon/image/gif/GifMediaDecoder.java @@ -1,4 +1,4 @@ -package ru.noties.markwon.il; +package ru.noties.markwon.image.gif; import android.graphics.drawable.Drawable; import android.support.annotation.NonNull; @@ -9,6 +9,8 @@ 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 @@ -16,8 +18,7 @@ import pl.droidsonroids.gif.GifDrawable; @SuppressWarnings("WeakerAccess") public class GifMediaDecoder extends MediaDecoder { - protected static final String CONTENT_TYPE_GIF = "image/gif"; - protected static final String FILE_EXTENSION_GIF = ".gif"; + public static final String CONTENT_TYPE = "image/gif"; @NonNull public static GifMediaDecoder create(boolean autoPlayGif) { @@ -30,16 +31,6 @@ public class GifMediaDecoder extends MediaDecoder { 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) { 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..d0db857e --- /dev/null +++ b/markwon-image-gif/src/main/java/ru/noties/markwon/image/gif/GifPlugin.java @@ -0,0 +1,38 @@ +package ru.noties.markwon.image.gif; + +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 { + + @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)); + } + + @NonNull + @Override + public Priority priority() { + return Priority.after(ImagesPlugin.class); + } +} 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 432e8797..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.spans.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/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-html-parser-api/build.gradle b/markwon-image-okhttp/build.gradle similarity index 80% rename from markwon-html-parser-api/build.gradle rename to markwon-image-okhttp/build.gradle index 8e38acbb..3f570553 100644 --- a/markwon-html-parser-api/build.gradle +++ b/markwon-image-okhttp/build.gradle @@ -15,9 +15,11 @@ android { dependencies { + api project(':markwon-core') + deps.with { - api it['support-annotations'] + api it['okhttp'] } } -registerArtifact(this) +registerArtifact(this) \ No newline at end of file diff --git a/markwon-image-okhttp/gradle.properties b/markwon-image-okhttp/gradle.properties new file mode 100644 index 00000000..8722d5cf --- /dev/null +++ b/markwon-image-okhttp/gradle.properties @@ -0,0 +1,4 @@ +POM_NAME=Image OkHttp +POM_ARTIFACT_ID=image-okhttp +POM_DESCRIPTION=Adds OkHttp client to retrieve images data from network +POM_PACKAGING=aar \ 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/OkHttpImagesPlugin.java b/markwon-image-okhttp/src/main/java/ru/noties/markwon/image/okhttp/OkHttpImagesPlugin.java new file mode 100644 index 00000000..fe43c289 --- /dev/null +++ b/markwon-image-okhttp/src/main/java/ru/noties/markwon/image/okhttp/OkHttpImagesPlugin.java @@ -0,0 +1,53 @@ +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.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) + * + * @see #create() + * @see #create(OkHttpClient) + * @since 3.0.0 + */ +@SuppressWarnings("WeakerAccess") +public class OkHttpImagesPlugin extends AbstractMarkwonPlugin { + + @NonNull + public static OkHttpImagesPlugin create() { + return new OkHttpImagesPlugin(new OkHttpClient()); + } + + @NonNull + public static OkHttpImagesPlugin create(@NonNull OkHttpClient okHttpClient) { + return new OkHttpImagesPlugin(okHttpClient); + } + + private final OkHttpClient client; + + OkHttpImagesPlugin(@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) + ); + } + + @NonNull + @Override + public Priority priority() { + return Priority.after(ImagesPlugin.class); + } +} diff --git a/markwon-image-loader/src/main/java/ru/noties/markwon/il/NetworkSchemeHandler.java b/markwon-image-okhttp/src/main/java/ru/noties/markwon/image/okhttp/OkHttpSchemeHandler.java similarity index 57% rename from markwon-image-loader/src/main/java/ru/noties/markwon/il/NetworkSchemeHandler.java rename to markwon-image-okhttp/src/main/java/ru/noties/markwon/image/okhttp/OkHttpSchemeHandler.java index d87c4019..eaf73bd6 100644 --- a/markwon-image-loader/src/main/java/ru/noties/markwon/il/NetworkSchemeHandler.java +++ b/markwon-image-okhttp/src/main/java/ru/noties/markwon/image/okhttp/OkHttpSchemeHandler.java @@ -1,4 +1,4 @@ -package ru.noties.markwon.il; +package ru.noties.markwon.image.okhttp; import android.net.Uri; import android.support.annotation.NonNull; @@ -6,39 +6,27 @@ 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; +import ru.noties.markwon.image.ImageItem; +import ru.noties.markwon.image.SchemeHandler; -/** - * @since 2.0.0 - */ -public class NetworkSchemeHandler extends SchemeHandler { - - @NonNull - public static NetworkSchemeHandler create(@NonNull OkHttpClient client) { - return new NetworkSchemeHandler(client); - } +class OkHttpSchemeHandler extends SchemeHandler { private static final String HEADER_CONTENT_TYPE = "Content-Type"; private final OkHttpClient client; - @SuppressWarnings("WeakerAccess") - NetworkSchemeHandler(@NonNull 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() @@ -59,31 +47,11 @@ public class NetworkSchemeHandler extends SchemeHandler { final InputStream inputStream = body.byteStream(); if (inputStream != null) { final String contentType = response.header(HEADER_CONTENT_TYPE); - out = new ImageItem(contentType, inputStream, null); + out = new ImageItem(contentType, inputStream); } } } 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-svg/build.gradle b/markwon-image-svg/build.gradle new file mode 100644 index 00000000..cfe7bcd1 --- /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-core') + + deps.with { + api it['android-svg'] + } +} + +registerArtifact(this) \ No newline at end of file diff --git a/markwon-image-svg/gradle.properties b/markwon-image-svg/gradle.properties new file mode 100644 index 00000000..26dce9a4 --- /dev/null +++ b/markwon-image-svg/gradle.properties @@ -0,0 +1,4 @@ +POM_NAME=Image SVG +POM_ARTIFACT_ID=image-svg +POM_DESCRIPTION=Adds SVG media support to Markwon markdown +POM_PACKAGING=aar \ 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-loader/src/main/java/ru/noties/markwon/il/SvgMediaDecoder.java b/markwon-image-svg/src/main/java/ru/noties/markwon/image/svg/SvgMediaDecoder.java similarity index 79% rename from markwon-image-loader/src/main/java/ru/noties/markwon/il/SvgMediaDecoder.java rename to markwon-image-svg/src/main/java/ru/noties/markwon/image/svg/SvgMediaDecoder.java index ff32255a..5c8661d8 100644 --- a/markwon-image-loader/src/main/java/ru/noties/markwon/il/SvgMediaDecoder.java +++ b/markwon-image-svg/src/main/java/ru/noties/markwon/image/svg/SvgMediaDecoder.java @@ -1,4 +1,4 @@ -package ru.noties.markwon.il; +package ru.noties.markwon.image.svg; import android.content.res.Resources; import android.graphics.Bitmap; @@ -13,13 +13,15 @@ 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 { - private static final String CONTENT_TYPE_SVG = "image/svg+xml"; - private static final String FILE_EXTENSION_SVG = ".svg"; + public static final String CONTENT_TYPE = "image/svg+xml"; @NonNull public static SvgMediaDecoder create(@NonNull Resources resources) { @@ -33,16 +35,6 @@ public class SvgMediaDecoder extends MediaDecoder { 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) { 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..be357480 --- /dev/null +++ b/markwon-image-svg/src/main/java/ru/noties/markwon/image/svg/SvgPlugin.java @@ -0,0 +1,34 @@ +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; +import ru.noties.markwon.image.ImagesPlugin; +import ru.noties.markwon.priority.Priority; + +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)); + } + + @NonNull + @Override + public Priority priority() { + return Priority.after(ImagesPlugin.class); + } +} 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/build.gradle b/markwon-recycler/build.gradle new file mode 100644 index 00000000..a5a16087 --- /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-core') + + deps.with { + api it['support-recycler-view'] + } +} + +registerArtifact(this) \ No newline at end of file diff --git a/markwon-recycler/gradle.properties b/markwon-recycler/gradle.properties new file mode 100644 index 00000000..dafdd46c --- /dev/null +++ b/markwon-recycler/gradle.properties @@ -0,0 +1,4 @@ +POM_NAME=Recycler +POM_ARTIFACT_ID=recycler +POM_DESCRIPTION=Provides RecyclerView.Adapter to display Markwon markdown +POM_PACKAGING=aar \ 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..64241d55 --- /dev/null +++ b/markwon-recycler/src/main/java/ru/noties/markwon/recycler/MarkwonAdapter.java @@ -0,0 +1,203 @@ +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; +import ru.noties.markwon.MarkwonReducer; + +/** + * Adapter to display markdown in a RecyclerView. It is done by extracting root blocks from a + * 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)}). + * + * @see #builder(int, int) + * @see #builder(Entry) + * @see #create(int, 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 + 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( + @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(); + } + + /** + * 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(Entry) + * @see #builder(int, int) + * @see SimpleEntry + */ + @NonNull + public static MarkwonAdapter create( + @LayoutRes int defaultEntryLayoutResId, + @IdRes int defaultEntryTextViewResId + ) { + return builder(defaultEntryLayoutResId, defaultEntryTextViewResId).build(); + } + + /** + * 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 #builder(Entry) + */ + @NonNull + public static MarkwonAdapter create(@NonNull Entry defaultEntry) { + return builder(defaultEntry).build(); + } + + /** + * Builder to create an instance of {@link MarkwonAdapter} + * + * @see #include(Class, Entry) + * @see #reducer(MarkwonReducer) + * @see #build() + */ + 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 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 + * register your own unless you require it). + * + * @param reducer {@link MarkwonReducer} + * @return self + * @see MarkwonReducer + */ + @NonNull + Builder reducer(@NonNull MarkwonReducer reducer); + + /** + * @return {@link MarkwonAdapter} + */ + @NonNull + MarkwonAdapter build(); + } + + /** + * @see SimpleEntry + */ + public static abstract class Entry { + + @NonNull + public abstract H createHolder(@NonNull LayoutInflater inflater, @NonNull ViewGroup parent); + + public abstract void bindHolder(@NonNull Markwon markwon, @NonNull H holder, @NonNull N node); + + /** + * Will be called when new content is available (clear internal cache if any) + */ + public 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); + + public abstract void setParsedMarkdown(@NonNull Markwon markwon, @NonNull Node document); + + public abstract void setParsedMarkdown(@NonNull Markwon markwon, @NonNull List nodes); + + public abstract int getNodeViewType(@NonNull Class node); + + @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) { + 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 new file mode 100644 index 00000000..30b093f5 --- /dev/null +++ b/markwon-recycler/src/main/java/ru/noties/markwon/recycler/MarkwonAdapterImpl.java @@ -0,0 +1,180 @@ +package ru.noties.markwon.recycler; + +import android.support.annotation.NonNull; +import android.util.SparseArray; +import android.view.LayoutInflater; +import android.view.ViewGroup; + +import org.commonmark.node.Node; + +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 MarkwonReducer reducer; + + private LayoutInflater layoutInflater; + + private Markwon markwon; + private List nodes; + + @SuppressWarnings("WeakerAccess") + MarkwonAdapterImpl( + @NonNull SparseArray> entries, + @NonNull Entry defaultEntry, + @NonNull MarkwonReducer 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 = getEntry(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 = getEntry(viewType); + + entry.bindHolder(markwon, holder, node); + } + + @Override + public int getItemCount() { + return nodes != null + ? nodes.size() + : 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() { + 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 = getEntry(type); + return entry.id(node); + } + + @Override + 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; + } + + @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); + + private final Entry defaultEntry; + + private MarkwonReducer reducer; + + BuilderImpl(@NonNull Entry defaultEntry) { + this.defaultEntry = defaultEntry; + } + + @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 reducer(@NonNull MarkwonReducer reducer) { + this.reducer = reducer; + return this; + } + + @NonNull + @Override + public MarkwonAdapter build() { + + if (reducer == null) { + reducer = MarkwonReducer.directChildren(); + } + + return new MarkwonAdapterImpl(entries, defaultEntry, reducer); + } + } +} 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 new file mode 100644 index 00000000..93ef0d30 --- /dev/null +++ b/markwon-recycler/src/main/java/ru/noties/markwon/recycler/SimpleEntry.java @@ -0,0 +1,93 @@ +package ru.noties.markwon.recycler; + +import android.support.annotation.IdRes; +import android.support.annotation.LayoutRes; +import android.support.annotation.NonNull; +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; +import ru.noties.markwon.utils.NoCopySpannableFactory; + +/** + * @since 3.0.0 + */ +@SuppressWarnings("WeakerAccess") +public class SimpleEntry extends MarkwonAdapter.Entry { + + /** + * 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(@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(textViewIdRes, 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 void clear() { + cache.clear(); + } + + public static class Holder extends MarkwonAdapter.Holder { + + final TextView textView; + + protected Holder(@IdRes int textViewIdRes, @NonNull View itemView) { + super(itemView); + + final TextView textView; + if (textViewIdRes == 0) { + if (!(itemView instanceof TextView)) { + throw new IllegalStateException("TextView is not root of layout " + + "(specify TextView ID explicitly): " + itemView); + } + textView = (TextView) itemView; + } else { + textView = requireView(textViewIdRes); + } + this.textView = textView; + this.textView.setSpannableFactory(NoCopySpannableFactory.getInstance()); + } + } +} diff --git a/markwon-syntax-highlight/build.gradle b/markwon-syntax-highlight/build.gradle index daa29c7c..6d9353ff 100644 --- a/markwon-syntax-highlight/build.gradle +++ b/markwon-syntax-highlight/build.gradle @@ -15,7 +15,7 @@ android { dependencies { - api project(':markwon') + api project(':markwon-core') deps.with { api it['support-annotations'] diff --git a/markwon-syntax-highlight/gradle.properties b/markwon-syntax-highlight/gradle.properties index 93358b27..7a6431f3 100644 --- a/markwon-syntax-highlight/gradle.properties +++ b/markwon-syntax-highlight/gradle.properties @@ -1,3 +1,4 @@ -POM_NAME=Markwon -POM_ARTIFACT_ID=markwon-syntax-highlight +POM_NAME=Syntax Highlight +POM_ARTIFACT_ID=syntax-highlight +POM_DESCRIPTION=Add syntax highlight to Markwon markdown via Prism4j library POM_PACKAGING=aar \ No newline at end of file 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 7d5c90b4..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 @@ -1,23 +1,39 @@ package ru.noties.markwon.syntax; +import android.support.annotation.ColorInt; import android.support.annotation.NonNull; 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 { @NonNull public static Prism4jThemeDarkula create() { - return new Prism4jThemeDarkula(); + return new Prism4jThemeDarkula(0xFF2d2d2d); + } + + /** + * @param background color + * @since 3.0.0 + */ + @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/Prism4jThemeDefault.java b/markwon-syntax-highlight/src/main/java/ru/noties/markwon/syntax/Prism4jThemeDefault.java index 22354dce..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,19 +7,33 @@ 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 { @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-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..b07f5c6e --- /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.core.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-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..f979d633 --- /dev/null +++ b/markwon-test-span/src/main/java/ru/noties/markwon/test/TestSpan.java @@ -0,0 +1,125 @@ +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(); + } + + // important: children should not be included in equals... + 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..03bc2ace --- /dev/null +++ b/markwon-test-span/src/main/java/ru/noties/markwon/test/TestSpanMatcher.java @@ -0,0 +1,135 @@ +package ru.noties.markwon.test; + +import android.support.annotation.NonNull; +import android.text.Spanned; + + +import org.junit.Assert; +import org.junit.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 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, Object.class)) + .cast(TestSpan.Span.class) + .filter(new IxPredicate() { + @Override + public boolean test(TestSpan.Span span) { + return expected.name().equals(span.name()) + && start == spanned.getSpanStart(span) + && end == spanned.getSpanEnd(span) + && expected.arguments().equals(span.arguments()); + } + }) + .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, text: \"%s\"", + expectedSpan, start, end, actualSpan, spanned.subSequence(start, end))); + } + } + + 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..d9669cb2 --- /dev/null +++ b/markwon-test-span/src/main/java/ru/noties/markwon/test/TestSpanSpan.java @@ -0,0 +1,58 @@ +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; + return arguments.equals(that.arguments); + } + + @Override + public int hashCode() { + int result = name.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/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-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/gradle.properties b/markwon-view/gradle.properties deleted file mode 100644 index a4413bba..00000000 --- a/markwon-view/gradle.properties +++ /dev/null @@ -1,3 +0,0 @@ -POM_NAME=Markwon-View -POM_ARTIFACT_ID=markwon-view -POM_PACKAGING=aar \ No newline at end of file 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 e530bab5..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.SpannableConfiguration; -import ru.noties.markwon.spans.SpannableTheme; -import ru.noties.markwon.view.IMarkwonView; - -public class DebugConfigurationProvider implements IMarkwonView.ConfigurationProvider { - - private SpannableConfiguration cached; - - @NonNull - @Override - public SpannableConfiguration provide(@NonNull Context context) { - if (cached == null) { - cached = SpannableConfiguration.builder(context) - .theme(debugTheme(context)) - .build(); - } - return cached; - } - - private static SpannableTheme debugTheme(@NonNull Context context) { - return SpannableTheme.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 4db6d2c7..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.SpannableConfiguration; - -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(); -} 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 19ab09b0..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.SpannableConfiguration; - -@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 SpannableConfiguration 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 da5c1934..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.SpannableConfiguration; - -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 SpannableConfiguration 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 c8c813f6..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.SpannableConfiguration; - -public class MarkwonViewHelper implements IMarkwonView { - - public static MarkwonViewHelper create(@NonNull V view) { - return new MarkwonViewHelper(view); - } - - private final TextView textView; - - private ConfigurationProvider provider; - - private SpannableConfiguration 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 SpannableConfiguration 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()); - } - } - 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/markwon/gradle.properties b/markwon/gradle.properties deleted file mode 100644 index e1d2806a..00000000 --- a/markwon/gradle.properties +++ /dev/null @@ -1,3 +0,0 @@ -POM_NAME=Markwon -POM_ARTIFACT_ID=markwon -POM_PACKAGING=aar \ No newline at end of file diff --git a/markwon/src/main/java/ru/noties/markwon/AsyncDrawableLoaderNoOp.java b/markwon/src/main/java/ru/noties/markwon/AsyncDrawableLoaderNoOp.java deleted file mode 100644 index 6a568e3a..00000000 --- a/markwon/src/main/java/ru/noties/markwon/AsyncDrawableLoaderNoOp.java +++ /dev/null @@ -1,17 +0,0 @@ -package ru.noties.markwon; - -import android.support.annotation.NonNull; - -import ru.noties.markwon.spans.AsyncDrawable; - -class AsyncDrawableLoaderNoOp implements AsyncDrawable.Loader { - @Override - public void load(@NonNull String destination, @NonNull AsyncDrawable drawable) { - - } - - @Override - public void cancel(@NonNull String destination) { - - } -} diff --git a/markwon/src/main/java/ru/noties/markwon/Markwon.java b/markwon/src/main/java/ru/noties/markwon/Markwon.java deleted file mode 100644 index 6ecb9ca8..00000000 --- a/markwon/src/main/java/ru/noties/markwon/Markwon.java +++ /dev/null @@ -1,214 +0,0 @@ -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.renderer.SpannableRenderer; -import ru.noties.markwon.spans.OrderedListItemSpan; -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(); - } - - /** - * @see #setMarkdown(TextView, SpannableConfiguration, String) - * @since 1.0.0 - */ - public static void setMarkdown(@NonNull TextView view, @NonNull String markdown) { - setMarkdown(view, SpannableConfiguration.create(view.getContext()), markdown); - } - - /** - * Parses submitted raw markdown, converts it to CharSequence (with Spannables) - * and applies it to view - * - * @param view {@link TextView} to set markdown into - * @param configuration a {@link SpannableConfiguration} instance - * @param markdown raw markdown String (for example: {@code `**Hello**`}) - * @see #markdown(SpannableConfiguration, String) - * @see #setText(TextView, CharSequence) - * @see SpannableConfiguration - * @since 1.0.0 - */ - public static void setMarkdown( - @NonNull TextView view, - @NonNull SpannableConfiguration configuration, - @NonNull String markdown - ) { - - setText(view, markdown(configuration, markdown)); - } - - /** - * Helper method to apply parsed markdown. - *

      - * 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 - * @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 markdown 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 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 markdown 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 markdown with default {@link SpannableConfiguration} obtained from {@link Context} - * - * @param context {@link Context} - * @param markdown raw markdown - * @return parsed markdown - * @since 1.0.0 - */ - @NonNull - public static CharSequence markdown(@NonNull Context context, @NonNull String markdown) { - final SpannableConfiguration configuration = SpannableConfiguration.create(context); - return markdown(configuration, markdown); - } - - /** - * Returns parsed markdown with provided {@link SpannableConfiguration} - * - * @param configuration a {@link SpannableConfiguration} - * @param markdown raw markdown - * @return parsed markdown - * @see SpannableConfiguration - * @since 1.0.0 - */ - @NonNull - public static CharSequence markdown(@NonNull SpannableConfiguration configuration, @NonNull String markdown) { - final Parser parser = createParser(); - final Node node = parser.parse(markdown); - final SpannableRenderer renderer = new SpannableRenderer(); - return renderer.render(configuration, node); - } - - /** - * This method adds support for {@link ru.noties.markwon.spans.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 ru.noties.markwon.spans.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); - } - - /** - * 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); - } - - /** - * 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 ru.noties.markwon.spans.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 - * - * @param view a {@link TextView} - * @see #unscheduleTableRows(TextView) - * @since 1.0.0 - */ - public static void scheduleTableRows(@NonNull TextView view) { - TableRowsScheduler.schedule(view); - } - - /** - * De-references previously scheduled {@link ru.noties.markwon.spans.TableRowSpan}'s - * - * @param view a {@link TextView} - * @see #scheduleTableRows(TextView) - * @since 1.0.0 - */ - public static void unscheduleTableRows(@NonNull TextView view) { - TableRowsScheduler.unschedule(view); - } - - private Markwon() { - } -} diff --git a/markwon/src/main/java/ru/noties/markwon/SpannableConfiguration.java b/markwon/src/main/java/ru/noties/markwon/SpannableConfiguration.java deleted file mode 100644 index 5df9d316..00000000 --- a/markwon/src/main/java/ru/noties/markwon/SpannableConfiguration.java +++ /dev/null @@ -1,307 +0,0 @@ -package ru.noties.markwon; - -import android.content.Context; -import android.support.annotation.NonNull; - -import ru.noties.markwon.html.api.MarkwonHtmlParser; -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.SpannableTheme; - -@SuppressWarnings("WeakerAccess") -public class SpannableConfiguration { - - // creates default configuration - @NonNull - public static SpannableConfiguration create(@NonNull Context context) { - return new Builder(context).build(); - } - - @NonNull - public static Builder builder(@NonNull Context context) { - return new Builder(context); - } - - private final SpannableTheme theme; - private final AsyncDrawable.Loader asyncDrawableLoader; - private final SyntaxHighlight syntaxHighlight; - private final LinkSpan.Resolver linkResolver; - 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 - - private SpannableConfiguration(@NonNull Builder builder) { - this.theme = builder.theme; - this.asyncDrawableLoader = builder.asyncDrawableLoader; - this.syntaxHighlight = builder.syntaxHighlight; - this.linkResolver = builder.linkResolver; - 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; - } - - /** - * 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; - } - - @NonNull - public AsyncDrawable.Loader asyncDrawableLoader() { - return asyncDrawableLoader; - } - - @NonNull - public SyntaxHighlight syntaxHighlight() { - return syntaxHighlight; - } - - @NonNull - public LinkSpan.Resolver linkResolver() { - return linkResolver; - } - - @NonNull - public UrlProcessor urlProcessor() { - return urlProcessor; - } - - @NonNull - public ImageSizeResolver imageSizeResolver() { - return imageSizeResolver; - } - - @NonNull - public SpannableFactory factory() { - 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 - */ - @NonNull - public MarkwonHtmlParser htmlParser() { - return htmlParser; - } - - /** - * @since 2.0.0 - */ - @NonNull - public MarkwonHtmlRenderer htmlRenderer() { - return htmlRenderer; - } - - /** - * @since 2.0.0 - */ - public boolean htmlAllowNonClosedTags() { - return htmlAllowNonClosedTags; - } - - @SuppressWarnings("unused") - public static class Builder { - - private final Context context; - private SpannableTheme theme; - private AsyncDrawable.Loader asyncDrawableLoader; - private SyntaxHighlight syntaxHighlight; - private LinkSpan.Resolver linkResolver; - 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 - - Builder(@NonNull Context context) { - 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; - 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; - return this; - } - - @NonNull - public Builder linkResolver(@NonNull LinkSpan.Resolver linkResolver) { - this.linkResolver = linkResolver; - return this; - } - - @NonNull - public Builder urlProcessor(@NonNull UrlProcessor urlProcessor) { - this.urlProcessor = urlProcessor; - return this; - } - - /** - * @since 1.0.1 - */ - @NonNull - public Builder imageSizeResolver(@NonNull ImageSizeResolver imageSizeResolver) { - this.imageSizeResolver = imageSizeResolver; - return this; - } - - /** - * @since 1.1.0 - */ - @NonNull - public Builder factory(@NonNull SpannableFactory factory) { - this.factory = factory; - 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 - */ - @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; - } - - /** - * @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 SpannableConfiguration build() { - - if (theme == null) { - theme = SpannableTheme.create(context); - } - - if (asyncDrawableLoader == null) { - asyncDrawableLoader = new AsyncDrawableLoaderNoOp(); - } - - if (syntaxHighlight == null) { - syntaxHighlight = new SyntaxHighlightNoOp(); - } - - if (linkResolver == null) { - linkResolver = new LinkResolverDef(); - } - - if (urlProcessor == null) { - urlProcessor = new UrlProcessorNoOp(); - } - - if (imageSizeResolver == null) { - imageSizeResolver = new ImageSizeResolverDef(); - } - - // @since 1.1.0 - if (factory == null) { - factory = SpannableFactoryDef.create(); - } - - // @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(); - } - } - - // @since 2.0.0 - if (htmlRenderer == null) { - htmlRenderer = MarkwonHtmlRenderer.create(); - } - - return new SpannableConfiguration(this); - } - } - -} diff --git a/markwon/src/main/java/ru/noties/markwon/SpannableFactory.java b/markwon/src/main/java/ru/noties/markwon/SpannableFactory.java deleted file mode 100644 index 472261a4..00000000 --- a/markwon/src/main/java/ru/noties/markwon/SpannableFactory.java +++ /dev/null @@ -1,91 +0,0 @@ -package ru.noties.markwon; - -import android.support.annotation.NonNull; -import android.support.annotation.Nullable; - -import java.util.List; - -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.TableRowSpan; - -/** - * Each method can return null or a Span object or an array of spans - * - * @since 1.1.0 - */ -public interface SpannableFactory { - - @Nullable - Object strongEmphasis(); - - @Nullable - Object emphasis(); - - @Nullable - Object blockQuote(@NonNull SpannableTheme theme); - - @Nullable - Object code(@NonNull SpannableTheme theme, boolean multiline); - - @Nullable - Object orderedListItem(@NonNull SpannableTheme theme, int startNumber); - - @Nullable - Object bulletListItem(@NonNull SpannableTheme theme, int level); - - @Nullable - Object thematicBreak(@NonNull SpannableTheme theme); - - @Nullable - Object heading(@NonNull SpannableTheme theme, int level); - - @Nullable - Object strikethrough(); - - @Nullable - Object taskListItem(@NonNull SpannableTheme theme, int blockIndent, boolean isDone); - - @Nullable - Object tableRow( - @NonNull SpannableTheme theme, - @NonNull List cells, - boolean isHeader, - boolean isOdd); - - /** - * @since 1.1.1 - */ - @Nullable - Object paragraph(boolean inTightList); - - @Nullable - Object image( - @NonNull SpannableTheme theme, - @NonNull String destination, - @NonNull AsyncDrawable.Loader loader, - @NonNull ImageSizeResolver imageSizeResolver, - @Nullable ImageSize imageSize, - boolean replacementTextIsLink); - - @Nullable - Object link( - @NonNull SpannableTheme theme, - @NonNull String destination, - @NonNull LinkSpan.Resolver resolver); - - // Currently used by HTML parser - @Nullable - Object superScript(@NonNull SpannableTheme theme); - - // Currently used by HTML parser - @Nullable - Object subScript(@NonNull SpannableTheme 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 deleted file mode 100644 index ee553329..00000000 --- a/markwon/src/main/java/ru/noties/markwon/SpannableFactoryDef.java +++ /dev/null @@ -1,153 +0,0 @@ -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 java.util.List; - -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; -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.SpannableTheme; -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.spans.ThematicBreakSpan; - -/** - * @since 1.1.0 - */ -public class SpannableFactoryDef implements SpannableFactory { - - @NonNull - public static SpannableFactoryDef create() { - return new SpannableFactoryDef(); - } - - @Nullable - @Override - public Object strongEmphasis() { - return new StrongEmphasisSpan(); - } - - @Nullable - @Override - public Object emphasis() { - return new EmphasisSpan(); - } - - @Nullable - @Override - public Object blockQuote(@NonNull SpannableTheme theme) { - return new BlockQuoteSpan(theme); - } - - @Nullable - @Override - public Object code(@NonNull SpannableTheme theme, boolean multiline) { - return new CodeSpan(theme, multiline); - } - - @Nullable - @Override - public Object orderedListItem(@NonNull SpannableTheme 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) { - return new BulletListItemSpan(theme, level); - } - - @Nullable - @Override - public Object thematicBreak(@NonNull SpannableTheme theme) { - return new ThematicBreakSpan(theme); - } - - @Nullable - @Override - public Object heading(@NonNull SpannableTheme theme, int level) { - return new HeadingSpan(theme, level); - } - - @Nullable - @Override - public Object strikethrough() { - return new StrikethroughSpan(); - } - - @Nullable - @Override - public Object taskListItem(@NonNull SpannableTheme 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) { - return new TableRowSpan(theme, cells, isHeader, isOdd); - } - - /** - * @since 1.1.1 - */ - @Nullable - @Override - public Object paragraph(boolean inTightList) { - return null; - } - - @Nullable - @Override - public Object image(@NonNull SpannableTheme theme, @NonNull String destination, @NonNull AsyncDrawable.Loader 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 SpannableTheme theme, @NonNull String destination, @NonNull LinkSpan.Resolver resolver) { - return new LinkSpan(theme, destination, resolver); - } - - @Nullable - @Override - public Object superScript(@NonNull SpannableTheme theme) { - return new SuperScriptSpan(theme); - } - - @Override - public Object subScript(@NonNull SpannableTheme theme) { - return new SubScriptSpan(theme); - } - - @Nullable - @Override - public Object underline() { - return new UnderlineSpan(); - } -} 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 81e2d36e..00000000 --- a/markwon/src/main/java/ru/noties/markwon/renderer/SpannableMarkdownVisitor.java +++ /dev/null @@ -1,556 +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.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; -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 java.util.ArrayList; -import java.util.List; - -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.spans.SpannableTheme; -import ru.noties.markwon.spans.TableRowSpan; -import ru.noties.markwon.tasklist.TaskListBlock; -import ru.noties.markwon.tasklist.TaskListItem; - -@SuppressWarnings("WeakerAccess") -public class SpannableMarkdownVisitor extends AbstractVisitor { - - private final SpannableConfiguration configuration; - private final SpannableBuilder builder; - private final MarkwonHtmlParser htmlParser; - - private final SpannableTheme theme; - private final SpannableFactory factory; - - private int blockIndent; - private int listLevel; - - private List pendingTableRow; - private boolean tableRowIsHeader; - private int tableRows; - - public SpannableMarkdownVisitor( - @NonNull SpannableConfiguration 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(); - - final int length = builder.length(); - - blockIndent += 1; - - visitChildren(blockQuote); - - setSpan(length, factory.blockQuote(theme)); - - blockIndent -= 1; - - if (hasNext(blockQuote)) { - newLine(); - forceNewLine(); - } - } - - @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(); - forceNewLine(); - } - } - - @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(); - forceNewLine(); - } - } - - @Override - public void visit(ListItem listItem) { - - final int length = builder.length(); - - blockIndent += 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)); - } - - blockIndent -= 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(); - forceNewLine(); - } - } - - @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) - forceNewLine(); - } - } - - @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) { - - blockIndent += 1; - visitChildren(customBlock); - blockIndent -= 1; - - if (hasNext(customBlock)) { - newLine(); - forceNewLine(); - } - - } 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 if (customNode instanceof TaskListItem) { - - // new in 1.0.1 - - final TaskListItem listItem = (TaskListItem) customNode; - - final int length = builder.length(); - - blockIndent += listItem.indent(); - - visitChildren(customNode); - - setSpan(length, factory.taskListItem(theme, blockIndent, listItem.done())); - - if (hasNext(customNode)) { - newLine(); - } - - blockIndent -= listItem.indent(); - - } else if (!handleTableNodes(customNode)) { - 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(); - forceNewLine(); - } - - } 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(); - forceNewLine(); - } - } - - @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 void forceNewLine() { - 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 32d620dd..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.SpannableBuilder; -import ru.noties.markwon.SpannableConfiguration; - -public class SpannableRenderer { - - @NonNull - public CharSequence render(@NonNull SpannableConfiguration 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 deleted file mode 100644 index bd69445a..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.SpannableConfiguration; -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 SpannableConfiguration 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 6de698f5..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.SpannableConfiguration; -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 SpannableConfiguration 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/main/java/ru/noties/markwon/renderer/html2/tag/BlockquoteHandler.java b/markwon/src/main/java/ru/noties/markwon/renderer/html2/tag/BlockquoteHandler.java deleted file mode 100644 index 99ddf153..00000000 --- a/markwon/src/main/java/ru/noties/markwon/renderer/html2/tag/BlockquoteHandler.java +++ /dev/null @@ -1,28 +0,0 @@ -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.html.api.HtmlTag; - -public class BlockquoteHandler extends TagHandler { - - @Override - public void handle( - @NonNull SpannableConfiguration configuration, - @NonNull SpannableBuilder builder, - @NonNull HtmlTag tag) { - - if (tag.isBlock()) { - visitChildren(configuration, builder, tag.getAsBlock()); - } - - SpannableBuilder.setSpans( - builder, - configuration.factory().blockQuote(configuration.theme()), - tag.start(), - tag.end() - ); - } -} 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 deleted file mode 100644 index d34218de..00000000 --- a/markwon/src/main/java/ru/noties/markwon/renderer/html2/tag/EmphasisHandler.java +++ /dev/null @@ -1,15 +0,0 @@ -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.html.api.HtmlTag; - -public class EmphasisHandler extends SimpleTagHandler { - @Nullable - @Override - public Object getSpans(@NonNull SpannableConfiguration 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 deleted file mode 100644 index e2138b05..00000000 --- a/markwon/src/main/java/ru/noties/markwon/renderer/html2/tag/HeadingHandler.java +++ /dev/null @@ -1,22 +0,0 @@ -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.html.api.HtmlTag; - -public class HeadingHandler extends SimpleTagHandler { - - private final int level; - - public HeadingHandler(int level) { - this.level = level; - } - - @Nullable - @Override - public Object getSpans(@NonNull SpannableConfiguration configuration, @NonNull HtmlTag tag) { - return configuration.factory().heading(configuration.theme(), level); - } -} 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 deleted file mode 100644 index 134874b9..00000000 --- a/markwon/src/main/java/ru/noties/markwon/renderer/html2/tag/LinkHandler.java +++ /dev/null @@ -1,24 +0,0 @@ -package ru.noties.markwon.renderer.html2.tag; - -import android.support.annotation.NonNull; -import android.support.annotation.Nullable; -import android.text.TextUtils; - -import ru.noties.markwon.SpannableConfiguration; -import ru.noties.markwon.html.api.HtmlTag; - -public class LinkHandler extends SimpleTagHandler { - @Nullable - @Override - public Object getSpans(@NonNull SpannableConfiguration configuration, @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() - ); - } - return null; - } -} 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 deleted file mode 100644 index fca098e7..00000000 --- a/markwon/src/main/java/ru/noties/markwon/renderer/html2/tag/ListHandler.java +++ /dev/null @@ -1,66 +0,0 @@ -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.html.api.HtmlTag; - -public class ListHandler extends TagHandler { - - @Override - public void handle( - @NonNull SpannableConfiguration configuration, - @NonNull SpannableBuilder builder, - @NonNull HtmlTag tag) { - - if (!tag.isBlock()) { - return; - } - - final HtmlTag.Block block = tag.getAsBlock(); - final boolean ol = "ol".equals(block.name()); - final boolean ul = "ul".equals(block.name()); - - if (!ol && !ul) { - return; - } - - int number = 1; - final int bulletLevel = currentBulletListLevel(block); - - Object spans; - - for (HtmlTag.Block child : block.children()) { - - visitChildren(configuration, builder, child); - - if ("li".equals(child.name())) { - // insert list item here - if (ol) { - spans = configuration.factory().orderedListItem( - configuration.theme(), - number++ - ); - } else { - spans = configuration.factory().bulletListItem( - configuration.theme(), - bulletLevel - ); - } - SpannableBuilder.setSpans(builder, spans, child.start(), child.end()); - } - } - } - - private static int currentBulletListLevel(@NonNull HtmlTag.Block block) { - int level = 0; - while ((block = block.parent()) != null) { - if ("ul".equals(block.name()) - || "ol".equals(block.name())) { - level += 1; - } - } - return level; - } -} 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 deleted file mode 100644 index e5940cc7..00000000 --- a/markwon/src/main/java/ru/noties/markwon/renderer/html2/tag/SimpleTagHandler.java +++ /dev/null @@ -1,22 +0,0 @@ -package ru.noties.markwon.renderer.html2.tag; - -import android.support.annotation.NonNull; -import android.support.annotation.Nullable; - -import ru.noties.markwon.SpannableBuilder; -import ru.noties.markwon.SpannableConfiguration; -import ru.noties.markwon.html.api.HtmlTag; - -public abstract class SimpleTagHandler extends TagHandler { - - @Nullable - public abstract Object getSpans(@NonNull SpannableConfiguration configuration, @NonNull HtmlTag tag); - - @Override - public void handle(@NonNull SpannableConfiguration 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 deleted file mode 100644 index 965ddfea..00000000 --- a/markwon/src/main/java/ru/noties/markwon/renderer/html2/tag/StrikeHandler.java +++ /dev/null @@ -1,28 +0,0 @@ -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.html.api.HtmlTag; - -public class StrikeHandler extends TagHandler { - - @Override - public void handle( - @NonNull SpannableConfiguration configuration, - @NonNull SpannableBuilder builder, - @NonNull HtmlTag tag) { - - if (tag.isBlock()) { - visitChildren(configuration, builder, tag.getAsBlock()); - } - - SpannableBuilder.setSpans( - builder, - configuration.factory().strikethrough(), - tag.start(), - tag.end() - ); - } -} 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 deleted file mode 100644 index 04d18a25..00000000 --- a/markwon/src/main/java/ru/noties/markwon/renderer/html2/tag/StrongEmphasisHandler.java +++ /dev/null @@ -1,15 +0,0 @@ -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.html.api.HtmlTag; - -public class StrongEmphasisHandler extends SimpleTagHandler { - @Nullable - @Override - public Object getSpans(@NonNull SpannableConfiguration 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 deleted file mode 100644 index a96f34bc..00000000 --- a/markwon/src/main/java/ru/noties/markwon/renderer/html2/tag/SubScriptHandler.java +++ /dev/null @@ -1,15 +0,0 @@ -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.html.api.HtmlTag; - -public class SubScriptHandler extends SimpleTagHandler { - @Nullable - @Override - public Object getSpans(@NonNull SpannableConfiguration 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 deleted file mode 100644 index c5eee815..00000000 --- a/markwon/src/main/java/ru/noties/markwon/renderer/html2/tag/SuperScriptHandler.java +++ /dev/null @@ -1,15 +0,0 @@ -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.html.api.HtmlTag; - -public class SuperScriptHandler extends SimpleTagHandler { - @Nullable - @Override - public Object getSpans(@NonNull SpannableConfiguration 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 deleted file mode 100644 index 818c4a98..00000000 --- a/markwon/src/main/java/ru/noties/markwon/renderer/html2/tag/TagHandler.java +++ /dev/null @@ -1,38 +0,0 @@ -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.html.api.HtmlTag; - -public abstract class TagHandler { - - public abstract void handle( - @NonNull SpannableConfiguration configuration, - @NonNull SpannableBuilder builder, - @NonNull HtmlTag tag - ); - - protected static void visitChildren( - @NonNull SpannableConfiguration configuration, - @NonNull SpannableBuilder builder, - @NonNull HtmlTag.Block block) { - - TagHandler handler; - - for (HtmlTag.Block child : block.children()) { - - if (!child.isClosed()) { - continue; - } - - handler = configuration.htmlRenderer().tagHandler(child.name()); - if (handler != null) { - handler.handle(configuration, builder, child); - } else { - visitChildren(configuration, builder, child); - } - } - } -} 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/CodeSpan.java b/markwon/src/main/java/ru/noties/markwon/spans/CodeSpan.java deleted file mode 100644 index b1098654..00000000 --- a/markwon/src/main/java/ru/noties/markwon/spans/CodeSpan.java +++ /dev/null @@ -1,70 +0,0 @@ -package ru.noties.markwon.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; - -public class CodeSpan extends MetricAffectingSpan implements LeadingMarginSpan { - - private final SpannableTheme theme; - private final Rect rect = ObjectsPool.rect(); - private final Paint paint = ObjectsPool.paint(); - - private final boolean multiline; - - public CodeSpan(@NonNull SpannableTheme theme, boolean multiline) { - this.theme = theme; - this.multiline = multiline; - } - - @Override - public void updateMeasureState(TextPaint p) { - apply(p); - } - - @Override - public void updateDrawState(TextPaint ds) { - apply(ds); - if (!multiline) { - ds.bgColor = theme.getCodeBackgroundColor(ds, false); - } - } - - 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); - } - } -} 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/LeadingMarginUtils.java b/markwon/src/main/java/ru/noties/markwon/spans/LeadingMarginUtils.java deleted file mode 100644 index ab1a7de3..00000000 --- a/markwon/src/main/java/ru/noties/markwon/spans/LeadingMarginUtils.java +++ /dev/null @@ -1,17 +0,0 @@ -package ru.noties.markwon.spans; - -import android.text.Spanned; - -abstract class LeadingMarginUtils { - - 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) { - return text instanceof Spanned && ((Spanned) text).getSpanEnd(span) == end; - } - - private LeadingMarginUtils() { - } -} 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 3bb49355..00000000 --- a/markwon/src/main/java/ru/noties/markwon/tasklist/TaskListExtension.java +++ /dev/null @@ -1,21 +0,0 @@ -package ru.noties.markwon.tasklist; - -import android.support.annotation.NonNull; - -import org.commonmark.parser.Parser; - -/** - * @since 1.0.1 - */ -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/SpannableConfigurationTest.java b/markwon/src/test/java/ru/noties/markwon/renderer/SpannableConfigurationTest.java deleted file mode 100644 index daa70332..00000000 --- a/markwon/src/test/java/ru/noties/markwon/renderer/SpannableConfigurationTest.java +++ /dev/null @@ -1,52 +0,0 @@ -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()); - } -} 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 047a0584..00000000 --- a/markwon/src/test/java/ru/noties/markwon/renderer/visitor/SpannableMarkdownVisitorTest.java +++ /dev/null @@ -1,95 +0,0 @@ -package ru.noties.markwon.renderer.visitor; - -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.annotation.Config; - -import java.util.Arrays; -import java.util.Collection; - -import ru.noties.markwon.LinkResolverDef; -import ru.noties.markwon.Markwon; -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 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 SpannableConfiguration 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(); - - 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); - } - - @SuppressWarnings("ConstantConditions") - @NonNull - private SpannableConfiguration 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 SpannableConfiguration.builder(null) - .theme(mock(SpannableTheme.class)) - .linkResolver(mock(LinkResolverDef.class)) - .htmlParser(htmlParser) - .factory(factory) - .softBreakAddsNewLine(softBreakAddsNewLine) - .htmlAllowNonClosedTags(htmlAllowNonClosedTags) - .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 61fc29a5..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 bfba93b1..00000000 --- a/markwon/src/test/java/ru/noties/markwon/renderer/visitor/TestDataReader.java +++ /dev/null @@ -1,351 +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 ru.noties.markwon.spans.TableRowSpan; - -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.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 { - - 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, - STRIKE_THROUGH, - TASK_LIST, - TABLE_ROW, - PARAGRAPH, - IMAGE, - LINK, - SUPER_SCRIPT, - SUB_SCRIPT, - UNDERLINE, - 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 { - // 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 89a0f646..00000000 --- a/markwon/src/test/java/ru/noties/markwon/renderer/visitor/TestFactory.java +++ /dev/null @@ -1,193 +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.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.spans.AsyncDrawable; -import ru.noties.markwon.spans.LinkSpan; -import ru.noties.markwon.spans.SpannableTheme; -import ru.noties.markwon.spans.TableRowSpan; - -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.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 { - - 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 SpannableTheme theme) { - return new TestSpan(BLOCK_QUOTE); - } - - @Nullable - @Override - public Object code(@NonNull SpannableTheme theme, boolean multiline) { - final String name = multiline - ? CODE_BLOCK - : CODE; - return new TestSpan(name); - } - - @Nullable - @Override - public Object orderedListItem(@NonNull SpannableTheme theme, int startNumber) { - return new TestSpan(ORDERED_LIST, map("start", startNumber)); - } - - @Nullable - @Override - public Object bulletListItem(@NonNull SpannableTheme theme, int level) { - return new TestSpan(BULLET_LIST, map("level", level)); - } - - @Nullable - @Override - public Object thematicBreak(@NonNull SpannableTheme theme) { - return new TestSpan(THEMATIC_BREAK); - } - - @Nullable - @Override - public Object heading(@NonNull SpannableTheme theme, int level) { - return new TestSpan(HEADING + level); - } - - @Nullable - @Override - public Object strikethrough() { - return new TestSpan(STRIKE_THROUGH); - } - - @Nullable - @Override - public Object taskListItem(@NonNull SpannableTheme 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 SpannableTheme 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) { - return !useParagraphs - ? null - : new TestSpan(PARAGRAPH); - } - - @Nullable - @Override - public Object image(@NonNull SpannableTheme 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), - Pair.of("replacementTextIsLink", replacementTextIsLink) - )); - } - - @Nullable - @Override - public Object link(@NonNull SpannableTheme theme, @NonNull String destination, @NonNull LinkSpan.Resolver resolver) { - return new TestSpan(LINK, map("href", destination)); - } - - @Nullable - @Override - public Object superScript(@NonNull SpannableTheme theme) { - return new TestSpan(SUPER_SCRIPT); - } - - @Nullable - @Override - public Object subScript(@NonNull SpannableTheme 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)); - } - - 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 f4c8d6ba..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/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() { - } -} 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.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.yaml b/markwon/src/test/resources/tests/deeply-nested.yaml deleted file mode 100644 index 2f40ddf3..00000000 --- a/markwon/src/test/resources/tests/deeply-nested.yaml +++ /dev/null @@ -1,15 +0,0 @@ -input: |- - **bold *bold italic ~~bold italic strike `bold italic strike code` bold italic strike~~ bold italic* bold** normal - -output: - - b: - - "bold " - - i: - - "bold italic " - - s: - - "bold italic strike " - - code: "bold italic strike code" - - " bold italic strike" - - " 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/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/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.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.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.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.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.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.yaml b/markwon/src/test/resources/tests/second.yaml deleted file mode 100644 index bd088dc2..00000000 --- a/markwon/src/test/resources/tests/second.yaml +++ /dev/null @@ -1,32 +0,0 @@ -input: |- - First **line** is *always* - ~~strike~~ down - - > 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: " " - - s: "strike" - - text: " down\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-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.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.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.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-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-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/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 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/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.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.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/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/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.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 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 5672dfe9..00000000 --- a/sample-custom-extension/build.gradle +++ /dev/null @@ -1,22 +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') -} diff --git a/sample-custom-extension/src/main/AndroidManifest.xml b/sample-custom-extension/src/main/AndroidManifest.xml deleted file mode 100644 index 1553a0a6..00000000 --- a/sample-custom-extension/src/main/AndroidManifest.xml +++ /dev/null @@ -1,21 +0,0 @@ - - - - - - - - - - - - - - \ No newline at end of file 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 d373ff75..00000000 --- a/sample-custom-extension/src/main/java/ru/noties/markwon/sample/extension/IconVisitor.java +++ /dev/null @@ -1,63 +0,0 @@ -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.renderer.SpannableMarkdownVisitor; - -@SuppressWarnings("WeakerAccess") -public class IconVisitor extends SpannableMarkdownVisitor { - - private final SpannableBuilder builder; - - private final IconSpanProvider iconSpanProvider; - - public IconVisitor( - @NonNull SpannableConfiguration 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 deleted file mode 100644 index 19a69704..00000000 --- a/sample-custom-extension/src/main/java/ru/noties/markwon/sample/extension/MainActivity.java +++ /dev/null @@ -1,75 +0,0 @@ -package ru.noties.markwon.sample.extension; - -import android.app.Activity; -import android.graphics.Typeface; -import android.os.Bundle; -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.SpannableBuilder; -import ru.noties.markwon.SpannableConfiguration; -import ru.noties.markwon.spans.SpannableTheme; -import ru.noties.markwon.tasklist.TaskListExtension; - -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); - - // 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()) - .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 markdown 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}; - SpannableConfiguration configuration = SpannableConfiguration.builder(this) - .theme(SpannableTheme.builder() - .headingTypeface(Typeface.MONOSPACE) - .headingTextSizeMultipliers(textSizeMultipliers) - .build()) - .build(); - // create an instance of visitor to process parsed markdown - final IconVisitor visitor = new IconVisitor( - configuration, - builder, - spanProvider - ); - - // trigger visit - node.accept(visitor); - - // apply - textView.setText(builder.text()); - } -} 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/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 a2f59082..00000000 Binary files a/sample-custom-extension/src/main/res/mipmap-hdpi/ic_launcher.png and /dev/null differ diff --git a/sample-custom-extension/src/main/res/mipmap-hdpi/ic_launcher_round.png b/sample-custom-extension/src/main/res/mipmap-hdpi/ic_launcher_round.png deleted file mode 100644 index 1b523998..00000000 Binary files a/sample-custom-extension/src/main/res/mipmap-hdpi/ic_launcher_round.png and /dev/null differ diff --git a/sample-custom-extension/src/main/res/mipmap-mdpi/ic_launcher.png b/sample-custom-extension/src/main/res/mipmap-mdpi/ic_launcher.png deleted file mode 100644 index ff10afd6..00000000 Binary files a/sample-custom-extension/src/main/res/mipmap-mdpi/ic_launcher.png and /dev/null differ diff --git a/sample-custom-extension/src/main/res/mipmap-mdpi/ic_launcher_round.png b/sample-custom-extension/src/main/res/mipmap-mdpi/ic_launcher_round.png deleted file mode 100644 index 115a4c76..00000000 Binary files a/sample-custom-extension/src/main/res/mipmap-mdpi/ic_launcher_round.png and /dev/null differ diff --git a/sample-custom-extension/src/main/res/mipmap-xhdpi/ic_launcher.png b/sample-custom-extension/src/main/res/mipmap-xhdpi/ic_launcher.png deleted file mode 100644 index dcd3cd80..00000000 Binary files a/sample-custom-extension/src/main/res/mipmap-xhdpi/ic_launcher.png and /dev/null differ 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 459ca609..00000000 Binary files a/sample-custom-extension/src/main/res/mipmap-xhdpi/ic_launcher_round.png and /dev/null differ 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 8ca12fe0..00000000 Binary files a/sample-custom-extension/src/main/res/mipmap-xxhdpi/ic_launcher.png and /dev/null differ 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 8e19b410..00000000 Binary files a/sample-custom-extension/src/main/res/mipmap-xxhdpi/ic_launcher_round.png and /dev/null differ diff --git a/sample-custom-extension/src/main/res/mipmap-xxxhdpi/ic_launcher.png b/sample-custom-extension/src/main/res/mipmap-xxxhdpi/ic_launcher.png deleted file mode 100644 index b824ebdd..00000000 Binary files a/sample-custom-extension/src/main/res/mipmap-xxxhdpi/ic_launcher.png and /dev/null differ 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 4c19a13c..00000000 Binary files a/sample-custom-extension/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png and /dev/null differ 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 new file mode 100644 index 00000000..dbe4bdaa --- /dev/null +++ b/sample/build.gradle @@ -0,0 +1,66 @@ +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") + + resConfig 'en' + } + + lintOptions { + abortOnError false + } + + dexOptions { + preDexLibraries true + javaMaxHeapSize '5g' + } + + compileOptions { + targetCompatibility JavaVersion.VERSION_1_8 + sourceCompatibility JavaVersion.VERSION_1_8 + } +} + +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') + 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') + implementation project(':markwon-recycler-table') + + 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 new file mode 100644 index 00000000..376348ca --- /dev/null +++ b/sample/src/main/AndroidManifest.xml @@ -0,0 +1,30 @@ + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file 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/MainActivity.java b/sample/src/main/java/ru/noties/markwon/sample/MainActivity.java new file mode 100644 index 00000000..16205066 --- /dev/null +++ b/sample/src/main/java/ru/noties/markwon/sample/MainActivity.java @@ -0,0 +1,106 @@ +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.basicplugins.BasicPluginsActivity; +import ru.noties.markwon.sample.core.CoreActivity; +import ru.noties.markwon.sample.customextension.CustomExtensionActivity; +import ru.noties.markwon.sample.latex.LatexActivity; +import ru.noties.markwon.sample.recycler.RecyclerActivity; + +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 BASIC_PLUGINS: + activity = BasicPluginsActivity.class; + break; + + case LATEX: + activity = LatexActivity.class; + break; + + case CUSTOM_EXTENSION: + activity = CustomExtensionActivity.class; + break; + + case RECYCLER: + activity = RecyclerActivity.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..e21444ba --- /dev/null +++ b/sample/src/main/java/ru/noties/markwon/sample/SampleItem.java @@ -0,0 +1,30 @@ +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), + + BASIC_PLUGINS(R.string.sample_basic_plugins), + + LATEX(R.string.sample_latex), + + CUSTOM_EXTENSION(R.string.sample_custom_extension), + + RECYCLER(R.string.sample_recycler), + + ; + + 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..dc4432aa --- /dev/null +++ b/sample/src/main/java/ru/noties/markwon/sample/SampleItemView.java @@ -0,0 +1,75 @@ +package ru.noties.markwon.sample; + +import android.support.annotation.NonNull; +import android.text.Spannable; +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; +import ru.noties.markwon.utils.NoCopySpannableFactory; + +class SampleItemView extends ItemView { + + private final Markwon markwon; + + // instance specific factory + private final Spannable.Factory factory; + + // instance specific cache + private final EnumMap cache; + + SampleItemView(@NonNull Markwon markwon) { + this.markwon = markwon; + this.factory = NoCopySpannableFactory.getInstance(); + 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); + } + } +} diff --git a/sample/src/main/java/ru/noties/markwon/sample/basicplugins/BasicPluginsActivity.java b/sample/src/main/java/ru/noties/markwon/sample/basicplugins/BasicPluginsActivity.java new file mode 100644 index 00000000..0728be6c --- /dev/null +++ b/sample/src/main/java/ru/noties/markwon/sample/basicplugins/BasicPluginsActivity.java @@ -0,0 +1,204 @@ +package ru.noties.markwon.sample.basicplugins; + +import android.app.Activity; +import android.graphics.Color; +import android.net.Uri; +import android.os.Bundle; +import android.support.annotation.NonNull; +import android.support.annotation.Nullable; +import android.text.TextUtils; +import android.text.style.ForegroundColorSpan; +import android.widget.TextView; + +import org.commonmark.node.Heading; +import org.commonmark.node.Paragraph; + +import ru.noties.markwon.AbstractMarkwonPlugin; +import ru.noties.markwon.Markwon; +import ru.noties.markwon.MarkwonConfiguration; +import ru.noties.markwon.MarkwonPlugin; +import ru.noties.markwon.MarkwonSpansFactory; +import ru.noties.markwon.MarkwonVisitor; +import ru.noties.markwon.movement.MovementMethodPlugin; +import ru.noties.markwon.core.MarkwonTheme; +import ru.noties.markwon.image.AsyncDrawableLoader; +import ru.noties.markwon.image.ImageItem; +import ru.noties.markwon.image.ImagesPlugin; +import ru.noties.markwon.image.SchemeHandler; +import ru.noties.markwon.image.network.NetworkSchemeHandler; + +public class BasicPluginsActivity 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(); + } + + /** + * 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_1() { + + 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_2() { + + 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); + + // same method can be used to override existing visitor by specifying + // a new NodeVisitor instance + } + }) + .build(); + + markwon.setMarkdown(textView, markdown); + } + + /** + * To customize core theme plugin can be used again + */ + private void step_3() { + + 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); + } + + /** + * MarkwonConfiguration contains these utilities: + *

        + *
      • SyntaxHighlight
      • + *
      • LinkSpan.Resolver
      • + *
      • UrlProcessor
      • + *
      • ImageSizeResolver
      • + *
      + *

      + * In order to customize them a custom plugin should be used + */ + private void step_4() { + + final String markdown = "[a link without scheme](github.com)"; + + final Markwon markwon = Markwon.builder(this) + // please note that Markwon does not handle MovementMethod, + // so if your markdown has links your should apply MovementMethod manually + // or use MovementMethodPlugin (which uses system LinkMovementMethod by default) + .usePlugin(MovementMethodPlugin.create()) + .usePlugin(new AbstractMarkwonPlugin() { + @Override + public void configureConfiguration(@NonNull MarkwonConfiguration.Builder builder) { + // for example if specified destination has no scheme info, we will + // _assume_ that it's network request and append HTTPS scheme + builder.urlProcessor(destination -> { + final Uri uri = Uri.parse(destination); + if (TextUtils.isEmpty(uri.getScheme())) { + return "https://" + destination; + } + return destination; + }); + } + }) + .build(); + + markwon.setMarkdown(textView, markdown); + } + + /** + * Images configuration. Can be used with (or without) ImagesPlugin, which does some basic + * images handling (parsing markdown containing images, obtain an image from network + * file system or assets). Please note that + */ + private void step_5() { + + final String markdown = "![image](myownscheme://en.wikipedia.org/static/images/project-logos/enwiki-2x.png)"; + + final Markwon markwon = Markwon.builder(this) + .usePlugin(ImagesPlugin.create(this)) + .usePlugin(new AbstractMarkwonPlugin() { + @Override + public void configureImages(@NonNull AsyncDrawableLoader.Builder builder) { + // we can have a custom SchemeHandler + // here we will just use networkSchemeHandler to redirect call + builder.addSchemeHandler("myownscheme", new SchemeHandler() { + + final NetworkSchemeHandler networkSchemeHandler = NetworkSchemeHandler.create(); + + @Nullable + @Override + public ImageItem handle(@NonNull String raw, @NonNull Uri uri) { + raw = raw.replace("myownscheme", "https"); + return networkSchemeHandler.handle(raw, Uri.parse(raw)); + } + }); + } + }) + .build(); + + markwon.setMarkdown(textView, markdown); + } + + // text lifecycle (after/before) + // rendering lifecycle (before/after) + // renderProps + // process + // priority +} 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..3f8118c5 --- /dev/null +++ b/sample/src/main/java/ru/noties/markwon/sample/core/CoreActivity.java @@ -0,0 +1,122 @@ +package ru.noties.markwon.sample.core; + +import android.app.Activity; +import android.os.Bundle; +import android.support.annotation.Nullable; +import android.text.Spanned; +import android.widget.TextView; +import android.widget.Toast; + +import org.commonmark.node.Node; + +import ru.noties.markwon.Markwon; +import ru.noties.markwon.core.CorePlugin; + +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(); + } + + /** + * Create a simple instance of Markwon with only Core plugin registered + * this will handle all _natively_ supported by commonmark-java nodes: + *

        + *
      • StrongEmphasis
      • + *
      • Emphasis
      • + *
      • BlockQuote
      • + *
      • Code
      • + *
      • FencedCodeBlock
      • + *
      • IndentedCodeBlock
      • + *
      • ListItem (bullet-list and ordered list
      • + *
      • Heading
      • + *
      • Link
      • + *
      • ThematicBreak
      • + *
      • Paragraph (please note that there is no default span for a paragraph registered)
      • + *
      + *

      + * 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); + } +} 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..4a9c2fd9 --- /dev/null +++ b/sample/src/main/java/ru/noties/markwon/sample/customextension/CustomExtensionActivity.java @@ -0,0 +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/src/main/java/ru/noties/markwon/sample/customextension/IconPlugin.java b/sample/src/main/java/ru/noties/markwon/sample/customextension/IconPlugin.java new file mode 100644 index 00000000..3de96315 --- /dev/null +++ b/sample/src/main/java/ru/noties/markwon/sample/customextension/IconPlugin.java @@ -0,0 +1,67 @@ +package ru.noties.markwon.sample.customextension; + +import android.support.annotation.NonNull; +import android.text.TextUtils; + +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 { + + @NonNull + public static IconPlugin create(@NonNull IconSpanProvider iconSpanProvider) { + return new IconPlugin(iconSpanProvider); + } + + private final IconSpanProvider iconSpanProvider; + + IconPlugin(@NonNull IconSpanProvider iconSpanProvider) { + 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()); + } + + @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/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 new file mode 100644 index 00000000..d4143a76 --- /dev/null +++ b/sample/src/main/java/ru/noties/markwon/sample/latex/LatexActivity.java @@ -0,0 +1,52 @@ +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.ext.latex.JLatexMathPlugin; +import ru.noties.markwon.image.ImagesPlugin; +import ru.noties.markwon.sample.R; + +public class LatexActivity extends Activity { + + @Override + 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 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(textView.getTextSize())) + .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 new file mode 100644 index 00000000..adeb6c4b --- /dev/null +++ b/sample/src/main/java/ru/noties/markwon/sample/recycler/RecyclerActivity.java @@ -0,0 +1,174 @@ +package ru.noties.markwon.sample.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.MarkwonVisitor; +import ru.noties.markwon.core.CorePlugin; +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.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; + +public class RecyclerActivity extends Activity { + + static { + Debug.init(new AndroidLogDebugOutput(true)); + } + + @Override + public void onCreate(@Nullable Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setContentView(R.layout.activity_recycler); + + // 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(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); + recyclerView.setLayoutManager(new LinearLayoutManager(this)); + recyclerView.setHasFixedSize(true); + recyclerView.setAdapter(adapter); + + final Markwon markwon = markwon(this); + adapter.setMarkdown(markwon, loadReadMe(this)); + + // please note that we should notify updates (adapter doesn't do it implicitly) + 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())) + // important to use TableEntryPlugin instead of TablePlugin + .usePlugin(TableEntryPlugin.create(context)) + .usePlugin(HtmlPlugin.create()) +// .usePlugin(SyntaxHighlightPlugin.create()) + .usePlugin(new AbstractMarkwonPlugin() { + @Override + public void configureConfiguration(@NonNull MarkwonConfiguration.Builder builder) { + builder.urlProcessor(new UrlProcessorInitialReadme()); + } + + @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(); + } + + @NonNull + 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); + } + + @NonNull + private static String readStream(@Nullable InputStream inputStream) { + + String out = null; + + if (inputStream != null) { + BufferedReader reader = null; + //noinspection TryFinallyCanBeTryWithResources + 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 + } + } + } + } + + if (out == null) { + throw new RuntimeException("Cannot read stream"); + } + + 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/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/src/main/res/drawable/bg_table_cell.xml b/sample/src/main/res/drawable/bg_table_cell.xml new file mode 100644 index 00000000..9a2b40b8 --- /dev/null +++ b/sample/src/main/res/drawable/bg_table_cell.xml @@ -0,0 +1,6 @@ + + + + \ No newline at end of file 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/src/main/res/drawable/ic_launcher_background.xml b/sample/src/main/res/drawable/ic_launcher_background.xml new file mode 100644 index 00000000..02e8ba59 --- /dev/null +++ b/sample/src/main/res/drawable/ic_launcher_background.xml @@ -0,0 +1,24 @@ + + + + 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/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/activity_recycler.xml b/sample/src/main/res/layout/activity_recycler.xml new file mode 100644 index 00000000..1405e07c --- /dev/null +++ b/sample/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/activity_main.xml b/sample/src/main/res/layout/activity_text_view.xml similarity index 65% 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..9828f257 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 @@ - - @@ -11,7 +9,8 @@ android:layout_height="wrap_content" android:padding="8dip" android:textAppearance="?android:attr/textAppearanceMedium" - android:textSize="15sp" - tools:text="@string/input"/> + android:textColor="#000" + android:textSize="16sp" + tools:text="whatever" /> - + \ 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/layout/adapter_default_entry.xml b/sample/src/main/res/layout/adapter_default_entry.xml new file mode 100644 index 00000000..e9fb8929 --- /dev/null +++ b/sample/src/main/res/layout/adapter_default_entry.xml @@ -0,0 +1,15 @@ + + \ No newline at end of file diff --git a/sample/src/main/res/layout/adapter_fenced_code_block.xml b/sample/src/main/res/layout/adapter_fenced_code_block.xml new file mode 100644 index 00000000..053d59cc --- /dev/null +++ b/sample/src/main/res/layout/adapter_fenced_code_block.xml @@ -0,0 +1,26 @@ + + + + + + \ No newline at end of file diff --git a/sample/src/main/res/layout/adapter_table_block.xml b/sample/src/main/res/layout/adapter_table_block.xml new file mode 100644 index 00000000..aaaaa369 --- /dev/null +++ b/sample/src/main/res/layout/adapter_table_block.xml @@ -0,0 +1,19 @@ + + + + + + \ No newline at end of file diff --git a/sample/src/main/res/layout/view_table_entry_cell.xml b/sample/src/main/res/layout/view_table_entry_cell.xml new file mode 100644 index 00000000..6d544918 --- /dev/null +++ b/sample/src/main/res/layout/view_table_entry_cell.xml @@ -0,0 +1,9 @@ + + \ No newline at end of file diff --git a/sample/src/main/res/layout/view_table_entry_row.xml b/sample/src/main/res/layout/view_table_entry_row.xml new file mode 100644 index 00000000..609353ee --- /dev/null +++ b/sample/src/main/res/layout/view_table_entry_row.xml @@ -0,0 +1,5 @@ + + \ 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 @@ + + + +