Stabilizing api
This commit is contained in:
parent
250dd7677d
commit
d5e2d756d9
22
README.md
22
README.md
@ -107,6 +107,28 @@ Lorem ipsum dolor sit amet
|
|||||||
Lorem ipsum dolor sit amet
|
Lorem ipsum dolor sit amet
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### H.T.M.L.
|
||||||
|
<b>O</b><i>K<s>A</s><sup>42<sup>43<sub><b>42</b></sub></sup></sup><u>Y</u></i>
|
||||||
|
|
||||||
|
<img src="h" /> <img src="h">
|
||||||
|
<img src="h" alt="alt text">
|
||||||
|
|
||||||
|
<hr>
|
||||||
|
|
||||||
|
<hr />
|
||||||
|
|
||||||
|
<h1>Hello</h1>
|
||||||
|
|
||||||
|
<h2>Hello</h2>
|
||||||
|
|
||||||
|
<h3>Hello</h3>
|
||||||
|
|
||||||
|
<h4>Hello</h4>
|
||||||
|
|
||||||
|
<h5>Hello</h5>
|
||||||
|
|
||||||
|
<h6>Hello</h6>
|
||||||
|
|
||||||
|
|
||||||
[1]: https://github.com
|
[1]: https://github.com
|
||||||
[github]: https://github.com
|
[github]: https://github.com
|
||||||
|
@ -15,12 +15,14 @@ android {
|
|||||||
}
|
}
|
||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
|
|
||||||
compile project(':library-renderer')
|
compile project(':library-renderer')
|
||||||
|
compile project(':library-image-loader')
|
||||||
|
|
||||||
compile 'ru.noties:debug:3.0.0@jar'
|
compile 'ru.noties:debug:3.0.0@jar'
|
||||||
compile 'com.squareup.picasso:picasso:2.5.2'
|
|
||||||
compile 'com.caverock:androidsvg:1.2.1'
|
compile OK_HTTP
|
||||||
compile 'pl.droidsonroids.gif:android-gif-drawable:1.2.7'
|
|
||||||
compile 'com.squareup.okhttp3:okhttp:3.8.0'
|
|
||||||
compile 'com.google.dagger:dagger:2.10'
|
compile 'com.google.dagger:dagger:2.10'
|
||||||
annotationProcessor 'com.google.dagger:dagger-compiler:2.10'
|
annotationProcessor 'com.google.dagger:dagger-compiler:2.10'
|
||||||
}
|
}
|
||||||
|
@ -3,6 +3,7 @@
|
|||||||
package="ru.noties.markwon">
|
package="ru.noties.markwon">
|
||||||
|
|
||||||
<uses-permission android:name="android.permission.INTERNET" />
|
<uses-permission android:name="android.permission.INTERNET" />
|
||||||
|
<uses-permission android:name="android.permission.ACCESS_ALL_DOWNLOADS" />
|
||||||
|
|
||||||
<application
|
<application
|
||||||
android:name=".App"
|
android:name=".App"
|
||||||
|
@ -4,6 +4,9 @@ import android.app.Application;
|
|||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
import android.support.annotation.NonNull;
|
import android.support.annotation.NonNull;
|
||||||
|
|
||||||
|
import ru.noties.debug.AndroidLogDebugOutput;
|
||||||
|
import ru.noties.debug.Debug;
|
||||||
|
|
||||||
public class App extends Application {
|
public class App extends Application {
|
||||||
|
|
||||||
private AppComponent component;
|
private AppComponent component;
|
||||||
@ -12,6 +15,8 @@ public class App extends Application {
|
|||||||
public void onCreate() {
|
public void onCreate() {
|
||||||
super.onCreate();
|
super.onCreate();
|
||||||
|
|
||||||
|
Debug.init(new AndroidLogDebugOutput(BuildConfig.DEBUG));
|
||||||
|
|
||||||
component = DaggerAppComponent.builder()
|
component = DaggerAppComponent.builder()
|
||||||
.appModule(new AppModule(this))
|
.appModule(new AppModule(this))
|
||||||
.build();
|
.build();
|
||||||
|
@ -5,8 +5,6 @@ import android.content.res.Resources;
|
|||||||
import android.os.Handler;
|
import android.os.Handler;
|
||||||
import android.os.Looper;
|
import android.os.Looper;
|
||||||
|
|
||||||
import com.squareup.picasso.Picasso;
|
|
||||||
|
|
||||||
import java.util.concurrent.ExecutorService;
|
import java.util.concurrent.ExecutorService;
|
||||||
import java.util.concurrent.Executors;
|
import java.util.concurrent.Executors;
|
||||||
|
|
||||||
@ -14,7 +12,10 @@ import javax.inject.Singleton;
|
|||||||
|
|
||||||
import dagger.Module;
|
import dagger.Module;
|
||||||
import dagger.Provides;
|
import dagger.Provides;
|
||||||
|
import okhttp3.Cache;
|
||||||
import okhttp3.OkHttpClient;
|
import okhttp3.OkHttpClient;
|
||||||
|
import ru.noties.markwon.il.AsyncDrawableLoader;
|
||||||
|
import ru.noties.markwon.spans.AsyncDrawable;
|
||||||
|
|
||||||
@Module
|
@Module
|
||||||
class AppModule {
|
class AppModule {
|
||||||
@ -38,7 +39,10 @@ class AppModule {
|
|||||||
@Provides
|
@Provides
|
||||||
@Singleton
|
@Singleton
|
||||||
OkHttpClient client() {
|
OkHttpClient client() {
|
||||||
return new OkHttpClient();
|
return new OkHttpClient.Builder()
|
||||||
|
.cache(new Cache(app.getCacheDir(), 1024L * 20))
|
||||||
|
.followRedirects(true)
|
||||||
|
.build();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Singleton
|
@Singleton
|
||||||
@ -60,7 +64,14 @@ class AppModule {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Provides
|
@Provides
|
||||||
Picasso picasso(Context context) {
|
AsyncDrawable.Loader asyncDrawableLoader(
|
||||||
return Picasso.with(context);
|
OkHttpClient client,
|
||||||
|
ExecutorService executorService,
|
||||||
|
Resources resources) {
|
||||||
|
return AsyncDrawableLoader.builder()
|
||||||
|
.client(client)
|
||||||
|
.executorService(executorService)
|
||||||
|
.resources(resources)
|
||||||
|
.build();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,155 +0,0 @@
|
|||||||
package ru.noties.markwon;
|
|
||||||
|
|
||||||
import android.content.res.Resources;
|
|
||||||
import android.graphics.Bitmap;
|
|
||||||
import android.graphics.Canvas;
|
|
||||||
import android.graphics.drawable.BitmapDrawable;
|
|
||||||
import android.graphics.drawable.ColorDrawable;
|
|
||||||
import android.graphics.drawable.Drawable;
|
|
||||||
import android.support.annotation.NonNull;
|
|
||||||
|
|
||||||
import com.caverock.androidsvg.SVG;
|
|
||||||
import com.squareup.picasso.Picasso;
|
|
||||||
|
|
||||||
import java.io.ByteArrayOutputStream;
|
|
||||||
import java.io.InputStream;
|
|
||||||
import java.net.HttpURLConnection;
|
|
||||||
import java.net.URL;
|
|
||||||
import java.util.HashMap;
|
|
||||||
import java.util.Map;
|
|
||||||
import java.util.concurrent.CopyOnWriteArrayList;
|
|
||||||
import java.util.concurrent.ExecutorService;
|
|
||||||
import java.util.concurrent.Future;
|
|
||||||
|
|
||||||
import javax.inject.Inject;
|
|
||||||
|
|
||||||
import pl.droidsonroids.gif.GifDrawable;
|
|
||||||
import ru.noties.debug.Debug;
|
|
||||||
import ru.noties.markwon.spans.AsyncDrawable;
|
|
||||||
|
|
||||||
@ActivityScope
|
|
||||||
public class AsyncDrawableLoader implements AsyncDrawable.Loader {
|
|
||||||
|
|
||||||
@Inject
|
|
||||||
Resources resources;
|
|
||||||
|
|
||||||
@Inject
|
|
||||||
Picasso picasso;
|
|
||||||
|
|
||||||
@Inject
|
|
||||||
ExecutorService executorService;
|
|
||||||
|
|
||||||
private final Map<String, Future<?>> requests = new HashMap<>(3);
|
|
||||||
private final CopyOnWriteArrayList<AsyncDrawableTarget> targets = new CopyOnWriteArrayList<>();
|
|
||||||
|
|
||||||
// sh*t..
|
|
||||||
@Inject
|
|
||||||
public AsyncDrawableLoader() {
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void load(@NonNull String destination, @NonNull AsyncDrawable drawable) {
|
|
||||||
|
|
||||||
if (destination.endsWith(".svg")) {
|
|
||||||
// load svg
|
|
||||||
requests.put(destination, loadSvg(destination, drawable));
|
|
||||||
} else if (destination.endsWith(".gif")) {
|
|
||||||
requests.put(destination, loadGif(destination, drawable));
|
|
||||||
} else {
|
|
||||||
|
|
||||||
final Drawable error = new ColorDrawable(0xFFff0000);
|
|
||||||
final Drawable placeholder = new ColorDrawable(0xFF00ff00);
|
|
||||||
error.setBounds(0, 0, 100, 100);
|
|
||||||
placeholder.setBounds(0, 0, 50, 50);
|
|
||||||
|
|
||||||
final AsyncDrawableTarget target = new AsyncDrawableTarget(resources, drawable, new AsyncDrawableTarget.DoneListener() {
|
|
||||||
@Override
|
|
||||||
public void onLoadingDone(AsyncDrawableTarget target) {
|
|
||||||
targets.remove(target);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
targets.add(target);
|
|
||||||
|
|
||||||
picasso
|
|
||||||
.load(destination)
|
|
||||||
.tag(destination)
|
|
||||||
.placeholder(placeholder)
|
|
||||||
.error(error)
|
|
||||||
.into(target);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void cancel(@NonNull String destination) {
|
|
||||||
Debug.i("destination: %s", destination);
|
|
||||||
picasso.cancelTag(destination);
|
|
||||||
|
|
||||||
final Future<?> future = requests.remove(destination);
|
|
||||||
if (future != null) {
|
|
||||||
future.cancel(true);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private Future<?> loadSvg(final String destination, final AsyncDrawable asyncDrawable) {
|
|
||||||
return executorService.submit(new Runnable() {
|
|
||||||
@Override
|
|
||||||
public void run() {
|
|
||||||
try {
|
|
||||||
|
|
||||||
final URL url = new URL(destination);
|
|
||||||
final HttpURLConnection connection = (HttpURLConnection) url.openConnection();
|
|
||||||
final InputStream inputStream = connection.getInputStream();
|
|
||||||
|
|
||||||
final SVG svg = SVG.getFromInputStream(inputStream);
|
|
||||||
final float w = svg.getDocumentWidth();
|
|
||||||
final float h = svg.getDocumentHeight();
|
|
||||||
Debug.i("w: %s, h: %s", w, h);
|
|
||||||
|
|
||||||
final float density = resources.getDisplayMetrics().density;
|
|
||||||
Debug.i(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);
|
|
||||||
|
|
||||||
final Drawable drawable = new BitmapDrawable(resources, bitmap);
|
|
||||||
drawable.setBounds(0, 0, drawable.getIntrinsicWidth(), drawable.getIntrinsicHeight());
|
|
||||||
asyncDrawable.setResult(drawable);
|
|
||||||
|
|
||||||
} catch (Throwable t) {
|
|
||||||
Debug.e(t);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
private Future<?> loadGif(final String destination, final AsyncDrawable asyncDrawable) {
|
|
||||||
return executorService.submit(new Runnable() {
|
|
||||||
@Override
|
|
||||||
public void run() {
|
|
||||||
try {
|
|
||||||
|
|
||||||
final URL url = new URL(destination);
|
|
||||||
final HttpURLConnection connection = (HttpURLConnection) url.openConnection();
|
|
||||||
final InputStream inputStream = connection.getInputStream();
|
|
||||||
|
|
||||||
final ByteArrayOutputStream baos = new ByteArrayOutputStream();
|
|
||||||
final byte[] buffer = new byte[1024 * 8];
|
|
||||||
int read;
|
|
||||||
while ((read = inputStream.read(buffer, 0, buffer.length)) != -1) {
|
|
||||||
baos.write(buffer, 0, read);
|
|
||||||
}
|
|
||||||
final GifDrawable drawable = new GifDrawable(baos.toByteArray());
|
|
||||||
drawable.setBounds(0, 0, drawable.getIntrinsicWidth(), drawable.getIntrinsicHeight());
|
|
||||||
asyncDrawable.setResult(drawable);
|
|
||||||
} catch (Throwable t) {
|
|
||||||
Debug.e(t);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,81 +0,0 @@
|
|||||||
package ru.noties.markwon;
|
|
||||||
|
|
||||||
import android.content.res.Resources;
|
|
||||||
import android.graphics.Bitmap;
|
|
||||||
import android.graphics.drawable.BitmapDrawable;
|
|
||||||
import android.graphics.drawable.Drawable;
|
|
||||||
|
|
||||||
import com.squareup.picasso.Picasso;
|
|
||||||
import com.squareup.picasso.Target;
|
|
||||||
|
|
||||||
import ru.noties.markwon.spans.AsyncDrawable;
|
|
||||||
|
|
||||||
public class AsyncDrawableTarget implements Target {
|
|
||||||
|
|
||||||
interface DoneListener {
|
|
||||||
void onLoadingDone(AsyncDrawableTarget target);
|
|
||||||
}
|
|
||||||
|
|
||||||
private final Resources resources;
|
|
||||||
private final AsyncDrawable asyncDrawable;
|
|
||||||
private final DoneListener listener;
|
|
||||||
|
|
||||||
public AsyncDrawableTarget(Resources resources, AsyncDrawable asyncDrawable, DoneListener listener) {
|
|
||||||
this.resources = resources;
|
|
||||||
this.asyncDrawable = asyncDrawable;
|
|
||||||
this.listener = listener;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onBitmapLoaded(Bitmap bitmap, Picasso.LoadedFrom from) {
|
|
||||||
if (bitmap != null) {
|
|
||||||
final Drawable drawable = new BitmapDrawable(resources, bitmap);
|
|
||||||
drawable.setBounds(0, 0, drawable.getIntrinsicWidth(), drawable.getIntrinsicHeight());
|
|
||||||
asyncDrawable.setResult(drawable);
|
|
||||||
}
|
|
||||||
notifyDone();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onBitmapFailed(Drawable errorDrawable) {
|
|
||||||
if (errorDrawable != null) {
|
|
||||||
asyncDrawable.setResult(errorDrawable);
|
|
||||||
}
|
|
||||||
notifyDone();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onPrepareLoad(Drawable placeHolderDrawable) {
|
|
||||||
if (placeHolderDrawable != null) {
|
|
||||||
asyncDrawable.setResult(placeHolderDrawable);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void notifyDone() {
|
|
||||||
if (listener != null) {
|
|
||||||
listener.onLoadingDone(this);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
//
|
|
||||||
// private void attach() {
|
|
||||||
//
|
|
||||||
// // amazing stuff here, in order to keep this target alive (picasso stores target in a WeakReference)
|
|
||||||
// // we need to do this
|
|
||||||
//
|
|
||||||
// //noinspection unchecked
|
|
||||||
// List<AsyncDrawableTarget> list = (List<AsyncDrawableTarget>) view.getTag(R.id.amazing);
|
|
||||||
// if (list == null) {
|
|
||||||
// list = new ArrayList<>(2);
|
|
||||||
// view.setTag(R.id.amazing, list);
|
|
||||||
// }
|
|
||||||
// list.add(this);
|
|
||||||
// }
|
|
||||||
//
|
|
||||||
// private void detach() {
|
|
||||||
// //noinspection unchecked
|
|
||||||
// final List<AsyncDrawableTarget> list = (List<AsyncDrawableTarget>) view.getTag(R.id.amazing);
|
|
||||||
// if (list != null) {
|
|
||||||
// list.remove(this);
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
}
|
|
@ -1,12 +0,0 @@
|
|||||||
package ru.noties.markwon;
|
|
||||||
|
|
||||||
import java.util.Collection;
|
|
||||||
|
|
||||||
public abstract class CollectionUtils {
|
|
||||||
|
|
||||||
public static boolean isEmpty(Collection<?> collection) {
|
|
||||||
return collection == null || collection.size() == 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
private CollectionUtils() {}
|
|
||||||
}
|
|
@ -9,15 +9,10 @@ import android.widget.TextView;
|
|||||||
|
|
||||||
import javax.inject.Inject;
|
import javax.inject.Inject;
|
||||||
|
|
||||||
import ru.noties.debug.AndroidLogDebugOutput;
|
|
||||||
import ru.noties.debug.Debug;
|
import ru.noties.debug.Debug;
|
||||||
|
|
||||||
public class MainActivity extends Activity {
|
public class MainActivity extends Activity {
|
||||||
|
|
||||||
static {
|
|
||||||
Debug.init(new AndroidLogDebugOutput(true));
|
|
||||||
}
|
|
||||||
|
|
||||||
@Inject
|
@Inject
|
||||||
MarkdownLoader markdownLoader;
|
MarkdownLoader markdownLoader;
|
||||||
|
|
||||||
@ -112,6 +107,16 @@ public class MainActivity extends Activity {
|
|||||||
: null;
|
: null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void onRestoreInstanceState(Bundle savedInstanceState) {
|
||||||
|
try {
|
||||||
|
super.onRestoreInstanceState(savedInstanceState);
|
||||||
|
} catch (Throwable t) {
|
||||||
|
// amazing stuff, we need this because on JB it will crash otherwise with NPE
|
||||||
|
Debug.e(t);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected void onDestroy() {
|
protected void onDestroy() {
|
||||||
super.onDestroy();
|
super.onDestroy();
|
||||||
|
@ -72,13 +72,20 @@ public class MarkdownLoader {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private boolean isCancelled() {
|
||||||
|
return task == null || task.isCancelled();
|
||||||
|
}
|
||||||
|
|
||||||
private void deliver(@NonNull final OnMarkdownTextLoaded loaded, final String text) {
|
private void deliver(@NonNull final OnMarkdownTextLoaded loaded, final String text) {
|
||||||
if (task != null
|
if (!isCancelled()) {
|
||||||
&& !task.isCancelled()) {
|
|
||||||
handler.post(new Runnable() {
|
handler.post(new Runnable() {
|
||||||
@Override
|
@Override
|
||||||
public void run() {
|
public void run() {
|
||||||
|
// as this call is async, we need to check again if we are cancelled
|
||||||
|
if (!isCancelled()) {
|
||||||
loaded.apply(text);
|
loaded.apply(text);
|
||||||
|
task = null;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -17,6 +17,7 @@ import java.util.concurrent.Future;
|
|||||||
import javax.inject.Inject;
|
import javax.inject.Inject;
|
||||||
|
|
||||||
import ru.noties.markwon.renderer.SpannableRenderer;
|
import ru.noties.markwon.renderer.SpannableRenderer;
|
||||||
|
import ru.noties.markwon.spans.AsyncDrawable;
|
||||||
|
|
||||||
@ActivityScope
|
@ActivityScope
|
||||||
public class MarkdownRenderer {
|
public class MarkdownRenderer {
|
||||||
@ -26,7 +27,7 @@ public class MarkdownRenderer {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Inject
|
@Inject
|
||||||
AsyncDrawableLoader loader;
|
AsyncDrawable.Loader loader;
|
||||||
|
|
||||||
@Inject
|
@Inject
|
||||||
ExecutorService service;
|
ExecutorService service;
|
||||||
@ -62,22 +63,19 @@ public class MarkdownRenderer {
|
|||||||
.urlProcessor(urlProcessor)
|
.urlProcessor(urlProcessor)
|
||||||
.build();
|
.build();
|
||||||
|
|
||||||
final Parser parser = Parser.builder()
|
final CharSequence text = Markwon.markdown(configuration, markdown);
|
||||||
.extensions(Collections.singleton(StrikethroughExtension.create()))
|
|
||||||
.build();
|
|
||||||
|
|
||||||
final Node node = parser.parse(markdown);
|
if (!isCancelled()) {
|
||||||
final SpannableRenderer renderer = new SpannableRenderer();
|
|
||||||
final CharSequence text = renderer.render(configuration, node);
|
|
||||||
|
|
||||||
// final CharSequence text = Markwon.markdown(configuration, markdown);
|
|
||||||
handler.post(new Runnable() {
|
handler.post(new Runnable() {
|
||||||
@Override
|
@Override
|
||||||
public void run() {
|
public void run() {
|
||||||
|
if (!isCancelled()) {
|
||||||
listener.onMarkdownReady(text);
|
listener.onMarkdownReady(text);
|
||||||
|
task = null;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
task = null;
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@ -88,4 +86,8 @@ public class MarkdownRenderer {
|
|||||||
task = null;
|
task = null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private boolean isCancelled() {
|
||||||
|
return task == null || task.isCancelled();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -22,7 +22,7 @@ public class Themes {
|
|||||||
|
|
||||||
public void apply(@NonNull Context context) {
|
public void apply(@NonNull Context context) {
|
||||||
final boolean dark = preferences.getBoolean(KEY_THEME_DARK, false);
|
final boolean dark = preferences.getBoolean(KEY_THEME_DARK, false);
|
||||||
// we have only 2 themes and Light one is default, so no need to apply it
|
// 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.AppThemeBaseDark;
|
||||||
|
@ -6,10 +6,12 @@ import android.support.annotation.IntDef;
|
|||||||
import android.support.annotation.NonNull;
|
import android.support.annotation.NonNull;
|
||||||
import android.view.View;
|
import android.view.View;
|
||||||
|
|
||||||
|
@SuppressWarnings("WeakerAccess")
|
||||||
public abstract class Views {
|
public abstract class Views {
|
||||||
|
|
||||||
@IntDef({View.INVISIBLE, View.GONE})
|
@IntDef({View.INVISIBLE, View.GONE})
|
||||||
@interface NotVisible {}
|
@interface NotVisible {
|
||||||
|
}
|
||||||
|
|
||||||
public static <V extends View> V findView(@NonNull View view, @IdRes int id) {
|
public static <V extends View> V findView(@NonNull View view, @IdRes int id) {
|
||||||
//noinspection unchecked
|
//noinspection unchecked
|
||||||
@ -32,5 +34,6 @@ public abstract class Views {
|
|||||||
view.setVisibility(visibility);
|
view.setVisibility(visibility);
|
||||||
}
|
}
|
||||||
|
|
||||||
private Views() {}
|
private Views() {
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -6,7 +6,7 @@
|
|||||||
<item name="ic_app_bar_theme">@drawable/ic_app_bar_theme_light</item>
|
<item name="ic_app_bar_theme">@drawable/ic_app_bar_theme_light</item>
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
<style name="AppThemeBaseLight" parent="android:Theme.Holo.Light">
|
<style name="AppThemeBaseLight" parent="android:Theme.Holo.Light.NoActionBar">
|
||||||
<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>
|
||||||
|
|
||||||
|
@ -32,4 +32,8 @@ ext {
|
|||||||
final def commonMarkVersion = '0.9.0'
|
final def commonMarkVersion = '0.9.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"
|
||||||
|
|
||||||
|
ANDROID_SVG = 'com.caverock:androidsvg:1.2.1'
|
||||||
|
ANDROID_GIF = 'pl.droidsonroids.gif:android-gif-drawable:1.2.7'
|
||||||
|
OK_HTTP = 'com.squareup.okhttp3:okhttp:3.8.0'
|
||||||
}
|
}
|
||||||
|
25
library-image-loader/build.gradle
Normal file
25
library-image-loader/build.gradle
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
apply plugin: 'com.android.library'
|
||||||
|
|
||||||
|
android {
|
||||||
|
|
||||||
|
compileSdkVersion TARGET_SDK
|
||||||
|
buildToolsVersion BUILD_TOOLS
|
||||||
|
|
||||||
|
defaultConfig {
|
||||||
|
minSdkVersion MIN_SDK
|
||||||
|
targetSdkVersion TARGET_SDK
|
||||||
|
versionCode 1
|
||||||
|
versionName version
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
dependencies {
|
||||||
|
|
||||||
|
compile project(':library-renderer')
|
||||||
|
compile ANDROID_SVG
|
||||||
|
compile ANDROID_GIF
|
||||||
|
compile OK_HTTP
|
||||||
|
|
||||||
|
// todo, debugging only
|
||||||
|
compile 'ru.noties:debug:3.0.0@jar'
|
||||||
|
}
|
1
library-image-loader/src/main/AndroidManifest.xml
Normal file
1
library-image-loader/src/main/AndroidManifest.xml
Normal file
@ -0,0 +1 @@
|
|||||||
|
<manifest package="ru.noties.markwon.il" />
|
@ -0,0 +1,298 @@
|
|||||||
|
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.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 java.io.ByteArrayOutputStream;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.io.InputStream;
|
||||||
|
import java.lang.ref.WeakReference;
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.concurrent.ExecutorService;
|
||||||
|
import java.util.concurrent.Future;
|
||||||
|
|
||||||
|
import okhttp3.Call;
|
||||||
|
import okhttp3.OkHttpClient;
|
||||||
|
import okhttp3.Request;
|
||||||
|
import okhttp3.Response;
|
||||||
|
import okhttp3.ResponseBody;
|
||||||
|
import pl.droidsonroids.gif.GifDrawable;
|
||||||
|
import ru.noties.debug.Debug;
|
||||||
|
import ru.noties.markwon.spans.AsyncDrawable;
|
||||||
|
|
||||||
|
public class AsyncDrawableLoader implements AsyncDrawable.Loader {
|
||||||
|
|
||||||
|
public static AsyncDrawableLoader create() {
|
||||||
|
return builder().build();
|
||||||
|
}
|
||||||
|
|
||||||
|
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 final OkHttpClient client;
|
||||||
|
private final Resources resources;
|
||||||
|
private final ExecutorService executorService;
|
||||||
|
private final Handler mainThread;
|
||||||
|
private final Drawable errorDrawable;
|
||||||
|
|
||||||
|
private final Map<String, Future<?>> requests;
|
||||||
|
|
||||||
|
AsyncDrawableLoader(Builder builder) {
|
||||||
|
this.client = builder.client;
|
||||||
|
this.resources = builder.resources;
|
||||||
|
this.executorService = builder.executorService;
|
||||||
|
this.mainThread = new Handler(Looper.getMainLooper());
|
||||||
|
this.errorDrawable = builder.errorDrawable;
|
||||||
|
this.requests = new HashMap<>(3);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void load(@NonNull String destination, @NonNull AsyncDrawable drawable) {
|
||||||
|
// if drawable is not a link -> show loading placeholder...
|
||||||
|
requests.put(destination, execute(destination, drawable));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void cancel(@NonNull String destination) {
|
||||||
|
|
||||||
|
final Future<?> request = requests.remove(destination);
|
||||||
|
if (request != null) {
|
||||||
|
request.cancel(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
final List<Call> calls = client.dispatcher().queuedCalls();
|
||||||
|
if (calls != null) {
|
||||||
|
for (Call call : calls) {
|
||||||
|
if (!call.isCanceled()) {
|
||||||
|
if (destination.equals(call.request().tag())) {
|
||||||
|
call.cancel();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private Future<?> execute(@NonNull final String destination, @NonNull AsyncDrawable drawable) {
|
||||||
|
final WeakReference<AsyncDrawable> reference = new WeakReference<AsyncDrawable>(drawable);
|
||||||
|
// todo, if not a link -> show placeholder
|
||||||
|
return executorService.submit(new Runnable() {
|
||||||
|
@Override
|
||||||
|
public void run() {
|
||||||
|
|
||||||
|
final Request request = new Request.Builder()
|
||||||
|
.url(destination)
|
||||||
|
.tag(destination)
|
||||||
|
.build();
|
||||||
|
|
||||||
|
Response response = null;
|
||||||
|
try {
|
||||||
|
response = client.newCall(request).execute();
|
||||||
|
} catch (IOException e) {
|
||||||
|
e.printStackTrace();
|
||||||
|
}
|
||||||
|
|
||||||
|
Debug.i(destination, response);
|
||||||
|
|
||||||
|
Drawable result = null;
|
||||||
|
|
||||||
|
if (response != null) {
|
||||||
|
|
||||||
|
final ResponseBody body = response.body();
|
||||||
|
if (body != null) {
|
||||||
|
final InputStream inputStream = body.byteStream();
|
||||||
|
if (inputStream != null) {
|
||||||
|
final String contentType = response.header(HEADER_CONTENT_TYPE);
|
||||||
|
try {
|
||||||
|
// svg can have `image/svg+xml;charset=...`
|
||||||
|
if (CONTENT_TYPE_SVG.equals(contentType)
|
||||||
|
|| (!TextUtils.isEmpty(contentType) && contentType.startsWith(CONTENT_TYPE_SVG))) {
|
||||||
|
// handle SVG
|
||||||
|
result = handleSvg(inputStream);
|
||||||
|
} else if (CONTENT_TYPE_GIF.equals(contentType)) {
|
||||||
|
// handle gif
|
||||||
|
result = handleGif(inputStream);
|
||||||
|
} else {
|
||||||
|
result = handleSimple(inputStream);
|
||||||
|
// just try to decode whatever it is
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
try {
|
||||||
|
inputStream.close();
|
||||||
|
} catch (IOException e) {
|
||||||
|
// no op
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// if result is null, we assume it's an error
|
||||||
|
if (result == null) {
|
||||||
|
result = errorDrawable;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (result != null) {
|
||||||
|
final Drawable out = result;
|
||||||
|
mainThread.post(new Runnable() {
|
||||||
|
@Override
|
||||||
|
public void run() {
|
||||||
|
final AsyncDrawable asyncDrawable = reference.get();
|
||||||
|
if (asyncDrawable != null && asyncDrawable.isAttached()) {
|
||||||
|
asyncDrawable.setResult(out);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
requests.remove(destination);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private Drawable handleSvg(InputStream stream) {
|
||||||
|
|
||||||
|
final Drawable out;
|
||||||
|
|
||||||
|
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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
private Drawable handleSimple(InputStream stream) {
|
||||||
|
|
||||||
|
final Drawable out;
|
||||||
|
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
out = outputStream.toByteArray();
|
||||||
|
} catch (IOException e) {
|
||||||
|
e.printStackTrace();
|
||||||
|
}
|
||||||
|
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static class Builder {
|
||||||
|
|
||||||
|
private OkHttpClient client;
|
||||||
|
private Resources resources;
|
||||||
|
private ExecutorService executorService;
|
||||||
|
private Drawable errorDrawable;
|
||||||
|
|
||||||
|
public Builder client(@NonNull OkHttpClient client) {
|
||||||
|
this.client = client;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Builder resources(@NonNull Resources resources) {
|
||||||
|
this.resources = resources;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Builder executorService(ExecutorService executorService) {
|
||||||
|
this.executorService = executorService;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Builder errorDrawable(Drawable errorDrawable) {
|
||||||
|
this.errorDrawable = errorDrawable;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
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();
|
||||||
|
}
|
||||||
|
return new AsyncDrawableLoader(this);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,13 @@
|
|||||||
|
package ru.noties.markwon.il;
|
||||||
|
|
||||||
|
import android.graphics.drawable.Drawable;
|
||||||
|
import android.support.annotation.NonNull;
|
||||||
|
|
||||||
|
abstract class DrawableUtils {
|
||||||
|
|
||||||
|
static void intrinsicBounds(@NonNull Drawable drawable) {
|
||||||
|
drawable.setBounds(0, 0, drawable.getIntrinsicWidth(), drawable.getIntrinsicHeight());
|
||||||
|
}
|
||||||
|
|
||||||
|
private DrawableUtils() {}
|
||||||
|
}
|
@ -14,7 +14,6 @@ import java.util.ArrayList;
|
|||||||
import java.util.Collections;
|
import java.util.Collections;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
import ru.noties.debug.Debug;
|
|
||||||
import ru.noties.markwon.spans.AsyncDrawable;
|
import ru.noties.markwon.spans.AsyncDrawable;
|
||||||
import ru.noties.markwon.spans.AsyncDrawableSpan;
|
import ru.noties.markwon.spans.AsyncDrawableSpan;
|
||||||
|
|
||||||
@ -22,7 +21,7 @@ abstract class DrawablesScheduler {
|
|||||||
|
|
||||||
static void schedule(@NonNull final TextView textView) {
|
static void schedule(@NonNull final TextView textView) {
|
||||||
|
|
||||||
final List<Pair> list = extract(textView, true);
|
final List<AsyncDrawable> list = extract(textView);
|
||||||
if (list.size() > 0) {
|
if (list.size() > 0) {
|
||||||
textView.addOnAttachStateChangeListener(new View.OnAttachStateChangeListener() {
|
textView.addOnAttachStateChangeListener(new View.OnAttachStateChangeListener() {
|
||||||
@Override
|
@Override
|
||||||
@ -37,22 +36,22 @@ abstract class DrawablesScheduler {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
for (Pair pair : list) {
|
for (AsyncDrawable drawable : list) {
|
||||||
pair.drawable.setCallback2(new DrawableCallbackImpl(textView, pair.coordinatesProvider, pair.drawable.getBounds()));
|
drawable.setCallback2(new DrawableCallbackImpl(textView, drawable.getBounds()));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// must be called when text manually changed in TextView
|
// must be called when text manually changed in TextView
|
||||||
static void unschedule(@NonNull TextView view) {
|
static void unschedule(@NonNull TextView view) {
|
||||||
for (Pair pair : extract(view, false)) {
|
for (AsyncDrawable drawable : extract(view)) {
|
||||||
pair.drawable.setCallback2(null);
|
drawable.setCallback2(null);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private static List<Pair> extract(@NonNull TextView view, boolean coordinates) {
|
private static List<AsyncDrawable> extract(@NonNull TextView view) {
|
||||||
|
|
||||||
final List<Pair> list;
|
final List<AsyncDrawable> list;
|
||||||
|
|
||||||
final CharSequence cs = view.getText();
|
final CharSequence cs = view.getText();
|
||||||
final int length = cs != null
|
final int length = cs != null
|
||||||
@ -74,18 +73,14 @@ abstract class DrawablesScheduler {
|
|||||||
if (span instanceof AsyncDrawableSpan) {
|
if (span instanceof AsyncDrawableSpan) {
|
||||||
|
|
||||||
final AsyncDrawableSpan asyncDrawableSpan = (AsyncDrawableSpan) span;
|
final AsyncDrawableSpan asyncDrawableSpan = (AsyncDrawableSpan) span;
|
||||||
final CoordinatesProvider provider = coordinates
|
list.add(asyncDrawableSpan.getDrawable());
|
||||||
? new AsyncDrawableSpanCoordinatesProvider(asyncDrawableSpan)
|
|
||||||
: null;
|
|
||||||
|
|
||||||
list.add(new Pair(asyncDrawableSpan.getDrawable(), provider));
|
|
||||||
} else if (span instanceof DynamicDrawableSpan) {
|
} else if (span instanceof DynamicDrawableSpan) {
|
||||||
// it's really not optimal thing because it stores Drawable in WeakReference...
|
// it's really not optimal thing because it stores Drawable in WeakReference...
|
||||||
// which is why it will be most likely already de-referenced...
|
// which is why it will be most likely already de-referenced...
|
||||||
final Drawable d = ((DynamicDrawableSpan) span).getDrawable();
|
final Drawable d = ((DynamicDrawableSpan) span).getDrawable();
|
||||||
if (d != null
|
if (d != null
|
||||||
&& d instanceof AsyncDrawable) {
|
&& d instanceof AsyncDrawable) {
|
||||||
list.add(new Pair((AsyncDrawable) d, null));
|
list.add((AsyncDrawable) d);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -101,21 +96,13 @@ abstract class DrawablesScheduler {
|
|||||||
private DrawablesScheduler() {
|
private DrawablesScheduler() {
|
||||||
}
|
}
|
||||||
|
|
||||||
private interface CoordinatesProvider {
|
|
||||||
int getX();
|
|
||||||
|
|
||||||
int getY();
|
|
||||||
}
|
|
||||||
|
|
||||||
private static class DrawableCallbackImpl implements Drawable.Callback {
|
private static class DrawableCallbackImpl implements Drawable.Callback {
|
||||||
|
|
||||||
private final TextView view;
|
private final TextView view;
|
||||||
private final CoordinatesProvider coordinatesProvider;
|
|
||||||
private Rect previousBounds;
|
private Rect previousBounds;
|
||||||
|
|
||||||
DrawableCallbackImpl(TextView view, CoordinatesProvider provider, Rect initialBounds) {
|
DrawableCallbackImpl(TextView view, Rect initialBounds) {
|
||||||
this.view = view;
|
this.view = view;
|
||||||
this.coordinatesProvider = provider;
|
|
||||||
this.previousBounds = new Rect(initialBounds);
|
this.previousBounds = new Rect(initialBounds);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -134,7 +121,7 @@ abstract class DrawablesScheduler {
|
|||||||
|
|
||||||
final Rect rect = who.getBounds();
|
final Rect rect = who.getBounds();
|
||||||
|
|
||||||
// okay... teh thing is IF we do not change bounds size, normal invalidate would do
|
// okay... the thing is IF we do not change bounds size, normal invalidate would do
|
||||||
// but if the size has changed, then we need to update the whole layout...
|
// but if the size has changed, then we need to update the whole layout...
|
||||||
|
|
||||||
if (!previousBounds.equals(rect)) {
|
if (!previousBounds.equals(rect)) {
|
||||||
@ -143,29 +130,7 @@ abstract class DrawablesScheduler {
|
|||||||
previousBounds = new Rect(rect);
|
previousBounds = new Rect(rect);
|
||||||
} else {
|
} else {
|
||||||
|
|
||||||
// if bounds are the same then simple invalidate would do
|
|
||||||
|
|
||||||
if (coordinatesProvider != null) {
|
|
||||||
final int x = coordinatesProvider.getX();
|
|
||||||
final int y = coordinatesProvider.getY();
|
|
||||||
view.postInvalidate(
|
|
||||||
x + rect.left,
|
|
||||||
y + rect.top,
|
|
||||||
x + rect.right,
|
|
||||||
y + rect.bottom
|
|
||||||
);
|
|
||||||
Debug.i(x + rect.left,
|
|
||||||
y + rect.top,
|
|
||||||
x + rect.right,
|
|
||||||
y + rect.bottom);
|
|
||||||
} else {
|
|
||||||
Debug.i();
|
|
||||||
// else all we can do is request full re-draw... maybe system is smart enough not re-draw what is not on screen?
|
|
||||||
view.postInvalidate();
|
view.postInvalidate();
|
||||||
// we do not need to invalidate if, for example, a gif is playing somewhere out of current viewPort...
|
|
||||||
// but i do not see...
|
|
||||||
}
|
|
||||||
// view.postInvalidate();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -180,33 +145,4 @@ abstract class DrawablesScheduler {
|
|||||||
view.removeCallbacks(what);
|
view.removeCallbacks(what);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private static class AsyncDrawableSpanCoordinatesProvider implements CoordinatesProvider {
|
|
||||||
|
|
||||||
private final AsyncDrawableSpan span;
|
|
||||||
|
|
||||||
private AsyncDrawableSpanCoordinatesProvider(AsyncDrawableSpan span) {
|
|
||||||
this.span = span;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public int getX() {
|
|
||||||
return span.lastKnownDrawX();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public int getY() {
|
|
||||||
return span.lastKnownDrawY();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private static class Pair {
|
|
||||||
final AsyncDrawable drawable;
|
|
||||||
final CoordinatesProvider coordinatesProvider;
|
|
||||||
|
|
||||||
Pair(AsyncDrawable drawable, CoordinatesProvider coordinatesProvider) {
|
|
||||||
this.drawable = drawable;
|
|
||||||
this.coordinatesProvider = coordinatesProvider;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
@ -11,7 +11,7 @@ import android.view.View;
|
|||||||
|
|
||||||
import ru.noties.markwon.spans.LinkSpan;
|
import ru.noties.markwon.spans.LinkSpan;
|
||||||
|
|
||||||
class LinkResolverDef implements LinkSpan.Resolver {
|
public class LinkResolverDef implements LinkSpan.Resolver {
|
||||||
@Override
|
@Override
|
||||||
public void resolve(View view, @NonNull String link) {
|
public void resolve(View view, @NonNull String link) {
|
||||||
final Uri uri = Uri.parse(link);
|
final Uri uri = Uri.parse(link);
|
||||||
|
@ -121,7 +121,7 @@ public class SpannableConfiguration {
|
|||||||
urlProcessor = new UrlProcessorNoOp();
|
urlProcessor = new UrlProcessorNoOp();
|
||||||
}
|
}
|
||||||
if (htmlParser == null) {
|
if (htmlParser == null) {
|
||||||
htmlParser = SpannableHtmlParser.create(theme, asyncDrawableLoader, urlProcessor);
|
htmlParser = SpannableHtmlParser.create(theme, asyncDrawableLoader, urlProcessor, linkResolver);
|
||||||
}
|
}
|
||||||
return new SpannableConfiguration(this);
|
return new SpannableConfiguration(this);
|
||||||
}
|
}
|
||||||
|
@ -6,6 +6,7 @@ import android.support.annotation.Nullable;
|
|||||||
import java.net.MalformedURLException;
|
import java.net.MalformedURLException;
|
||||||
import java.net.URL;
|
import java.net.URL;
|
||||||
|
|
||||||
|
@SuppressWarnings("WeakerAccess")
|
||||||
public class UrlProcessorRelativeToAbsolute implements UrlProcessor {
|
public class UrlProcessorRelativeToAbsolute implements UrlProcessor {
|
||||||
|
|
||||||
private final URL base;
|
private final URL base;
|
||||||
|
@ -33,7 +33,6 @@ import org.commonmark.node.ThematicBreak;
|
|||||||
import java.util.ArrayDeque;
|
import java.util.ArrayDeque;
|
||||||
import java.util.Deque;
|
import java.util.Deque;
|
||||||
|
|
||||||
import ru.noties.debug.Debug;
|
|
||||||
import ru.noties.markwon.SpannableConfiguration;
|
import ru.noties.markwon.SpannableConfiguration;
|
||||||
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.AsyncDrawable;
|
||||||
@ -51,8 +50,6 @@ import ru.noties.markwon.spans.ThematicBreakSpan;
|
|||||||
@SuppressWarnings("WeakerAccess")
|
@SuppressWarnings("WeakerAccess")
|
||||||
public class SpannableMarkdownVisitor extends AbstractVisitor {
|
public class SpannableMarkdownVisitor extends AbstractVisitor {
|
||||||
|
|
||||||
private static final String HTML_CONTENT = "<%1$s>%2$s</%3$s>";
|
|
||||||
|
|
||||||
private final SpannableConfiguration configuration;
|
private final SpannableConfiguration configuration;
|
||||||
private final SpannableStringBuilder builder;
|
private final SpannableStringBuilder builder;
|
||||||
private final Deque<HtmlInlineItem> htmlInlineItems;
|
private final Deque<HtmlInlineItem> htmlInlineItems;
|
||||||
@ -302,7 +299,7 @@ public class SpannableMarkdownVisitor extends AbstractVisitor {
|
|||||||
|
|
||||||
// we must check if anything _was_ added, as we need at least one char to render
|
// we must check if anything _was_ added, as we need at least one char to render
|
||||||
if (length == builder.length()) {
|
if (length == builder.length()) {
|
||||||
builder.append(' '); // breakable space
|
builder.append('\uFFFC');
|
||||||
}
|
}
|
||||||
|
|
||||||
final Node parent = image.getParent();
|
final Node parent = image.getParent();
|
||||||
@ -321,17 +318,23 @@ public class SpannableMarkdownVisitor extends AbstractVisitor {
|
|||||||
link
|
link
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// todo, maybe, if image is not inside a link, we should make it clickable, so
|
||||||
|
// user can open it in external viewer?
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void visit(HtmlBlock htmlBlock) {
|
public void visit(HtmlBlock htmlBlock) {
|
||||||
// http://spec.commonmark.org/0.18/#html-blocks
|
// http://spec.commonmark.org/0.18/#html-blocks
|
||||||
Debug.i(htmlBlock, htmlBlock.getLiteral());
|
final Spanned spanned = configuration.htmlParser().getSpanned(null, htmlBlock.getLiteral());
|
||||||
super.visit(htmlBlock);
|
if (!TextUtils.isEmpty(spanned)) {
|
||||||
|
builder.append(spanned);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void visit(HtmlInline htmlInline) {
|
public void visit(HtmlInline htmlInline) {
|
||||||
|
|
||||||
final SpannableHtmlParser htmlParser = configuration.htmlParser();
|
final SpannableHtmlParser htmlParser = configuration.htmlParser();
|
||||||
final SpannableHtmlParser.Tag tag = htmlParser.parseTag(htmlInline.getLiteral());
|
final SpannableHtmlParser.Tag tag = htmlParser.parseTag(htmlInline.getLiteral());
|
||||||
|
|
||||||
@ -340,37 +343,25 @@ public class SpannableMarkdownVisitor extends AbstractVisitor {
|
|||||||
final boolean voidTag = tag.voidTag();
|
final boolean voidTag = tag.voidTag();
|
||||||
if (!voidTag && tag.opening()) {
|
if (!voidTag && tag.opening()) {
|
||||||
// push in stack
|
// push in stack
|
||||||
htmlInlineItems.push(new HtmlInlineItem(tag.name(), builder.length()));
|
htmlInlineItems.push(new HtmlInlineItem(tag, builder.length()));
|
||||||
visitChildren(htmlInline);
|
visitChildren(htmlInline);
|
||||||
} else {
|
} else {
|
||||||
|
|
||||||
if (!voidTag) {
|
if (!voidTag) {
|
||||||
if (htmlInlineItems.size() > 0) {
|
if (htmlInlineItems.size() > 0) {
|
||||||
final HtmlInlineItem item = htmlInlineItems.pop();
|
final HtmlInlineItem item = htmlInlineItems.pop();
|
||||||
final Object span = htmlParser.handleTag(item.tag);
|
final Object span = htmlParser.getSpanForTag(item.tag);
|
||||||
final int start = item.start;
|
|
||||||
if (span != null) {
|
if (span != null) {
|
||||||
setSpan(item.start, span);
|
setSpan(item.start, span);
|
||||||
} else {
|
|
||||||
final String content = builder.subSequence(start, builder.length()).toString();
|
|
||||||
final String html = String.format(HTML_CONTENT, item.tag, content, tag.name());
|
|
||||||
final Object[] spans = htmlParser.htmlSpans(html);
|
|
||||||
final int length = spans != null
|
|
||||||
? spans.length
|
|
||||||
: 0;
|
|
||||||
for (int i = 0; i < length; i++) {
|
|
||||||
setSpan(start, spans[i]);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
final String content = htmlInline.getLiteral();
|
|
||||||
if (!TextUtils.isEmpty(content)) {
|
final Spanned html = htmlParser.getSpanned(tag, htmlInline.getLiteral());
|
||||||
final Spanned html = htmlParser.html(content);
|
|
||||||
if (!TextUtils.isEmpty(html)) {
|
if (!TextUtils.isEmpty(html)) {
|
||||||
builder.append(html);
|
builder.append(html);
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
@ -412,10 +403,11 @@ public class SpannableMarkdownVisitor extends AbstractVisitor {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private static class HtmlInlineItem {
|
private static class HtmlInlineItem {
|
||||||
final String tag;
|
|
||||||
|
final SpannableHtmlParser.Tag tag;
|
||||||
final int start;
|
final int start;
|
||||||
|
|
||||||
HtmlInlineItem(String tag, int start) {
|
HtmlInlineItem(SpannableHtmlParser.Tag tag, int start) {
|
||||||
this.tag = tag;
|
this.tag = tag;
|
||||||
this.start = start;
|
this.start = start;
|
||||||
}
|
}
|
||||||
|
@ -1,10 +1,13 @@
|
|||||||
package ru.noties.markwon.renderer.html;
|
package ru.noties.markwon.renderer.html;
|
||||||
|
|
||||||
|
import android.support.annotation.NonNull;
|
||||||
|
|
||||||
import ru.noties.markwon.spans.StrongEmphasisSpan;
|
import ru.noties.markwon.spans.StrongEmphasisSpan;
|
||||||
|
|
||||||
class BoldProvider implements SpannableHtmlParser.SpanProvider {
|
class BoldProvider implements SpannableHtmlParser.SpanProvider {
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public Object provide() {
|
public Object provide(@NonNull SpannableHtmlParser.Tag tag) {
|
||||||
return new StrongEmphasisSpan();
|
return new StrongEmphasisSpan();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,31 +0,0 @@
|
|||||||
package ru.noties.markwon.renderer.html;
|
|
||||||
|
|
||||||
import android.graphics.drawable.Drawable;
|
|
||||||
import android.support.annotation.NonNull;
|
|
||||||
import android.support.annotation.Nullable;
|
|
||||||
import android.text.Html;
|
|
||||||
|
|
||||||
import ru.noties.markwon.UrlProcessor;
|
|
||||||
import ru.noties.markwon.spans.AsyncDrawable;
|
|
||||||
|
|
||||||
class HtmlImageGetter implements Html.ImageGetter {
|
|
||||||
|
|
||||||
private final AsyncDrawable.Loader loader;
|
|
||||||
private final UrlProcessor urlProcessor;
|
|
||||||
|
|
||||||
HtmlImageGetter(@NonNull AsyncDrawable.Loader loader, @Nullable UrlProcessor urlProcessor) {
|
|
||||||
this.loader = loader;
|
|
||||||
this.urlProcessor = urlProcessor;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public Drawable getDrawable(String source) {
|
|
||||||
final String destination;
|
|
||||||
if (urlProcessor == null) {
|
|
||||||
destination = source;
|
|
||||||
} else {
|
|
||||||
destination = urlProcessor.process(source);
|
|
||||||
}
|
|
||||||
return new AsyncDrawable(destination, loader);
|
|
||||||
}
|
|
||||||
}
|
|
@ -0,0 +1,63 @@
|
|||||||
|
package ru.noties.markwon.renderer.html;
|
||||||
|
|
||||||
|
import android.support.annotation.NonNull;
|
||||||
|
import android.text.SpannableString;
|
||||||
|
import android.text.Spanned;
|
||||||
|
import android.text.TextUtils;
|
||||||
|
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
import ru.noties.markwon.UrlProcessor;
|
||||||
|
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 SpannableTheme theme;
|
||||||
|
private final AsyncDrawable.Loader loader;
|
||||||
|
private final UrlProcessor urlProcessor;
|
||||||
|
|
||||||
|
ImageProviderImpl(
|
||||||
|
@NonNull SpannableTheme theme,
|
||||||
|
@NonNull AsyncDrawable.Loader loader,
|
||||||
|
@NonNull UrlProcessor urlProcessor) {
|
||||||
|
this.theme = theme;
|
||||||
|
this.loader = loader;
|
||||||
|
this.urlProcessor = urlProcessor;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Spanned provide(@NonNull SpannableHtmlParser.Tag tag) {
|
||||||
|
|
||||||
|
final Spanned spanned;
|
||||||
|
|
||||||
|
final Map<String, String> attributes = tag.attributes();
|
||||||
|
final String src = attributes.get("src");
|
||||||
|
final String alt = attributes.get("alt");
|
||||||
|
|
||||||
|
if (!TextUtils.isEmpty(src)) {
|
||||||
|
|
||||||
|
final String destination = urlProcessor.process(src);
|
||||||
|
|
||||||
|
final String replacement;
|
||||||
|
if (!TextUtils.isEmpty(alt)) {
|
||||||
|
replacement = alt;
|
||||||
|
} else {
|
||||||
|
replacement = "\uFFFC";
|
||||||
|
}
|
||||||
|
|
||||||
|
final AsyncDrawable drawable = new AsyncDrawable(destination, loader);
|
||||||
|
final AsyncDrawableSpan span = new AsyncDrawableSpan(theme, drawable);
|
||||||
|
|
||||||
|
final SpannableString string = new SpannableString(replacement);
|
||||||
|
string.setSpan(span, 0, string.length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
|
||||||
|
|
||||||
|
spanned = string;
|
||||||
|
} else {
|
||||||
|
spanned = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return spanned;
|
||||||
|
}
|
||||||
|
}
|
@ -1,10 +1,13 @@
|
|||||||
package ru.noties.markwon.renderer.html;
|
package ru.noties.markwon.renderer.html;
|
||||||
|
|
||||||
|
import android.support.annotation.NonNull;
|
||||||
|
|
||||||
import ru.noties.markwon.spans.EmphasisSpan;
|
import ru.noties.markwon.spans.EmphasisSpan;
|
||||||
|
|
||||||
class ItalicsProvider implements SpannableHtmlParser.SpanProvider {
|
class ItalicsProvider implements SpannableHtmlParser.SpanProvider {
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public Object provide() {
|
public Object provide(@NonNull SpannableHtmlParser.Tag tag) {
|
||||||
return new EmphasisSpan();
|
return new EmphasisSpan();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,45 @@
|
|||||||
|
package ru.noties.markwon.renderer.html;
|
||||||
|
|
||||||
|
import android.support.annotation.NonNull;
|
||||||
|
import android.text.TextUtils;
|
||||||
|
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
import ru.noties.markwon.UrlProcessor;
|
||||||
|
import ru.noties.markwon.spans.LinkSpan;
|
||||||
|
import ru.noties.markwon.spans.SpannableTheme;
|
||||||
|
|
||||||
|
class LinkProvider implements SpannableHtmlParser.SpanProvider {
|
||||||
|
|
||||||
|
private final SpannableTheme theme;
|
||||||
|
private final UrlProcessor urlProcessor;
|
||||||
|
private final LinkSpan.Resolver resolver;
|
||||||
|
|
||||||
|
LinkProvider(
|
||||||
|
@NonNull SpannableTheme theme,
|
||||||
|
@NonNull UrlProcessor urlProcessor,
|
||||||
|
@NonNull LinkSpan.Resolver resolver) {
|
||||||
|
this.theme = theme;
|
||||||
|
this.urlProcessor = urlProcessor;
|
||||||
|
this.resolver = resolver;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Object provide(@NonNull SpannableHtmlParser.Tag tag) {
|
||||||
|
|
||||||
|
final Object span;
|
||||||
|
|
||||||
|
final Map<String, String> attributes = tag.attributes();
|
||||||
|
final String href = attributes.get("href");
|
||||||
|
if (!TextUtils.isEmpty(href)) {
|
||||||
|
|
||||||
|
final String destination = urlProcessor.process(href);
|
||||||
|
span = new LinkSpan(theme, destination, resolver);
|
||||||
|
|
||||||
|
} else {
|
||||||
|
span = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return span;
|
||||||
|
}
|
||||||
|
}
|
@ -7,37 +7,35 @@ import android.support.annotation.Nullable;
|
|||||||
import android.text.Html;
|
import android.text.Html;
|
||||||
import android.text.Spanned;
|
import android.text.Spanned;
|
||||||
|
|
||||||
import java.util.Collections;
|
|
||||||
import java.util.HashMap;
|
import java.util.HashMap;
|
||||||
import java.util.HashSet;
|
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.util.Set;
|
|
||||||
|
|
||||||
import ru.noties.debug.Debug;
|
import ru.noties.markwon.LinkResolverDef;
|
||||||
import ru.noties.markwon.UrlProcessor;
|
import ru.noties.markwon.UrlProcessor;
|
||||||
|
import ru.noties.markwon.UrlProcessorNoOp;
|
||||||
import ru.noties.markwon.spans.AsyncDrawable;
|
import ru.noties.markwon.spans.AsyncDrawable;
|
||||||
|
import ru.noties.markwon.spans.LinkSpan;
|
||||||
import ru.noties.markwon.spans.SpannableTheme;
|
import ru.noties.markwon.spans.SpannableTheme;
|
||||||
|
|
||||||
@SuppressWarnings("WeakerAccess")
|
@SuppressWarnings("WeakerAccess")
|
||||||
public class SpannableHtmlParser {
|
public class SpannableHtmlParser {
|
||||||
|
|
||||||
// we need to handle images independently (in order to parse alt, width, height, etc)
|
|
||||||
|
|
||||||
// creates default parser
|
// creates default parser
|
||||||
public static SpannableHtmlParser create(
|
public static SpannableHtmlParser create(
|
||||||
@NonNull SpannableTheme theme,
|
@NonNull SpannableTheme theme,
|
||||||
@NonNull AsyncDrawable.Loader loader
|
@NonNull AsyncDrawable.Loader loader
|
||||||
) {
|
) {
|
||||||
return builderWithDefaults(theme, loader, null)
|
return builderWithDefaults(theme, loader, null, null)
|
||||||
.build();
|
.build();
|
||||||
}
|
}
|
||||||
|
|
||||||
public static SpannableHtmlParser create(
|
public static SpannableHtmlParser create(
|
||||||
@NonNull SpannableTheme theme,
|
@NonNull SpannableTheme theme,
|
||||||
@NonNull AsyncDrawable.Loader loader,
|
@NonNull AsyncDrawable.Loader loader,
|
||||||
@NonNull UrlProcessor urlProcessor
|
@NonNull UrlProcessor urlProcessor,
|
||||||
|
@NonNull LinkSpan.Resolver resolver
|
||||||
) {
|
) {
|
||||||
return builderWithDefaults(theme, loader, urlProcessor)
|
return builderWithDefaults(theme, loader, urlProcessor, resolver)
|
||||||
.build();
|
.build();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -45,151 +43,133 @@ public class SpannableHtmlParser {
|
|||||||
return new Builder();
|
return new Builder();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static Builder builderWithDefaults(@NonNull SpannableTheme theme) {
|
||||||
|
return builderWithDefaults(theme, null, null, null);
|
||||||
|
}
|
||||||
|
|
||||||
public static Builder builderWithDefaults(
|
public static Builder builderWithDefaults(
|
||||||
@NonNull SpannableTheme theme,
|
@NonNull SpannableTheme theme,
|
||||||
@Nullable AsyncDrawable.Loader asyncDrawableLoader,
|
@Nullable AsyncDrawable.Loader asyncDrawableLoader,
|
||||||
@Nullable UrlProcessor urlProcessor
|
@Nullable UrlProcessor urlProcessor,
|
||||||
|
@Nullable LinkSpan.Resolver resolver
|
||||||
) {
|
) {
|
||||||
|
|
||||||
|
if (urlProcessor == null) {
|
||||||
|
urlProcessor = new UrlProcessorNoOp();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (resolver == null) {
|
||||||
|
resolver = new LinkResolverDef();
|
||||||
|
}
|
||||||
|
|
||||||
final BoldProvider boldProvider = new BoldProvider();
|
final BoldProvider boldProvider = new BoldProvider();
|
||||||
final ItalicsProvider italicsProvider = new ItalicsProvider();
|
final ItalicsProvider italicsProvider = new ItalicsProvider();
|
||||||
final StrikeProvider strikeProvider = new StrikeProvider();
|
final StrikeProvider strikeProvider = new StrikeProvider();
|
||||||
|
|
||||||
final HtmlParser parser;
|
final ImageProvider imageProvider;
|
||||||
if (asyncDrawableLoader != null) {
|
if (asyncDrawableLoader != null) {
|
||||||
parser = DefaultHtmlParser.create(new HtmlImageGetter(asyncDrawableLoader, urlProcessor), null);
|
imageProvider = new ImageProviderImpl(theme, asyncDrawableLoader, urlProcessor);
|
||||||
} else {
|
} else {
|
||||||
parser = DefaultHtmlParser.create(null, null);
|
imageProvider = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
return new Builder()
|
return new Builder()
|
||||||
.customTag("b", boldProvider)
|
.simpleTag("b", boldProvider)
|
||||||
.customTag("strong", boldProvider)
|
.simpleTag("strong", boldProvider)
|
||||||
.customTag("i", italicsProvider)
|
.simpleTag("i", italicsProvider)
|
||||||
.customTag("em", italicsProvider)
|
.simpleTag("em", italicsProvider)
|
||||||
.customTag("cite", italicsProvider)
|
.simpleTag("cite", italicsProvider)
|
||||||
.customTag("dfn", italicsProvider)
|
.simpleTag("dfn", italicsProvider)
|
||||||
.customTag("sup", new SuperScriptProvider(theme))
|
.simpleTag("sup", new SuperScriptProvider(theme))
|
||||||
.customTag("sub", new SubScriptProvider(theme))
|
.simpleTag("sub", new SubScriptProvider(theme))
|
||||||
.customTag("u", new UnderlineProvider())
|
.simpleTag("u", new UnderlineProvider())
|
||||||
.customTag("del", strikeProvider)
|
.simpleTag("del", strikeProvider)
|
||||||
.customTag("s", strikeProvider)
|
.simpleTag("s", strikeProvider)
|
||||||
.customTag("strike", strikeProvider)
|
.simpleTag("strike", strikeProvider)
|
||||||
.parser(parser);
|
.simpleTag("a", new LinkProvider(theme, urlProcessor, resolver))
|
||||||
|
.imageProvider(imageProvider);
|
||||||
}
|
}
|
||||||
|
|
||||||
// for simple tags without arguments
|
// for simple tags without arguments
|
||||||
// <b>, <i>, etc
|
// <b>, <i>, etc
|
||||||
public interface SpanProvider {
|
public interface SpanProvider {
|
||||||
Object provide();
|
Object provide(@NonNull Tag tag);
|
||||||
|
}
|
||||||
|
|
||||||
|
public interface ImageProvider {
|
||||||
|
Spanned provide(@NonNull Tag tag);
|
||||||
}
|
}
|
||||||
|
|
||||||
public interface HtmlParser {
|
public interface HtmlParser {
|
||||||
Object[] getSpans(@NonNull String html);
|
|
||||||
|
// returns span for a simple content
|
||||||
|
Object getSpan(@NonNull String html);
|
||||||
|
|
||||||
Spanned parse(@NonNull String html);
|
Spanned parse(@NonNull String html);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static final String LINK_START = "<a ";
|
private final Map<String, SpanProvider> simpleTags;
|
||||||
|
private final ImageProvider imageProvider;
|
||||||
private final Map<String, SpanProvider> customTags;
|
|
||||||
private final Set<String> voidTags;
|
|
||||||
private final HtmlParser parser;
|
private final HtmlParser parser;
|
||||||
|
private final TagParser tagParser;
|
||||||
|
|
||||||
private SpannableHtmlParser(Builder builder) {
|
private SpannableHtmlParser(Builder builder) {
|
||||||
this.customTags = builder.customTags;
|
this.simpleTags = builder.simpleTags;
|
||||||
this.voidTags = voidTags();
|
this.imageProvider = builder.imageProvider;
|
||||||
this.parser = builder.parser;
|
this.parser = builder.parser;
|
||||||
|
this.tagParser = new TagParser();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Nullable
|
@Nullable
|
||||||
public Tag parseTag(String html) {
|
public Tag parseTag(String html) {
|
||||||
|
return tagParser.parse(html);
|
||||||
final Tag tag;
|
|
||||||
|
|
||||||
final int length = html != null
|
|
||||||
? html.length()
|
|
||||||
: 0;
|
|
||||||
|
|
||||||
// absolutely minimum (`<i>`)
|
|
||||||
if (length < 3) {
|
|
||||||
tag = null;
|
|
||||||
} else {
|
|
||||||
// okay, we will consider a tag a void one if it's in our void list tag
|
|
||||||
final boolean closing = '<' == html.charAt(0) && '/' == html.charAt(1);
|
|
||||||
final boolean voidTag;
|
|
||||||
if (closing) {
|
|
||||||
voidTag = false;
|
|
||||||
} else {
|
|
||||||
int firstNonChar = -1;
|
|
||||||
for (int i = 1; i < length; i++) {
|
|
||||||
if (!Character.isLetterOrDigit(html.charAt(i))) {
|
|
||||||
firstNonChar = i;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (firstNonChar > 1) {
|
|
||||||
final String name = html.substring(1, firstNonChar);
|
|
||||||
voidTag = voidTags.contains(name);
|
|
||||||
} else {
|
|
||||||
voidTag = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// todo, we do not strip to void tag name, so it can be possibly ended with `/`
|
|
||||||
final String name = closing
|
|
||||||
? html.substring(2, length - 1)
|
|
||||||
: html.substring(1, length - 1);
|
|
||||||
|
|
||||||
tag = new Tag(name, !closing, voidTag);
|
|
||||||
}
|
|
||||||
|
|
||||||
return tag;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Nullable
|
@Nullable
|
||||||
public Object handleTag(String tag) {
|
public Object getSpanForTag(@NonNull Tag tag) {
|
||||||
|
|
||||||
|
// check if we have specific handler for tag.name
|
||||||
|
|
||||||
final Object out;
|
final Object out;
|
||||||
final SpanProvider provider = customTags.get(tag);
|
|
||||||
|
final SpanProvider provider = simpleTags.get(tag.name);
|
||||||
if (provider != null) {
|
if (provider != null) {
|
||||||
out = provider.provide();
|
out = provider.provide(tag);
|
||||||
} else {
|
} else {
|
||||||
out = null;
|
// let's prepare mock content & extract spans from it
|
||||||
|
// actual content doesn't matter, here it's just `abc`
|
||||||
|
final String mock = tag.raw + "abc" + "</" + tag.name + ">";
|
||||||
|
out = parser.getSpan(mock);
|
||||||
}
|
}
|
||||||
|
|
||||||
return out;
|
return out;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Nullable
|
// if tag is NULL, then it's HtmlBlock... else just a void tag
|
||||||
public Object[] htmlSpans(String html) {
|
public Spanned getSpanned(@Nullable Tag tag, String html) {
|
||||||
// todo, additional handling of: image & link
|
final Spanned spanned;
|
||||||
Debug.i("html: %s", html);
|
if (tag != null && "img".equals(tag.name) && imageProvider != null) {
|
||||||
return parser.getSpans(html);
|
spanned = imageProvider.provide(tag);
|
||||||
|
} else {
|
||||||
|
spanned = parser.parse(html);
|
||||||
}
|
}
|
||||||
|
return spanned;
|
||||||
// this is called when we encounter `void` tag
|
|
||||||
// `img` is a void tag
|
|
||||||
public Spanned html(String html) {
|
|
||||||
Debug.i("html: %s", html);
|
|
||||||
return parser.parse(html);
|
|
||||||
}
|
|
||||||
|
|
||||||
private static Set<String> voidTags() {
|
|
||||||
final String[] tags = {
|
|
||||||
"area", "base", "br", "col", "embed", "hr", "img", "input",
|
|
||||||
"keygen", "link", "meta", "param", "source", "track", "wbr"
|
|
||||||
};
|
|
||||||
final Set<String> set = new HashSet<>(tags.length);
|
|
||||||
Collections.addAll(set, tags);
|
|
||||||
return set;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public static class Builder {
|
public static class Builder {
|
||||||
|
|
||||||
private final Map<String, SpanProvider> customTags = new HashMap<>(3);
|
private final Map<String, SpanProvider> simpleTags = new HashMap<>(3);
|
||||||
|
|
||||||
|
private ImageProvider imageProvider;
|
||||||
private HtmlParser parser;
|
private HtmlParser parser;
|
||||||
|
|
||||||
public Builder customTag(@NonNull String tag, @NonNull SpanProvider provider) {
|
public Builder simpleTag(@NonNull String tag, @NonNull SpanProvider provider) {
|
||||||
customTags.put(tag, provider);
|
simpleTags.put(tag, provider);
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Builder imageProvider(ImageProvider imageProvider) {
|
||||||
|
this.imageProvider = imageProvider;
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -200,7 +180,7 @@ public class SpannableHtmlParser {
|
|||||||
|
|
||||||
public SpannableHtmlParser build() {
|
public SpannableHtmlParser build() {
|
||||||
if (parser == null) {
|
if (parser == null) {
|
||||||
parser = DefaultHtmlParser.create(null, null);
|
parser = DefaultHtmlParser.create();
|
||||||
}
|
}
|
||||||
return new SpannableHtmlParser(this);
|
return new SpannableHtmlParser(this);
|
||||||
}
|
}
|
||||||
@ -208,20 +188,34 @@ public class SpannableHtmlParser {
|
|||||||
|
|
||||||
public static class Tag {
|
public static class Tag {
|
||||||
|
|
||||||
|
private final String raw;
|
||||||
private final String name;
|
private final String name;
|
||||||
|
private final Map<String, String> attributes;
|
||||||
|
|
||||||
private final boolean opening;
|
private final boolean opening;
|
||||||
private final boolean voidTag;
|
private final boolean voidTag;
|
||||||
|
|
||||||
public Tag(String name, boolean opening, boolean voidTag) {
|
public Tag(String raw, String name, @NonNull Map<String, String> attributes, boolean opening, boolean voidTag) {
|
||||||
|
this.raw = raw;
|
||||||
this.name = name;
|
this.name = name;
|
||||||
|
this.attributes = attributes;
|
||||||
this.opening = opening;
|
this.opening = opening;
|
||||||
this.voidTag = voidTag;
|
this.voidTag = voidTag;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public String raw() {
|
||||||
|
return raw;
|
||||||
|
}
|
||||||
|
|
||||||
public String name() {
|
public String name() {
|
||||||
return name;
|
return name;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@NonNull
|
||||||
|
public Map<String, String> attributes() {
|
||||||
|
return attributes;
|
||||||
|
}
|
||||||
|
|
||||||
public boolean opening() {
|
public boolean opening() {
|
||||||
return opening;
|
return opening;
|
||||||
}
|
}
|
||||||
@ -233,7 +227,9 @@ public class SpannableHtmlParser {
|
|||||||
@Override
|
@Override
|
||||||
public String toString() {
|
public String toString() {
|
||||||
return "Tag{" +
|
return "Tag{" +
|
||||||
"name='" + name + '\'' +
|
"raw='" + raw + '\'' +
|
||||||
|
", name='" + name + '\'' +
|
||||||
|
", attributes=" + attributes +
|
||||||
", opening=" + opening +
|
", opening=" + opening +
|
||||||
", voidTag=" + voidTag +
|
", voidTag=" + voidTag +
|
||||||
'}';
|
'}';
|
||||||
@ -242,25 +238,20 @@ public class SpannableHtmlParser {
|
|||||||
|
|
||||||
public static abstract class DefaultHtmlParser implements HtmlParser {
|
public static abstract class DefaultHtmlParser implements HtmlParser {
|
||||||
|
|
||||||
public static DefaultHtmlParser create(@Nullable Html.ImageGetter imageGetter, @Nullable Html.TagHandler tagHandler) {
|
public static DefaultHtmlParser create() {
|
||||||
final DefaultHtmlParser parser;
|
final DefaultHtmlParser parser;
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
|
||||||
parser = new Parser24(imageGetter, tagHandler);
|
parser = new Parser24();
|
||||||
} else {
|
} else {
|
||||||
parser = new ParserPre24(imageGetter, tagHandler);
|
parser = new ParserPre24();
|
||||||
}
|
}
|
||||||
return parser;
|
return parser;
|
||||||
}
|
}
|
||||||
|
|
||||||
final Html.ImageGetter imageGetter;
|
Object getSpan(Spanned spanned) {
|
||||||
final Html.TagHandler tagHandler;
|
|
||||||
|
|
||||||
DefaultHtmlParser(Html.ImageGetter imageGetter, Html.TagHandler tagHandler) {
|
final Object out;
|
||||||
this.imageGetter = imageGetter;
|
|
||||||
this.tagHandler = tagHandler;
|
|
||||||
}
|
|
||||||
|
|
||||||
Object[] getSpans(Spanned spanned) {
|
|
||||||
final Object[] spans;
|
final Object[] spans;
|
||||||
final int length = spanned != null ? spanned.length() : 0;
|
final int length = spanned != null ? spanned.length() : 0;
|
||||||
if (length == 0) {
|
if (length == 0) {
|
||||||
@ -268,42 +259,42 @@ public class SpannableHtmlParser {
|
|||||||
} else {
|
} else {
|
||||||
spans = spanned.getSpans(0, length, Object.class);
|
spans = spanned.getSpans(0, length, Object.class);
|
||||||
}
|
}
|
||||||
return spans;
|
|
||||||
|
if (spans != null
|
||||||
|
&& spans.length > 0) {
|
||||||
|
out = spans[0];
|
||||||
|
} else {
|
||||||
|
out = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return out;
|
||||||
}
|
}
|
||||||
|
|
||||||
@SuppressWarnings("deprecation")
|
@SuppressWarnings("deprecation")
|
||||||
private static class ParserPre24 extends DefaultHtmlParser {
|
private static class ParserPre24 extends DefaultHtmlParser {
|
||||||
|
|
||||||
ParserPre24(Html.ImageGetter imageGetter, Html.TagHandler tagHandler) {
|
|
||||||
super(imageGetter, tagHandler);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public Object[] getSpans(@NonNull String html) {
|
public Object getSpan(@NonNull String html) {
|
||||||
return getSpans(parse(html));
|
return getSpan(parse(html));
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public Spanned parse(@NonNull String html) {
|
public Spanned parse(@NonNull String html) {
|
||||||
return Html.fromHtml(html, imageGetter, tagHandler);
|
return Html.fromHtml(html, null, null);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@TargetApi(Build.VERSION_CODES.N)
|
@TargetApi(Build.VERSION_CODES.N)
|
||||||
private static class Parser24 extends DefaultHtmlParser {
|
private static class Parser24 extends DefaultHtmlParser {
|
||||||
|
|
||||||
Parser24(Html.ImageGetter imageGetter, Html.TagHandler tagHandler) {
|
|
||||||
super(imageGetter, tagHandler);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public Object[] getSpans(@NonNull String html) {
|
public Object getSpan(@NonNull String html) {
|
||||||
return getSpans(parse(html));
|
return getSpan(parse(html));
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public Spanned parse(@NonNull String html) {
|
public Spanned parse(@NonNull String html) {
|
||||||
return Html.fromHtml(html, Html.FROM_HTML_MODE_COMPACT, imageGetter, tagHandler);
|
return Html.fromHtml(html, Html.FROM_HTML_MODE_COMPACT, null, null);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,10 +1,11 @@
|
|||||||
package ru.noties.markwon.renderer.html;
|
package ru.noties.markwon.renderer.html;
|
||||||
|
|
||||||
|
import android.support.annotation.NonNull;
|
||||||
import android.text.style.StrikethroughSpan;
|
import android.text.style.StrikethroughSpan;
|
||||||
|
|
||||||
class StrikeProvider implements SpannableHtmlParser.SpanProvider {
|
class StrikeProvider implements SpannableHtmlParser.SpanProvider {
|
||||||
@Override
|
@Override
|
||||||
public Object provide() {
|
public Object provide(@NonNull SpannableHtmlParser.Tag tag) {
|
||||||
return new StrikethroughSpan();
|
return new StrikethroughSpan();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,5 +1,7 @@
|
|||||||
package ru.noties.markwon.renderer.html;
|
package ru.noties.markwon.renderer.html;
|
||||||
|
|
||||||
|
import android.support.annotation.NonNull;
|
||||||
|
|
||||||
import ru.noties.markwon.spans.SpannableTheme;
|
import ru.noties.markwon.spans.SpannableTheme;
|
||||||
import ru.noties.markwon.spans.SubScriptSpan;
|
import ru.noties.markwon.spans.SubScriptSpan;
|
||||||
|
|
||||||
@ -12,7 +14,7 @@ class SubScriptProvider implements SpannableHtmlParser.SpanProvider {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public Object provide() {
|
public Object provide(@NonNull SpannableHtmlParser.Tag tag) {
|
||||||
return new SubScriptSpan(theme);
|
return new SubScriptSpan(theme);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,5 +1,7 @@
|
|||||||
package ru.noties.markwon.renderer.html;
|
package ru.noties.markwon.renderer.html;
|
||||||
|
|
||||||
|
import android.support.annotation.NonNull;
|
||||||
|
|
||||||
import ru.noties.markwon.spans.SpannableTheme;
|
import ru.noties.markwon.spans.SpannableTheme;
|
||||||
import ru.noties.markwon.spans.SuperScriptSpan;
|
import ru.noties.markwon.spans.SuperScriptSpan;
|
||||||
|
|
||||||
@ -12,7 +14,7 @@ class SuperScriptProvider implements SpannableHtmlParser.SpanProvider {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public Object provide() {
|
public Object provide(@NonNull SpannableHtmlParser.Tag tag) {
|
||||||
return new SuperScriptSpan(theme);
|
return new SuperScriptSpan(theme);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,155 @@
|
|||||||
|
package ru.noties.markwon.renderer.html;
|
||||||
|
|
||||||
|
import android.support.annotation.Nullable;
|
||||||
|
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.HashSet;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.Set;
|
||||||
|
|
||||||
|
class TagParser {
|
||||||
|
|
||||||
|
|
||||||
|
private static final Set<String> VOID_TAGS;
|
||||||
|
static {
|
||||||
|
final String[] tags = {
|
||||||
|
"area", "base", "br", "col", "embed", "hr", "img", "input",
|
||||||
|
"keygen", "link", "meta", "param", "source", "track", "wbr"
|
||||||
|
};
|
||||||
|
final Set<String> set = new HashSet<>(tags.length);
|
||||||
|
Collections.addAll(set, tags);
|
||||||
|
VOID_TAGS = Collections.unmodifiableSet(set);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
TagParser() {
|
||||||
|
}
|
||||||
|
|
||||||
|
@Nullable
|
||||||
|
SpannableHtmlParser.Tag parse(String html) {
|
||||||
|
|
||||||
|
final SpannableHtmlParser.Tag tag;
|
||||||
|
|
||||||
|
final int length = html != null
|
||||||
|
? html.length()
|
||||||
|
: 0;
|
||||||
|
|
||||||
|
// absolutely minimum (`<i>`)
|
||||||
|
if (length < 3) {
|
||||||
|
tag = null;
|
||||||
|
} else {
|
||||||
|
|
||||||
|
// // okay, we will consider a tag a void one if it's in our void list tag
|
||||||
|
|
||||||
|
final boolean closing = '<' == html.charAt(0) && '/' == html.charAt(1);
|
||||||
|
final boolean voidTag;
|
||||||
|
|
||||||
|
Map<String, String> attributes = null;
|
||||||
|
|
||||||
|
final StringBuilder builder = new StringBuilder();
|
||||||
|
|
||||||
|
String name = null;
|
||||||
|
String pendingAttribute = null;
|
||||||
|
|
||||||
|
char c;
|
||||||
|
char valueDelimiter = '\0';
|
||||||
|
|
||||||
|
for (int i = 0; i < length; i++) {
|
||||||
|
|
||||||
|
c = html.charAt(i);
|
||||||
|
|
||||||
|
// no more handling
|
||||||
|
if ('>' == c
|
||||||
|
|| '\\' == c) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (name == null) {
|
||||||
|
if (Character.isSpaceChar(c)) {
|
||||||
|
//noinspection StatementWithEmptyBody
|
||||||
|
if (builder.length() == 0) {
|
||||||
|
// ignore it, we must wait until we have tagName
|
||||||
|
} else {
|
||||||
|
|
||||||
|
name = builder.toString();
|
||||||
|
|
||||||
|
// clear buffer
|
||||||
|
builder.setLength(0);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (Character.isLetterOrDigit(c)) {
|
||||||
|
builder.append(c);
|
||||||
|
} /*else {
|
||||||
|
// we allow non-letter-digit only if builder.length == 0
|
||||||
|
// if we have already started
|
||||||
|
}*/
|
||||||
|
}
|
||||||
|
} else if (pendingAttribute == null) {
|
||||||
|
// we start checking for attribute
|
||||||
|
// ignore non-letter-digits before
|
||||||
|
if (Character.isLetterOrDigit(c)) {
|
||||||
|
builder.append(c);
|
||||||
|
} else /*if ('=' == c)*/ {
|
||||||
|
|
||||||
|
// attribute name is finished (only if we have already added something)
|
||||||
|
// else it's trailing chars that we are not interested in
|
||||||
|
if (builder.length() > 0) {
|
||||||
|
pendingAttribute = builder.toString();
|
||||||
|
builder.setLength(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// first char that we will meet will be the delimiter
|
||||||
|
if (valueDelimiter == '\0') {
|
||||||
|
valueDelimiter = c;
|
||||||
|
} else {
|
||||||
|
if (c == valueDelimiter) {
|
||||||
|
if (attributes == null) {
|
||||||
|
attributes = new HashMap<>(3);
|
||||||
|
}
|
||||||
|
attributes.put(pendingAttribute, builder.toString());
|
||||||
|
pendingAttribute = null;
|
||||||
|
valueDelimiter = '\0';
|
||||||
|
builder.setLength(0);
|
||||||
|
} else {
|
||||||
|
builder.append(c);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (builder.length() > 0) {
|
||||||
|
if (name == null) {
|
||||||
|
name = builder.toString();
|
||||||
|
} else if (pendingAttribute != null) {
|
||||||
|
if (attributes == null) {
|
||||||
|
attributes = new HashMap<>(3);
|
||||||
|
}
|
||||||
|
attributes.put(pendingAttribute, builder.toString());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// in case of wrong parsing
|
||||||
|
if (name == null) {
|
||||||
|
tag = null;
|
||||||
|
} else {
|
||||||
|
|
||||||
|
voidTag = !closing && VOID_TAGS.contains(name);
|
||||||
|
|
||||||
|
final Map<String, String> attributesMap;
|
||||||
|
if (attributes == null
|
||||||
|
|| attributes.size() == 0) {
|
||||||
|
//noinspection unchecked
|
||||||
|
attributesMap = Collections.EMPTY_MAP;
|
||||||
|
} else {
|
||||||
|
attributesMap = Collections.unmodifiableMap(attributes);
|
||||||
|
}
|
||||||
|
|
||||||
|
tag = new SpannableHtmlParser.Tag(html, name, attributesMap, !closing, voidTag);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return tag;
|
||||||
|
}
|
||||||
|
}
|
@ -1,11 +1,12 @@
|
|||||||
package ru.noties.markwon.renderer.html;
|
package ru.noties.markwon.renderer.html;
|
||||||
|
|
||||||
|
import android.support.annotation.NonNull;
|
||||||
import android.text.style.UnderlineSpan;
|
import android.text.style.UnderlineSpan;
|
||||||
|
|
||||||
class UnderlineProvider implements SpannableHtmlParser.SpanProvider {
|
class UnderlineProvider implements SpannableHtmlParser.SpanProvider {
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public Object provide() {
|
public Object provide(@NonNull SpannableHtmlParser.Tag tag) {
|
||||||
return new UnderlineSpan();
|
return new UnderlineSpan();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1 +1 @@
|
|||||||
include ':app', ':library-renderer'
|
include ':app', ':library-renderer', ':library-image-loader'
|
||||||
|
Loading…
x
Reference in New Issue
Block a user