V1.1.0 (#53)
* Update build configuration * Update commonmark to 0.11.0 and android-gif to 1.2.14 * Add module `library-syntax` * Add default prism4j theme implementation * Add syntax highlight to sample app * Update syntax highlight to use SpannableStringBuilder * Working with syntax rendering * Add darkula theme to syntax highlight * Add attribute for image-loader module * Update version to 1.1.0-SNAPSHOT * Updating build configuration for snapshot publish * Add headingTypeface, headingTextSizes to SpannableTheme (#51) * Add headingTypeface to SpannableTheme, use a custom heading typeface in the sample app * Add headingTextSizes * Switching to headingTextSizeMultipliers, adding validating annotations, adding example * Consolidate logic, add crash if header index is out of bounds * Add small version clarifications * Introduce MediaDecoder abstraction for image-loader module * Switch to use SpannableFactory * Switch to use SpannableFactory for html parsing * Update sample application to add play-pause functionality for gifs * Small cleanup * Update prism4j version 1.1.0 * Update build configuration * Add README to library-syntax module * Update README
This commit is contained in:
parent
5ef985670a
commit
7a20c38d33
20
README.md
20
README.md
@ -4,6 +4,7 @@
|
|||||||
|
|
||||||
[](http://search.maven.org/#search|ga|1|g%3A%22ru.noties%22%20AND%20a%3A%22markwon%22)
|
[](http://search.maven.org/#search|ga|1|g%3A%22ru.noties%22%20AND%20a%3A%22markwon%22)
|
||||||
[](http://search.maven.org/#search|ga|1|g%3A%22ru.noties%22%20AND%20a%3A%22markwon-image-loader%22)
|
[](http://search.maven.org/#search|ga|1|g%3A%22ru.noties%22%20AND%20a%3A%22markwon-image-loader%22)
|
||||||
|
[](http://search.maven.org/#search|ga|1|g%3A%22ru.noties%22%20AND%20a%3A%22markwon-syntax%22)
|
||||||
[](http://search.maven.org/#search|ga|1|g%3A%22ru.noties%22%20AND%20a%3A%22markwon-view%22)
|
[](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
|
||||||
|
@ -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
|
||||||
}
|
}
|
||||||
|
@ -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();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -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
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
77
app/src/main/java/ru/noties/markwon/GifPlaceholder.java
Normal file
77
app/src/main/java/ru/noties/markwon/GifPlaceholder.java
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
125
app/src/main/java/ru/noties/markwon/GifProcessor.java
Normal file
125
app/src/main/java/ru/noties/markwon/GifProcessor.java
Normal 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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -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);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
@ -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();
|
||||||
|
@ -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 |
@ -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>
|
||||||
|
@ -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>
|
||||||
|
18
build.gradle
18
build.gradle
@ -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'
|
||||||
}
|
}
|
||||||
|
@ -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
|
||||||
|
BIN
gradle/wrapper/gradle-wrapper.jar
vendored
BIN
gradle/wrapper/gradle-wrapper.jar
vendored
Binary file not shown.
2
gradle/wrapper/gradle-wrapper.properties
vendored
2
gradle/wrapper/gradle-wrapper.properties
vendored
@ -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
|
||||||
|
@ -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'
|
||||||
}
|
}
|
@ -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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
@ -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);
|
||||||
|
}
|
@ -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
64
library-syntax/README.md
Normal 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.
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
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
|
BIN
library-syntax/art/markwon-syntax-darkula.png
Normal file
BIN
library-syntax/art/markwon-syntax-darkula.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 30 KiB |
BIN
library-syntax/art/markwon-syntax-default.png
Normal file
BIN
library-syntax/art/markwon-syntax-default.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 33 KiB |
32
library-syntax/build.gradle
Normal file
32
library-syntax/build.gradle
Normal 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'
|
||||||
|
}
|
3
library-syntax/gradle.properties
Normal file
3
library-syntax/gradle.properties
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
POM_NAME=Markwon
|
||||||
|
POM_ARTIFACT_ID=markwon-syntax
|
||||||
|
POM_PACKAGING=aar
|
1
library-syntax/src/main/AndroidManifest.xml
Normal file
1
library-syntax/src/main/AndroidManifest.xml
Normal file
@ -0,0 +1 @@
|
|||||||
|
<manifest package="ru.noties.markwon.syntax" />
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -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
|
||||||
|
);
|
||||||
|
}
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -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'
|
||||||
}
|
}
|
||||||
|
@ -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'
|
||||||
}
|
}
|
||||||
|
@ -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]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
@ -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);
|
||||||
|
@ -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();
|
||||||
|
}
|
144
library/src/main/java/ru/noties/markwon/SpannableFactoryDef.java
Normal file
144
library/src/main/java/ru/noties/markwon/SpannableFactoryDef.java
Normal 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();
|
||||||
|
}
|
||||||
|
}
|
@ -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() {
|
||||||
|
@ -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();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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 {
|
||||||
|
@ -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();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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;
|
||||||
|
@ -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);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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;
|
||||||
|
@ -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
|
||||||
);
|
);
|
||||||
|
@ -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>
|
@ -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>
|
||||||
|
@ -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'
|
||||||
|
Loading…
x
Reference in New Issue
Block a user