* Update build configuration

* Update commonmark to 0.11.0 and android-gif to 1.2.14

* Add  module `library-syntax`

* Add default prism4j theme implementation

* Add syntax highlight to sample app

* Update syntax highlight to use SpannableStringBuilder

* Working with syntax rendering

* Add darkula theme to syntax highlight

* Add  attribute for image-loader module

* Update version to 1.1.0-SNAPSHOT

* Updating build configuration for snapshot publish

* Add headingTypeface, headingTextSizes to SpannableTheme (#51)

* Add headingTypeface to SpannableTheme, use a custom heading typeface in the sample app

* Add headingTextSizes

* Switching to headingTextSizeMultipliers, adding validating annotations, adding example

* Consolidate logic, add crash if header index is out of bounds

* Add small version clarifications

* Introduce MediaDecoder abstraction for image-loader module

* Switch to use SpannableFactory

* Switch to use SpannableFactory for html parsing

* Update sample application to add play-pause functionality for gifs

* Small cleanup

* Update prism4j version 1.1.0

* Update build configuration

* Add README to library-syntax module

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

View File

@ -4,6 +4,7 @@
[![markwon](https://img.shields.io/maven-central/v/ru.noties/markwon.svg?label=markwon)](http://search.maven.org/#search|ga|1|g%3A%22ru.noties%22%20AND%20a%3A%22markwon%22)
[![markwon-image-loader](https://img.shields.io/maven-central/v/ru.noties/markwon-image-loader.svg?label=markwon-image-loader)](http://search.maven.org/#search|ga|1|g%3A%22ru.noties%22%20AND%20a%3A%22markwon-image-loader%22)
[![markwon-syntax](https://img.shields.io/maven-central/v/ru.noties/markwon-syntax.svg?label=markwon-syntax)](http://search.maven.org/#search|ga|1|g%3A%22ru.noties%22%20AND%20a%3A%22markwon-syntax%22)
[![markwon-view](https://img.shields.io/maven-central/v/ru.noties/markwon-view.svg?label=markwon-view)](http://search.maven.org/#search|ga|1|g%3A%22ru.noties%22%20AND%20a%3A%22markwon-view%22)
**Markwon** is a library for Android that renders markdown as system-native Spannables. It gives ability to display markdown in all TextView widgets (**TextView**, **Button**, **Switch**, **CheckBox**, etc), **Notifications**, **Toasts**, etc. <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
```groovy
compile 'ru.noties:markwon:1.0.6'
compile 'ru.noties:markwon-image-loader:1.0.6' // optional
compile 'ru.noties:markwon-view:1.0.6' // optional
implementation 'ru.noties:markwon:1.1.0'
implementation 'ru.noties:markwon-image-loader:1.1.0' // optional
implementation 'ru.noties:markwon-syntax:1.1.0' // optional
implementation 'ru.noties:markwon-view:1.1.0' // optional
```
### Snapshot
@ -35,10 +37,10 @@ allprojects {
and then in your module `build.gradle`:
```groovy
implementation 'ru.noties:markwon:1.0.6-SNAPSHOT'
implementation 'ru.noties:markwon:1.1.0-SNAPSHOT'
```
Please note that `markwon-image-loader` and `markwon-view` are also present in `SNAPSHOT` repository and share the same version as main `markwon` artifact.
Please note that `markwon-image-loader`, `markwon-syntax` and `markwon-view` are also present in `SNAPSHOT` repository and share the same version as main `markwon` artifact.
## Supported markdown features:
* Emphasis (`*`, `_`)
@ -141,6 +143,14 @@ you can use [Better-Link-Movement-Method][better-link-movement-method].
Please refer to [SpannableConfiguration] document for more info
## Syntax highlight
Starting with version `1.1.0` there is an artifact (`markwon-syntax`) that allows you to have syntax highlight functionality.
It is based on [Prism4j](https://github.com/noties/Prism4j) project. It contains 2 builtin themes:
`Default` (light, `Prism4jThemeDefault`) and `Darkula` (dark, `Prism4jThemeDarkula`).
[library-syntax](./library-syntax/)
---
# Demo

View File

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

View File

@ -15,9 +15,17 @@ import dagger.Provides;
import okhttp3.Cache;
import okhttp3.OkHttpClient;
import ru.noties.markwon.il.AsyncDrawableLoader;
import ru.noties.markwon.il.GifMediaDecoder;
import ru.noties.markwon.il.ImageMediaDecoder;
import ru.noties.markwon.il.SvgMediaDecoder;
import ru.noties.markwon.spans.AsyncDrawable;
import ru.noties.markwon.syntax.Prism4jThemeDarkula;
import ru.noties.markwon.syntax.Prism4jThemeDefault;
import ru.noties.prism4j.Prism4j;
import ru.noties.prism4j.annotations.PrismBundle;
@Module
@PrismBundle(includeAll = true)
class AppModule {
private final App app;
@ -40,7 +48,7 @@ class AppModule {
@Singleton
OkHttpClient client() {
return new OkHttpClient.Builder()
.cache(new Cache(app.getCacheDir(), 1024L * 20))
.cache(new Cache(app.getCacheDir(), 1024L * 1024 * 20)) // 20 mb
.followRedirects(true)
.retryOnConnectionFailure(true)
.build();
@ -73,6 +81,35 @@ class AppModule {
.client(client)
.executorService(executorService)
.resources(resources)
.mediaDecoders(
SvgMediaDecoder.create(resources),
GifMediaDecoder.create(false),
ImageMediaDecoder.create(resources)
)
.build();
}
@Provides
@Singleton
Prism4j prism4j() {
return new Prism4j(new GrammarLocatorDef());
}
@Singleton
@Provides
Prism4jThemeDefault prism4jThemeDefault() {
return Prism4jThemeDefault.create();
}
@Singleton
@Provides
Prism4jThemeDarkula prism4jThemeDarkula() {
return Prism4jThemeDarkula.create();
}
@Singleton
@Provides
GifProcessor gifProcessor() {
return GifProcessor.create();
}
}

View File

@ -0,0 +1,58 @@
package ru.noties.markwon;
import android.graphics.Canvas;
import android.graphics.drawable.Drawable;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import pl.droidsonroids.gif.GifDrawable;
import ru.noties.markwon.renderer.ImageSize;
import ru.noties.markwon.renderer.ImageSizeResolver;
import ru.noties.markwon.spans.AsyncDrawable;
public class GifAwareAsyncDrawable extends AsyncDrawable {
public interface OnGifResultListener {
void onGifResult(@NonNull GifAwareAsyncDrawable drawable);
}
private final Drawable gifPlaceholder;
private OnGifResultListener onGifResultListener;
private boolean isGif;
public GifAwareAsyncDrawable(
@NonNull Drawable gifPlaceholder,
@NonNull String destination,
@NonNull Loader loader,
@Nullable ImageSizeResolver imageSizeResolver,
@Nullable ImageSize imageSize) {
super(destination, loader, imageSizeResolver, imageSize);
this.gifPlaceholder = gifPlaceholder;
}
public void onGifResultListener(@Nullable OnGifResultListener onGifResultListener) {
this.onGifResultListener = onGifResultListener;
}
@Override
public void setResult(@NonNull Drawable result) {
super.setResult(result);
isGif = result instanceof GifDrawable;
if (isGif && onGifResultListener != null) {
onGifResultListener.onGifResult(this);
}
}
@Override
public void draw(@NonNull Canvas canvas) {
super.draw(canvas);
if (isGif) {
final GifDrawable drawable = (GifDrawable) getResult();
if (!drawable.isPlaying()) {
gifPlaceholder.setBounds(drawable.getBounds());
gifPlaceholder.draw(canvas);
}
}
}
}

View File

@ -0,0 +1,36 @@
package ru.noties.markwon;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import ru.noties.markwon.renderer.ImageSize;
import ru.noties.markwon.renderer.ImageSizeResolver;
import ru.noties.markwon.spans.AsyncDrawable;
import ru.noties.markwon.spans.AsyncDrawableSpan;
import ru.noties.markwon.spans.SpannableTheme;
public class GifAwareSpannableFactory extends SpannableFactoryDef {
private final GifPlaceholder gifPlaceholder;
public GifAwareSpannableFactory(@NonNull GifPlaceholder gifPlaceholder) {
this.gifPlaceholder = gifPlaceholder;
}
@Nullable
@Override
public Object image(@NonNull SpannableTheme theme, @NonNull String destination, @NonNull AsyncDrawable.Loader loader, @NonNull ImageSizeResolver imageSizeResolver, @Nullable ImageSize imageSize, boolean replacementTextIsLink) {
return new AsyncDrawableSpan(
theme,
new GifAwareAsyncDrawable(
gifPlaceholder,
destination,
loader,
imageSizeResolver,
imageSize
),
AsyncDrawableSpan.ALIGN_BOTTOM,
replacementTextIsLink
);
}
}

View File

@ -0,0 +1,77 @@
package ru.noties.markwon;
import android.graphics.Canvas;
import android.graphics.ColorFilter;
import android.graphics.Paint;
import android.graphics.PixelFormat;
import android.graphics.Rect;
import android.graphics.drawable.Drawable;
import android.support.annotation.ColorInt;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
public class GifPlaceholder extends Drawable {
private final Drawable icon;
private final Paint paint;
private float left;
private float top;
public GifPlaceholder(@NonNull Drawable icon, @ColorInt int background) {
this.icon = icon;
if (icon.getBounds().isEmpty()) {
icon.setBounds(0, 0, icon.getIntrinsicWidth(), icon.getIntrinsicHeight());
}
if (background != 0) {
paint = new Paint(Paint.ANTI_ALIAS_FLAG);
paint.setStyle(Paint.Style.FILL);
paint.setColor(background);
} else {
paint = null;
}
}
@Override
protected void onBoundsChange(Rect bounds) {
super.onBoundsChange(bounds);
final int w = bounds.width();
final int h = bounds.height();
this.left = (w - icon.getBounds().width()) / 2;
this.top = (h - icon.getBounds().height()) / 2;
}
@Override
public void draw(@NonNull Canvas canvas) {
if (paint != null) {
canvas.drawRect(getBounds(), paint);
}
final int save = canvas.save();
try {
canvas.translate(left, top);
icon.draw(canvas);
} finally {
canvas.restoreToCount(save);
}
}
@Override
public void setAlpha(int alpha) {
// no op
}
@Override
public void setColorFilter(@Nullable ColorFilter colorFilter) {
// no op
}
@Override
public int getOpacity() {
return PixelFormat.OPAQUE;
}
}

View File

@ -0,0 +1,125 @@
package ru.noties.markwon;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.text.Spannable;
import android.text.Spanned;
import android.text.style.ClickableSpan;
import android.view.View;
import android.widget.TextView;
import pl.droidsonroids.gif.GifDrawable;
import ru.noties.markwon.spans.AsyncDrawableSpan;
public abstract class GifProcessor {
public abstract void process(@NonNull TextView textView);
@NonNull
public static GifProcessor create() {
return new Impl();
}
static class Impl extends GifProcessor {
@Override
public void process(@NonNull final TextView textView) {
// here is what we will do additionally:
// we query for all asyncDrawableSpans
// we check if they are inside clickableSpan
// if not we apply onGifListener
final Spannable spannable = spannable(textView);
if (spannable == null) {
return;
}
final AsyncDrawableSpan[] asyncDrawableSpans =
spannable.getSpans(0, spannable.length(), AsyncDrawableSpan.class);
if (asyncDrawableSpans == null
|| asyncDrawableSpans.length == 0) {
return;
}
int start;
int end;
ClickableSpan[] clickableSpans;
for (final AsyncDrawableSpan asyncDrawableSpan : asyncDrawableSpans) {
start = spannable.getSpanStart(asyncDrawableSpan);
end = spannable.getSpanEnd(asyncDrawableSpan);
if (start < 0
|| end < 0) {
continue;
}
clickableSpans = spannable.getSpans(start, end, ClickableSpan.class);
if (clickableSpans != null
&& clickableSpans.length > 0) {
continue;
}
((GifAwareAsyncDrawable) asyncDrawableSpan.getDrawable()).onGifResultListener(new GifAwareAsyncDrawable.OnGifResultListener() {
@Override
public void onGifResult(@NonNull GifAwareAsyncDrawable drawable) {
addGifClickSpan(textView, asyncDrawableSpan, drawable);
}
});
}
}
@Nullable
private static Spannable spannable(@NonNull TextView textView) {
final CharSequence charSequence = textView.getText();
if (charSequence instanceof Spannable) {
return (Spannable) charSequence;
}
return null;
}
private static void addGifClickSpan(
@NonNull TextView textView,
@NonNull AsyncDrawableSpan span,
@NonNull GifAwareAsyncDrawable drawable) {
// important thing here is to obtain new spannable from textView
// as with each `setText()` new spannable is created and keeping reference
// to an older one won't affect textView
final Spannable spannable = spannable(textView);
if (spannable == null) {
return;
}
final int start = spannable.getSpanStart(span);
final int end = spannable.getSpanEnd(span);
if (start < 0
|| end < 0) {
return;
}
final GifDrawable gifDrawable = (GifDrawable) drawable.getResult();
spannable.setSpan(new GifToggleClickableSpan(gifDrawable), start, end, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
}
private static class GifToggleClickableSpan extends ClickableSpan {
private final GifDrawable gifDrawable;
GifToggleClickableSpan(@NonNull GifDrawable gifDrawable) {
this.gifDrawable = gifDrawable;
}
@Override
public void onClick(View widget) {
if (gifDrawable.isPlaying()) {
gifDrawable.pause();
} else {
gifDrawable.start();
}
}
}
}
}

View File

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

View File

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

View File

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 322 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 378 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 536 B

View File

@ -1,6 +1,17 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<color name="colorPrimary">#424242</color>
<color name="colorPrimaryDark">#212121</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>

View File

@ -10,7 +10,14 @@
<item name="ic_app_bar_theme">@drawable/ic_app_bar_theme_dark</item>
</style>
<style name="AppThemeLight" parent="AppThemeBaseLight" />
<style name="AppThemeDark" parent="AppThemeBaseDark" />
<style name="AppThemeLight" parent="AppThemeBaseLight">
<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>

View File

@ -10,6 +10,9 @@ buildscript {
allprojects {
repositories {
if (project.hasProperty('LOCAL_MAVEN_URL')) {
maven { url LOCAL_MAVEN_URL }
}
jcenter()
google()
}
@ -22,28 +25,31 @@ task clean(type: Delete) {
}
task wrapper(type: Wrapper) {
gradleVersion '4.5'
gradleVersion '4.8.1'
distributionType 'all'
}
ext {
// Config
BUILD_TOOLS = '26.0.3'
TARGET_SDK = 26
BUILD_TOOLS = '27.0.3'
TARGET_SDK = 27
MIN_SDK = 16
// Dependencies
final def supportVersion = '26.1.0'
final def supportVersion = '27.1.1'
SUPPORT_ANNOTATIONS = "com.android.support:support-annotations:$supportVersion"
SUPPORT_APP_COMPAT = "com.android.support:appcompat-v7:$supportVersion"
final def commonMarkVersion = '0.10.0'
final def commonMarkVersion = '0.11.0'
COMMON_MARK = "com.atlassian.commonmark:commonmark:$commonMarkVersion"
COMMON_MARK_STRIKETHROUGHT = "com.atlassian.commonmark:commonmark-ext-gfm-strikethrough:$commonMarkVersion"
COMMON_MARK_TABLE = "com.atlassian.commonmark:commonmark-ext-gfm-tables:$commonMarkVersion"
ANDROID_SVG = 'com.caverock:androidsvg:1.2.1'
ANDROID_GIF = 'pl.droidsonroids.gif:android-gif-drawable:1.2.8'
ANDROID_GIF = 'pl.droidsonroids.gif:android-gif-drawable:1.2.14'
OK_HTTP = 'com.squareup.okhttp3:okhttp:3.9.0'
PRISM_4J = 'ru.noties:prism4j:1.1.0'
PRISM_4J_BUNDLER = 'ru.noties:prism4j-bundler:1.1.0'
}

View File

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

Binary file not shown.

View File

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

View File

@ -25,6 +25,14 @@ dependencies {
api OK_HTTP
}
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'
}

View File

@ -1,28 +1,22 @@
package ru.noties.markwon.il;
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.net.Uri;
import android.os.Handler;
import android.os.Looper;
import android.support.annotation.NonNull;
import android.text.TextUtils;
import com.caverock.androidsvg.SVG;
import com.caverock.androidsvg.SVGParseException;
import android.support.annotation.Nullable;
import java.io.BufferedInputStream;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.lang.ref.WeakReference;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
@ -34,22 +28,21 @@ import okhttp3.OkHttpClient;
import okhttp3.Request;
import okhttp3.Response;
import okhttp3.ResponseBody;
import pl.droidsonroids.gif.GifDrawable;
import ru.noties.markwon.spans.AsyncDrawable;
public class AsyncDrawableLoader implements AsyncDrawable.Loader {
@NonNull
public static AsyncDrawableLoader create() {
return builder().build();
}
@NonNull
public static AsyncDrawableLoader.Builder builder() {
return new Builder();
}
private 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";
@ -58,6 +51,7 @@ public class AsyncDrawableLoader implements AsyncDrawable.Loader {
private final ExecutorService executorService;
private final Handler mainThread;
private final Drawable errorDrawable;
private final List<MediaDecoder> mediaDecoders;
private final Map<String, Future<?>> requests;
@ -67,6 +61,7 @@ public class AsyncDrawableLoader implements AsyncDrawable.Loader {
this.executorService = builder.executorService;
this.mainThread = new Handler(Looper.getMainLooper());
this.errorDrawable = builder.errorDrawable;
this.mediaDecoders = builder.mediaDecoders;
this.requests = new HashMap<>(3);
}
@ -105,12 +100,15 @@ public class AsyncDrawableLoader implements AsyncDrawable.Loader {
public void run() {
final Item item;
final boolean isFromFile;
final Uri uri = Uri.parse(destination);
if ("file".equals(uri.getScheme())) {
item = fromFile(uri);
isFromFile = true;
} else {
item = fromNetwork(destination);
isFromFile = false;
}
Drawable result = null;
@ -118,13 +116,15 @@ public class AsyncDrawableLoader implements AsyncDrawable.Loader {
if (item != null
&& item.inputStream != null) {
try {
if (CONTENT_TYPE_SVG.equals(item.type)) {
result = handleSvg(item.inputStream);
} else if (CONTENT_TYPE_GIF.equals(item.type)) {
result = handleGif(item.inputStream);
} else {
result = handleSimple(item.inputStream);
final MediaDecoder mediaDecoder = isFromFile
? mediaDecoderFromFile(item.fileName)
: mediaDecoderFromContentType(item.contentType);
if (mediaDecoder != null) {
result = mediaDecoder.decode(item.inputStream);
}
} finally {
try {
item.inputStream.close();
@ -157,7 +157,8 @@ public class AsyncDrawableLoader implements AsyncDrawable.Loader {
});
}
private Item fromFile(Uri uri) {
@Nullable
private Item fromFile(@NonNull Uri uri) {
final List<String> segments = uri.getPathSegments();
if (segments == null
@ -167,19 +168,10 @@ public class AsyncDrawableLoader implements AsyncDrawable.Loader {
}
final Item out;
final String type;
final InputStream inputStream;
final boolean assets = FILE_ANDROID_ASSETS.equals(segments.get(0));
final String lastSegment = uri.getLastPathSegment();
if (lastSegment.endsWith(".svg")) {
type = CONTENT_TYPE_SVG;
} else if (lastSegment.endsWith(".gif")) {
type = CONTENT_TYPE_GIF;
} else {
type = null;
}
final String fileName = uri.getLastPathSegment();
if (assets) {
final StringBuilder path = new StringBuilder();
@ -208,7 +200,7 @@ public class AsyncDrawableLoader implements AsyncDrawable.Loader {
}
if (inputStream != null) {
out = new Item(type, inputStream);
out = new Item(fileName, null, inputStream);
} else {
out = null;
}
@ -216,7 +208,8 @@ public class AsyncDrawableLoader implements AsyncDrawable.Loader {
return out;
}
private Item fromNetwork(String destination) {
@Nullable
private Item fromNetwork(@NonNull String destination) {
Item out = null;
@ -237,15 +230,8 @@ public class AsyncDrawableLoader implements AsyncDrawable.Loader {
if (body != null) {
final InputStream inputStream = body.byteStream();
if (inputStream != null) {
final String type;
final String contentType = response.header(HEADER_CONTENT_TYPE);
if (!TextUtils.isEmpty(contentType)
&& contentType.startsWith(CONTENT_TYPE_SVG)) {
type = CONTENT_TYPE_SVG;
} else {
type = contentType;
}
out = new Item(type, inputStream);
out = new Item(null, contentType, inputStream);
}
}
}
@ -253,87 +239,31 @@ public class AsyncDrawableLoader implements AsyncDrawable.Loader {
return out;
}
private Drawable handleSvg(InputStream stream) {
@Nullable
private MediaDecoder mediaDecoderFromFile(@NonNull String fileName) {
final Drawable out;
MediaDecoder out = null;
SVG svg = null;
try {
svg = SVG.getFromInputStream(stream);
} catch (SVGParseException e) {
e.printStackTrace();
}
if (svg == null) {
out = null;
} else {
final float w = svg.getDocumentWidth();
final float h = svg.getDocumentHeight();
final float density = resources.getDisplayMetrics().density;
final int width = (int) (w * density + .5F);
final int height = (int) (h * density + .5F);
final Bitmap bitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_4444);
final Canvas canvas = new Canvas(bitmap);
canvas.scale(density, density);
svg.renderToCanvas(canvas);
out = new BitmapDrawable(resources, bitmap);
DrawableUtils.intrinsicBounds(out);
}
return out;
}
private Drawable handleGif(InputStream stream) {
Drawable out = null;
final byte[] bytes = readBytes(stream);
if (bytes != null) {
try {
out = new GifDrawable(bytes);
DrawableUtils.intrinsicBounds(out);
} catch (IOException e) {
e.printStackTrace();
for (MediaDecoder mediaDecoder : mediaDecoders) {
if (mediaDecoder.canDecodeByFileName(fileName)) {
out = mediaDecoder;
break;
}
}
return out;
}
private Drawable handleSimple(InputStream stream) {
@Nullable
private MediaDecoder mediaDecoderFromContentType(@Nullable String contentType) {
final Drawable out;
MediaDecoder out = null;
final Bitmap bitmap = BitmapFactory.decodeStream(stream);
if (bitmap != null) {
out = new BitmapDrawable(resources, bitmap);
DrawableUtils.intrinsicBounds(out);
} else {
out = null;
}
return out;
}
private static byte[] readBytes(InputStream stream) {
byte[] out = null;
try {
final ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
final int length = 1024 * 8;
final byte[] buffer = new byte[length];
int read;
while ((read = stream.read(buffer, 0, length)) != -1) {
outputStream.write(buffer, 0, read);
for (MediaDecoder mediaDecoder : mediaDecoders) {
if (mediaDecoder.canDecodeByContentType(contentType)) {
out = mediaDecoder;
break;
}
out = outputStream.toByteArray();
} catch (IOException e) {
e.printStackTrace();
}
return out;
@ -346,47 +276,93 @@ public class AsyncDrawableLoader implements AsyncDrawable.Loader {
private ExecutorService executorService;
private Drawable errorDrawable;
// @since 1.1.0
private final List<MediaDecoder> mediaDecoders = new ArrayList<>(3);
@NonNull
public Builder client(@NonNull OkHttpClient client) {
this.client = client;
return this;
}
/**
* Supplied resources argument will be used to open files from assets directory
* and to create default {@link MediaDecoder}\'s which require resources instance
*
* @return self
*/
@NonNull
public Builder resources(@NonNull Resources resources) {
this.resources = resources;
return this;
}
public Builder executorService(ExecutorService executorService) {
@NonNull
public Builder executorService(@NonNull ExecutorService executorService) {
this.executorService = executorService;
return this;
}
public Builder errorDrawable(Drawable errorDrawable) {
@NonNull
public Builder errorDrawable(@NonNull Drawable errorDrawable) {
this.errorDrawable = errorDrawable;
return this;
}
@NonNull
public Builder mediaDecoders(@NonNull List<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() {
if (client == null) {
client = new OkHttpClient();
}
if (resources == null) {
resources = Resources.getSystem();
}
if (executorService == null) {
// we will use executor from okHttp
executorService = client.dispatcher().executorService();
}
// add default media decoders if not specified
if (mediaDecoders.size() == 0) {
mediaDecoders.add(SvgMediaDecoder.create(resources));
mediaDecoders.add(GifMediaDecoder.create(true));
mediaDecoders.add(ImageMediaDecoder.create(resources));
}
return new AsyncDrawableLoader(this);
}
}
private static class Item {
final String type;
final String fileName;
final String contentType;
final InputStream inputStream;
Item(String type, InputStream inputStream) {
this.type = type;
Item(@Nullable String fileName, @Nullable String contentType, @Nullable InputStream inputStream) {
this.fileName = fileName;
this.contentType = contentType;
this.inputStream = inputStream;
}
}

View File

@ -0,0 +1,90 @@
package ru.noties.markwon.il;
import android.graphics.drawable.Drawable;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import pl.droidsonroids.gif.GifDrawable;
/**
* @since 1.1.0
*/
public class GifMediaDecoder extends MediaDecoder {
protected static final String CONTENT_TYPE_GIF = "image/gif";
protected static final String FILE_EXTENSION_GIF = ".gif";
@NonNull
public static GifMediaDecoder create(boolean autoPlayGif) {
return new GifMediaDecoder(autoPlayGif);
}
private final boolean autoPlayGif;
protected GifMediaDecoder(boolean autoPlayGif) {
this.autoPlayGif = autoPlayGif;
}
@Override
public boolean canDecodeByContentType(@Nullable String contentType) {
return CONTENT_TYPE_GIF.equals(contentType);
}
@Override
public boolean canDecodeByFileName(@NonNull String fileName) {
return fileName.endsWith(FILE_EXTENSION_GIF);
}
@Nullable
@Override
public Drawable decode(@NonNull InputStream inputStream) {
Drawable out = null;
final byte[] bytes = readBytes(inputStream);
if (bytes != null) {
try {
out = newGifDrawable(bytes);
DrawableUtils.intrinsicBounds(out);
if (!autoPlayGif) {
((GifDrawable) out).pause();
}
} catch (IOException e) {
e.printStackTrace();
}
}
return out;
}
@NonNull
protected Drawable newGifDrawable(@NonNull byte[] bytes) throws IOException {
return new GifDrawable(bytes);
}
@Nullable
protected static byte[] readBytes(@NonNull InputStream stream) {
byte[] out = null;
try {
final ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
final int length = 1024 * 8;
final byte[] buffer = new byte[length];
int read;
while ((read = stream.read(buffer, 0, length)) != -1) {
outputStream.write(buffer, 0, read);
}
out = outputStream.toByteArray();
} catch (IOException e) {
e.printStackTrace();
}
return out;
}
}

View File

@ -0,0 +1,58 @@
package ru.noties.markwon.il;
import android.content.res.Resources;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.graphics.drawable.BitmapDrawable;
import android.graphics.drawable.Drawable;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import java.io.InputStream;
/**
* This class can be used as the last {@link MediaDecoder} to _try_ to handle all rest cases.
* Here we just assume that supplied InputStream is of image type and try to decode it.
*
* @since 1.1.0
*/
public class ImageMediaDecoder extends MediaDecoder {
@NonNull
public static ImageMediaDecoder create(@NonNull Resources resources) {
return new ImageMediaDecoder(resources);
}
private final Resources resources;
ImageMediaDecoder(Resources resources) {
this.resources = resources;
}
@Override
public boolean canDecodeByContentType(@Nullable String contentType) {
return true;
}
@Override
public boolean canDecodeByFileName(@NonNull String fileName) {
return true;
}
@Nullable
@Override
public Drawable decode(@NonNull InputStream inputStream) {
final Drawable out;
final Bitmap bitmap = BitmapFactory.decodeStream(inputStream);
if (bitmap != null) {
out = new BitmapDrawable(resources, bitmap);
DrawableUtils.intrinsicBounds(out);
} else {
out = null;
}
return out;
}
}

View File

@ -0,0 +1,20 @@
package ru.noties.markwon.il;
import android.graphics.drawable.Drawable;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import java.io.InputStream;
/**
* @since 1.1.0
*/
public abstract class MediaDecoder {
public abstract boolean canDecodeByContentType(@Nullable String contentType);
public abstract boolean canDecodeByFileName(@NonNull String fileName);
@Nullable
public abstract Drawable decode(@NonNull InputStream inputStream);
}

View File

@ -0,0 +1,80 @@
package ru.noties.markwon.il;
import android.content.res.Resources;
import android.graphics.Bitmap;
import android.graphics.Canvas;
import android.graphics.drawable.BitmapDrawable;
import android.graphics.drawable.Drawable;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import com.caverock.androidsvg.SVG;
import com.caverock.androidsvg.SVGParseException;
import java.io.InputStream;
/**
* @since 1.1.0
*/
public class SvgMediaDecoder extends MediaDecoder {
private static final String CONTENT_TYPE_SVG = "image/svg+xml";
private static final String FILE_EXTENSION_SVG = ".svg";
@NonNull
public static SvgMediaDecoder create(@NonNull Resources resources) {
return new SvgMediaDecoder(resources);
}
private final Resources resources;
SvgMediaDecoder(Resources resources) {
this.resources = resources;
}
@Override
public boolean canDecodeByContentType(@Nullable String contentType) {
return contentType != null && contentType.startsWith(CONTENT_TYPE_SVG);
}
@Override
public boolean canDecodeByFileName(@NonNull String fileName) {
return fileName.endsWith(FILE_EXTENSION_SVG);
}
@Nullable
@Override
public Drawable decode(@NonNull InputStream inputStream) {
final Drawable out;
SVG svg = null;
try {
svg = SVG.getFromInputStream(inputStream);
} catch (SVGParseException e) {
e.printStackTrace();
}
if (svg == null) {
out = null;
} else {
final float w = svg.getDocumentWidth();
final float h = svg.getDocumentHeight();
final float density = resources.getDisplayMetrics().density;
final int width = (int) (w * density + .5F);
final int height = (int) (h * density + .5F);
final Bitmap bitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_4444);
final Canvas canvas = new Canvas(bitmap);
canvas.scale(density, density);
svg.renderToCanvas(canvas);
out = new BitmapDrawable(resources, bitmap);
DrawableUtils.intrinsicBounds(out);
}
return out;
}
}

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

@ -0,0 +1,64 @@
# Markwon-syntax
This is a simple module to add **syntax-highlight** functionality to your markdown rendered with Markwon library. It is based on [Prism4j](https://github.com/noties/Prism4j) so lead there to understand how to configure `Prism4j` instance.
![theme-default](./art/markwon-syntax-default.png)
![theme-darkula](./art/markwon-syntax-darkula.png)
---
First, we need to obtain an instance of `Prism4jSyntaxHighlight` which implements Markwon's `SyntaxHighlight`:
```java
final SyntaxHighlight highlight =
Prism4jSyntaxHighlight.create(Prism4j, Prism4jTheme);
```
we also can obtain an instance of `Prism4jSyntaxHighlight` that has a _fallback_ option (if a language is not defined in `Prism4j` instance, fallback language can be used):
```java
final SyntaxHighlight highlight =
Prism4jSyntaxHighlight.create(Prism4j, Prism4jTheme, String);
```
Generally obtaining a `Prism4j` instance is pretty easy:
```java
final Prism4j prism4j = new Prism4j(new GrammarLocatorDef());
```
Where `GrammarLocatorDef` is a generated grammar locator (if you use `prism4j-bundler` annotation processor)
`Prism4jTheme` is a specific type that is defined in this module (`prism4j` doesn't know anything about rendering). It has 2 implementations:
* `Prism4jThemeDefault`
* `Prism4jThemeDarkula`
Both of them can be obtained via factory method `create`:
* `Prism4jThemeDefault.create()`
* `Prism4jThemeDarkula.create()`
But of cause nothing is stopping you from defining your own theme:
```java
public interface Prism4jTheme {
@ColorInt
int background();
@ColorInt
int textColor();
void apply(
@NonNull String language,
@NonNull Prism4j.Syntax syntax,
@NonNull SpannableStringBuilder builder,
int start,
int end
);
}
```
> You can extend `Prism4jThemeBase` which has some helper methods

Binary file not shown.

After

Width:  |  Height:  |  Size: 30 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 33 KiB

View File

@ -0,0 +1,32 @@
apply plugin: 'com.android.library'
android {
compileSdkVersion TARGET_SDK
buildToolsVersion BUILD_TOOLS
defaultConfig {
minSdkVersion MIN_SDK
targetSdkVersion TARGET_SDK
versionCode 1
versionName version
}
}
dependencies {
api SUPPORT_ANNOTATIONS
api PRISM_4J
api project(':library')
}
afterEvaluate {
generateReleaseBuildConfig.enabled = false
}
if (hasProperty('release')) {
if (hasProperty('local')) {
ext.RELEASE_REPOSITORY_URL = LOCAL_MAVEN_URL
ext.SNAPSHOT_REPOSITORY_URL = LOCAL_MAVEN_URL
}
apply from: 'https://raw.githubusercontent.com/noties/gradle-mvn-push/master/gradle-mvn-push-aar.gradle'
}

View File

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

View File

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

View File

@ -0,0 +1,105 @@
package ru.noties.markwon.syntax;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.text.SpannableStringBuilder;
import android.text.TextUtils;
import ru.noties.markwon.SyntaxHighlight;
import ru.noties.prism4j.Prism4j;
public class Prism4jSyntaxHighlight implements SyntaxHighlight {
@NonNull
public static Prism4jSyntaxHighlight create(
@NonNull Prism4j prism4j,
@NonNull Prism4jTheme theme) {
return new Prism4jSyntaxHighlight(prism4j, theme, null);
}
@NonNull
public static Prism4jSyntaxHighlight create(
@NonNull Prism4j prism4j,
@NonNull Prism4jTheme theme,
@Nullable String fallback) {
return new Prism4jSyntaxHighlight(prism4j, theme, fallback);
}
private final Prism4j prism4j;
private final Prism4jTheme theme;
private final String fallback;
protected Prism4jSyntaxHighlight(
@NonNull Prism4j prism4j,
@NonNull Prism4jTheme theme,
@Nullable String fallback) {
this.prism4j = prism4j;
this.theme = theme;
this.fallback = fallback;
}
@NonNull
@Override
public CharSequence highlight(@Nullable String info, @NonNull String code) {
// if info is null, do not highlight -> LICENCE footer very commonly wrapped inside code
// block without syntax name specified (so, do not highlight)
return info == null
? highlightNoLanguageInfo(code)
: highlightWithLanguageInfo(info, code);
}
@NonNull
protected CharSequence highlightNoLanguageInfo(@NonNull String code) {
return code;
}
@NonNull
protected CharSequence highlightWithLanguageInfo(@NonNull String info, @NonNull String code) {
final CharSequence out;
final String language;
final Prism4j.Grammar grammar;
{
String _language = info;
Prism4j.Grammar _grammar = prism4j.grammar(info);
if (_grammar == null && !TextUtils.isEmpty(fallback)) {
_language = fallback;
_grammar = prism4j.grammar(fallback);
}
language = _language;
grammar = _grammar;
}
if (grammar != null) {
out = highlight(language, grammar, code);
} else {
out = code;
}
return out;
}
@NonNull
protected CharSequence highlight(@NonNull String language, @NonNull Prism4j.Grammar grammar, @NonNull String code) {
final SpannableStringBuilder builder = new SpannableStringBuilder();
final Prism4jSyntaxVisitor visitor = new Prism4jSyntaxVisitor(language, theme, builder);
visitor.visit(prism4j.tokenize(code, grammar));
return builder;
}
@NonNull
protected Prism4j prism4j() {
return prism4j;
}
@NonNull
protected Prism4jTheme theme() {
return theme;
}
@Nullable
protected String fallback() {
return fallback;
}
}

View File

@ -0,0 +1,40 @@
package ru.noties.markwon.syntax;
import android.support.annotation.NonNull;
import android.text.SpannableStringBuilder;
import ru.noties.prism4j.AbsVisitor;
import ru.noties.prism4j.Prism4j;
class Prism4jSyntaxVisitor extends AbsVisitor {
private final String language;
private final Prism4jTheme theme;
private final SpannableStringBuilder builder;
Prism4jSyntaxVisitor(
@NonNull String language,
@NonNull Prism4jTheme theme,
@NonNull SpannableStringBuilder builder) {
this.language = language;
this.theme = theme;
this.builder = builder;
}
@Override
protected void visitText(@NonNull Prism4j.Text text) {
builder.append(text.literal());
}
@Override
protected void visitSyntax(@NonNull Prism4j.Syntax syntax) {
final int start = builder.length();
visit(syntax.children());
final int end = builder.length();
if (end != start) {
theme.apply(language, syntax, builder, start, end);
}
}
}

View File

@ -0,0 +1,24 @@
package ru.noties.markwon.syntax;
import android.support.annotation.ColorInt;
import android.support.annotation.NonNull;
import android.text.SpannableStringBuilder;
import ru.noties.prism4j.Prism4j;
public interface Prism4jTheme {
@ColorInt
int background();
@ColorInt
int textColor();
void apply(
@NonNull String language,
@NonNull Prism4j.Syntax syntax,
@NonNull SpannableStringBuilder builder,
int start,
int end
);
}

View File

@ -0,0 +1,140 @@
package ru.noties.markwon.syntax;
import android.support.annotation.ColorInt;
import android.support.annotation.FloatRange;
import android.support.annotation.IntRange;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.text.SpannableStringBuilder;
import android.text.Spanned;
import android.text.style.ForegroundColorSpan;
import java.util.HashMap;
import ru.noties.prism4j.Prism4j;
public abstract class Prism4jThemeBase implements Prism4jTheme {
@ColorInt
protected static int applyAlpha(@IntRange(from = 0, to = 255) int alpha, @ColorInt int color) {
return (color & 0x00FFFFFF) | (alpha << 24);
}
@ColorInt
protected static int applyAlpha(@FloatRange(from = .0F, to = 1.F) float alpha, @ColorInt int color) {
return applyAlpha((int) (255 * alpha + .5F), color);
}
protected static boolean isOfType(@NonNull String expected, @NonNull String type, @Nullable String alias) {
return expected.equals(type) || expected.equals(alias);
}
private final ColorHashMap colorHashMap;
protected Prism4jThemeBase() {
this.colorHashMap = init();
}
@NonNull
protected abstract ColorHashMap init();
@ColorInt
protected int color(@NonNull String language, @NonNull String type, @Nullable String alias) {
Color color = colorHashMap.get(type);
if (color == null
&& alias != null) {
color = colorHashMap.get(alias);
}
return color != null
? color.color
: 0;
}
@Override
public void apply(
@NonNull String language,
@NonNull Prism4j.Syntax syntax,
@NonNull SpannableStringBuilder builder,
int start,
int end) {
final String type = syntax.type();
final String alias = syntax.alias();
final int color = color(language, type, alias);
if (color != 0) {
applyColor(language, type, alias, color, builder, start, end);
}
}
@SuppressWarnings("unused")
protected void applyColor(
@NonNull String language,
@NonNull String type,
@Nullable String alias,
@ColorInt int color,
@NonNull SpannableStringBuilder builder,
int start,
int end) {
builder.setSpan(new ForegroundColorSpan(color), start, end, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
}
protected static class Color {
@NonNull
public static Color of(@ColorInt int color) {
return new Color(color);
}
@ColorInt
protected final int color;
protected Color(@ColorInt int color) {
this.color = color;
}
}
protected static class ColorHashMap extends HashMap<String, Color> {
@NonNull
protected ColorHashMap add(@ColorInt int color, String name) {
put(name, Color.of(color));
return this;
}
@NonNull
protected ColorHashMap add(
@ColorInt int color,
@NonNull String name1,
@NonNull String name2) {
final Color c = Color.of(color);
put(name1, c);
put(name2, c);
return this;
}
@NonNull
protected ColorHashMap add(
@ColorInt int color,
@NonNull String name1,
@NonNull String name2,
@NonNull String name3) {
final Color c = Color.of(color);
put(name1, c);
put(name2, c);
put(name3, c);
return this;
}
@NonNull
protected ColorHashMap add(@ColorInt int color, String... names) {
final Color c = Color.of(color);
for (String name : names) {
put(name, c);
}
return this;
}
}
}

View File

@ -0,0 +1,61 @@
package ru.noties.markwon.syntax;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.text.SpannableStringBuilder;
import android.text.Spanned;
import ru.noties.markwon.spans.EmphasisSpan;
import ru.noties.markwon.spans.StrongEmphasisSpan;
public class Prism4jThemeDarkula extends Prism4jThemeBase {
@NonNull
public static Prism4jThemeDarkula create() {
return new Prism4jThemeDarkula();
}
@Override
public int background() {
return 0xFF2d2d2d;
}
@Override
public int textColor() {
return 0xFFa9b7c6;
}
@NonNull
@Override
protected ColorHashMap init() {
return new ColorHashMap()
.add(0xFF808080, "comment", "prolog", "cdata")
.add(0xFFcc7832, "delimiter", "boolean", "keyword", "selector", "important", "atrule")
.add(0xFFa9b7c6, "operator", "punctuation", "attr-name")
.add(0xFFe8bf6a, "tag", "doctype", "builtin")
.add(0xFF6897bb, "entity", "number", "symbol")
.add(0xFF9876aa, "property", "constant", "variable")
.add(0xFF6a8759, "string", "char")
.add(0xFFbbb438, "annotation")
.add(0xFFa5c261, "attr-value")
.add(0xFF287bde, "url")
.add(0xFFffc66d, "function")
.add(0xFF364135, "regex")
.add(0xFF294436, "inserted")
.add(0xFF484a4a, "deleted");
}
@Override
protected void applyColor(@NonNull String language, @NonNull String type, @Nullable String alias, int color, @NonNull SpannableStringBuilder builder, int start, int end) {
super.applyColor(language, type, alias, color, builder, start, end);
if (isOfType("important", type, alias)
|| isOfType("bold", type, alias)) {
builder.setSpan(new StrongEmphasisSpan(), start, end, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
}
if (isOfType("italic", type, alias)) {
builder.setSpan(new EmphasisSpan(), start, end, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
}
}
}

View File

@ -0,0 +1,75 @@
package ru.noties.markwon.syntax;
import android.support.annotation.ColorInt;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.text.SpannableStringBuilder;
import android.text.Spanned;
import android.text.style.BackgroundColorSpan;
import ru.noties.markwon.spans.EmphasisSpan;
import ru.noties.markwon.spans.StrongEmphasisSpan;
public class Prism4jThemeDefault extends Prism4jThemeBase {
@NonNull
public static Prism4jThemeDefault create() {
return new Prism4jThemeDefault();
}
@Override
public int background() {
return 0xFFf5f2f0;
}
@Override
public int textColor() {
return 0xdd000000;
}
@NonNull
@Override
protected ColorHashMap init() {
return new ColorHashMap()
.add(0xFF708090, "comment", "prolog", "doctype", "cdata")
.add(0xFF999999, "punctuation")
.add(0xFF990055, "property", "tag", "boolean", "number", "constant", "symbol", "deleted")
.add(0xFF669900, "selector", "attr-name", "string", "char", "builtin", "inserted")
.add(0xFF9a6e3a, "operator", "entity", "url")
.add(0xFF0077aa, "atrule", "attr-value", "keyword")
.add(0xFFDD4A68, "function", "class-name")
.add(0xFFee9900, "regex", "important", "variable");
}
@Override
protected void applyColor(
@NonNull String language,
@NonNull String type,
@Nullable String alias,
@ColorInt int color,
@NonNull SpannableStringBuilder builder,
int start,
int end) {
if ("css".equals(language) && isOfType("string", type, alias)) {
super.applyColor(language, type, alias, 0xFF9a6e3a, builder, start, end);
builder.setSpan(new BackgroundColorSpan(0x80ffffff), start, end, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
return;
}
if (isOfType("namespace", type, alias)) {
color = applyAlpha(.7F, color);
}
super.applyColor(language, type, alias, color, builder, start, end);
if (isOfType("important", type, alias)
|| isOfType("bold", type, alias)) {
builder.setSpan(new StrongEmphasisSpan(), start, end, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
}
if (isOfType("italic", type, alias)) {
builder.setSpan(new EmphasisSpan(), start, end, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
}
}
}

View File

@ -18,6 +18,14 @@ dependencies {
compileOnly SUPPORT_APP_COMPAT
}
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'
}

View File

@ -20,6 +20,14 @@ dependencies {
api COMMON_MARK_TABLE
}
if (project.hasProperty('release')) {
afterEvaluate {
generateReleaseBuildConfig.enabled = false
}
if (hasProperty('release')) {
if (hasProperty('local')) {
ext.RELEASE_REPOSITORY_URL = LOCAL_MAVEN_URL
ext.SNAPSHOT_REPOSITORY_URL = LOCAL_MAVEN_URL
}
apply from: 'https://raw.githubusercontent.com/noties/gradle-mvn-push/master/gradle-mvn-push-aar.gradle'
}

View File

@ -166,7 +166,7 @@ public class SpannableBuilder {
final int length = impl.length();
if (length > 0) {
int amount = 0;
for (int i = length - 1; i >=0 ; i--) {
for (int i = length - 1; i >= 0; i--) {
if (Character.isWhitespace(impl.charAt(i))) {
amount += 1;
} else {
@ -192,18 +192,35 @@ public class SpannableBuilder {
final boolean reverse = spanned instanceof SpannedReversed;
final Object[] spans = spanned.getSpans(0, spanned.length(), Object.class);
final int length = spans != null
? spans.length
: 0;
iterate(reverse, spans, new Action() {
@Override
public void apply(Object o) {
setSpan(
o,
index + spanned.getSpanStart(o),
index + spanned.getSpanEnd(o),
spanned.getSpanFlags(o)
);
if (length > 0) {
if (reverse) {
Object o;
for (int i = length - 1; i >= 0; i--) {
o = spans[i];
setSpan(
o,
index + spanned.getSpanStart(o),
index + spanned.getSpanEnd(o),
spanned.getSpanFlags(o)
);
}
} else {
Object o;
for (int i = 0; i < length; i++) {
o = spans[i];
setSpan(
o,
index + spanned.getSpanStart(o),
index + spanned.getSpanEnd(o),
spanned.getSpanFlags(o)
);
}
}
});
}
}
}
@ -221,25 +238,4 @@ public class SpannableBuilder {
this.flags = flags;
}
}
private interface Action {
void apply(Object o);
}
private static void iterate(boolean reverse, @Nullable Object[] array, @NonNull Action action) {
final int length = array != null
? array.length
: 0;
if (length > 0) {
if (reverse) {
for (int i = length - 1; i >= 0; i--) {
action.apply(array[i]);
}
} else {
for (int i = 0; i < length; i++) {
action.apply(array[i]);
}
}
}
}
}

View File

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

View File

@ -0,0 +1,85 @@
package ru.noties.markwon;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import java.util.List;
import ru.noties.markwon.renderer.ImageSize;
import ru.noties.markwon.renderer.ImageSizeResolver;
import ru.noties.markwon.spans.AsyncDrawable;
import ru.noties.markwon.spans.LinkSpan;
import ru.noties.markwon.spans.SpannableTheme;
import ru.noties.markwon.spans.TableRowSpan;
/**
* Each method can return null or a Span object or an array of spans
*
* @since 1.1.0
*/
public interface SpannableFactory {
@Nullable
Object strongEmphasis();
@Nullable
Object emphasis();
@Nullable
Object blockQuote(@NonNull SpannableTheme theme);
@Nullable
Object code(@NonNull SpannableTheme theme, boolean multiline);
@Nullable
Object orderedListItem(@NonNull SpannableTheme theme, int startNumber);
@Nullable
Object bulletListItem(@NonNull SpannableTheme theme, int level);
@Nullable
Object thematicBreak(@NonNull SpannableTheme theme);
@Nullable
Object heading(@NonNull SpannableTheme theme, int level);
@Nullable
Object strikethrough();
@Nullable
Object taskListItem(@NonNull SpannableTheme theme, int blockIndent, boolean isDone);
@Nullable
Object tableRow(
@NonNull SpannableTheme theme,
@NonNull List<TableRowSpan.Cell> cells,
boolean isHeader,
boolean isOdd);
@Nullable
Object image(
@NonNull SpannableTheme theme,
@NonNull String destination,
@NonNull AsyncDrawable.Loader loader,
@NonNull ImageSizeResolver imageSizeResolver,
@Nullable ImageSize imageSize,
boolean replacementTextIsLink);
@Nullable
Object link(
@NonNull SpannableTheme theme,
@NonNull String destination,
@NonNull LinkSpan.Resolver resolver);
// Currently used by HTML parser
@Nullable
Object superScript(@NonNull SpannableTheme theme);
// Currently used by HTML parser
@Nullable
Object subScript(@NonNull SpannableTheme theme);
// Currently used by HTML parser
@Nullable
Object underline();
}

View File

@ -0,0 +1,144 @@
package ru.noties.markwon;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.text.style.StrikethroughSpan;
import android.text.style.UnderlineSpan;
import java.util.List;
import ru.noties.markwon.renderer.ImageSize;
import ru.noties.markwon.renderer.ImageSizeResolver;
import ru.noties.markwon.spans.AsyncDrawable;
import ru.noties.markwon.spans.AsyncDrawableSpan;
import ru.noties.markwon.spans.BlockQuoteSpan;
import ru.noties.markwon.spans.BulletListItemSpan;
import ru.noties.markwon.spans.CodeSpan;
import ru.noties.markwon.spans.EmphasisSpan;
import ru.noties.markwon.spans.HeadingSpan;
import ru.noties.markwon.spans.LinkSpan;
import ru.noties.markwon.spans.OrderedListItemSpan;
import ru.noties.markwon.spans.SpannableTheme;
import ru.noties.markwon.spans.StrongEmphasisSpan;
import ru.noties.markwon.spans.SubScriptSpan;
import ru.noties.markwon.spans.SuperScriptSpan;
import ru.noties.markwon.spans.TableRowSpan;
import ru.noties.markwon.spans.TaskListSpan;
import ru.noties.markwon.spans.ThematicBreakSpan;
/**
* @since 1.1.0
*/
public class SpannableFactoryDef implements SpannableFactory {
@NonNull
public static SpannableFactoryDef create() {
return new SpannableFactoryDef();
}
@Nullable
@Override
public Object strongEmphasis() {
return new StrongEmphasisSpan();
}
@Nullable
@Override
public Object emphasis() {
return new EmphasisSpan();
}
@Nullable
@Override
public Object blockQuote(@NonNull SpannableTheme theme) {
return new BlockQuoteSpan(theme);
}
@Nullable
@Override
public Object code(@NonNull SpannableTheme theme, boolean multiline) {
return new CodeSpan(theme, multiline);
}
@Nullable
@Override
public Object orderedListItem(@NonNull SpannableTheme theme, int startNumber) {
// todo| in order to provide real RTL experience there must be a way to provide this string
return new OrderedListItemSpan(theme, String.valueOf(startNumber) + "." + '\u00a0');
}
@Nullable
@Override
public Object bulletListItem(@NonNull SpannableTheme theme, int level) {
return new BulletListItemSpan(theme, level);
}
@Nullable
@Override
public Object thematicBreak(@NonNull SpannableTheme theme) {
return new ThematicBreakSpan(theme);
}
@Nullable
@Override
public Object heading(@NonNull SpannableTheme theme, int level) {
return new HeadingSpan(theme, level);
}
@Nullable
@Override
public Object strikethrough() {
return new StrikethroughSpan();
}
@Nullable
@Override
public Object taskListItem(@NonNull SpannableTheme theme, int blockIndent, boolean isDone) {
return new TaskListSpan(theme, blockIndent, isDone);
}
@Nullable
@Override
public Object tableRow(@NonNull SpannableTheme theme, @NonNull List<TableRowSpan.Cell> cells, boolean isHeader, boolean isOdd) {
return new TableRowSpan(theme, cells, isHeader, isOdd);
}
@Nullable
@Override
public Object image(@NonNull SpannableTheme theme, @NonNull String destination, @NonNull AsyncDrawable.Loader loader, @NonNull ImageSizeResolver imageSizeResolver, @Nullable ImageSize imageSize, boolean replacementTextIsLink) {
return new AsyncDrawableSpan(
theme,
new AsyncDrawable(
destination,
loader,
imageSizeResolver,
imageSize
),
AsyncDrawableSpan.ALIGN_BOTTOM,
replacementTextIsLink
);
}
@Nullable
@Override
public Object link(@NonNull SpannableTheme theme, @NonNull String destination, @NonNull LinkSpan.Resolver resolver) {
return new LinkSpan(theme, destination, resolver);
}
@Nullable
@Override
public Object superScript(@NonNull SpannableTheme theme) {
return new SuperScriptSpan(theme);
}
@Override
public Object subScript(@NonNull SpannableTheme theme) {
return new SubScriptSpan(theme);
}
@Nullable
@Override
public Object underline() {
return new UnderlineSpan();
}
}

View File

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

View File

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

View File

@ -10,26 +10,29 @@ import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
import ru.noties.markwon.SpannableFactory;
import ru.noties.markwon.UrlProcessor;
import ru.noties.markwon.renderer.ImageSize;
import ru.noties.markwon.renderer.ImageSizeResolver;
import ru.noties.markwon.spans.AsyncDrawable;
import ru.noties.markwon.spans.AsyncDrawableSpan;
import ru.noties.markwon.spans.SpannableTheme;
class ImageProviderImpl implements SpannableHtmlParser.ImageProvider {
private final SpannableFactory factory;
private final SpannableTheme theme;
private final AsyncDrawable.Loader loader;
private final UrlProcessor urlProcessor;
private final ImageSizeResolver imageSizeResolver;
ImageProviderImpl(
@NonNull SpannableFactory factory,
@NonNull SpannableTheme theme,
@NonNull AsyncDrawable.Loader loader,
@NonNull UrlProcessor urlProcessor,
@NonNull ImageSizeResolver imageSizeResolver
) {
this.factory = factory;
this.theme = theme;
this.loader = loader;
this.urlProcessor = urlProcessor;
@ -56,11 +59,26 @@ class ImageProviderImpl implements SpannableHtmlParser.ImageProvider {
replacement = "\uFFFC";
}
final AsyncDrawable drawable = new AsyncDrawable(destination, loader, imageSizeResolver, parseImageSize(attributes));
final AsyncDrawableSpan span = new AsyncDrawableSpan(theme, drawable);
final Object span = factory.image(
theme,
destination,
loader,
imageSizeResolver,
parseImageSize(attributes),
false);
final SpannableString string = new SpannableString(replacement);
string.setSpan(span, 0, string.length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
if (span != null) {
final int length = string.length();
if (span.getClass().isArray()) {
for (Object o : ((Object[]) span)) {
string.setSpan(o, 0, length, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
}
} else {
string.setSpan(span, 0, length, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
}
}
spanned = string;
} else {

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -12,9 +12,13 @@ import android.support.annotation.FloatRange;
import android.support.annotation.IntRange;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.support.annotation.Size;
import android.text.TextPaint;
import android.util.TypedValue;
import java.util.Arrays;
import java.util.Locale;
@SuppressWarnings("WeakerAccess")
public class SpannableTheme {
@ -173,6 +177,15 @@ public class SpannableTheme {
// by default, text color with `HEADING_DEF_BREAK_COLOR_ALPHA` applied alpha
protected final int headingBreakColor;
// by default, whatever typeface is set on the TextView
// @since 1.1.0
protected final Typeface headingTypeface;
// by default, we use standard multipliers from the HTML spec (see HEADING_SIZES for values).
// this library supports 6 heading sizes, so make sure the array you pass here has 6 elements.
// @since 1.1.0
protected final float[] headingTextSizeMultipliers;
// by default `SCRIPT_DEF_TEXT_SIZE_RATIO`
protected final float scriptTextSizeRatio;
@ -214,6 +227,8 @@ public class SpannableTheme {
this.codeTextSize = builder.codeTextSize;
this.headingBreakHeight = builder.headingBreakHeight;
this.headingBreakColor = builder.headingBreakColor;
this.headingTypeface = builder.headingTypeface;
this.headingTextSizeMultipliers = builder.headingTextSizeMultipliers;
this.scriptTextSizeRatio = builder.scriptTextSizeRatio;
this.thematicBreakColor = builder.thematicBreakColor;
this.thematicBreakHeight = builder.thematicBreakHeight;
@ -368,8 +383,23 @@ public class SpannableTheme {
}
public void applyHeadingTextStyle(@NonNull Paint paint, @IntRange(from = 1, to = 6) int level) {
paint.setFakeBoldText(true);
paint.setTextSize(paint.getTextSize() * HEADING_SIZES[level - 1]);
if (headingTypeface == null) {
paint.setFakeBoldText(true);
} else {
paint.setTypeface(headingTypeface);
}
final float[] textSizes = headingTextSizeMultipliers != null
? headingTextSizeMultipliers
: HEADING_SIZES;
if (textSizes != null && textSizes.length >= level) {
paint.setTextSize(paint.getTextSize() * textSizes[level - 1]);
} else {
throw new IllegalStateException(String.format(
Locale.US,
"Supplied heading level: %d is invalid, where configured heading sizes are: `%s`",
level, Arrays.toString(textSizes)));
}
}
public void applyHeadingBreakStyle(@NonNull Paint paint) {
@ -491,6 +521,8 @@ public class SpannableTheme {
private int codeTextSize;
private int headingBreakHeight = -1;
private int headingBreakColor;
private Typeface headingTypeface;
private float[] headingTextSizeMultipliers;
private float scriptTextSizeRatio;
private int thematicBreakColor;
private int thematicBreakHeight = -1;
@ -520,6 +552,8 @@ public class SpannableTheme {
this.codeTextSize = theme.codeTextSize;
this.headingBreakHeight = theme.headingBreakHeight;
this.headingBreakColor = theme.headingBreakColor;
this.headingTypeface = theme.headingTypeface;
this.headingTextSizeMultipliers = theme.headingTextSizeMultipliers;
this.scriptTextSizeRatio = theme.scriptTextSizeRatio;
this.thematicBreakColor = theme.thematicBreakColor;
this.thematicBreakHeight = theme.thematicBreakHeight;
@ -634,6 +668,29 @@ public class SpannableTheme {
return this;
}
/**
* @param headingTypeface Typeface to use for heading elements
* @return self
* @since 1.1.0
*/
@NonNull
public Builder headingTypeface(@NonNull Typeface headingTypeface) {
this.headingTypeface = headingTypeface;
return this;
}
/**
* @param headingTextSizeMultipliers an array of multipliers values for heading elements.
* The base value for this multipliers is TextView\'s text size
* @return self
* @since 1.1.0
*/
@NonNull
public Builder headingTextSizeMultipliers(@Size(6) @NonNull float[] headingTextSizeMultipliers) {
this.headingTextSizeMultipliers = headingTextSizeMultipliers;
return this;
}
@NonNull
public Builder scriptTextSizeRatio(@FloatRange(from = .0F, to = Float.MAX_VALUE) float scriptTextSizeRatio) {
this.scriptTextSizeRatio = scriptTextSizeRatio;

View File

@ -1,6 +1,7 @@
package ru.noties.markwon.sample.extension;
import android.app.Activity;
import android.graphics.Typeface;
import android.os.Bundle;
import android.widget.TextView;
@ -13,6 +14,7 @@ import java.util.Arrays;
import ru.noties.markwon.SpannableBuilder;
import ru.noties.markwon.SpannableConfiguration;
import ru.noties.markwon.spans.SpannableTheme;
import ru.noties.markwon.tasklist.TaskListExtension;
public class MainActivity extends Activity {
@ -50,9 +52,16 @@ public class MainActivity extends Activity {
// better to provide a valid fallback option
final IconSpanProvider spanProvider = IconSpanProvider.create(this, 0);
final float[] textSizeMultipliers = new float[]{3f, 2f, 1.5f, 1f, .5f, .25f};
SpannableConfiguration configuration = SpannableConfiguration.builder(this)
.theme(SpannableTheme.builder()
.headingTypeface(Typeface.MONOSPACE)
.headingTextSizeMultipliers(textSizeMultipliers)
.build())
.build();
// create an instance of visitor to process parsed markdown
final IconVisitor visitor = new IconVisitor(
SpannableConfiguration.create(this),
configuration,
builder,
spanProvider
);

View File

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

View File

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

View File

@ -1 +1 @@
include ':app', ':library', ':library-image-loader', ':library-view', ':sample-custom-extension'
include ':app', ':library', ':library-image-loader', ':library-view', ':sample-custom-extension', ':library-syntax'