* 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
This commit is contained in:
Dimitry 2018-07-30 15:19:42 +02:00 committed by GitHub
parent 5ef985670a
commit 7a20c38d33
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
58 changed files with 1869 additions and 302 deletions

View File

@ -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](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-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-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. <u>**No WebView is required**</u>. 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). **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. <u>**No WebView is required**</u>. 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 ## Installation
```groovy ```groovy
compile 'ru.noties:markwon:1.0.6' implementation 'ru.noties:markwon:1.1.0'
compile 'ru.noties:markwon-image-loader:1.0.6' // optional implementation 'ru.noties:markwon-image-loader:1.1.0' // optional
compile 'ru.noties:markwon-view:1.0.6' // optional implementation 'ru.noties:markwon-syntax:1.1.0' // optional
implementation 'ru.noties:markwon-view:1.1.0' // optional
``` ```
### Snapshot ### Snapshot
@ -35,10 +37,10 @@ allprojects {
and then in your module `build.gradle`: and then in your module `build.gradle`:
```groovy ```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: ## Supported markdown features:
* Emphasis (`*`, `_`) * Emphasis (`*`, `_`)
@ -141,6 +143,14 @@ you can use [Better-Link-Movement-Method][better-link-movement-method].
Please refer to [SpannableConfiguration] document for more info 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 # Demo

View File

@ -30,6 +30,7 @@ dependencies {
implementation project(':library') implementation project(':library')
implementation project(':library-image-loader') implementation project(':library-image-loader')
implementation project(':library-syntax')
implementation 'ru.noties:debug:3.0.0@jar' implementation 'ru.noties:debug:3.0.0@jar'
implementation 'me.saket:better-link-movement-method:2.2.0' implementation 'me.saket:better-link-movement-method:2.2.0'
@ -38,4 +39,7 @@ dependencies {
implementation 'com.google.dagger:dagger:2.10' implementation 'com.google.dagger:dagger:2.10'
annotationProcessor 'com.google.dagger:dagger-compiler:2.10' annotationProcessor 'com.google.dagger:dagger-compiler:2.10'
implementation PRISM_4J
annotationProcessor PRISM_4J_BUNDLER
} }

View File

@ -15,9 +15,17 @@ import dagger.Provides;
import okhttp3.Cache; import okhttp3.Cache;
import okhttp3.OkHttpClient; import okhttp3.OkHttpClient;
import ru.noties.markwon.il.AsyncDrawableLoader; 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.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 @Module
@PrismBundle(includeAll = true)
class AppModule { class AppModule {
private final App app; private final App app;
@ -40,7 +48,7 @@ class AppModule {
@Singleton @Singleton
OkHttpClient client() { OkHttpClient client() {
return new OkHttpClient.Builder() return new OkHttpClient.Builder()
.cache(new Cache(app.getCacheDir(), 1024L * 20)) .cache(new Cache(app.getCacheDir(), 1024L * 1024 * 20)) // 20 mb
.followRedirects(true) .followRedirects(true)
.retryOnConnectionFailure(true) .retryOnConnectionFailure(true)
.build(); .build();
@ -73,6 +81,35 @@ class AppModule {
.client(client) .client(client)
.executorService(executorService) .executorService(executorService)
.resources(resources) .resources(resources)
.mediaDecoders(
SvgMediaDecoder.create(resources),
GifMediaDecoder.create(false),
ImageMediaDecoder.create(resources)
)
.build(); .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();
}
} }

View File

@ -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);
}
}
}
}

View File

@ -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
);
}
}

View File

@ -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;
}
}

View File

@ -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();
}
}
}
}
}

View File

@ -28,8 +28,11 @@ public class MainActivity extends Activity {
@Inject @Inject
UriProcessor uriProcessor; UriProcessor uriProcessor;
@Inject
GifProcessor gifProcessor;
@Override @Override
protected void onCreate(Bundle savedInstanceState) { protected void onCreate(final Bundle savedInstanceState) {
super.onCreate(savedInstanceState); super.onCreate(savedInstanceState);
App.component(this) App.component(this)
@ -64,10 +67,14 @@ public class MainActivity extends Activity {
markdownLoader.load(uri(), new MarkdownLoader.OnMarkdownTextLoaded() { markdownLoader.load(uri(), new MarkdownLoader.OnMarkdownTextLoaded() {
@Override @Override
public void apply(final String text) { 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 @Override
public void onMarkdownReady(CharSequence markdown) { public void onMarkdownReady(CharSequence markdown) {
Markwon.setText(textView, markdown, BetterLinkMovementMethod.getInstance()); Markwon.setText(textView, markdown, BetterLinkMovementMethod.getInstance());
gifProcessor.process(textView);
Views.setVisible(progress, false); Views.setVisible(progress, false);
} }
}); });

View File

@ -14,6 +14,12 @@ import javax.inject.Inject;
import ru.noties.debug.Debug; import ru.noties.debug.Debug;
import ru.noties.markwon.spans.AsyncDrawable; 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 @ActivityScope
public class MarkdownRenderer { public class MarkdownRenderer {
@ -31,6 +37,15 @@ public class MarkdownRenderer {
@Inject @Inject
Handler handler; Handler handler;
@Inject
Prism4j prism4j;
@Inject
Prism4jThemeDefault prism4jThemeDefault;
@Inject
Prism4jThemeDarkula prism4JThemeDarkula;
private Future<?> task; private Future<?> task;
@Inject @Inject
@ -39,10 +54,15 @@ public class MarkdownRenderer {
public void render( public void render(
@NonNull final Context context, @NonNull final Context context,
final boolean isLightTheme,
@Nullable final Uri uri, @Nullable final Uri uri,
@NonNull final String markdown, @NonNull final String markdown,
@NonNull final MarkdownReadyListener listener) { @NonNull final MarkdownReadyListener listener) {
// todo: create prism4j theme factory (accepting light/dark argument)
cancel(); cancel();
task = service.submit(new Runnable() { task = service.submit(new Runnable() {
@Override @Override
public void run() { public void run() {
@ -54,9 +74,28 @@ public class MarkdownRenderer {
urlProcessor = new UrlProcessorRelativeToAbsolute(uri.toString()); 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) final SpannableConfiguration configuration = SpannableConfiguration.builder(context)
.asyncDrawableLoader(loader) .asyncDrawableLoader(loader)
.urlProcessor(urlProcessor) .urlProcessor(urlProcessor)
.syntaxHighlight(Prism4jSyntaxHighlight.create(prism4j, prism4jTheme))
.theme(SpannableTheme.builderWithDefaults(context)
.codeBackgroundColor(background)
.codeTextColor(prism4jTheme.textColor())
.build())
.factory(new GifAwareSpannableFactory(gifPlaceholder))
.build(); .build();
final long start = SystemClock.uptimeMillis(); final long start = SystemClock.uptimeMillis();

View File

@ -25,9 +25,9 @@ public class Themes {
// we have only 2 themes and Light one is default // we have only 2 themes and Light one is default
final int theme; final int theme;
if (dark) { if (dark) {
theme = R.style.AppThemeBaseDark; theme = R.style.AppThemeDark;
} else { } else {
theme = R.style.AppThemeBaseLight; theme = R.style.AppThemeLight;
} }
final Context appContext = context.getApplicationContext(); final Context appContext = context.getApplicationContext();
@ -43,4 +43,8 @@ public class Themes {
.putBoolean(KEY_THEME_DARK, newValue) .putBoolean(KEY_THEME_DARK, newValue)
.apply(); .apply();
} }
public boolean isLight() {
return !preferences.getBoolean(KEY_THEME_DARK, false);
}
} }

Binary file not shown.

After

Width:  |  Height:  |  Size: 322 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 378 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 536 B

View File

@ -1,6 +1,17 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<resources> <resources>
<color name="colorPrimary">#424242</color> <color name="colorPrimary">#424242</color>
<color name="colorPrimaryDark">#212121</color> <color name="colorPrimaryDark">#212121</color>
<color name="colorAccent">#4caf50</color> <color name="colorAccent">#4caf50</color>
<color name="white">#FFF</color>
<color name="black">#dd000000</color>
<color name="theme_light_window_background">#FFF</color>
<color name="theme_light_text_color">#dd000000</color>
<color name="theme_dark_window_background">#303030</color>
<color name="theme_dark_text_color">#ddffffff</color>
</resources> </resources>

View File

@ -10,7 +10,14 @@
<item name="ic_app_bar_theme">@drawable/ic_app_bar_theme_dark</item> <item name="ic_app_bar_theme">@drawable/ic_app_bar_theme_dark</item>
</style> </style>
<style name="AppThemeLight" parent="AppThemeBaseLight" /> <style name="AppThemeLight" parent="AppThemeBaseLight">
<style name="AppThemeDark" parent="AppThemeBaseDark" /> <item name="android:windowBackground">@color/theme_light_window_background</item>
<item name="android:textColor">@color/theme_light_text_color</item>
</style>
<style name="AppThemeDark" parent="AppThemeBaseDark">
<item name="android:windowBackground">@color/theme_dark_window_background</item>
<item name="android:textColor">@color/theme_dark_text_color</item>
</style>
</resources> </resources>

View File

@ -10,6 +10,9 @@ buildscript {
allprojects { allprojects {
repositories { repositories {
if (project.hasProperty('LOCAL_MAVEN_URL')) {
maven { url LOCAL_MAVEN_URL }
}
jcenter() jcenter()
google() google()
} }
@ -22,28 +25,31 @@ task clean(type: Delete) {
} }
task wrapper(type: Wrapper) { task wrapper(type: Wrapper) {
gradleVersion '4.5' gradleVersion '4.8.1'
distributionType 'all' distributionType 'all'
} }
ext { ext {
// Config // Config
BUILD_TOOLS = '26.0.3' BUILD_TOOLS = '27.0.3'
TARGET_SDK = 26 TARGET_SDK = 27
MIN_SDK = 16 MIN_SDK = 16
// Dependencies // Dependencies
final def supportVersion = '26.1.0' final def supportVersion = '27.1.1'
SUPPORT_ANNOTATIONS = "com.android.support:support-annotations:$supportVersion" SUPPORT_ANNOTATIONS = "com.android.support:support-annotations:$supportVersion"
SUPPORT_APP_COMPAT = "com.android.support:appcompat-v7:$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 = "com.atlassian.commonmark:commonmark:$commonMarkVersion"
COMMON_MARK_STRIKETHROUGHT = "com.atlassian.commonmark:commonmark-ext-gfm-strikethrough:$commonMarkVersion" COMMON_MARK_STRIKETHROUGHT = "com.atlassian.commonmark:commonmark-ext-gfm-strikethrough:$commonMarkVersion"
COMMON_MARK_TABLE = "com.atlassian.commonmark:commonmark-ext-gfm-tables:$commonMarkVersion" COMMON_MARK_TABLE = "com.atlassian.commonmark:commonmark-ext-gfm-tables:$commonMarkVersion"
ANDROID_SVG = 'com.caverock:androidsvg:1.2.1' 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' 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'
} }

View File

@ -6,7 +6,7 @@ org.gradle.configureondemand=true
android.enableBuildCache=true android.enableBuildCache=true
android.buildCacheDir=build/pre-dex-cache android.buildCacheDir=build/pre-dex-cache
VERSION_NAME=1.0.6 VERSION_NAME=1.1.0
GROUP=ru.noties GROUP=ru.noties
POM_DESCRIPTION=Markwon POM_DESCRIPTION=Markwon

Binary file not shown.

View File

@ -2,4 +2,4 @@ distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists distributionPath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists zipStorePath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-4.5-all.zip distributionUrl=https\://services.gradle.org/distributions/gradle-4.8.1-all.zip

View File

@ -25,6 +25,14 @@ dependencies {
api OK_HTTP api OK_HTTP
} }
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' apply from: 'https://raw.githubusercontent.com/noties/gradle-mvn-push/master/gradle-mvn-push-aar.gradle'
} }

View File

@ -1,28 +1,22 @@
package ru.noties.markwon.il; package ru.noties.markwon.il;
import android.content.res.Resources; import android.content.res.Resources;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.graphics.Canvas;
import android.graphics.drawable.BitmapDrawable;
import android.graphics.drawable.Drawable; import android.graphics.drawable.Drawable;
import android.net.Uri; import android.net.Uri;
import android.os.Handler; import android.os.Handler;
import android.os.Looper; import android.os.Looper;
import android.support.annotation.NonNull; import android.support.annotation.NonNull;
import android.text.TextUtils; import android.support.annotation.Nullable;
import com.caverock.androidsvg.SVG;
import com.caverock.androidsvg.SVGParseException;
import java.io.BufferedInputStream; import java.io.BufferedInputStream;
import java.io.ByteArrayOutputStream;
import java.io.File; import java.io.File;
import java.io.FileInputStream; import java.io.FileInputStream;
import java.io.FileNotFoundException; import java.io.FileNotFoundException;
import java.io.IOException; import java.io.IOException;
import java.io.InputStream; import java.io.InputStream;
import java.lang.ref.WeakReference; import java.lang.ref.WeakReference;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap; import java.util.HashMap;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
@ -34,22 +28,21 @@ import okhttp3.OkHttpClient;
import okhttp3.Request; import okhttp3.Request;
import okhttp3.Response; import okhttp3.Response;
import okhttp3.ResponseBody; import okhttp3.ResponseBody;
import pl.droidsonroids.gif.GifDrawable;
import ru.noties.markwon.spans.AsyncDrawable; import ru.noties.markwon.spans.AsyncDrawable;
public class AsyncDrawableLoader implements AsyncDrawable.Loader { public class AsyncDrawableLoader implements AsyncDrawable.Loader {
@NonNull
public static AsyncDrawableLoader create() { public static AsyncDrawableLoader create() {
return builder().build(); return builder().build();
} }
@NonNull
public static AsyncDrawableLoader.Builder builder() { public static AsyncDrawableLoader.Builder builder() {
return new Builder(); return new Builder();
} }
private static final String HEADER_CONTENT_TYPE = "Content-Type"; private static final String HEADER_CONTENT_TYPE = "Content-Type";
private static final String CONTENT_TYPE_SVG = "image/svg+xml";
private static final String CONTENT_TYPE_GIF = "image/gif";
private static final String FILE_ANDROID_ASSETS = "android_asset"; private static final String FILE_ANDROID_ASSETS = "android_asset";
@ -58,6 +51,7 @@ public class AsyncDrawableLoader implements AsyncDrawable.Loader {
private final ExecutorService executorService; private final ExecutorService executorService;
private final Handler mainThread; private final Handler mainThread;
private final Drawable errorDrawable; private final Drawable errorDrawable;
private final List<MediaDecoder> mediaDecoders;
private final Map<String, Future<?>> requests; private final Map<String, Future<?>> requests;
@ -67,6 +61,7 @@ public class AsyncDrawableLoader implements AsyncDrawable.Loader {
this.executorService = builder.executorService; this.executorService = builder.executorService;
this.mainThread = new Handler(Looper.getMainLooper()); this.mainThread = new Handler(Looper.getMainLooper());
this.errorDrawable = builder.errorDrawable; this.errorDrawable = builder.errorDrawable;
this.mediaDecoders = builder.mediaDecoders;
this.requests = new HashMap<>(3); this.requests = new HashMap<>(3);
} }
@ -105,12 +100,15 @@ public class AsyncDrawableLoader implements AsyncDrawable.Loader {
public void run() { public void run() {
final Item item; final Item item;
final boolean isFromFile;
final Uri uri = Uri.parse(destination); final Uri uri = Uri.parse(destination);
if ("file".equals(uri.getScheme())) { if ("file".equals(uri.getScheme())) {
item = fromFile(uri); item = fromFile(uri);
isFromFile = true;
} else { } else {
item = fromNetwork(destination); item = fromNetwork(destination);
isFromFile = false;
} }
Drawable result = null; Drawable result = null;
@ -118,13 +116,15 @@ public class AsyncDrawableLoader implements AsyncDrawable.Loader {
if (item != null if (item != null
&& item.inputStream != null) { && item.inputStream != null) {
try { try {
if (CONTENT_TYPE_SVG.equals(item.type)) {
result = handleSvg(item.inputStream); final MediaDecoder mediaDecoder = isFromFile
} else if (CONTENT_TYPE_GIF.equals(item.type)) { ? mediaDecoderFromFile(item.fileName)
result = handleGif(item.inputStream); : mediaDecoderFromContentType(item.contentType);
} else {
result = handleSimple(item.inputStream); if (mediaDecoder != null) {
result = mediaDecoder.decode(item.inputStream);
} }
} finally { } finally {
try { try {
item.inputStream.close(); 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<String> segments = uri.getPathSegments(); final List<String> segments = uri.getPathSegments();
if (segments == null if (segments == null
@ -167,19 +168,10 @@ public class AsyncDrawableLoader implements AsyncDrawable.Loader {
} }
final Item out; final Item out;
final String type;
final InputStream inputStream; final InputStream inputStream;
final boolean assets = FILE_ANDROID_ASSETS.equals(segments.get(0)); final boolean assets = FILE_ANDROID_ASSETS.equals(segments.get(0));
final String lastSegment = uri.getLastPathSegment(); final String fileName = uri.getLastPathSegment();
if (lastSegment.endsWith(".svg")) {
type = CONTENT_TYPE_SVG;
} else if (lastSegment.endsWith(".gif")) {
type = CONTENT_TYPE_GIF;
} else {
type = null;
}
if (assets) { if (assets) {
final StringBuilder path = new StringBuilder(); final StringBuilder path = new StringBuilder();
@ -208,7 +200,7 @@ public class AsyncDrawableLoader implements AsyncDrawable.Loader {
} }
if (inputStream != null) { if (inputStream != null) {
out = new Item(type, inputStream); out = new Item(fileName, null, inputStream);
} else { } else {
out = null; out = null;
} }
@ -216,7 +208,8 @@ public class AsyncDrawableLoader implements AsyncDrawable.Loader {
return out; return out;
} }
private Item fromNetwork(String destination) { @Nullable
private Item fromNetwork(@NonNull String destination) {
Item out = null; Item out = null;
@ -237,15 +230,8 @@ public class AsyncDrawableLoader implements AsyncDrawable.Loader {
if (body != null) { if (body != null) {
final InputStream inputStream = body.byteStream(); final InputStream inputStream = body.byteStream();
if (inputStream != null) { if (inputStream != null) {
final String type;
final String contentType = response.header(HEADER_CONTENT_TYPE); final String contentType = response.header(HEADER_CONTENT_TYPE);
if (!TextUtils.isEmpty(contentType) out = new Item(null, contentType, inputStream);
&& contentType.startsWith(CONTENT_TYPE_SVG)) {
type = CONTENT_TYPE_SVG;
} else {
type = contentType;
}
out = new Item(type, inputStream);
} }
} }
} }
@ -253,87 +239,31 @@ public class AsyncDrawableLoader implements AsyncDrawable.Loader {
return out; return out;
} }
private Drawable handleSvg(InputStream stream) { @Nullable
private MediaDecoder mediaDecoderFromFile(@NonNull String fileName) {
final Drawable out; MediaDecoder out = null;
SVG svg = null; for (MediaDecoder mediaDecoder : mediaDecoders) {
try { if (mediaDecoder.canDecodeByFileName(fileName)) {
svg = SVG.getFromInputStream(stream); out = mediaDecoder;
} catch (SVGParseException e) { break;
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();
} }
} }
return out; 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); for (MediaDecoder mediaDecoder : mediaDecoders) {
if (bitmap != null) { if (mediaDecoder.canDecodeByContentType(contentType)) {
out = new BitmapDrawable(resources, bitmap); out = mediaDecoder;
DrawableUtils.intrinsicBounds(out); break;
} 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);
}
out = outputStream.toByteArray();
} catch (IOException e) {
e.printStackTrace();
} }
return out; return out;
@ -346,47 +276,93 @@ public class AsyncDrawableLoader implements AsyncDrawable.Loader {
private ExecutorService executorService; private ExecutorService executorService;
private Drawable errorDrawable; private Drawable errorDrawable;
// @since 1.1.0
private final List<MediaDecoder> mediaDecoders = new ArrayList<>(3);
@NonNull
public Builder client(@NonNull OkHttpClient client) { public Builder client(@NonNull OkHttpClient client) {
this.client = client; this.client = client;
return this; 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) { public Builder resources(@NonNull Resources resources) {
this.resources = resources; this.resources = resources;
return this; return this;
} }
public Builder executorService(ExecutorService executorService) { @NonNull
public Builder executorService(@NonNull ExecutorService executorService) {
this.executorService = executorService; this.executorService = executorService;
return this; return this;
} }
public Builder errorDrawable(Drawable errorDrawable) { @NonNull
public Builder errorDrawable(@NonNull Drawable errorDrawable) {
this.errorDrawable = errorDrawable; this.errorDrawable = errorDrawable;
return this; return this;
} }
@NonNull
public Builder mediaDecoders(@NonNull List<MediaDecoder> 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() { public AsyncDrawableLoader build() {
if (client == null) { if (client == null) {
client = new OkHttpClient(); client = new OkHttpClient();
} }
if (resources == null) { if (resources == null) {
resources = Resources.getSystem(); resources = Resources.getSystem();
} }
if (executorService == null) { if (executorService == null) {
// we will use executor from okHttp // we will use executor from okHttp
executorService = client.dispatcher().executorService(); 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); return new AsyncDrawableLoader(this);
} }
} }
private static class Item { private static class Item {
final String type;
final String fileName;
final String contentType;
final InputStream inputStream; final InputStream inputStream;
Item(String type, InputStream inputStream) { Item(@Nullable String fileName, @Nullable String contentType, @Nullable InputStream inputStream) {
this.type = type; this.fileName = fileName;
this.contentType = contentType;
this.inputStream = inputStream; this.inputStream = inputStream;
} }
} }

View File

@ -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;
}
}

View File

@ -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;
}
}

View File

@ -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);
}

View File

@ -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;
}
}

64
library-syntax/README.md Normal file
View File

@ -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

Binary file not shown.

After

Width:  |  Height:  |  Size: 30 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 33 KiB

View File

@ -0,0 +1,32 @@
apply plugin: 'com.android.library'
android {
compileSdkVersion TARGET_SDK
buildToolsVersion BUILD_TOOLS
defaultConfig {
minSdkVersion MIN_SDK
targetSdkVersion TARGET_SDK
versionCode 1
versionName version
}
}
dependencies {
api SUPPORT_ANNOTATIONS
api PRISM_4J
api project(':library')
}
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'
}

View File

@ -0,0 +1,3 @@
POM_NAME=Markwon
POM_ARTIFACT_ID=markwon-syntax
POM_PACKAGING=aar

View File

@ -0,0 +1 @@
<manifest package="ru.noties.markwon.syntax" />

View File

@ -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;
}
}

View File

@ -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);
}
}
}

View File

@ -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
);
}

View File

@ -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<String, Color> {
@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;
}
}
}

View File

@ -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);
}
}
}

View File

@ -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);
}
}
}

View File

@ -18,6 +18,14 @@ dependencies {
compileOnly SUPPORT_APP_COMPAT 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' apply from: 'https://raw.githubusercontent.com/noties/gradle-mvn-push/master/gradle-mvn-push-aar.gradle'
} }

View File

@ -20,6 +20,14 @@ dependencies {
api COMMON_MARK_TABLE 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' apply from: 'https://raw.githubusercontent.com/noties/gradle-mvn-push/master/gradle-mvn-push-aar.gradle'
} }

View File

@ -166,7 +166,7 @@ public class SpannableBuilder {
final int length = impl.length(); final int length = impl.length();
if (length > 0) { if (length > 0) {
int amount = 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))) { if (Character.isWhitespace(impl.charAt(i))) {
amount += 1; amount += 1;
} else { } else {
@ -192,10 +192,15 @@ public class SpannableBuilder {
final boolean reverse = spanned instanceof SpannedReversed; final boolean reverse = spanned instanceof SpannedReversed;
final Object[] spans = spanned.getSpans(0, spanned.length(), Object.class); final Object[] spans = spanned.getSpans(0, spanned.length(), Object.class);
final int length = spans != null
? spans.length
: 0;
iterate(reverse, spans, new Action() { if (length > 0) {
@Override if (reverse) {
public void apply(Object o) { Object o;
for (int i = length - 1; i >= 0; i--) {
o = spans[i];
setSpan( setSpan(
o, o,
index + spanned.getSpanStart(o), index + spanned.getSpanStart(o),
@ -203,7 +208,19 @@ public class SpannableBuilder {
spanned.getSpanFlags(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; 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]);
}
}
}
}
} }

View File

@ -31,6 +31,7 @@ public class SpannableConfiguration {
private final UrlProcessor urlProcessor; private final UrlProcessor urlProcessor;
private final SpannableHtmlParser htmlParser; private final SpannableHtmlParser htmlParser;
private final ImageSizeResolver imageSizeResolver; private final ImageSizeResolver imageSizeResolver;
private final SpannableFactory factory; // @since 1.1.0
private SpannableConfiguration(@NonNull Builder builder) { private SpannableConfiguration(@NonNull Builder builder) {
this.theme = builder.theme; this.theme = builder.theme;
@ -40,6 +41,7 @@ public class SpannableConfiguration {
this.urlProcessor = builder.urlProcessor; this.urlProcessor = builder.urlProcessor;
this.htmlParser = builder.htmlParser; this.htmlParser = builder.htmlParser;
this.imageSizeResolver = builder.imageSizeResolver; this.imageSizeResolver = builder.imageSizeResolver;
this.factory = builder.factory;
} }
@NonNull @NonNull
@ -77,6 +79,11 @@ public class SpannableConfiguration {
return imageSizeResolver; return imageSizeResolver;
} }
@NonNull
public SpannableFactory factory() {
return factory;
}
@SuppressWarnings("unused") @SuppressWarnings("unused")
public static class Builder { public static class Builder {
@ -88,6 +95,7 @@ public class SpannableConfiguration {
private UrlProcessor urlProcessor; private UrlProcessor urlProcessor;
private SpannableHtmlParser htmlParser; private SpannableHtmlParser htmlParser;
private ImageSizeResolver imageSizeResolver; private ImageSizeResolver imageSizeResolver;
private SpannableFactory factory;
Builder(@NonNull Context context) { Builder(@NonNull Context context) {
this.context = context; this.context = context;
@ -138,6 +146,15 @@ public class SpannableConfiguration {
return this; return this;
} }
/**
* @since 1.1.0
*/
@NonNull
public Builder factory(@NonNull SpannableFactory factory) {
this.factory = factory;
return this;
}
@NonNull @NonNull
public SpannableConfiguration build() { public SpannableConfiguration build() {
@ -165,8 +182,19 @@ public class SpannableConfiguration {
imageSizeResolver = new ImageSizeResolverDef(); imageSizeResolver = new ImageSizeResolverDef();
} }
// @since 1.1.0
if (factory == null) {
factory = SpannableFactoryDef.create();
}
if (htmlParser == null) { if (htmlParser == null) {
htmlParser = SpannableHtmlParser.create(theme, asyncDrawableLoader, urlProcessor, linkResolver, imageSizeResolver); htmlParser = SpannableHtmlParser.create(
factory,
theme,
asyncDrawableLoader,
urlProcessor,
linkResolver,
imageSizeResolver);
} }
return new SpannableConfiguration(this); return new SpannableConfiguration(this);

View File

@ -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<TableRowSpan.Cell> 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();
}

View File

@ -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<TableRowSpan.Cell> 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();
}
}

View File

@ -4,7 +4,6 @@ import android.support.annotation.NonNull;
import android.support.annotation.Nullable; import android.support.annotation.Nullable;
import android.text.Spanned; import android.text.Spanned;
import android.text.TextUtils; import android.text.TextUtils;
import android.text.style.StrikethroughSpan;
import org.commonmark.ext.gfm.strikethrough.Strikethrough; import org.commonmark.ext.gfm.strikethrough.Strikethrough;
import org.commonmark.ext.gfm.tables.TableBody; import org.commonmark.ext.gfm.tables.TableBody;
@ -42,20 +41,10 @@ import java.util.List;
import ru.noties.markwon.SpannableBuilder; import ru.noties.markwon.SpannableBuilder;
import ru.noties.markwon.SpannableConfiguration; import ru.noties.markwon.SpannableConfiguration;
import ru.noties.markwon.SpannableFactory;
import ru.noties.markwon.renderer.html.SpannableHtmlParser; import ru.noties.markwon.renderer.html.SpannableHtmlParser;
import ru.noties.markwon.spans.AsyncDrawable; import ru.noties.markwon.spans.SpannableTheme;
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.TableRowSpan; 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.TaskListBlock;
import ru.noties.markwon.tasklist.TaskListItem; import ru.noties.markwon.tasklist.TaskListItem;
@ -66,6 +55,9 @@ public class SpannableMarkdownVisitor extends AbstractVisitor {
private final SpannableBuilder builder; private final SpannableBuilder builder;
private final Deque<HtmlInlineItem> htmlInlineItems; private final Deque<HtmlInlineItem> htmlInlineItems;
private final SpannableTheme theme;
private final SpannableFactory factory;
private int blockQuoteIndent; private int blockQuoteIndent;
private int listLevel; private int listLevel;
@ -80,6 +72,9 @@ public class SpannableMarkdownVisitor extends AbstractVisitor {
this.configuration = configuration; this.configuration = configuration;
this.builder = builder; this.builder = builder;
this.htmlInlineItems = new ArrayDeque<>(2); this.htmlInlineItems = new ArrayDeque<>(2);
this.theme = configuration.theme();
this.factory = configuration.factory();
} }
@Override @Override
@ -91,14 +86,14 @@ public class SpannableMarkdownVisitor extends AbstractVisitor {
public void visit(StrongEmphasis strongEmphasis) { public void visit(StrongEmphasis strongEmphasis) {
final int length = builder.length(); final int length = builder.length();
visitChildren(strongEmphasis); visitChildren(strongEmphasis);
setSpan(length, new StrongEmphasisSpan()); setSpan(length, factory.strongEmphasis());
} }
@Override @Override
public void visit(Emphasis emphasis) { public void visit(Emphasis emphasis) {
final int length = builder.length(); final int length = builder.length();
visitChildren(emphasis); visitChildren(emphasis);
setSpan(length, new EmphasisSpan()); setSpan(length, factory.emphasis());
} }
@Override @Override
@ -115,7 +110,7 @@ public class SpannableMarkdownVisitor extends AbstractVisitor {
visitChildren(blockQuote); visitChildren(blockQuote);
setSpan(length, new BlockQuoteSpan(configuration.theme())); setSpan(length, factory.blockQuote(theme));
blockQuoteIndent -= 1; blockQuoteIndent -= 1;
@ -136,10 +131,7 @@ public class SpannableMarkdownVisitor extends AbstractVisitor {
builder.append(code.getLiteral()); builder.append(code.getLiteral());
builder.append('\u00a0'); builder.append('\u00a0');
setSpan(length, new CodeSpan( setSpan(length, factory.code(theme, false));
configuration.theme(),
false
));
} }
@Override @Override
@ -174,10 +166,7 @@ public class SpannableMarkdownVisitor extends AbstractVisitor {
); );
builder.append('\u00a0').append('\n'); builder.append('\u00a0').append('\n');
setSpan(length, new CodeSpan( setSpan(length, factory.code(theme, true));
configuration.theme(),
true
));
newLine(); newLine();
builder.append('\n'); builder.append('\n');
@ -217,11 +206,7 @@ public class SpannableMarkdownVisitor extends AbstractVisitor {
visitChildren(listItem); visitChildren(listItem);
// todo| in order to provide real RTL experience there must be a way to provide this string setSpan(length, factory.orderedListItem(theme, start));
setSpan(length, new OrderedListItemSpan(
configuration.theme(),
String.valueOf(start) + "." + '\u00a0'
));
// after we have visited the children increment start number // after we have visited the children increment start number
final OrderedList orderedList = (OrderedList) parent; final OrderedList orderedList = (OrderedList) parent;
@ -231,10 +216,7 @@ public class SpannableMarkdownVisitor extends AbstractVisitor {
visitChildren(listItem); visitChildren(listItem);
setSpan(length, new BulletListItemSpan( setSpan(length, factory.bulletListItem(theme, listLevel - 1));
configuration.theme(),
listLevel - 1
));
} }
blockQuoteIndent -= 1; blockQuoteIndent -= 1;
@ -250,7 +232,8 @@ public class SpannableMarkdownVisitor extends AbstractVisitor {
final int length = builder.length(); final int length = builder.length();
builder.append(' '); // without space it won't render builder.append(' '); // without space it won't render
setSpan(length, new ThematicBreakSpan(configuration.theme()));
setSpan(length, factory.thematicBreak(theme));
newLine(); newLine();
builder.append('\n'); builder.append('\n');
@ -263,7 +246,7 @@ public class SpannableMarkdownVisitor extends AbstractVisitor {
final int length = builder.length(); final int length = builder.length();
visitChildren(heading); visitChildren(heading);
setSpan(length, new HeadingSpan(configuration.theme(), heading.getLevel())); setSpan(length, factory.heading(theme, heading.getLevel()));
newLine(); newLine();
@ -305,7 +288,7 @@ public class SpannableMarkdownVisitor extends AbstractVisitor {
final int length = builder.length(); final int length = builder.length();
visitChildren(customNode); visitChildren(customNode);
setSpan(length, new StrikethroughSpan()); setSpan(length, factory.strikethrough());
} else if (customNode instanceof TaskListItem) { } else if (customNode instanceof TaskListItem) {
@ -319,11 +302,7 @@ public class SpannableMarkdownVisitor extends AbstractVisitor {
visitChildren(customNode); visitChildren(customNode);
setSpan(length, new TaskListSpan( setSpan(length, factory.taskListItem(theme, blockQuoteIndent, listItem.done()));
configuration.theme(),
blockQuoteIndent,
listItem.done()
));
newLine(); newLine();
@ -356,12 +335,11 @@ public class SpannableMarkdownVisitor extends AbstractVisitor {
// trimmed from the final result // trimmed from the final result
builder.append('\u00a0'); builder.append('\u00a0');
final TableRowSpan span = new TableRowSpan( final Object span = factory.tableRow(
configuration.theme(), theme,
pendingTableRow, pendingTableRow,
tableRowIsHeader, tableRowIsHeader,
tableRows % 2 == 1 tableRows % 2 == 1);
);
tableRows = tableRowIsHeader tableRows = tableRowIsHeader
? 0 ? 0
@ -434,15 +412,12 @@ public class SpannableMarkdownVisitor extends AbstractVisitor {
setSpan( setSpan(
length, length,
new AsyncDrawableSpan( factory.image(
configuration.theme(), theme,
new AsyncDrawable(
destination, destination,
configuration.asyncDrawableLoader(), configuration.asyncDrawableLoader(),
configuration.imageSizeResolver(), configuration.imageSizeResolver(),
null null,
),
AsyncDrawableSpan.ALIGN_BOTTOM,
link link
) )
); );
@ -479,10 +454,8 @@ public class SpannableMarkdownVisitor extends AbstractVisitor {
if (htmlInlineItems.size() > 0) { if (htmlInlineItems.size() > 0) {
final HtmlInlineItem item = htmlInlineItems.pop(); final HtmlInlineItem item = htmlInlineItems.pop();
final Object span = htmlParser.getSpanForTag(item.tag); final Object span = htmlParser.getSpanForTag(item.tag);
if (span != null) {
setSpan(item.start, span); setSpan(item.start, span);
} }
}
} else { } else {
final Spanned html = htmlParser.getSpanned(tag, htmlInline.getLiteral()); final Spanned html = htmlParser.getSpanned(tag, htmlInline.getLiteral());
@ -504,11 +477,22 @@ public class SpannableMarkdownVisitor extends AbstractVisitor {
final int length = builder.length(); final int length = builder.length();
visitChildren(link); visitChildren(link);
final String destination = configuration.urlProcessor().process(link.getDestination()); 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) { private void setSpan(int start, @Nullable Object span) {
builder.setSpan(span, start, builder.length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); 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() { private void newLine() {

View File

@ -2,12 +2,21 @@ package ru.noties.markwon.renderer.html;
import android.support.annotation.NonNull; import android.support.annotation.NonNull;
import ru.noties.markwon.spans.StrongEmphasisSpan; import ru.noties.markwon.SpannableFactory;
class BoldProvider implements SpannableHtmlParser.SpanProvider { class BoldProvider implements SpannableHtmlParser.SpanProvider {
private final SpannableFactory factory;
/**
* @since 1.1.0
*/
BoldProvider(@NonNull SpannableFactory factory) {
this.factory = factory;
}
@Override @Override
public Object provide(@NonNull SpannableHtmlParser.Tag tag) { public Object provide(@NonNull SpannableHtmlParser.Tag tag) {
return new StrongEmphasisSpan(); return factory.strongEmphasis();
} }
} }

View File

@ -10,26 +10,29 @@ import java.util.Collections;
import java.util.HashMap; import java.util.HashMap;
import java.util.Map; import java.util.Map;
import ru.noties.markwon.SpannableFactory;
import ru.noties.markwon.UrlProcessor; import ru.noties.markwon.UrlProcessor;
import ru.noties.markwon.renderer.ImageSize; import ru.noties.markwon.renderer.ImageSize;
import ru.noties.markwon.renderer.ImageSizeResolver; import ru.noties.markwon.renderer.ImageSizeResolver;
import ru.noties.markwon.spans.AsyncDrawable; import ru.noties.markwon.spans.AsyncDrawable;
import ru.noties.markwon.spans.AsyncDrawableSpan;
import ru.noties.markwon.spans.SpannableTheme; import ru.noties.markwon.spans.SpannableTheme;
class ImageProviderImpl implements SpannableHtmlParser.ImageProvider { class ImageProviderImpl implements SpannableHtmlParser.ImageProvider {
private final SpannableFactory factory;
private final SpannableTheme theme; private final SpannableTheme theme;
private final AsyncDrawable.Loader loader; private final AsyncDrawable.Loader loader;
private final UrlProcessor urlProcessor; private final UrlProcessor urlProcessor;
private final ImageSizeResolver imageSizeResolver; private final ImageSizeResolver imageSizeResolver;
ImageProviderImpl( ImageProviderImpl(
@NonNull SpannableFactory factory,
@NonNull SpannableTheme theme, @NonNull SpannableTheme theme,
@NonNull AsyncDrawable.Loader loader, @NonNull AsyncDrawable.Loader loader,
@NonNull UrlProcessor urlProcessor, @NonNull UrlProcessor urlProcessor,
@NonNull ImageSizeResolver imageSizeResolver @NonNull ImageSizeResolver imageSizeResolver
) { ) {
this.factory = factory;
this.theme = theme; this.theme = theme;
this.loader = loader; this.loader = loader;
this.urlProcessor = urlProcessor; this.urlProcessor = urlProcessor;
@ -56,11 +59,26 @@ class ImageProviderImpl implements SpannableHtmlParser.ImageProvider {
replacement = "\uFFFC"; replacement = "\uFFFC";
} }
final AsyncDrawable drawable = new AsyncDrawable(destination, loader, imageSizeResolver, parseImageSize(attributes)); final Object span = factory.image(
final AsyncDrawableSpan span = new AsyncDrawableSpan(theme, drawable); theme,
destination,
loader,
imageSizeResolver,
parseImageSize(attributes),
false);
final SpannableString string = new SpannableString(replacement); 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; spanned = string;
} else { } else {

View File

@ -2,12 +2,21 @@ package ru.noties.markwon.renderer.html;
import android.support.annotation.NonNull; import android.support.annotation.NonNull;
import ru.noties.markwon.spans.EmphasisSpan; import ru.noties.markwon.SpannableFactory;
class ItalicsProvider implements SpannableHtmlParser.SpanProvider { class ItalicsProvider implements SpannableHtmlParser.SpanProvider {
private final SpannableFactory factory;
/**
* @since 1.1.0
*/
ItalicsProvider(@NonNull SpannableFactory factory) {
this.factory = factory;
}
@Override @Override
public Object provide(@NonNull SpannableHtmlParser.Tag tag) { public Object provide(@NonNull SpannableHtmlParser.Tag tag) {
return new EmphasisSpan(); return factory.emphasis();
} }
} }

View File

@ -5,20 +5,24 @@ import android.text.TextUtils;
import java.util.Map; import java.util.Map;
import ru.noties.markwon.SpannableFactory;
import ru.noties.markwon.UrlProcessor; import ru.noties.markwon.UrlProcessor;
import ru.noties.markwon.spans.LinkSpan; import ru.noties.markwon.spans.LinkSpan;
import ru.noties.markwon.spans.SpannableTheme; import ru.noties.markwon.spans.SpannableTheme;
class LinkProvider implements SpannableHtmlParser.SpanProvider { class LinkProvider implements SpannableHtmlParser.SpanProvider {
private final SpannableFactory factory;
private final SpannableTheme theme; private final SpannableTheme theme;
private final UrlProcessor urlProcessor; private final UrlProcessor urlProcessor;
private final LinkSpan.Resolver resolver; private final LinkSpan.Resolver resolver;
LinkProvider( LinkProvider(
@NonNull SpannableFactory factory,
@NonNull SpannableTheme theme, @NonNull SpannableTheme theme,
@NonNull UrlProcessor urlProcessor, @NonNull UrlProcessor urlProcessor,
@NonNull LinkSpan.Resolver resolver) { @NonNull LinkSpan.Resolver resolver) {
this.factory = factory;
this.theme = theme; this.theme = theme;
this.urlProcessor = urlProcessor; this.urlProcessor = urlProcessor;
this.resolver = resolver; this.resolver = resolver;
@ -34,7 +38,7 @@ class LinkProvider implements SpannableHtmlParser.SpanProvider {
if (!TextUtils.isEmpty(href)) { if (!TextUtils.isEmpty(href)) {
final String destination = urlProcessor.process(href); final String destination = urlProcessor.process(href);
span = new LinkSpan(theme, destination, resolver); span = factory.link(theme, destination, resolver);
} else { } else {
span = null; span = null;

View File

@ -11,6 +11,7 @@ import java.util.HashMap;
import java.util.Map; import java.util.Map;
import ru.noties.markwon.LinkResolverDef; import ru.noties.markwon.LinkResolverDef;
import ru.noties.markwon.SpannableFactory;
import ru.noties.markwon.UrlProcessor; import ru.noties.markwon.UrlProcessor;
import ru.noties.markwon.UrlProcessorNoOp; import ru.noties.markwon.UrlProcessorNoOp;
import ru.noties.markwon.renderer.ImageSizeResolver; import ru.noties.markwon.renderer.ImageSizeResolver;
@ -22,53 +23,19 @@ import ru.noties.markwon.spans.SpannableTheme;
@SuppressWarnings("WeakerAccess") @SuppressWarnings("WeakerAccess")
public class SpannableHtmlParser { 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 * @since 1.1.0
*/
@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
*/ */
@NonNull @NonNull
public static SpannableHtmlParser create( public static SpannableHtmlParser create(
@NonNull SpannableFactory factory,
@NonNull SpannableTheme theme, @NonNull SpannableTheme theme,
@NonNull AsyncDrawable.Loader loader, @NonNull AsyncDrawable.Loader loader,
@NonNull UrlProcessor urlProcessor, @NonNull UrlProcessor urlProcessor,
@NonNull LinkSpan.Resolver resolver, @NonNull LinkSpan.Resolver resolver,
@NonNull ImageSizeResolver imageSizeResolver @NonNull ImageSizeResolver imageSizeResolver
) { ) {
return builderWithDefaults(theme, loader, urlProcessor, resolver, imageSizeResolver) return builderWithDefaults(factory, theme, loader, urlProcessor, resolver, imageSizeResolver).build();
.build();
} }
@NonNull @NonNull
@ -76,16 +43,27 @@ public class SpannableHtmlParser {
return new Builder(); return new Builder();
} }
/**
* @since 1.1.0
*/
@NonNull @NonNull
public static Builder builderWithDefaults(@NonNull SpannableTheme theme) { public static Builder builderWithDefaults(@NonNull SpannableFactory factory, @NonNull SpannableTheme theme) {
return builderWithDefaults(theme, null, null, null, null); return builderWithDefaults(
factory,
theme,
null,
null,
null,
null);
} }
/** /**
* Updated in 1.0.1: added imageSizeResolverArgument * Updated in 1.0.1: added imageSizeResolverArgument
* Updated in 1.1.0: add SpannableFactory
*/ */
@NonNull @NonNull
public static Builder builderWithDefaults( public static Builder builderWithDefaults(
@NonNull SpannableFactory factory,
@NonNull SpannableTheme theme, @NonNull SpannableTheme theme,
@Nullable AsyncDrawable.Loader asyncDrawableLoader, @Nullable AsyncDrawable.Loader asyncDrawableLoader,
@Nullable UrlProcessor urlProcessor, @Nullable UrlProcessor urlProcessor,
@ -101,9 +79,9 @@ public class SpannableHtmlParser {
resolver = new LinkResolverDef(); resolver = new LinkResolverDef();
} }
final BoldProvider boldProvider = new BoldProvider(); final BoldProvider boldProvider = new BoldProvider(factory);
final ItalicsProvider italicsProvider = new ItalicsProvider(); final ItalicsProvider italicsProvider = new ItalicsProvider(factory);
final StrikeProvider strikeProvider = new StrikeProvider(); final StrikeProvider strikeProvider = new StrikeProvider(factory);
final ImageProvider imageProvider; final ImageProvider imageProvider;
if (asyncDrawableLoader != null) { if (asyncDrawableLoader != null) {
@ -112,7 +90,12 @@ public class SpannableHtmlParser {
imageSizeResolver = new ImageSizeResolverDef(); imageSizeResolver = new ImageSizeResolverDef();
} }
imageProvider = new ImageProviderImpl(theme, asyncDrawableLoader, urlProcessor, imageSizeResolver); imageProvider = new ImageProviderImpl(
factory,
theme,
asyncDrawableLoader,
urlProcessor,
imageSizeResolver);
} else { } else {
imageProvider = null; imageProvider = null;
} }
@ -124,13 +107,13 @@ public class SpannableHtmlParser {
.simpleTag("em", italicsProvider) .simpleTag("em", italicsProvider)
.simpleTag("cite", italicsProvider) .simpleTag("cite", italicsProvider)
.simpleTag("dfn", italicsProvider) .simpleTag("dfn", italicsProvider)
.simpleTag("sup", new SuperScriptProvider(theme)) .simpleTag("sup", new SuperScriptProvider(factory, theme))
.simpleTag("sub", new SubScriptProvider(theme)) .simpleTag("sub", new SubScriptProvider(factory, theme))
.simpleTag("u", new UnderlineProvider()) .simpleTag("u", new UnderlineProvider(factory))
.simpleTag("del", strikeProvider) .simpleTag("del", strikeProvider)
.simpleTag("s", strikeProvider) .simpleTag("s", strikeProvider)
.simpleTag("strike", strikeProvider) .simpleTag("strike", strikeProvider)
.simpleTag("a", new LinkProvider(theme, urlProcessor, resolver)) .simpleTag("a", new LinkProvider(factory, theme, urlProcessor, resolver))
.imageProvider(imageProvider); .imageProvider(imageProvider);
} }

View File

@ -1,11 +1,22 @@
package ru.noties.markwon.renderer.html; package ru.noties.markwon.renderer.html;
import android.support.annotation.NonNull; import android.support.annotation.NonNull;
import android.text.style.StrikethroughSpan;
import ru.noties.markwon.SpannableFactory;
class StrikeProvider implements SpannableHtmlParser.SpanProvider { class StrikeProvider implements SpannableHtmlParser.SpanProvider {
private final SpannableFactory factory;
/**
* @since 1.1.0
*/
StrikeProvider(@NonNull SpannableFactory factory) {
this.factory = factory;
}
@Override @Override
public Object provide(@NonNull SpannableHtmlParser.Tag tag) { public Object provide(@NonNull SpannableHtmlParser.Tag tag) {
return new StrikethroughSpan(); return factory.strikethrough();
} }
} }

View File

@ -2,19 +2,21 @@ package ru.noties.markwon.renderer.html;
import android.support.annotation.NonNull; import android.support.annotation.NonNull;
import ru.noties.markwon.SpannableFactory;
import ru.noties.markwon.spans.SpannableTheme; import ru.noties.markwon.spans.SpannableTheme;
import ru.noties.markwon.spans.SubScriptSpan;
class SubScriptProvider implements SpannableHtmlParser.SpanProvider { class SubScriptProvider implements SpannableHtmlParser.SpanProvider {
private final SpannableFactory factory;
private final SpannableTheme theme; private final SpannableTheme theme;
public SubScriptProvider(SpannableTheme theme) { SubScriptProvider(@NonNull SpannableFactory factory, @NonNull SpannableTheme theme) {
this.factory = factory;
this.theme = theme; this.theme = theme;
} }
@Override @Override
public Object provide(@NonNull SpannableHtmlParser.Tag tag) { public Object provide(@NonNull SpannableHtmlParser.Tag tag) {
return new SubScriptSpan(theme); return factory.subScript(theme);
} }
} }

View File

@ -2,19 +2,21 @@ package ru.noties.markwon.renderer.html;
import android.support.annotation.NonNull; import android.support.annotation.NonNull;
import ru.noties.markwon.SpannableFactory;
import ru.noties.markwon.spans.SpannableTheme; import ru.noties.markwon.spans.SpannableTheme;
import ru.noties.markwon.spans.SuperScriptSpan;
class SuperScriptProvider implements SpannableHtmlParser.SpanProvider { class SuperScriptProvider implements SpannableHtmlParser.SpanProvider {
private final SpannableFactory factory;
private final SpannableTheme theme; private final SpannableTheme theme;
SuperScriptProvider(SpannableTheme theme) { SuperScriptProvider(@NonNull SpannableFactory factory, @NonNull SpannableTheme theme) {
this.factory = factory;
this.theme = theme; this.theme = theme;
} }
@Override @Override
public Object provide(@NonNull SpannableHtmlParser.Tag tag) { public Object provide(@NonNull SpannableHtmlParser.Tag tag) {
return new SuperScriptSpan(theme); return factory.superScript(theme);
} }
} }

View File

@ -1,12 +1,22 @@
package ru.noties.markwon.renderer.html; package ru.noties.markwon.renderer.html;
import android.support.annotation.NonNull; import android.support.annotation.NonNull;
import android.text.style.UnderlineSpan;
import ru.noties.markwon.SpannableFactory;
class UnderlineProvider implements SpannableHtmlParser.SpanProvider { class UnderlineProvider implements SpannableHtmlParser.SpanProvider {
private final SpannableFactory factory;
/**
* @since 1.1.0
*/
UnderlineProvider(@NonNull SpannableFactory factory) {
this.factory = factory;
}
@Override @Override
public Object provide(@NonNull SpannableHtmlParser.Tag tag) { public Object provide(@NonNull SpannableHtmlParser.Tag tag) {
return new UnderlineSpan(); return factory.underline();
} }
} }

View File

@ -12,9 +12,13 @@ import android.support.annotation.FloatRange;
import android.support.annotation.IntRange; import android.support.annotation.IntRange;
import android.support.annotation.NonNull; import android.support.annotation.NonNull;
import android.support.annotation.Nullable; import android.support.annotation.Nullable;
import android.support.annotation.Size;
import android.text.TextPaint; import android.text.TextPaint;
import android.util.TypedValue; import android.util.TypedValue;
import java.util.Arrays;
import java.util.Locale;
@SuppressWarnings("WeakerAccess") @SuppressWarnings("WeakerAccess")
public class SpannableTheme { public class SpannableTheme {
@ -173,6 +177,15 @@ public class SpannableTheme {
// by default, text color with `HEADING_DEF_BREAK_COLOR_ALPHA` applied alpha // by default, text color with `HEADING_DEF_BREAK_COLOR_ALPHA` applied alpha
protected final int headingBreakColor; 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` // by default `SCRIPT_DEF_TEXT_SIZE_RATIO`
protected final float scriptTextSizeRatio; protected final float scriptTextSizeRatio;
@ -214,6 +227,8 @@ public class SpannableTheme {
this.codeTextSize = builder.codeTextSize; this.codeTextSize = builder.codeTextSize;
this.headingBreakHeight = builder.headingBreakHeight; this.headingBreakHeight = builder.headingBreakHeight;
this.headingBreakColor = builder.headingBreakColor; this.headingBreakColor = builder.headingBreakColor;
this.headingTypeface = builder.headingTypeface;
this.headingTextSizeMultipliers = builder.headingTextSizeMultipliers;
this.scriptTextSizeRatio = builder.scriptTextSizeRatio; this.scriptTextSizeRatio = builder.scriptTextSizeRatio;
this.thematicBreakColor = builder.thematicBreakColor; this.thematicBreakColor = builder.thematicBreakColor;
this.thematicBreakHeight = builder.thematicBreakHeight; this.thematicBreakHeight = builder.thematicBreakHeight;
@ -368,8 +383,23 @@ public class SpannableTheme {
} }
public void applyHeadingTextStyle(@NonNull Paint paint, @IntRange(from = 1, to = 6) int level) { public void applyHeadingTextStyle(@NonNull Paint paint, @IntRange(from = 1, to = 6) int level) {
if (headingTypeface == null) {
paint.setFakeBoldText(true); paint.setFakeBoldText(true);
paint.setTextSize(paint.getTextSize() * HEADING_SIZES[level - 1]); } 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) { public void applyHeadingBreakStyle(@NonNull Paint paint) {
@ -491,6 +521,8 @@ public class SpannableTheme {
private int codeTextSize; private int codeTextSize;
private int headingBreakHeight = -1; private int headingBreakHeight = -1;
private int headingBreakColor; private int headingBreakColor;
private Typeface headingTypeface;
private float[] headingTextSizeMultipliers;
private float scriptTextSizeRatio; private float scriptTextSizeRatio;
private int thematicBreakColor; private int thematicBreakColor;
private int thematicBreakHeight = -1; private int thematicBreakHeight = -1;
@ -520,6 +552,8 @@ public class SpannableTheme {
this.codeTextSize = theme.codeTextSize; this.codeTextSize = theme.codeTextSize;
this.headingBreakHeight = theme.headingBreakHeight; this.headingBreakHeight = theme.headingBreakHeight;
this.headingBreakColor = theme.headingBreakColor; this.headingBreakColor = theme.headingBreakColor;
this.headingTypeface = theme.headingTypeface;
this.headingTextSizeMultipliers = theme.headingTextSizeMultipliers;
this.scriptTextSizeRatio = theme.scriptTextSizeRatio; this.scriptTextSizeRatio = theme.scriptTextSizeRatio;
this.thematicBreakColor = theme.thematicBreakColor; this.thematicBreakColor = theme.thematicBreakColor;
this.thematicBreakHeight = theme.thematicBreakHeight; this.thematicBreakHeight = theme.thematicBreakHeight;
@ -634,6 +668,29 @@ public class SpannableTheme {
return this; 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 @NonNull
public Builder scriptTextSizeRatio(@FloatRange(from = .0F, to = Float.MAX_VALUE) float scriptTextSizeRatio) { public Builder scriptTextSizeRatio(@FloatRange(from = .0F, to = Float.MAX_VALUE) float scriptTextSizeRatio) {
this.scriptTextSizeRatio = scriptTextSizeRatio; this.scriptTextSizeRatio = scriptTextSizeRatio;

View File

@ -1,6 +1,7 @@
package ru.noties.markwon.sample.extension; package ru.noties.markwon.sample.extension;
import android.app.Activity; import android.app.Activity;
import android.graphics.Typeface;
import android.os.Bundle; import android.os.Bundle;
import android.widget.TextView; import android.widget.TextView;
@ -13,6 +14,7 @@ import java.util.Arrays;
import ru.noties.markwon.SpannableBuilder; import ru.noties.markwon.SpannableBuilder;
import ru.noties.markwon.SpannableConfiguration; import ru.noties.markwon.SpannableConfiguration;
import ru.noties.markwon.spans.SpannableTheme;
import ru.noties.markwon.tasklist.TaskListExtension; import ru.noties.markwon.tasklist.TaskListExtension;
public class MainActivity extends Activity { public class MainActivity extends Activity {
@ -50,9 +52,16 @@ public class MainActivity extends Activity {
// better to provide a valid fallback option // better to provide a valid fallback option
final IconSpanProvider spanProvider = IconSpanProvider.create(this, 0); 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 // create an instance of visitor to process parsed markdown
final IconVisitor visitor = new IconVisitor( final IconVisitor visitor = new IconVisitor(
SpannableConfiguration.create(this), configuration,
builder, builder,
spanProvider spanProvider
); );

View File

@ -11,6 +11,7 @@
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:padding="8dip" android:padding="8dip"
android:textAppearance="?android:attr/textAppearanceMedium" android:textAppearance="?android:attr/textAppearanceMedium"
android:textSize="15sp"
tools:text="@string/input"/> tools:text="@string/input"/>
</ScrollView> </ScrollView>

View File

@ -6,6 +6,7 @@
# Hello! @ic-android-black-24\n\n # Hello! @ic-android-black-24\n\n
Home 36 black: @ic-home-black-36\n\n Home 36 black: @ic-home-black-36\n\n
Memory 48 black: @ic-memory-black-48\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 Sentiment Satisfied 64 red: @ic-sentiment_satisfied-red-64
]]> ]]>
</string> </string>

View File

@ -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'