From 7c7b1f59a83f721d49a9b323efa151d37e322b28 Mon Sep 17 00:00:00 2001 From: Dimitry Date: Mon, 30 Jul 2018 15:19:42 +0200 Subject: [PATCH] V1.1.0 (#53) * Update build configuration * Update commonmark to 0.11.0 and android-gif to 1.2.14 * Add module `library-syntax` * Add default prism4j theme implementation * Add syntax highlight to sample app * Update syntax highlight to use SpannableStringBuilder * Working with syntax rendering * Add darkula theme to syntax highlight * Add attribute for image-loader module * Update version to 1.1.0-SNAPSHOT * Updating build configuration for snapshot publish * Add headingTypeface, headingTextSizes to SpannableTheme (#51) * Add headingTypeface to SpannableTheme, use a custom heading typeface in the sample app * Add headingTextSizes * Switching to headingTextSizeMultipliers, adding validating annotations, adding example * Consolidate logic, add crash if header index is out of bounds * Add small version clarifications * Introduce MediaDecoder abstraction for image-loader module * Switch to use SpannableFactory * Switch to use SpannableFactory for html parsing * Update sample application to add play-pause functionality for gifs * Small cleanup * Update prism4j version 1.1.0 * Update build configuration * Add README to library-syntax module * Update README --- README.md | 20 +- app/build.gradle | 4 + .../java/ru/noties/markwon/AppModule.java | 39 +++- .../noties/markwon/GifAwareAsyncDrawable.java | 58 +++++ .../markwon/GifAwareSpannableFactory.java | 36 ++++ .../ru/noties/markwon/GifPlaceholder.java | 77 +++++++ .../java/ru/noties/markwon/GifProcessor.java | 125 +++++++++++ .../java/ru/noties/markwon/MainActivity.java | 11 +- .../ru/noties/markwon/MarkdownRenderer.java | 39 ++++ .../main/java/ru/noties/markwon/Themes.java | 8 +- .../ic_play_circle_filled_18dp_white.png | Bin 0 -> 322 bytes .../ic_play_circle_filled_18dp_white.png | Bin 0 -> 378 bytes .../ic_play_circle_filled_18dp_white.png | Bin 0 -> 536 bytes app/src/main/res/values/colors.xml | 11 + app/src/main/res/values/styles.xml | 11 +- build.gradle | 18 +- gradle.properties | 2 +- gradle/wrapper/gradle-wrapper.jar | Bin 54727 -> 54329 bytes gradle/wrapper/gradle-wrapper.properties | 2 +- library-image-loader/build.gradle | 12 +- .../markwon/il/AsyncDrawableLoader.java | 204 ++++++++---------- .../ru/noties/markwon/il/GifMediaDecoder.java | 90 ++++++++ .../noties/markwon/il/ImageMediaDecoder.java | 58 +++++ .../ru/noties/markwon/il/MediaDecoder.java | 20 ++ .../ru/noties/markwon/il/SvgMediaDecoder.java | 80 +++++++ library-syntax/README.md | 64 ++++++ library-syntax/art/markwon-syntax-darkula.png | Bin 0 -> 30908 bytes library-syntax/art/markwon-syntax-default.png | Bin 0 -> 33603 bytes library-syntax/build.gradle | 32 +++ library-syntax/gradle.properties | 3 + library-syntax/src/main/AndroidManifest.xml | 1 + .../syntax/Prism4jSyntaxHighlight.java | 105 +++++++++ .../markwon/syntax/Prism4jSyntaxVisitor.java | 40 ++++ .../noties/markwon/syntax/Prism4jTheme.java | 24 +++ .../markwon/syntax/Prism4jThemeBase.java | 140 ++++++++++++ .../markwon/syntax/Prism4jThemeDarkula.java | 61 ++++++ .../markwon/syntax/Prism4jThemeDefault.java | 75 +++++++ library-view/build.gradle | 10 +- library/build.gradle | 10 +- .../ru/noties/markwon/SpannableBuilder.java | 60 +++--- .../markwon/SpannableConfiguration.java | 30 ++- .../ru/noties/markwon/SpannableFactory.java | 85 ++++++++ .../noties/markwon/SpannableFactoryDef.java | 144 +++++++++++++ .../renderer/SpannableMarkdownVisitor.java | 104 ++++----- .../markwon/renderer/html/BoldProvider.java | 13 +- .../renderer/html/ImageProviderImpl.java | 26 ++- .../renderer/html/ItalicsProvider.java | 13 +- .../markwon/renderer/html/LinkProvider.java | 6 +- .../renderer/html/SpannableHtmlParser.java | 77 +++---- .../markwon/renderer/html/StrikeProvider.java | 15 +- .../renderer/html/SubScriptProvider.java | 8 +- .../renderer/html/SuperScriptProvider.java | 8 +- .../renderer/html/UnderlineProvider.java | 14 +- .../noties/markwon/spans/SpannableTheme.java | 61 +++++- .../sample/extension/MainActivity.java | 11 +- .../src/main/res/layout/activity_main.xml | 3 +- .../src/main/res/values/strings.xml | 1 + settings.gradle | 2 +- 58 files changed, 1869 insertions(+), 302 deletions(-) create mode 100644 app/src/main/java/ru/noties/markwon/GifAwareAsyncDrawable.java create mode 100644 app/src/main/java/ru/noties/markwon/GifAwareSpannableFactory.java create mode 100644 app/src/main/java/ru/noties/markwon/GifPlaceholder.java create mode 100644 app/src/main/java/ru/noties/markwon/GifProcessor.java create mode 100644 app/src/main/res/drawable-hdpi/ic_play_circle_filled_18dp_white.png create mode 100644 app/src/main/res/drawable-xhdpi/ic_play_circle_filled_18dp_white.png create mode 100644 app/src/main/res/drawable-xxhdpi/ic_play_circle_filled_18dp_white.png create mode 100644 library-image-loader/src/main/java/ru/noties/markwon/il/GifMediaDecoder.java create mode 100644 library-image-loader/src/main/java/ru/noties/markwon/il/ImageMediaDecoder.java create mode 100644 library-image-loader/src/main/java/ru/noties/markwon/il/MediaDecoder.java create mode 100644 library-image-loader/src/main/java/ru/noties/markwon/il/SvgMediaDecoder.java create mode 100644 library-syntax/README.md create mode 100644 library-syntax/art/markwon-syntax-darkula.png create mode 100644 library-syntax/art/markwon-syntax-default.png create mode 100644 library-syntax/build.gradle create mode 100644 library-syntax/gradle.properties create mode 100644 library-syntax/src/main/AndroidManifest.xml create mode 100644 library-syntax/src/main/java/ru/noties/markwon/syntax/Prism4jSyntaxHighlight.java create mode 100644 library-syntax/src/main/java/ru/noties/markwon/syntax/Prism4jSyntaxVisitor.java create mode 100644 library-syntax/src/main/java/ru/noties/markwon/syntax/Prism4jTheme.java create mode 100644 library-syntax/src/main/java/ru/noties/markwon/syntax/Prism4jThemeBase.java create mode 100644 library-syntax/src/main/java/ru/noties/markwon/syntax/Prism4jThemeDarkula.java create mode 100644 library-syntax/src/main/java/ru/noties/markwon/syntax/Prism4jThemeDefault.java create mode 100644 library/src/main/java/ru/noties/markwon/SpannableFactory.java create mode 100644 library/src/main/java/ru/noties/markwon/SpannableFactoryDef.java diff --git a/README.md b/README.md index 68d3f334..cf7ea17b 100644 --- a/README.md +++ b/README.md @@ -4,6 +4,7 @@ [![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](https://img.shields.io/maven-central/v/ru.noties/markwon-syntax.svg?label=markwon-syntax)](http://search.maven.org/#search|ga|1|g%3A%22ru.noties%22%20AND%20a%3A%22markwon-syntax%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) **Markwon** is a library for Android that renders markdown as system-native Spannables. It gives ability to display markdown in all TextView widgets (**TextView**, **Button**, **Switch**, **CheckBox**, etc), **Notifications**, **Toasts**, etc. **No WebView is required**. Library provides reasonable defaults for display style of markdown but also gives all the means to tweak the appearance if desired. All markdown features are supported (including limited support for inlined HTML code, markdown tables and images). @@ -12,9 +13,10 @@ ## Installation ```groovy -compile 'ru.noties:markwon:1.0.6' -compile 'ru.noties:markwon-image-loader:1.0.6' // optional -compile 'ru.noties:markwon-view:1.0.6' // optional +implementation 'ru.noties:markwon:1.1.0' +implementation 'ru.noties:markwon-image-loader:1.1.0' // optional +implementation 'ru.noties:markwon-syntax:1.1.0' // optional +implementation 'ru.noties:markwon-view:1.1.0' // optional ``` ### Snapshot @@ -35,10 +37,10 @@ allprojects { and then in your module `build.gradle`: ```groovy -implementation 'ru.noties:markwon:1.0.6-SNAPSHOT' +implementation 'ru.noties:markwon:1.1.0-SNAPSHOT' ``` -Please note that `markwon-image-loader` and `markwon-view` are also present in `SNAPSHOT` repository and share the same version as main `markwon` artifact. +Please note that `markwon-image-loader`, `markwon-syntax` and `markwon-view` are also present in `SNAPSHOT` repository and share the same version as main `markwon` artifact. ## Supported markdown features: * Emphasis (`*`, `_`) @@ -141,6 +143,14 @@ you can use [Better-Link-Movement-Method][better-link-movement-method]. Please refer to [SpannableConfiguration] document for more info +## Syntax highlight + +Starting with version `1.1.0` there is an artifact (`markwon-syntax`) that allows you to have syntax highlight functionality. +It is based on [Prism4j](https://github.com/noties/Prism4j) project. It contains 2 builtin themes: +`Default` (light, `Prism4jThemeDefault`) and `Darkula` (dark, `Prism4jThemeDarkula`). + +[library-syntax](./library-syntax/) + --- # Demo diff --git a/app/build.gradle b/app/build.gradle index 4b761b94..9fb02274 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -30,6 +30,7 @@ dependencies { implementation project(':library') implementation project(':library-image-loader') + implementation project(':library-syntax') implementation 'ru.noties:debug:3.0.0@jar' implementation 'me.saket:better-link-movement-method:2.2.0' @@ -38,4 +39,7 @@ dependencies { implementation 'com.google.dagger:dagger:2.10' annotationProcessor 'com.google.dagger:dagger-compiler:2.10' + + implementation PRISM_4J + annotationProcessor PRISM_4J_BUNDLER } diff --git a/app/src/main/java/ru/noties/markwon/AppModule.java b/app/src/main/java/ru/noties/markwon/AppModule.java index 09c5e230..3e2f3967 100644 --- a/app/src/main/java/ru/noties/markwon/AppModule.java +++ b/app/src/main/java/ru/noties/markwon/AppModule.java @@ -15,9 +15,17 @@ 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; +import ru.noties.prism4j.annotations.PrismBundle; @Module +@PrismBundle(includeAll = true) class AppModule { private final App app; @@ -40,7 +48,7 @@ class AppModule { @Singleton OkHttpClient client() { return new OkHttpClient.Builder() - .cache(new Cache(app.getCacheDir(), 1024L * 20)) + .cache(new Cache(app.getCacheDir(), 1024L * 1024 * 20)) // 20 mb .followRedirects(true) .retryOnConnectionFailure(true) .build(); @@ -73,6 +81,35 @@ class AppModule { .client(client) .executorService(executorService) .resources(resources) + .mediaDecoders( + SvgMediaDecoder.create(resources), + GifMediaDecoder.create(false), + ImageMediaDecoder.create(resources) + ) .build(); } + + @Provides + @Singleton + Prism4j prism4j() { + return new Prism4j(new GrammarLocatorDef()); + } + + @Singleton + @Provides + Prism4jThemeDefault prism4jThemeDefault() { + return Prism4jThemeDefault.create(); + } + + @Singleton + @Provides + Prism4jThemeDarkula prism4jThemeDarkula() { + return Prism4jThemeDarkula.create(); + } + + @Singleton + @Provides + GifProcessor gifProcessor() { + return GifProcessor.create(); + } } diff --git a/app/src/main/java/ru/noties/markwon/GifAwareAsyncDrawable.java b/app/src/main/java/ru/noties/markwon/GifAwareAsyncDrawable.java new file mode 100644 index 00000000..78d286af --- /dev/null +++ b/app/src/main/java/ru/noties/markwon/GifAwareAsyncDrawable.java @@ -0,0 +1,58 @@ +package ru.noties.markwon; + +import android.graphics.Canvas; +import android.graphics.drawable.Drawable; +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; + +public class GifAwareAsyncDrawable extends AsyncDrawable { + + public interface OnGifResultListener { + void onGifResult(@NonNull GifAwareAsyncDrawable drawable); + } + + private final Drawable gifPlaceholder; + private OnGifResultListener onGifResultListener; + private boolean isGif; + + public GifAwareAsyncDrawable( + @NonNull Drawable gifPlaceholder, + @NonNull String destination, + @NonNull Loader loader, + @Nullable ImageSizeResolver imageSizeResolver, + @Nullable ImageSize imageSize) { + super(destination, loader, imageSizeResolver, imageSize); + this.gifPlaceholder = gifPlaceholder; + } + + public void onGifResultListener(@Nullable OnGifResultListener onGifResultListener) { + this.onGifResultListener = onGifResultListener; + } + + @Override + public void setResult(@NonNull Drawable result) { + super.setResult(result); + isGif = result instanceof GifDrawable; + if (isGif && onGifResultListener != null) { + onGifResultListener.onGifResult(this); + } + } + + @Override + public void draw(@NonNull Canvas canvas) { + super.draw(canvas); + + if (isGif) { + final GifDrawable drawable = (GifDrawable) getResult(); + if (!drawable.isPlaying()) { + gifPlaceholder.setBounds(drawable.getBounds()); + gifPlaceholder.draw(canvas); + } + } + } +} diff --git a/app/src/main/java/ru/noties/markwon/GifAwareSpannableFactory.java b/app/src/main/java/ru/noties/markwon/GifAwareSpannableFactory.java new file mode 100644 index 00000000..f070e9fc --- /dev/null +++ b/app/src/main/java/ru/noties/markwon/GifAwareSpannableFactory.java @@ -0,0 +1,36 @@ +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/GifPlaceholder.java b/app/src/main/java/ru/noties/markwon/GifPlaceholder.java new file mode 100644 index 00000000..0ee66d0c --- /dev/null +++ b/app/src/main/java/ru/noties/markwon/GifPlaceholder.java @@ -0,0 +1,77 @@ +package ru.noties.markwon; + +import android.graphics.Canvas; +import android.graphics.ColorFilter; +import android.graphics.Paint; +import android.graphics.PixelFormat; +import android.graphics.Rect; +import android.graphics.drawable.Drawable; +import android.support.annotation.ColorInt; +import android.support.annotation.NonNull; +import android.support.annotation.Nullable; + +public class GifPlaceholder extends Drawable { + + private final Drawable icon; + private final Paint paint; + + private float left; + private float top; + + public GifPlaceholder(@NonNull Drawable icon, @ColorInt int background) { + this.icon = icon; + if (icon.getBounds().isEmpty()) { + icon.setBounds(0, 0, icon.getIntrinsicWidth(), icon.getIntrinsicHeight()); + } + + if (background != 0) { + paint = new Paint(Paint.ANTI_ALIAS_FLAG); + paint.setStyle(Paint.Style.FILL); + paint.setColor(background); + } else { + paint = null; + } + } + + @Override + protected void onBoundsChange(Rect bounds) { + super.onBoundsChange(bounds); + + final int w = bounds.width(); + final int h = bounds.height(); + + this.left = (w - icon.getBounds().width()) / 2; + this.top = (h - icon.getBounds().height()) / 2; + } + + @Override + public void draw(@NonNull Canvas canvas) { + + if (paint != null) { + canvas.drawRect(getBounds(), paint); + } + + final int save = canvas.save(); + try { + canvas.translate(left, top); + icon.draw(canvas); + } finally { + canvas.restoreToCount(save); + } + } + + @Override + public void setAlpha(int alpha) { + // no op + } + + @Override + public void setColorFilter(@Nullable ColorFilter colorFilter) { + // no op + } + + @Override + public int getOpacity() { + return PixelFormat.OPAQUE; + } +} diff --git a/app/src/main/java/ru/noties/markwon/GifProcessor.java b/app/src/main/java/ru/noties/markwon/GifProcessor.java new file mode 100644 index 00000000..7d2cd7c6 --- /dev/null +++ b/app/src/main/java/ru/noties/markwon/GifProcessor.java @@ -0,0 +1,125 @@ +package ru.noties.markwon; + +import android.support.annotation.NonNull; +import android.support.annotation.Nullable; +import android.text.Spannable; +import android.text.Spanned; +import android.text.style.ClickableSpan; +import android.view.View; +import android.widget.TextView; + +import pl.droidsonroids.gif.GifDrawable; +import ru.noties.markwon.spans.AsyncDrawableSpan; + +public abstract class GifProcessor { + + public abstract void process(@NonNull TextView textView); + + @NonNull + public static GifProcessor create() { + return new Impl(); + } + + static class Impl extends GifProcessor { + + @Override + public void process(@NonNull final TextView textView) { + + // here is what we will do additionally: + // we query for all asyncDrawableSpans + // we check if they are inside clickableSpan + // if not we apply onGifListener + + final Spannable spannable = spannable(textView); + if (spannable == null) { + return; + } + + final AsyncDrawableSpan[] asyncDrawableSpans = + spannable.getSpans(0, spannable.length(), AsyncDrawableSpan.class); + if (asyncDrawableSpans == null + || asyncDrawableSpans.length == 0) { + return; + } + + int start; + int end; + ClickableSpan[] clickableSpans; + + for (final AsyncDrawableSpan asyncDrawableSpan : asyncDrawableSpans) { + + start = spannable.getSpanStart(asyncDrawableSpan); + end = spannable.getSpanEnd(asyncDrawableSpan); + + if (start < 0 + || end < 0) { + continue; + } + + clickableSpans = spannable.getSpans(start, end, ClickableSpan.class); + if (clickableSpans != null + && clickableSpans.length > 0) { + continue; + } + + ((GifAwareAsyncDrawable) asyncDrawableSpan.getDrawable()).onGifResultListener(new GifAwareAsyncDrawable.OnGifResultListener() { + @Override + public void onGifResult(@NonNull GifAwareAsyncDrawable drawable) { + addGifClickSpan(textView, asyncDrawableSpan, drawable); + } + }); + } + } + + @Nullable + private static Spannable spannable(@NonNull TextView textView) { + final CharSequence charSequence = textView.getText(); + if (charSequence instanceof Spannable) { + return (Spannable) charSequence; + } + return null; + } + + private static void addGifClickSpan( + @NonNull TextView textView, + @NonNull AsyncDrawableSpan span, + @NonNull GifAwareAsyncDrawable drawable) { + + // important thing here is to obtain new spannable from textView + // as with each `setText()` new spannable is created and keeping reference + // to an older one won't affect textView + final Spannable spannable = spannable(textView); + if (spannable == null) { + return; + } + + final int start = spannable.getSpanStart(span); + final int end = spannable.getSpanEnd(span); + if (start < 0 + || end < 0) { + return; + } + + final GifDrawable gifDrawable = (GifDrawable) drawable.getResult(); + spannable.setSpan(new GifToggleClickableSpan(gifDrawable), start, end, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); + } + + private static class GifToggleClickableSpan extends ClickableSpan { + + private final GifDrawable gifDrawable; + + GifToggleClickableSpan(@NonNull GifDrawable gifDrawable) { + this.gifDrawable = gifDrawable; + } + + @Override + public void onClick(View widget) { + if (gifDrawable.isPlaying()) { + gifDrawable.pause(); + } else { + gifDrawable.start(); + } + } + } + } +} diff --git a/app/src/main/java/ru/noties/markwon/MainActivity.java b/app/src/main/java/ru/noties/markwon/MainActivity.java index 11212635..19882a74 100644 --- a/app/src/main/java/ru/noties/markwon/MainActivity.java +++ b/app/src/main/java/ru/noties/markwon/MainActivity.java @@ -28,8 +28,11 @@ public class MainActivity extends Activity { @Inject UriProcessor uriProcessor; + @Inject + GifProcessor gifProcessor; + @Override - protected void onCreate(Bundle savedInstanceState) { + protected void onCreate(final Bundle savedInstanceState) { super.onCreate(savedInstanceState); App.component(this) @@ -64,10 +67,14 @@ public class MainActivity extends Activity { markdownLoader.load(uri(), new MarkdownLoader.OnMarkdownTextLoaded() { @Override public void apply(final String text) { - markdownRenderer.render(MainActivity.this, uri(), text, new MarkdownRenderer.MarkdownReadyListener() { + markdownRenderer.render(MainActivity.this, themes.isLight(), uri(), text, new MarkdownRenderer.MarkdownReadyListener() { @Override public void onMarkdownReady(CharSequence markdown) { + Markwon.setText(textView, markdown, BetterLinkMovementMethod.getInstance()); + + gifProcessor.process(textView); + 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 f276dfe2..23ff268b 100644 --- a/app/src/main/java/ru/noties/markwon/MarkdownRenderer.java +++ b/app/src/main/java/ru/noties/markwon/MarkdownRenderer.java @@ -14,6 +14,12 @@ 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.syntax.Prism4jTheme; +import ru.noties.markwon.syntax.Prism4jThemeDarkula; +import ru.noties.markwon.syntax.Prism4jThemeDefault; +import ru.noties.prism4j.Prism4j; @ActivityScope public class MarkdownRenderer { @@ -31,6 +37,15 @@ public class MarkdownRenderer { @Inject Handler handler; + @Inject + Prism4j prism4j; + + @Inject + Prism4jThemeDefault prism4jThemeDefault; + + @Inject + Prism4jThemeDarkula prism4JThemeDarkula; + private Future task; @Inject @@ -39,10 +54,15 @@ public class MarkdownRenderer { public void render( @NonNull final Context context, + final boolean isLightTheme, @Nullable final Uri uri, @NonNull final String markdown, @NonNull final MarkdownReadyListener listener) { + + // todo: create prism4j theme factory (accepting light/dark argument) + cancel(); + task = service.submit(new Runnable() { @Override public void run() { @@ -54,9 +74,28 @@ public class MarkdownRenderer { urlProcessor = new UrlProcessorRelativeToAbsolute(uri.toString()); } + final Prism4jTheme prism4jTheme = isLightTheme + ? 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)) .build(); final long start = SystemClock.uptimeMillis(); diff --git a/app/src/main/java/ru/noties/markwon/Themes.java b/app/src/main/java/ru/noties/markwon/Themes.java index d6a9ae8b..85ab846c 100644 --- a/app/src/main/java/ru/noties/markwon/Themes.java +++ b/app/src/main/java/ru/noties/markwon/Themes.java @@ -25,9 +25,9 @@ public class Themes { // we have only 2 themes and Light one is default final int theme; if (dark) { - theme = R.style.AppThemeBaseDark; + theme = R.style.AppThemeDark; } else { - theme = R.style.AppThemeBaseLight; + theme = R.style.AppThemeLight; } final Context appContext = context.getApplicationContext(); @@ -43,4 +43,8 @@ public class Themes { .putBoolean(KEY_THEME_DARK, newValue) .apply(); } + + public boolean isLight() { + return !preferences.getBoolean(KEY_THEME_DARK, false); + } } diff --git a/app/src/main/res/drawable-hdpi/ic_play_circle_filled_18dp_white.png b/app/src/main/res/drawable-hdpi/ic_play_circle_filled_18dp_white.png new file mode 100644 index 0000000000000000000000000000000000000000..7354e7958159ee8f693916467ce6258e81fce2cf GIT binary patch literal 322 zcmV-I0lof-P)835loK2;l-A!BEQu?7f6PmD-`tF~ltFp9BlvWSd#O zFgyEZ8T+%)!yGX(vm_GX|a*ElM{h;={}m>+m$M)xFl)l5?(i#N2BpwNA%?h2*}}f z74UWQ7FodpOVe}*G#aMzeA`>%l0R=s_xR^bChQ#!xtx<5cC28iLDiG>WKW<=(3sg!lfo}^M(Dyl=5ST$FEMcRF zRx+Rxwy@PkM#-QhT(r=E25?oN0yPXJR+rs>2TherR|eEL52|YVMqr3514^)puTR@MoyTJ8~z(>%o-kl z94##W#xoy~fuZcj*V^z$wokJ-(e%yexl?A)BPujRLvqrY{GHNuM`mbe?dJk=jNb#@ Y0nTR@NN%!G9RL6T07*qoM6N<$f-^>&X#fBK literal 0 HcmV?d00001 diff --git a/app/src/main/res/drawable-xxhdpi/ic_play_circle_filled_18dp_white.png b/app/src/main/res/drawable-xxhdpi/ic_play_circle_filled_18dp_white.png new file mode 100644 index 0000000000000000000000000000000000000000..53dadfdf450c5745cd9aaa63d32caac1f42ff1ec GIT binary patch literal 536 zcmV+z0_XjSP)@EXyj28N^u_zlo+#+cT zvyB_PLZtyp2nR)7VF<21k<|ws;1N=Y62%=v$jtrb4 zs3u7B(qaS?`evkxMh1Fjruto()P;Yw@PER#5cc6yRdB5czQer`SM-LdYG9r2lrR>+ zp1mtO)G)eth&KT)?hb45V03dwU}w`E*5|_Jx4Cw9-Mg?YfwAqJA>xFk7=iVL7@nO8 zCoC_AG40GbgDQG3hMi?+NKF@}w6prpFnt?iavN8gAS+G%UGbxiX+LTT?k%NUbyiu3 z8}6_qv&Fu2bo;aS-OAtOj2hcc>k%Jh|R?ZO+a ak^TTD{k)j@7Xn-W0000 + #424242 #212121 #4caf50 + + #FFF + #dd000000 + + #FFF + #dd000000 + + #303030 + #ddffffff + diff --git a/app/src/main/res/values/styles.xml b/app/src/main/res/values/styles.xml index d8703dd5..1da91be9 100644 --- a/app/src/main/res/values/styles.xml +++ b/app/src/main/res/values/styles.xml @@ -10,7 +10,14 @@ @drawable/ic_app_bar_theme_dark - + + diff --git a/build.gradle b/build.gradle index ef441eaf..c0b701ed 100644 --- a/build.gradle +++ b/build.gradle @@ -10,6 +10,9 @@ buildscript { allprojects { repositories { + if (project.hasProperty('LOCAL_MAVEN_URL')) { + maven { url LOCAL_MAVEN_URL } + } jcenter() google() } @@ -22,28 +25,31 @@ task clean(type: Delete) { } task wrapper(type: Wrapper) { - gradleVersion '4.5' + gradleVersion '4.8.1' distributionType 'all' } ext { // Config - BUILD_TOOLS = '26.0.3' - TARGET_SDK = 26 + BUILD_TOOLS = '27.0.3' + TARGET_SDK = 27 MIN_SDK = 16 // Dependencies - final def supportVersion = '26.1.0' + final def supportVersion = '27.1.1' SUPPORT_ANNOTATIONS = "com.android.support:support-annotations:$supportVersion" SUPPORT_APP_COMPAT = "com.android.support:appcompat-v7:$supportVersion" - final def commonMarkVersion = '0.10.0' + final def commonMarkVersion = '0.11.0' COMMON_MARK = "com.atlassian.commonmark:commonmark:$commonMarkVersion" COMMON_MARK_STRIKETHROUGHT = "com.atlassian.commonmark:commonmark-ext-gfm-strikethrough:$commonMarkVersion" COMMON_MARK_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.8' + ANDROID_GIF = 'pl.droidsonroids.gif:android-gif-drawable:1.2.14' OK_HTTP = 'com.squareup.okhttp3:okhttp:3.9.0' + + PRISM_4J = 'ru.noties:prism4j:1.1.0' + PRISM_4J_BUNDLER = 'ru.noties:prism4j-bundler:1.1.0' } diff --git a/gradle.properties b/gradle.properties index f3424164..c9111b02 100644 --- a/gradle.properties +++ b/gradle.properties @@ -6,7 +6,7 @@ org.gradle.configureondemand=true android.enableBuildCache=true android.buildCacheDir=build/pre-dex-cache -VERSION_NAME=1.0.6 +VERSION_NAME=1.1.0 GROUP=ru.noties POM_DESCRIPTION=Markwon diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar index 27768f1bbac3ce2d055b20d521f12da78d331e8e..a5fe1cb94b9ee5ce57e6113458225bcba12d83e3 100644 GIT binary patch delta 15755 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 delta 16228 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 mediaDecoders; private final Map> requests; @@ -67,6 +61,7 @@ public class AsyncDrawableLoader implements AsyncDrawable.Loader { this.executorService = builder.executorService; this.mainThread = new Handler(Looper.getMainLooper()); this.errorDrawable = builder.errorDrawable; + this.mediaDecoders = builder.mediaDecoders; this.requests = new HashMap<>(3); } @@ -105,12 +100,15 @@ public class AsyncDrawableLoader implements AsyncDrawable.Loader { public void run() { final Item item; + final boolean isFromFile; final Uri uri = Uri.parse(destination); if ("file".equals(uri.getScheme())) { item = fromFile(uri); + isFromFile = true; } else { item = fromNetwork(destination); + isFromFile = false; } Drawable result = null; @@ -118,13 +116,15 @@ public class AsyncDrawableLoader implements AsyncDrawable.Loader { if (item != null && item.inputStream != null) { try { - if (CONTENT_TYPE_SVG.equals(item.type)) { - result = handleSvg(item.inputStream); - } else if (CONTENT_TYPE_GIF.equals(item.type)) { - result = handleGif(item.inputStream); - } else { - result = handleSimple(item.inputStream); + + final MediaDecoder mediaDecoder = isFromFile + ? mediaDecoderFromFile(item.fileName) + : mediaDecoderFromContentType(item.contentType); + + if (mediaDecoder != null) { + result = mediaDecoder.decode(item.inputStream); } + } finally { try { item.inputStream.close(); @@ -157,7 +157,8 @@ public class AsyncDrawableLoader implements AsyncDrawable.Loader { }); } - private Item fromFile(Uri uri) { + @Nullable + private Item fromFile(@NonNull Uri uri) { final List segments = uri.getPathSegments(); if (segments == null @@ -167,19 +168,10 @@ public class AsyncDrawableLoader implements AsyncDrawable.Loader { } final Item out; - final String type; final InputStream inputStream; final boolean assets = FILE_ANDROID_ASSETS.equals(segments.get(0)); - final String lastSegment = uri.getLastPathSegment(); - - if (lastSegment.endsWith(".svg")) { - type = CONTENT_TYPE_SVG; - } else if (lastSegment.endsWith(".gif")) { - type = CONTENT_TYPE_GIF; - } else { - type = null; - } + final String fileName = uri.getLastPathSegment(); if (assets) { final StringBuilder path = new StringBuilder(); @@ -208,7 +200,7 @@ public class AsyncDrawableLoader implements AsyncDrawable.Loader { } if (inputStream != null) { - out = new Item(type, inputStream); + out = new Item(fileName, null, inputStream); } else { out = null; } @@ -216,7 +208,8 @@ public class AsyncDrawableLoader implements AsyncDrawable.Loader { return out; } - private Item fromNetwork(String destination) { + @Nullable + private Item fromNetwork(@NonNull String destination) { Item out = null; @@ -237,15 +230,8 @@ public class AsyncDrawableLoader implements AsyncDrawable.Loader { if (body != null) { final InputStream inputStream = body.byteStream(); if (inputStream != null) { - final String type; final String contentType = response.header(HEADER_CONTENT_TYPE); - if (!TextUtils.isEmpty(contentType) - && contentType.startsWith(CONTENT_TYPE_SVG)) { - type = CONTENT_TYPE_SVG; - } else { - type = contentType; - } - out = new Item(type, inputStream); + out = new Item(null, contentType, inputStream); } } } @@ -253,87 +239,31 @@ public class AsyncDrawableLoader implements AsyncDrawable.Loader { return out; } - private Drawable handleSvg(InputStream stream) { + @Nullable + private MediaDecoder mediaDecoderFromFile(@NonNull String fileName) { - final Drawable out; + MediaDecoder out = null; - SVG svg = null; - try { - svg = SVG.getFromInputStream(stream); - } catch (SVGParseException e) { - e.printStackTrace(); - } - - if (svg == null) { - out = null; - } else { - - final float w = svg.getDocumentWidth(); - final float h = svg.getDocumentHeight(); - final float density = resources.getDisplayMetrics().density; - - final int width = (int) (w * density + .5F); - final int height = (int) (h * density + .5F); - - final Bitmap bitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_4444); - final Canvas canvas = new Canvas(bitmap); - canvas.scale(density, density); - svg.renderToCanvas(canvas); - - out = new BitmapDrawable(resources, bitmap); - DrawableUtils.intrinsicBounds(out); - } - - return out; - } - - private Drawable handleGif(InputStream stream) { - - Drawable out = null; - - final byte[] bytes = readBytes(stream); - if (bytes != null) { - try { - out = new GifDrawable(bytes); - DrawableUtils.intrinsicBounds(out); - } catch (IOException e) { - e.printStackTrace(); + for (MediaDecoder mediaDecoder : mediaDecoders) { + if (mediaDecoder.canDecodeByFileName(fileName)) { + out = mediaDecoder; + break; } } return out; } - private Drawable handleSimple(InputStream stream) { + @Nullable + private MediaDecoder mediaDecoderFromContentType(@Nullable String contentType) { - final Drawable out; + MediaDecoder out = null; - final Bitmap bitmap = BitmapFactory.decodeStream(stream); - if (bitmap != null) { - out = new BitmapDrawable(resources, bitmap); - DrawableUtils.intrinsicBounds(out); - } else { - out = null; - } - - return out; - } - - private static byte[] readBytes(InputStream stream) { - - byte[] out = null; - - try { - final ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); - final int length = 1024 * 8; - final byte[] buffer = new byte[length]; - int read; - while ((read = stream.read(buffer, 0, length)) != -1) { - outputStream.write(buffer, 0, read); + for (MediaDecoder mediaDecoder : mediaDecoders) { + if (mediaDecoder.canDecodeByContentType(contentType)) { + out = mediaDecoder; + break; } - out = outputStream.toByteArray(); - } catch (IOException e) { - e.printStackTrace(); } return out; @@ -346,47 +276,93 @@ public class AsyncDrawableLoader implements AsyncDrawable.Loader { private ExecutorService executorService; private Drawable errorDrawable; + // @since 1.1.0 + private final List mediaDecoders = new ArrayList<>(3); + + + @NonNull 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; } - public Builder executorService(ExecutorService executorService) { + @NonNull + public Builder executorService(@NonNull ExecutorService executorService) { this.executorService = executorService; return this; } - public Builder errorDrawable(Drawable errorDrawable) { + @NonNull + public Builder errorDrawable(@NonNull Drawable errorDrawable) { this.errorDrawable = errorDrawable; return this; } + @NonNull + public Builder mediaDecoders(@NonNull List mediaDecoders) { + this.mediaDecoders.clear(); + this.mediaDecoders.addAll(mediaDecoders); + return this; + } + + @NonNull + public Builder mediaDecoders(MediaDecoder... mediaDecoders) { + this.mediaDecoders.clear(); + if (mediaDecoders != null + && mediaDecoders.length > 0) { + Collections.addAll(this.mediaDecoders, mediaDecoders); + } + return this; + } + + @NonNull public AsyncDrawableLoader build() { + if (client == null) { client = new OkHttpClient(); } + if (resources == null) { resources = Resources.getSystem(); } + if (executorService == null) { // we will use executor from okHttp executorService = client.dispatcher().executorService(); } + + // add default media decoders if not specified + if (mediaDecoders.size() == 0) { + mediaDecoders.add(SvgMediaDecoder.create(resources)); + mediaDecoders.add(GifMediaDecoder.create(true)); + mediaDecoders.add(ImageMediaDecoder.create(resources)); + } + return new AsyncDrawableLoader(this); } } private static class Item { - final String type; + + final String fileName; + final String contentType; final InputStream inputStream; - Item(String type, InputStream inputStream) { - this.type = type; + Item(@Nullable String fileName, @Nullable String contentType, @Nullable InputStream inputStream) { + this.fileName = fileName; + this.contentType = contentType; this.inputStream = inputStream; } } diff --git a/library-image-loader/src/main/java/ru/noties/markwon/il/GifMediaDecoder.java b/library-image-loader/src/main/java/ru/noties/markwon/il/GifMediaDecoder.java new file mode 100644 index 00000000..ac2a7cbf --- /dev/null +++ b/library-image-loader/src/main/java/ru/noties/markwon/il/GifMediaDecoder.java @@ -0,0 +1,90 @@ +package ru.noties.markwon.il; + +import android.graphics.drawable.Drawable; +import android.support.annotation.NonNull; +import android.support.annotation.Nullable; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.InputStream; + +import pl.droidsonroids.gif.GifDrawable; + +/** + * @since 1.1.0 + */ +public class GifMediaDecoder extends MediaDecoder { + + protected static final String CONTENT_TYPE_GIF = "image/gif"; + protected static final String FILE_EXTENSION_GIF = ".gif"; + + @NonNull + public static GifMediaDecoder create(boolean autoPlayGif) { + return new GifMediaDecoder(autoPlayGif); + } + + private final boolean autoPlayGif; + + protected GifMediaDecoder(boolean autoPlayGif) { + this.autoPlayGif = autoPlayGif; + } + + @Override + public boolean canDecodeByContentType(@Nullable String contentType) { + return CONTENT_TYPE_GIF.equals(contentType); + } + + @Override + public boolean canDecodeByFileName(@NonNull String fileName) { + return fileName.endsWith(FILE_EXTENSION_GIF); + } + + @Nullable + @Override + public Drawable decode(@NonNull InputStream inputStream) { + + Drawable out = null; + + final byte[] bytes = readBytes(inputStream); + if (bytes != null) { + try { + out = newGifDrawable(bytes); + DrawableUtils.intrinsicBounds(out); + + if (!autoPlayGif) { + ((GifDrawable) out).pause(); + } + } catch (IOException e) { + e.printStackTrace(); + } + } + + return out; + } + + @NonNull + protected Drawable newGifDrawable(@NonNull byte[] bytes) throws IOException { + return new GifDrawable(bytes); + } + + @Nullable + protected static byte[] readBytes(@NonNull InputStream stream) { + + byte[] out = null; + + try { + final ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); + final int length = 1024 * 8; + final byte[] buffer = new byte[length]; + int read; + while ((read = stream.read(buffer, 0, length)) != -1) { + outputStream.write(buffer, 0, read); + } + out = outputStream.toByteArray(); + } catch (IOException e) { + e.printStackTrace(); + } + + return out; + } +} diff --git a/library-image-loader/src/main/java/ru/noties/markwon/il/ImageMediaDecoder.java b/library-image-loader/src/main/java/ru/noties/markwon/il/ImageMediaDecoder.java new file mode 100644 index 00000000..b59ea65a --- /dev/null +++ b/library-image-loader/src/main/java/ru/noties/markwon/il/ImageMediaDecoder.java @@ -0,0 +1,58 @@ +package ru.noties.markwon.il; + +import android.content.res.Resources; +import android.graphics.Bitmap; +import android.graphics.BitmapFactory; +import android.graphics.drawable.BitmapDrawable; +import android.graphics.drawable.Drawable; +import android.support.annotation.NonNull; +import android.support.annotation.Nullable; + +import java.io.InputStream; + +/** + * This class can be used as the last {@link MediaDecoder} to _try_ to handle all rest cases. + * Here we just assume that supplied InputStream is of image type and try to decode it. + * + * @since 1.1.0 + */ +public class ImageMediaDecoder extends MediaDecoder { + + @NonNull + public static ImageMediaDecoder create(@NonNull Resources resources) { + return new ImageMediaDecoder(resources); + } + + private final Resources resources; + + ImageMediaDecoder(Resources resources) { + this.resources = resources; + } + + @Override + public boolean canDecodeByContentType(@Nullable String contentType) { + return true; + } + + @Override + public boolean canDecodeByFileName(@NonNull String fileName) { + return true; + } + + @Nullable + @Override + public Drawable decode(@NonNull InputStream inputStream) { + + final Drawable out; + + final Bitmap bitmap = BitmapFactory.decodeStream(inputStream); + if (bitmap != null) { + out = new BitmapDrawable(resources, bitmap); + DrawableUtils.intrinsicBounds(out); + } else { + out = null; + } + + return out; + } +} diff --git a/library-image-loader/src/main/java/ru/noties/markwon/il/MediaDecoder.java b/library-image-loader/src/main/java/ru/noties/markwon/il/MediaDecoder.java new file mode 100644 index 00000000..294b716b --- /dev/null +++ b/library-image-loader/src/main/java/ru/noties/markwon/il/MediaDecoder.java @@ -0,0 +1,20 @@ +package ru.noties.markwon.il; + +import android.graphics.drawable.Drawable; +import android.support.annotation.NonNull; +import android.support.annotation.Nullable; + +import java.io.InputStream; + +/** + * @since 1.1.0 + */ +public abstract class MediaDecoder { + + public abstract boolean canDecodeByContentType(@Nullable String contentType); + + public abstract boolean canDecodeByFileName(@NonNull String fileName); + + @Nullable + public abstract Drawable decode(@NonNull InputStream inputStream); +} diff --git a/library-image-loader/src/main/java/ru/noties/markwon/il/SvgMediaDecoder.java b/library-image-loader/src/main/java/ru/noties/markwon/il/SvgMediaDecoder.java new file mode 100644 index 00000000..0bd62644 --- /dev/null +++ b/library-image-loader/src/main/java/ru/noties/markwon/il/SvgMediaDecoder.java @@ -0,0 +1,80 @@ +package ru.noties.markwon.il; + +import android.content.res.Resources; +import android.graphics.Bitmap; +import android.graphics.Canvas; +import android.graphics.drawable.BitmapDrawable; +import android.graphics.drawable.Drawable; +import android.support.annotation.NonNull; +import android.support.annotation.Nullable; + +import com.caverock.androidsvg.SVG; +import com.caverock.androidsvg.SVGParseException; + +import java.io.InputStream; + +/** + * @since 1.1.0 + */ +public class SvgMediaDecoder extends MediaDecoder { + + private static final String CONTENT_TYPE_SVG = "image/svg+xml"; + private static final String FILE_EXTENSION_SVG = ".svg"; + + @NonNull + public static SvgMediaDecoder create(@NonNull Resources resources) { + return new SvgMediaDecoder(resources); + } + + private final Resources resources; + + SvgMediaDecoder(Resources resources) { + this.resources = resources; + } + + @Override + public boolean canDecodeByContentType(@Nullable String contentType) { + return contentType != null && contentType.startsWith(CONTENT_TYPE_SVG); + } + + @Override + public boolean canDecodeByFileName(@NonNull String fileName) { + return fileName.endsWith(FILE_EXTENSION_SVG); + } + + @Nullable + @Override + public Drawable decode(@NonNull InputStream inputStream) { + + final Drawable out; + + SVG svg = null; + try { + svg = SVG.getFromInputStream(inputStream); + } catch (SVGParseException e) { + e.printStackTrace(); + } + + if (svg == null) { + out = null; + } else { + + final float w = svg.getDocumentWidth(); + final float h = svg.getDocumentHeight(); + final float density = resources.getDisplayMetrics().density; + + final int width = (int) (w * density + .5F); + final int height = (int) (h * density + .5F); + + final Bitmap bitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_4444); + final Canvas canvas = new Canvas(bitmap); + canvas.scale(density, density); + svg.renderToCanvas(canvas); + + out = new BitmapDrawable(resources, bitmap); + DrawableUtils.intrinsicBounds(out); + } + + return out; + } +} diff --git a/library-syntax/README.md b/library-syntax/README.md new file mode 100644 index 00000000..38d65358 --- /dev/null +++ b/library-syntax/README.md @@ -0,0 +1,64 @@ +# Markwon-syntax + +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](./art/markwon-syntax-default.png) + +![theme-darkula](./art/markwon-syntax-darkula.png) + +--- + +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 + ); +} +``` + +> You can extend `Prism4jThemeBase` which has some helper methods diff --git a/library-syntax/art/markwon-syntax-darkula.png b/library-syntax/art/markwon-syntax-darkula.png new file mode 100644 index 0000000000000000000000000000000000000000..e9cde95e4690663189a362eb5681e9d5da453f83 GIT binary patch literal 30908 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 NNQ*0o)ruGf{U4Xl9gqM3 literal 0 HcmV?d00001 diff --git a/library-syntax/art/markwon-syntax-default.png b/library-syntax/art/markwon-syntax-default.png new file mode 100644 index 0000000000000000000000000000000000000000..5cae944f2c67a710b245118695ec8777cbb81f0c GIT binary patch literal 33603 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 diff --git a/library-syntax/src/main/java/ru/noties/markwon/syntax/Prism4jSyntaxHighlight.java b/library-syntax/src/main/java/ru/noties/markwon/syntax/Prism4jSyntaxHighlight.java new file mode 100644 index 00000000..4f3c1c04 --- /dev/null +++ b/library-syntax/src/main/java/ru/noties/markwon/syntax/Prism4jSyntaxHighlight.java @@ -0,0 +1,105 @@ +package ru.noties.markwon.syntax; + +import android.support.annotation.NonNull; +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 { + + @NonNull + public static Prism4jSyntaxHighlight create( + @NonNull Prism4j prism4j, + @NonNull Prism4jTheme theme) { + return new Prism4jSyntaxHighlight(prism4j, theme, null); + } + + @NonNull + public static Prism4jSyntaxHighlight create( + @NonNull Prism4j prism4j, + @NonNull Prism4jTheme theme, + @Nullable String fallback) { + return new Prism4jSyntaxHighlight(prism4j, theme, fallback); + } + + private final Prism4j prism4j; + private final Prism4jTheme theme; + private final String fallback; + + protected Prism4jSyntaxHighlight( + @NonNull Prism4j prism4j, + @NonNull Prism4jTheme theme, + @Nullable String fallback) { + this.prism4j = prism4j; + this.theme = theme; + this.fallback = fallback; + } + + @NonNull + @Override + public CharSequence highlight(@Nullable String info, @NonNull String code) { + // if info is null, do not highlight -> LICENCE footer very commonly wrapped inside code + // block without syntax name specified (so, do not highlight) + return info == null + ? highlightNoLanguageInfo(code) + : highlightWithLanguageInfo(info, code); + } + + @NonNull + protected CharSequence highlightNoLanguageInfo(@NonNull String code) { + return code; + } + + @NonNull + protected CharSequence highlightWithLanguageInfo(@NonNull String info, @NonNull String code) { + + final CharSequence out; + + final String language; + final Prism4j.Grammar grammar; + { + String _language = info; + Prism4j.Grammar _grammar = prism4j.grammar(info); + if (_grammar == null && !TextUtils.isEmpty(fallback)) { + _language = fallback; + _grammar = prism4j.grammar(fallback); + } + language = _language; + grammar = _grammar; + } + + if (grammar != null) { + out = highlight(language, grammar, code); + } else { + out = code; + } + + return out; + } + + @NonNull + protected CharSequence highlight(@NonNull String language, @NonNull Prism4j.Grammar grammar, @NonNull String code) { + final SpannableStringBuilder builder = new SpannableStringBuilder(); + final Prism4jSyntaxVisitor visitor = new Prism4jSyntaxVisitor(language, theme, builder); + visitor.visit(prism4j.tokenize(code, grammar)); + return builder; + } + + @NonNull + protected Prism4j prism4j() { + return prism4j; + } + + @NonNull + protected Prism4jTheme theme() { + return theme; + } + + @Nullable + protected String fallback() { + return fallback; + } +} diff --git a/library-syntax/src/main/java/ru/noties/markwon/syntax/Prism4jSyntaxVisitor.java b/library-syntax/src/main/java/ru/noties/markwon/syntax/Prism4jSyntaxVisitor.java new file mode 100644 index 00000000..cd7f2f04 --- /dev/null +++ b/library-syntax/src/main/java/ru/noties/markwon/syntax/Prism4jSyntaxVisitor.java @@ -0,0 +1,40 @@ +package ru.noties.markwon.syntax; + +import android.support.annotation.NonNull; +import android.text.SpannableStringBuilder; + +import ru.noties.prism4j.AbsVisitor; +import ru.noties.prism4j.Prism4j; + +class Prism4jSyntaxVisitor extends AbsVisitor { + + private final String language; + private final Prism4jTheme theme; + private final SpannableStringBuilder builder; + + Prism4jSyntaxVisitor( + @NonNull String language, + @NonNull Prism4jTheme theme, + @NonNull SpannableStringBuilder builder) { + this.language = language; + this.theme = theme; + this.builder = builder; + } + + @Override + protected void visitText(@NonNull Prism4j.Text text) { + builder.append(text.literal()); + } + + @Override + protected void visitSyntax(@NonNull Prism4j.Syntax syntax) { + + final int start = builder.length(); + visit(syntax.children()); + final int end = builder.length(); + + if (end != start) { + theme.apply(language, syntax, builder, start, end); + } + } +} diff --git a/library-syntax/src/main/java/ru/noties/markwon/syntax/Prism4jTheme.java b/library-syntax/src/main/java/ru/noties/markwon/syntax/Prism4jTheme.java new file mode 100644 index 00000000..3932bafc --- /dev/null +++ b/library-syntax/src/main/java/ru/noties/markwon/syntax/Prism4jTheme.java @@ -0,0 +1,24 @@ +package ru.noties.markwon.syntax; + +import android.support.annotation.ColorInt; +import android.support.annotation.NonNull; +import android.text.SpannableStringBuilder; + +import ru.noties.prism4j.Prism4j; + +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 + ); +} diff --git a/library-syntax/src/main/java/ru/noties/markwon/syntax/Prism4jThemeBase.java b/library-syntax/src/main/java/ru/noties/markwon/syntax/Prism4jThemeBase.java new file mode 100644 index 00000000..5cf784df --- /dev/null +++ b/library-syntax/src/main/java/ru/noties/markwon/syntax/Prism4jThemeBase.java @@ -0,0 +1,140 @@ +package ru.noties.markwon.syntax; + +import android.support.annotation.ColorInt; +import android.support.annotation.FloatRange; +import android.support.annotation.IntRange; +import android.support.annotation.NonNull; +import android.support.annotation.Nullable; +import android.text.SpannableStringBuilder; +import android.text.Spanned; +import android.text.style.ForegroundColorSpan; + +import java.util.HashMap; + +import ru.noties.prism4j.Prism4j; + +public abstract class Prism4jThemeBase implements Prism4jTheme { + + @ColorInt + protected static int applyAlpha(@IntRange(from = 0, to = 255) int alpha, @ColorInt int color) { + return (color & 0x00FFFFFF) | (alpha << 24); + } + + @ColorInt + protected static int applyAlpha(@FloatRange(from = .0F, to = 1.F) float alpha, @ColorInt int color) { + return applyAlpha((int) (255 * alpha + .5F), color); + } + + protected static boolean isOfType(@NonNull String expected, @NonNull String type, @Nullable String alias) { + return expected.equals(type) || expected.equals(alias); + } + + private final ColorHashMap colorHashMap; + + protected Prism4jThemeBase() { + this.colorHashMap = init(); + } + + @NonNull + protected abstract ColorHashMap init(); + + @ColorInt + protected int color(@NonNull String language, @NonNull String type, @Nullable String alias) { + + Color color = colorHashMap.get(type); + if (color == null + && alias != null) { + color = colorHashMap.get(alias); + } + + return color != null + ? color.color + : 0; + } + + @Override + public void apply( + @NonNull String language, + @NonNull Prism4j.Syntax syntax, + @NonNull SpannableStringBuilder builder, + int start, + int end) { + + final String type = syntax.type(); + final String alias = syntax.alias(); + + final int color = color(language, type, alias); + if (color != 0) { + applyColor(language, type, alias, color, builder, start, end); + } + } + + @SuppressWarnings("unused") + protected void applyColor( + @NonNull String language, + @NonNull String type, + @Nullable String alias, + @ColorInt int color, + @NonNull SpannableStringBuilder builder, + int start, + int end) { + builder.setSpan(new ForegroundColorSpan(color), start, end, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); + } + + protected static class Color { + + @NonNull + public static Color of(@ColorInt int color) { + return new Color(color); + } + + @ColorInt + protected final int color; + + protected Color(@ColorInt int color) { + this.color = color; + } + } + + protected static class ColorHashMap extends HashMap { + + @NonNull + protected ColorHashMap add(@ColorInt int color, String name) { + put(name, Color.of(color)); + return this; + } + + @NonNull + protected ColorHashMap add( + @ColorInt int color, + @NonNull String name1, + @NonNull String name2) { + final Color c = Color.of(color); + put(name1, c); + put(name2, c); + return this; + } + + @NonNull + protected ColorHashMap add( + @ColorInt int color, + @NonNull String name1, + @NonNull String name2, + @NonNull String name3) { + final Color c = Color.of(color); + put(name1, c); + put(name2, c); + put(name3, c); + return this; + } + + @NonNull + protected ColorHashMap add(@ColorInt int color, String... names) { + final Color c = Color.of(color); + for (String name : names) { + put(name, c); + } + return this; + } + } +} diff --git a/library-syntax/src/main/java/ru/noties/markwon/syntax/Prism4jThemeDarkula.java b/library-syntax/src/main/java/ru/noties/markwon/syntax/Prism4jThemeDarkula.java new file mode 100644 index 00000000..7d5c90b4 --- /dev/null +++ b/library-syntax/src/main/java/ru/noties/markwon/syntax/Prism4jThemeDarkula.java @@ -0,0 +1,61 @@ +package ru.noties.markwon.syntax; + +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; + +public class Prism4jThemeDarkula extends Prism4jThemeBase { + + @NonNull + public static Prism4jThemeDarkula create() { + return new Prism4jThemeDarkula(); + } + + @Override + public int background() { + return 0xFF2d2d2d; + } + + @Override + public int textColor() { + return 0xFFa9b7c6; + } + + @NonNull + @Override + protected ColorHashMap init() { + return new ColorHashMap() + .add(0xFF808080, "comment", "prolog", "cdata") + .add(0xFFcc7832, "delimiter", "boolean", "keyword", "selector", "important", "atrule") + .add(0xFFa9b7c6, "operator", "punctuation", "attr-name") + .add(0xFFe8bf6a, "tag", "doctype", "builtin") + .add(0xFF6897bb, "entity", "number", "symbol") + .add(0xFF9876aa, "property", "constant", "variable") + .add(0xFF6a8759, "string", "char") + .add(0xFFbbb438, "annotation") + .add(0xFFa5c261, "attr-value") + .add(0xFF287bde, "url") + .add(0xFFffc66d, "function") + .add(0xFF364135, "regex") + .add(0xFF294436, "inserted") + .add(0xFF484a4a, "deleted"); + } + + @Override + protected void applyColor(@NonNull String language, @NonNull String type, @Nullable String alias, int color, @NonNull SpannableStringBuilder builder, int start, int end) { + super.applyColor(language, type, alias, color, builder, start, end); + + if (isOfType("important", type, alias) + || isOfType("bold", type, alias)) { + builder.setSpan(new StrongEmphasisSpan(), start, end, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); + } + + if (isOfType("italic", type, alias)) { + builder.setSpan(new EmphasisSpan(), start, end, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); + } + } +} diff --git a/library-syntax/src/main/java/ru/noties/markwon/syntax/Prism4jThemeDefault.java b/library-syntax/src/main/java/ru/noties/markwon/syntax/Prism4jThemeDefault.java new file mode 100644 index 00000000..22354dce --- /dev/null +++ b/library-syntax/src/main/java/ru/noties/markwon/syntax/Prism4jThemeDefault.java @@ -0,0 +1,75 @@ +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 android.text.style.BackgroundColorSpan; + +import ru.noties.markwon.spans.EmphasisSpan; +import ru.noties.markwon.spans.StrongEmphasisSpan; + +public class Prism4jThemeDefault extends Prism4jThemeBase { + + @NonNull + public static Prism4jThemeDefault create() { + return new Prism4jThemeDefault(); + } + + @Override + public int background() { + return 0xFFf5f2f0; + } + + @Override + public int textColor() { + return 0xdd000000; + } + + @NonNull + @Override + protected ColorHashMap init() { + return new ColorHashMap() + .add(0xFF708090, "comment", "prolog", "doctype", "cdata") + .add(0xFF999999, "punctuation") + .add(0xFF990055, "property", "tag", "boolean", "number", "constant", "symbol", "deleted") + .add(0xFF669900, "selector", "attr-name", "string", "char", "builtin", "inserted") + .add(0xFF9a6e3a, "operator", "entity", "url") + .add(0xFF0077aa, "atrule", "attr-value", "keyword") + .add(0xFFDD4A68, "function", "class-name") + .add(0xFFee9900, "regex", "important", "variable"); + } + + @Override + protected void applyColor( + @NonNull String language, + @NonNull String type, + @Nullable String alias, + @ColorInt int color, + @NonNull SpannableStringBuilder builder, + int start, + int end) { + + if ("css".equals(language) && isOfType("string", type, alias)) { + super.applyColor(language, type, alias, 0xFF9a6e3a, builder, start, end); + builder.setSpan(new BackgroundColorSpan(0x80ffffff), start, end, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); + return; + } + + if (isOfType("namespace", type, alias)) { + color = applyAlpha(.7F, color); + } + + super.applyColor(language, type, alias, color, builder, start, end); + + if (isOfType("important", type, alias) + || isOfType("bold", type, alias)) { + builder.setSpan(new StrongEmphasisSpan(), start, end, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); + } + + if (isOfType("italic", type, alias)) { + builder.setSpan(new EmphasisSpan(), start, end, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); + } + } +} diff --git a/library-view/build.gradle b/library-view/build.gradle index bc867655..6982037a 100644 --- a/library-view/build.gradle +++ b/library-view/build.gradle @@ -18,6 +18,14 @@ dependencies { compileOnly SUPPORT_APP_COMPAT } -if (project.hasProperty('release')) { +afterEvaluate { + generateReleaseBuildConfig.enabled = false +} + +if (hasProperty('release')) { + if (hasProperty('local')) { + ext.RELEASE_REPOSITORY_URL = LOCAL_MAVEN_URL + ext.SNAPSHOT_REPOSITORY_URL = LOCAL_MAVEN_URL + } apply from: 'https://raw.githubusercontent.com/noties/gradle-mvn-push/master/gradle-mvn-push-aar.gradle' } diff --git a/library/build.gradle b/library/build.gradle index dc757dfe..795f7407 100644 --- a/library/build.gradle +++ b/library/build.gradle @@ -20,6 +20,14 @@ dependencies { api COMMON_MARK_TABLE } -if (project.hasProperty('release')) { +afterEvaluate { + generateReleaseBuildConfig.enabled = false +} + +if (hasProperty('release')) { + if (hasProperty('local')) { + ext.RELEASE_REPOSITORY_URL = LOCAL_MAVEN_URL + ext.SNAPSHOT_REPOSITORY_URL = LOCAL_MAVEN_URL + } apply from: 'https://raw.githubusercontent.com/noties/gradle-mvn-push/master/gradle-mvn-push-aar.gradle' } diff --git a/library/src/main/java/ru/noties/markwon/SpannableBuilder.java b/library/src/main/java/ru/noties/markwon/SpannableBuilder.java index 95570900..5cc2420f 100644 --- a/library/src/main/java/ru/noties/markwon/SpannableBuilder.java +++ b/library/src/main/java/ru/noties/markwon/SpannableBuilder.java @@ -166,7 +166,7 @@ public class SpannableBuilder { final int length = impl.length(); if (length > 0) { int amount = 0; - for (int i = length - 1; i >=0 ; i--) { + for (int i = length - 1; i >= 0; i--) { if (Character.isWhitespace(impl.charAt(i))) { amount += 1; } else { @@ -192,18 +192,35 @@ public class SpannableBuilder { final boolean reverse = spanned instanceof SpannedReversed; final Object[] spans = spanned.getSpans(0, spanned.length(), Object.class); + final int length = spans != null + ? spans.length + : 0; - iterate(reverse, spans, new Action() { - @Override - public void apply(Object o) { - setSpan( - o, - index + spanned.getSpanStart(o), - index + spanned.getSpanEnd(o), - spanned.getSpanFlags(o) - ); + if (length > 0) { + if (reverse) { + Object o; + for (int i = length - 1; i >= 0; i--) { + o = spans[i]; + setSpan( + o, + index + spanned.getSpanStart(o), + index + spanned.getSpanEnd(o), + spanned.getSpanFlags(o) + ); + } + } else { + Object o; + for (int i = 0; i < length; i++) { + o = spans[i]; + setSpan( + o, + index + spanned.getSpanStart(o), + index + spanned.getSpanEnd(o), + spanned.getSpanFlags(o) + ); + } } - }); + } } } @@ -221,25 +238,4 @@ public class SpannableBuilder { this.flags = flags; } } - - private interface Action { - void apply(Object o); - } - - private static void iterate(boolean reverse, @Nullable Object[] array, @NonNull Action action) { - final int length = array != null - ? array.length - : 0; - if (length > 0) { - if (reverse) { - for (int i = length - 1; i >= 0; i--) { - action.apply(array[i]); - } - } else { - for (int i = 0; i < length; i++) { - action.apply(array[i]); - } - } - } - } } diff --git a/library/src/main/java/ru/noties/markwon/SpannableConfiguration.java b/library/src/main/java/ru/noties/markwon/SpannableConfiguration.java index 3aa18cb1..dda590f5 100644 --- a/library/src/main/java/ru/noties/markwon/SpannableConfiguration.java +++ b/library/src/main/java/ru/noties/markwon/SpannableConfiguration.java @@ -31,6 +31,7 @@ public class SpannableConfiguration { private final UrlProcessor urlProcessor; private final SpannableHtmlParser htmlParser; private final ImageSizeResolver imageSizeResolver; + private final SpannableFactory factory; // @since 1.1.0 private SpannableConfiguration(@NonNull Builder builder) { this.theme = builder.theme; @@ -40,6 +41,7 @@ public class SpannableConfiguration { this.urlProcessor = builder.urlProcessor; this.htmlParser = builder.htmlParser; this.imageSizeResolver = builder.imageSizeResolver; + this.factory = builder.factory; } @NonNull @@ -77,6 +79,11 @@ public class SpannableConfiguration { return imageSizeResolver; } + @NonNull + public SpannableFactory factory() { + return factory; + } + @SuppressWarnings("unused") public static class Builder { @@ -88,6 +95,7 @@ public class SpannableConfiguration { private UrlProcessor urlProcessor; private SpannableHtmlParser htmlParser; private ImageSizeResolver imageSizeResolver; + private SpannableFactory factory; Builder(@NonNull Context context) { this.context = context; @@ -138,6 +146,15 @@ public class SpannableConfiguration { return this; } + /** + * @since 1.1.0 + */ + @NonNull + public Builder factory(@NonNull SpannableFactory factory) { + this.factory = factory; + return this; + } + @NonNull public SpannableConfiguration build() { @@ -165,8 +182,19 @@ public class SpannableConfiguration { imageSizeResolver = new ImageSizeResolverDef(); } + // @since 1.1.0 + if (factory == null) { + factory = SpannableFactoryDef.create(); + } + if (htmlParser == null) { - htmlParser = SpannableHtmlParser.create(theme, asyncDrawableLoader, urlProcessor, linkResolver, imageSizeResolver); + htmlParser = SpannableHtmlParser.create( + factory, + theme, + asyncDrawableLoader, + urlProcessor, + linkResolver, + imageSizeResolver); } return new SpannableConfiguration(this); diff --git a/library/src/main/java/ru/noties/markwon/SpannableFactory.java b/library/src/main/java/ru/noties/markwon/SpannableFactory.java new file mode 100644 index 00000000..4cd6b947 --- /dev/null +++ b/library/src/main/java/ru/noties/markwon/SpannableFactory.java @@ -0,0 +1,85 @@ +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); + + @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/library/src/main/java/ru/noties/markwon/SpannableFactoryDef.java b/library/src/main/java/ru/noties/markwon/SpannableFactoryDef.java new file mode 100644 index 00000000..f7672ac7 --- /dev/null +++ b/library/src/main/java/ru/noties/markwon/SpannableFactoryDef.java @@ -0,0 +1,144 @@ +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); + } + + @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/library/src/main/java/ru/noties/markwon/renderer/SpannableMarkdownVisitor.java b/library/src/main/java/ru/noties/markwon/renderer/SpannableMarkdownVisitor.java index ceb22ef0..82745e50 100644 --- a/library/src/main/java/ru/noties/markwon/renderer/SpannableMarkdownVisitor.java +++ b/library/src/main/java/ru/noties/markwon/renderer/SpannableMarkdownVisitor.java @@ -4,7 +4,6 @@ import android.support.annotation.NonNull; import android.support.annotation.Nullable; import android.text.Spanned; import android.text.TextUtils; -import android.text.style.StrikethroughSpan; import org.commonmark.ext.gfm.strikethrough.Strikethrough; import org.commonmark.ext.gfm.tables.TableBody; @@ -42,20 +41,10 @@ import java.util.List; import ru.noties.markwon.SpannableBuilder; import ru.noties.markwon.SpannableConfiguration; +import ru.noties.markwon.SpannableFactory; import ru.noties.markwon.renderer.html.SpannableHtmlParser; -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.StrongEmphasisSpan; +import ru.noties.markwon.spans.SpannableTheme; import ru.noties.markwon.spans.TableRowSpan; -import ru.noties.markwon.spans.TaskListSpan; -import ru.noties.markwon.spans.ThematicBreakSpan; import ru.noties.markwon.tasklist.TaskListBlock; import ru.noties.markwon.tasklist.TaskListItem; @@ -66,6 +55,9 @@ public class SpannableMarkdownVisitor extends AbstractVisitor { private final SpannableBuilder builder; private final Deque htmlInlineItems; + private final SpannableTheme theme; + private final SpannableFactory factory; + private int blockQuoteIndent; private int listLevel; @@ -80,6 +72,9 @@ public class SpannableMarkdownVisitor extends AbstractVisitor { this.configuration = configuration; this.builder = builder; this.htmlInlineItems = new ArrayDeque<>(2); + + this.theme = configuration.theme(); + this.factory = configuration.factory(); } @Override @@ -91,14 +86,14 @@ public class SpannableMarkdownVisitor extends AbstractVisitor { public void visit(StrongEmphasis strongEmphasis) { final int length = builder.length(); visitChildren(strongEmphasis); - setSpan(length, new StrongEmphasisSpan()); + setSpan(length, factory.strongEmphasis()); } @Override public void visit(Emphasis emphasis) { final int length = builder.length(); visitChildren(emphasis); - setSpan(length, new EmphasisSpan()); + setSpan(length, factory.emphasis()); } @Override @@ -115,7 +110,7 @@ public class SpannableMarkdownVisitor extends AbstractVisitor { visitChildren(blockQuote); - setSpan(length, new BlockQuoteSpan(configuration.theme())); + setSpan(length, factory.blockQuote(theme)); blockQuoteIndent -= 1; @@ -136,10 +131,7 @@ public class SpannableMarkdownVisitor extends AbstractVisitor { builder.append(code.getLiteral()); builder.append('\u00a0'); - setSpan(length, new CodeSpan( - configuration.theme(), - false - )); + setSpan(length, factory.code(theme, false)); } @Override @@ -174,10 +166,7 @@ public class SpannableMarkdownVisitor extends AbstractVisitor { ); builder.append('\u00a0').append('\n'); - setSpan(length, new CodeSpan( - configuration.theme(), - true - )); + setSpan(length, factory.code(theme, true)); newLine(); builder.append('\n'); @@ -217,11 +206,7 @@ public class SpannableMarkdownVisitor extends AbstractVisitor { visitChildren(listItem); - // todo| in order to provide real RTL experience there must be a way to provide this string - setSpan(length, new OrderedListItemSpan( - configuration.theme(), - String.valueOf(start) + "." + '\u00a0' - )); + setSpan(length, factory.orderedListItem(theme, start)); // after we have visited the children increment start number final OrderedList orderedList = (OrderedList) parent; @@ -231,10 +216,7 @@ public class SpannableMarkdownVisitor extends AbstractVisitor { visitChildren(listItem); - setSpan(length, new BulletListItemSpan( - configuration.theme(), - listLevel - 1 - )); + setSpan(length, factory.bulletListItem(theme, listLevel - 1)); } blockQuoteIndent -= 1; @@ -250,7 +232,8 @@ public class SpannableMarkdownVisitor extends AbstractVisitor { final int length = builder.length(); builder.append(' '); // without space it won't render - setSpan(length, new ThematicBreakSpan(configuration.theme())); + + setSpan(length, factory.thematicBreak(theme)); newLine(); builder.append('\n'); @@ -263,7 +246,7 @@ public class SpannableMarkdownVisitor extends AbstractVisitor { final int length = builder.length(); visitChildren(heading); - setSpan(length, new HeadingSpan(configuration.theme(), heading.getLevel())); + setSpan(length, factory.heading(theme, heading.getLevel())); newLine(); @@ -305,7 +288,7 @@ public class SpannableMarkdownVisitor extends AbstractVisitor { final int length = builder.length(); visitChildren(customNode); - setSpan(length, new StrikethroughSpan()); + setSpan(length, factory.strikethrough()); } else if (customNode instanceof TaskListItem) { @@ -319,11 +302,7 @@ public class SpannableMarkdownVisitor extends AbstractVisitor { visitChildren(customNode); - setSpan(length, new TaskListSpan( - configuration.theme(), - blockQuoteIndent, - listItem.done() - )); + setSpan(length, factory.taskListItem(theme, blockQuoteIndent, listItem.done())); newLine(); @@ -356,12 +335,11 @@ public class SpannableMarkdownVisitor extends AbstractVisitor { // trimmed from the final result builder.append('\u00a0'); - final TableRowSpan span = new TableRowSpan( - configuration.theme(), + final Object span = factory.tableRow( + theme, pendingTableRow, tableRowIsHeader, - tableRows % 2 == 1 - ); + tableRows % 2 == 1); tableRows = tableRowIsHeader ? 0 @@ -434,15 +412,12 @@ public class SpannableMarkdownVisitor extends AbstractVisitor { setSpan( length, - new AsyncDrawableSpan( - configuration.theme(), - new AsyncDrawable( - destination, - configuration.asyncDrawableLoader(), - configuration.imageSizeResolver(), - null - ), - AsyncDrawableSpan.ALIGN_BOTTOM, + factory.image( + theme, + destination, + configuration.asyncDrawableLoader(), + configuration.imageSizeResolver(), + null, link ) ); @@ -479,9 +454,7 @@ public class SpannableMarkdownVisitor extends AbstractVisitor { if (htmlInlineItems.size() > 0) { final HtmlInlineItem item = htmlInlineItems.pop(); final Object span = htmlParser.getSpanForTag(item.tag); - if (span != null) { - setSpan(item.start, span); - } + setSpan(item.start, span); } } else { @@ -504,11 +477,22 @@ public class SpannableMarkdownVisitor extends AbstractVisitor { final int length = builder.length(); visitChildren(link); final String destination = configuration.urlProcessor().process(link.getDestination()); - setSpan(length, new LinkSpan(configuration.theme(), destination, configuration.linkResolver())); + setSpan(length, factory.link(theme, destination, configuration.linkResolver())); } - private void setSpan(int start, @NonNull Object span) { - builder.setSpan(span, start, builder.length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); + private void setSpan(int start, @Nullable Object span) { + if (span != null) { + + final int length = builder.length(); + + if (span.getClass().isArray()) { + for (Object o : ((Object[]) span)) { + builder.setSpan(o, start, length, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); + } + } else { + builder.setSpan(span, start, length, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); + } + } } private void newLine() { diff --git a/library/src/main/java/ru/noties/markwon/renderer/html/BoldProvider.java b/library/src/main/java/ru/noties/markwon/renderer/html/BoldProvider.java index 7613b8b4..fbd42fc4 100644 --- a/library/src/main/java/ru/noties/markwon/renderer/html/BoldProvider.java +++ b/library/src/main/java/ru/noties/markwon/renderer/html/BoldProvider.java @@ -2,12 +2,21 @@ package ru.noties.markwon.renderer.html; import android.support.annotation.NonNull; -import ru.noties.markwon.spans.StrongEmphasisSpan; +import ru.noties.markwon.SpannableFactory; class BoldProvider implements SpannableHtmlParser.SpanProvider { + private final SpannableFactory factory; + + /** + * @since 1.1.0 + */ + BoldProvider(@NonNull SpannableFactory factory) { + this.factory = factory; + } + @Override public Object provide(@NonNull SpannableHtmlParser.Tag tag) { - return new StrongEmphasisSpan(); + return factory.strongEmphasis(); } } diff --git a/library/src/main/java/ru/noties/markwon/renderer/html/ImageProviderImpl.java b/library/src/main/java/ru/noties/markwon/renderer/html/ImageProviderImpl.java index 1d5968b7..10f62c41 100644 --- a/library/src/main/java/ru/noties/markwon/renderer/html/ImageProviderImpl.java +++ b/library/src/main/java/ru/noties/markwon/renderer/html/ImageProviderImpl.java @@ -10,26 +10,29 @@ import java.util.Collections; import java.util.HashMap; import java.util.Map; +import ru.noties.markwon.SpannableFactory; import ru.noties.markwon.UrlProcessor; 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; class ImageProviderImpl implements SpannableHtmlParser.ImageProvider { + private final SpannableFactory factory; private final SpannableTheme theme; private final AsyncDrawable.Loader loader; private final UrlProcessor urlProcessor; private final ImageSizeResolver imageSizeResolver; ImageProviderImpl( + @NonNull SpannableFactory factory, @NonNull SpannableTheme theme, @NonNull AsyncDrawable.Loader loader, @NonNull UrlProcessor urlProcessor, @NonNull ImageSizeResolver imageSizeResolver ) { + this.factory = factory; this.theme = theme; this.loader = loader; this.urlProcessor = urlProcessor; @@ -56,11 +59,26 @@ class ImageProviderImpl implements SpannableHtmlParser.ImageProvider { replacement = "\uFFFC"; } - final AsyncDrawable drawable = new AsyncDrawable(destination, loader, imageSizeResolver, parseImageSize(attributes)); - final AsyncDrawableSpan span = new AsyncDrawableSpan(theme, drawable); + final Object span = factory.image( + theme, + destination, + loader, + imageSizeResolver, + parseImageSize(attributes), + false); final SpannableString string = new SpannableString(replacement); - string.setSpan(span, 0, string.length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); + + if (span != null) { + final int length = string.length(); + if (span.getClass().isArray()) { + for (Object o : ((Object[]) span)) { + string.setSpan(o, 0, length, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); + } + } else { + string.setSpan(span, 0, length, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); + } + } spanned = string; } else { diff --git a/library/src/main/java/ru/noties/markwon/renderer/html/ItalicsProvider.java b/library/src/main/java/ru/noties/markwon/renderer/html/ItalicsProvider.java index 3fd7f068..51f33ff7 100644 --- a/library/src/main/java/ru/noties/markwon/renderer/html/ItalicsProvider.java +++ b/library/src/main/java/ru/noties/markwon/renderer/html/ItalicsProvider.java @@ -2,12 +2,21 @@ package ru.noties.markwon.renderer.html; import android.support.annotation.NonNull; -import ru.noties.markwon.spans.EmphasisSpan; +import ru.noties.markwon.SpannableFactory; class ItalicsProvider implements SpannableHtmlParser.SpanProvider { + private final SpannableFactory factory; + + /** + * @since 1.1.0 + */ + ItalicsProvider(@NonNull SpannableFactory factory) { + this.factory = factory; + } + @Override public Object provide(@NonNull SpannableHtmlParser.Tag tag) { - return new EmphasisSpan(); + return factory.emphasis(); } } diff --git a/library/src/main/java/ru/noties/markwon/renderer/html/LinkProvider.java b/library/src/main/java/ru/noties/markwon/renderer/html/LinkProvider.java index b85456f6..d15668bd 100644 --- a/library/src/main/java/ru/noties/markwon/renderer/html/LinkProvider.java +++ b/library/src/main/java/ru/noties/markwon/renderer/html/LinkProvider.java @@ -5,20 +5,24 @@ import android.text.TextUtils; import java.util.Map; +import ru.noties.markwon.SpannableFactory; import ru.noties.markwon.UrlProcessor; import ru.noties.markwon.spans.LinkSpan; import ru.noties.markwon.spans.SpannableTheme; class LinkProvider implements SpannableHtmlParser.SpanProvider { + private final SpannableFactory factory; private final SpannableTheme theme; private final UrlProcessor urlProcessor; private final LinkSpan.Resolver resolver; LinkProvider( + @NonNull SpannableFactory factory, @NonNull SpannableTheme theme, @NonNull UrlProcessor urlProcessor, @NonNull LinkSpan.Resolver resolver) { + this.factory = factory; this.theme = theme; this.urlProcessor = urlProcessor; this.resolver = resolver; @@ -34,7 +38,7 @@ class LinkProvider implements SpannableHtmlParser.SpanProvider { if (!TextUtils.isEmpty(href)) { final String destination = urlProcessor.process(href); - span = new LinkSpan(theme, destination, resolver); + span = factory.link(theme, destination, resolver); } else { span = null; diff --git a/library/src/main/java/ru/noties/markwon/renderer/html/SpannableHtmlParser.java b/library/src/main/java/ru/noties/markwon/renderer/html/SpannableHtmlParser.java index 3d7236a9..118e0a1c 100644 --- a/library/src/main/java/ru/noties/markwon/renderer/html/SpannableHtmlParser.java +++ b/library/src/main/java/ru/noties/markwon/renderer/html/SpannableHtmlParser.java @@ -11,6 +11,7 @@ import java.util.HashMap; import java.util.Map; import ru.noties.markwon.LinkResolverDef; +import ru.noties.markwon.SpannableFactory; import ru.noties.markwon.UrlProcessor; import ru.noties.markwon.UrlProcessorNoOp; import ru.noties.markwon.renderer.ImageSizeResolver; @@ -22,53 +23,19 @@ import ru.noties.markwon.spans.SpannableTheme; @SuppressWarnings("WeakerAccess") public class SpannableHtmlParser { - // creates default parser - @NonNull - public static SpannableHtmlParser create( - @NonNull SpannableTheme theme, - @NonNull AsyncDrawable.Loader loader - ) { - return builderWithDefaults(theme, loader, null, null, null) - .build(); - } - /** - * @since 1.0.1 - */ - @NonNull - public static SpannableHtmlParser create( - @NonNull SpannableTheme theme, - @NonNull AsyncDrawable.Loader loader, - @NonNull ImageSizeResolver imageSizeResolver - ) { - return builderWithDefaults(theme, loader, null, null, imageSizeResolver) - .build(); - } - - @NonNull - public static SpannableHtmlParser create( - @NonNull SpannableTheme theme, - @NonNull AsyncDrawable.Loader loader, - @NonNull UrlProcessor urlProcessor, - @NonNull LinkSpan.Resolver resolver - ) { - return builderWithDefaults(theme, loader, urlProcessor, resolver, null) - .build(); - } - - /** - * @since 1.0.1 + * @since 1.1.0 */ @NonNull public static SpannableHtmlParser create( + @NonNull SpannableFactory factory, @NonNull SpannableTheme theme, @NonNull AsyncDrawable.Loader loader, @NonNull UrlProcessor urlProcessor, @NonNull LinkSpan.Resolver resolver, @NonNull ImageSizeResolver imageSizeResolver ) { - return builderWithDefaults(theme, loader, urlProcessor, resolver, imageSizeResolver) - .build(); + return builderWithDefaults(factory, theme, loader, urlProcessor, resolver, imageSizeResolver).build(); } @NonNull @@ -76,16 +43,27 @@ public class SpannableHtmlParser { return new Builder(); } + /** + * @since 1.1.0 + */ @NonNull - public static Builder builderWithDefaults(@NonNull SpannableTheme theme) { - return builderWithDefaults(theme, null, null, null, null); + public static Builder builderWithDefaults(@NonNull SpannableFactory factory, @NonNull SpannableTheme theme) { + return builderWithDefaults( + factory, + theme, + null, + null, + null, + null); } /** * Updated in 1.0.1: added imageSizeResolverArgument + * Updated in 1.1.0: add SpannableFactory */ @NonNull public static Builder builderWithDefaults( + @NonNull SpannableFactory factory, @NonNull SpannableTheme theme, @Nullable AsyncDrawable.Loader asyncDrawableLoader, @Nullable UrlProcessor urlProcessor, @@ -101,9 +79,9 @@ public class SpannableHtmlParser { resolver = new LinkResolverDef(); } - final BoldProvider boldProvider = new BoldProvider(); - final ItalicsProvider italicsProvider = new ItalicsProvider(); - final StrikeProvider strikeProvider = new StrikeProvider(); + final BoldProvider boldProvider = new BoldProvider(factory); + final ItalicsProvider italicsProvider = new ItalicsProvider(factory); + final StrikeProvider strikeProvider = new StrikeProvider(factory); final ImageProvider imageProvider; if (asyncDrawableLoader != null) { @@ -112,7 +90,12 @@ public class SpannableHtmlParser { imageSizeResolver = new ImageSizeResolverDef(); } - imageProvider = new ImageProviderImpl(theme, asyncDrawableLoader, urlProcessor, imageSizeResolver); + imageProvider = new ImageProviderImpl( + factory, + theme, + asyncDrawableLoader, + urlProcessor, + imageSizeResolver); } else { imageProvider = null; } @@ -124,13 +107,13 @@ public class SpannableHtmlParser { .simpleTag("em", italicsProvider) .simpleTag("cite", italicsProvider) .simpleTag("dfn", italicsProvider) - .simpleTag("sup", new SuperScriptProvider(theme)) - .simpleTag("sub", new SubScriptProvider(theme)) - .simpleTag("u", new UnderlineProvider()) + .simpleTag("sup", new SuperScriptProvider(factory, theme)) + .simpleTag("sub", new SubScriptProvider(factory, theme)) + .simpleTag("u", new UnderlineProvider(factory)) .simpleTag("del", strikeProvider) .simpleTag("s", strikeProvider) .simpleTag("strike", strikeProvider) - .simpleTag("a", new LinkProvider(theme, urlProcessor, resolver)) + .simpleTag("a", new LinkProvider(factory, theme, urlProcessor, resolver)) .imageProvider(imageProvider); } diff --git a/library/src/main/java/ru/noties/markwon/renderer/html/StrikeProvider.java b/library/src/main/java/ru/noties/markwon/renderer/html/StrikeProvider.java index df01f9e6..c2d3fbc8 100644 --- a/library/src/main/java/ru/noties/markwon/renderer/html/StrikeProvider.java +++ b/library/src/main/java/ru/noties/markwon/renderer/html/StrikeProvider.java @@ -1,11 +1,22 @@ package ru.noties.markwon.renderer.html; import android.support.annotation.NonNull; -import android.text.style.StrikethroughSpan; + +import ru.noties.markwon.SpannableFactory; class StrikeProvider implements SpannableHtmlParser.SpanProvider { + + private final SpannableFactory factory; + + /** + * @since 1.1.0 + */ + StrikeProvider(@NonNull SpannableFactory factory) { + this.factory = factory; + } + @Override public Object provide(@NonNull SpannableHtmlParser.Tag tag) { - return new StrikethroughSpan(); + return factory.strikethrough(); } } diff --git a/library/src/main/java/ru/noties/markwon/renderer/html/SubScriptProvider.java b/library/src/main/java/ru/noties/markwon/renderer/html/SubScriptProvider.java index 920ff01e..451ae299 100644 --- a/library/src/main/java/ru/noties/markwon/renderer/html/SubScriptProvider.java +++ b/library/src/main/java/ru/noties/markwon/renderer/html/SubScriptProvider.java @@ -2,19 +2,21 @@ package ru.noties.markwon.renderer.html; import android.support.annotation.NonNull; +import ru.noties.markwon.SpannableFactory; import ru.noties.markwon.spans.SpannableTheme; -import ru.noties.markwon.spans.SubScriptSpan; class SubScriptProvider implements SpannableHtmlParser.SpanProvider { + private final SpannableFactory factory; private final SpannableTheme theme; - public SubScriptProvider(SpannableTheme theme) { + SubScriptProvider(@NonNull SpannableFactory factory, @NonNull SpannableTheme theme) { + this.factory = factory; this.theme = theme; } @Override public Object provide(@NonNull SpannableHtmlParser.Tag tag) { - return new SubScriptSpan(theme); + return factory.subScript(theme); } } diff --git a/library/src/main/java/ru/noties/markwon/renderer/html/SuperScriptProvider.java b/library/src/main/java/ru/noties/markwon/renderer/html/SuperScriptProvider.java index 4faf9078..db747e3d 100644 --- a/library/src/main/java/ru/noties/markwon/renderer/html/SuperScriptProvider.java +++ b/library/src/main/java/ru/noties/markwon/renderer/html/SuperScriptProvider.java @@ -2,19 +2,21 @@ package ru.noties.markwon.renderer.html; import android.support.annotation.NonNull; +import ru.noties.markwon.SpannableFactory; import ru.noties.markwon.spans.SpannableTheme; -import ru.noties.markwon.spans.SuperScriptSpan; class SuperScriptProvider implements SpannableHtmlParser.SpanProvider { + private final SpannableFactory factory; private final SpannableTheme theme; - SuperScriptProvider(SpannableTheme theme) { + SuperScriptProvider(@NonNull SpannableFactory factory, @NonNull SpannableTheme theme) { + this.factory = factory; this.theme = theme; } @Override public Object provide(@NonNull SpannableHtmlParser.Tag tag) { - return new SuperScriptSpan(theme); + return factory.superScript(theme); } } diff --git a/library/src/main/java/ru/noties/markwon/renderer/html/UnderlineProvider.java b/library/src/main/java/ru/noties/markwon/renderer/html/UnderlineProvider.java index 38acc73c..f44fc913 100644 --- a/library/src/main/java/ru/noties/markwon/renderer/html/UnderlineProvider.java +++ b/library/src/main/java/ru/noties/markwon/renderer/html/UnderlineProvider.java @@ -1,12 +1,22 @@ package ru.noties.markwon.renderer.html; import android.support.annotation.NonNull; -import android.text.style.UnderlineSpan; + +import ru.noties.markwon.SpannableFactory; class UnderlineProvider implements SpannableHtmlParser.SpanProvider { + private final SpannableFactory factory; + + /** + * @since 1.1.0 + */ + UnderlineProvider(@NonNull SpannableFactory factory) { + this.factory = factory; + } + @Override public Object provide(@NonNull SpannableHtmlParser.Tag tag) { - return new UnderlineSpan(); + return factory.underline(); } } diff --git a/library/src/main/java/ru/noties/markwon/spans/SpannableTheme.java b/library/src/main/java/ru/noties/markwon/spans/SpannableTheme.java index 0701ed39..10c4c762 100644 --- a/library/src/main/java/ru/noties/markwon/spans/SpannableTheme.java +++ b/library/src/main/java/ru/noties/markwon/spans/SpannableTheme.java @@ -12,9 +12,13 @@ import android.support.annotation.FloatRange; import android.support.annotation.IntRange; import android.support.annotation.NonNull; import android.support.annotation.Nullable; +import android.support.annotation.Size; import android.text.TextPaint; import android.util.TypedValue; +import java.util.Arrays; +import java.util.Locale; + @SuppressWarnings("WeakerAccess") public class SpannableTheme { @@ -173,6 +177,15 @@ public class SpannableTheme { // by default, text color with `HEADING_DEF_BREAK_COLOR_ALPHA` applied alpha protected final int headingBreakColor; + // by default, whatever typeface is set on the TextView + // @since 1.1.0 + protected final Typeface headingTypeface; + + // by default, we use standard multipliers from the HTML spec (see HEADING_SIZES for values). + // this library supports 6 heading sizes, so make sure the array you pass here has 6 elements. + // @since 1.1.0 + protected final float[] headingTextSizeMultipliers; + // by default `SCRIPT_DEF_TEXT_SIZE_RATIO` protected final float scriptTextSizeRatio; @@ -214,6 +227,8 @@ public class SpannableTheme { this.codeTextSize = builder.codeTextSize; 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; @@ -368,8 +383,23 @@ public class SpannableTheme { } public void applyHeadingTextStyle(@NonNull Paint paint, @IntRange(from = 1, to = 6) int level) { - paint.setFakeBoldText(true); - paint.setTextSize(paint.getTextSize() * HEADING_SIZES[level - 1]); + if (headingTypeface == null) { + paint.setFakeBoldText(true); + } else { + paint.setTypeface(headingTypeface); + } + final float[] textSizes = headingTextSizeMultipliers != null + ? headingTextSizeMultipliers + : HEADING_SIZES; + + if (textSizes != null && textSizes.length >= level) { + paint.setTextSize(paint.getTextSize() * textSizes[level - 1]); + } else { + throw new IllegalStateException(String.format( + Locale.US, + "Supplied heading level: %d is invalid, where configured heading sizes are: `%s`", + level, Arrays.toString(textSizes))); + } } public void applyHeadingBreakStyle(@NonNull Paint paint) { @@ -491,6 +521,8 @@ public class SpannableTheme { private int codeTextSize; private int headingBreakHeight = -1; private int headingBreakColor; + private Typeface headingTypeface; + private float[] headingTextSizeMultipliers; private float scriptTextSizeRatio; private int thematicBreakColor; private int thematicBreakHeight = -1; @@ -520,6 +552,8 @@ public class SpannableTheme { 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; @@ -634,6 +668,29 @@ public class SpannableTheme { return this; } + /** + * @param headingTypeface Typeface to use for heading elements + * @return self + * @since 1.1.0 + */ + @NonNull + public Builder headingTypeface(@NonNull Typeface headingTypeface) { + this.headingTypeface = headingTypeface; + return this; + } + + /** + * @param headingTextSizeMultipliers an array of multipliers values for heading elements. + * The base value for this multipliers is TextView\'s text size + * @return self + * @since 1.1.0 + */ + @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; diff --git a/sample-custom-extension/src/main/java/ru/noties/markwon/sample/extension/MainActivity.java b/sample-custom-extension/src/main/java/ru/noties/markwon/sample/extension/MainActivity.java index a81f8eb0..19a69704 100644 --- a/sample-custom-extension/src/main/java/ru/noties/markwon/sample/extension/MainActivity.java +++ b/sample-custom-extension/src/main/java/ru/noties/markwon/sample/extension/MainActivity.java @@ -1,6 +1,7 @@ package ru.noties.markwon.sample.extension; import android.app.Activity; +import android.graphics.Typeface; import android.os.Bundle; import android.widget.TextView; @@ -13,6 +14,7 @@ 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 { @@ -50,9 +52,16 @@ public class MainActivity extends Activity { // 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( - SpannableConfiguration.create(this), + configuration, builder, spanProvider ); diff --git a/sample-custom-extension/src/main/res/layout/activity_main.xml b/sample-custom-extension/src/main/res/layout/activity_main.xml index 439dac71..e4f2a936 100644 --- a/sample-custom-extension/src/main/res/layout/activity_main.xml +++ b/sample-custom-extension/src/main/res/layout/activity_main.xml @@ -11,6 +11,7 @@ android:layout_height="wrap_content" android:padding="8dip" android:textAppearance="?android:attr/textAppearanceMedium" + android:textSize="15sp" tools:text="@string/input"/> - \ No newline at end of file + diff --git a/sample-custom-extension/src/main/res/values/strings.xml b/sample-custom-extension/src/main/res/values/strings.xml index 7dc177e7..fdc01039 100644 --- a/sample-custom-extension/src/main/res/values/strings.xml +++ b/sample-custom-extension/src/main/res/values/strings.xml @@ -6,6 +6,7 @@ # Hello! @ic-android-black-24\n\n Home 36 black: @ic-home-black-36\n\n Memory 48 black: @ic-memory-black-48\n\n + ### I AM ANOTHER HEADER\n\n Sentiment Satisfied 64 red: @ic-sentiment_satisfied-red-64 ]]> diff --git a/settings.gradle b/settings.gradle index fbd8c3c4..29dc38f9 100644 --- a/settings.gradle +++ b/settings.gradle @@ -1 +1 @@ -include ':app', ':library', ':library-image-loader', ':library-view', ':sample-custom-extension' +include ':app', ':library', ':library-image-loader', ':library-view', ':sample-custom-extension', ':library-syntax'