Stabilizing api

This commit is contained in:
Dimitry Ivanov 2017-05-24 18:11:17 +03:00
parent 250dd7677d
commit d5e2d756d9
36 changed files with 884 additions and 569 deletions

View File

@ -107,6 +107,28 @@ 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
[github]: https://github.com

View File

@ -15,12 +15,14 @@ android {
}
dependencies {
compile project(':library-renderer')
compile project(':library-image-loader')
compile 'ru.noties:debug:3.0.0@jar'
compile 'com.squareup.picasso:picasso:2.5.2'
compile 'com.caverock:androidsvg:1.2.1'
compile 'pl.droidsonroids.gif:android-gif-drawable:1.2.7'
compile 'com.squareup.okhttp3:okhttp:3.8.0'
compile OK_HTTP
compile 'com.google.dagger:dagger:2.10'
annotationProcessor 'com.google.dagger:dagger-compiler:2.10'
}

View File

@ -3,6 +3,7 @@
package="ru.noties.markwon">
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.ACCESS_ALL_DOWNLOADS" />
<application
android:name=".App"

View File

@ -4,6 +4,9 @@ import android.app.Application;
import android.content.Context;
import android.support.annotation.NonNull;
import ru.noties.debug.AndroidLogDebugOutput;
import ru.noties.debug.Debug;
public class App extends Application {
private AppComponent component;
@ -12,6 +15,8 @@ public class App extends Application {
public void onCreate() {
super.onCreate();
Debug.init(new AndroidLogDebugOutput(BuildConfig.DEBUG));
component = DaggerAppComponent.builder()
.appModule(new AppModule(this))
.build();

View File

@ -5,8 +5,6 @@ import android.content.res.Resources;
import android.os.Handler;
import android.os.Looper;
import com.squareup.picasso.Picasso;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
@ -14,7 +12,10 @@ import javax.inject.Singleton;
import dagger.Module;
import dagger.Provides;
import okhttp3.Cache;
import okhttp3.OkHttpClient;
import ru.noties.markwon.il.AsyncDrawableLoader;
import ru.noties.markwon.spans.AsyncDrawable;
@Module
class AppModule {
@ -38,7 +39,10 @@ class AppModule {
@Provides
@Singleton
OkHttpClient client() {
return new OkHttpClient();
return new OkHttpClient.Builder()
.cache(new Cache(app.getCacheDir(), 1024L * 20))
.followRedirects(true)
.build();
}
@Singleton
@ -60,7 +64,14 @@ class AppModule {
}
@Provides
Picasso picasso(Context context) {
return Picasso.with(context);
AsyncDrawable.Loader asyncDrawableLoader(
OkHttpClient client,
ExecutorService executorService,
Resources resources) {
return AsyncDrawableLoader.builder()
.client(client)
.executorService(executorService)
.resources(resources)
.build();
}
}

View File

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

View File

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

View File

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

View File

@ -9,15 +9,10 @@ import android.widget.TextView;
import javax.inject.Inject;
import ru.noties.debug.AndroidLogDebugOutput;
import ru.noties.debug.Debug;
public class MainActivity extends Activity {
static {
Debug.init(new AndroidLogDebugOutput(true));
}
@Inject
MarkdownLoader markdownLoader;
@ -112,6 +107,16 @@ public class MainActivity extends Activity {
: 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
protected void onDestroy() {
super.onDestroy();

View File

@ -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) {
if (task != null
&& !task.isCancelled()) {
if (!isCancelled()) {
handler.post(new Runnable() {
@Override
public void run() {
// as this call is async, we need to check again if we are cancelled
if (!isCancelled()) {
loaded.apply(text);
task = null;
}
}
});
}

View File

@ -17,6 +17,7 @@ import java.util.concurrent.Future;
import javax.inject.Inject;
import ru.noties.markwon.renderer.SpannableRenderer;
import ru.noties.markwon.spans.AsyncDrawable;
@ActivityScope
public class MarkdownRenderer {
@ -26,7 +27,7 @@ public class MarkdownRenderer {
}
@Inject
AsyncDrawableLoader loader;
AsyncDrawable.Loader loader;
@Inject
ExecutorService service;
@ -62,22 +63,19 @@ public class MarkdownRenderer {
.urlProcessor(urlProcessor)
.build();
final Parser parser = Parser.builder()
.extensions(Collections.singleton(StrikethroughExtension.create()))
.build();
final CharSequence text = Markwon.markdown(configuration, markdown);
final Node node = parser.parse(markdown);
final SpannableRenderer renderer = new SpannableRenderer();
final CharSequence text = renderer.render(configuration, node);
// final CharSequence text = Markwon.markdown(configuration, markdown);
if (!isCancelled()) {
handler.post(new Runnable() {
@Override
public void run() {
if (!isCancelled()) {
listener.onMarkdownReady(text);
task = null;
}
}
});
task = null;
}
}
});
}
@ -88,4 +86,8 @@ public class MarkdownRenderer {
task = null;
}
}
private boolean isCancelled() {
return task == null || task.isCancelled();
}
}

View File

@ -22,7 +22,7 @@ public class Themes {
public void apply(@NonNull Context context) {
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;
if (dark) {
theme = R.style.AppThemeBaseDark;

View File

@ -6,10 +6,12 @@ import android.support.annotation.IntDef;
import android.support.annotation.NonNull;
import android.view.View;
@SuppressWarnings("WeakerAccess")
public abstract class Views {
@IntDef({View.INVISIBLE, View.GONE})
@interface NotVisible {}
@interface NotVisible {
}
public static <V extends View> V findView(@NonNull View view, @IdRes int id) {
//noinspection unchecked
@ -32,5 +34,6 @@ public abstract class Views {
view.setVisibility(visibility);
}
private Views() {}
private Views() {
}
}

View File

@ -6,7 +6,7 @@
<item name="ic_app_bar_theme">@drawable/ic_app_bar_theme_light</item>
</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>
</style>

View File

@ -32,4 +32,8 @@ ext {
final def commonMarkVersion = '0.9.0'
COMMON_MARK = "com.atlassian.commonmark:commonmark:$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'
}

View 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'
}

View File

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

View File

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

View File

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

View File

@ -14,7 +14,6 @@ import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import ru.noties.debug.Debug;
import ru.noties.markwon.spans.AsyncDrawable;
import ru.noties.markwon.spans.AsyncDrawableSpan;
@ -22,7 +21,7 @@ abstract class DrawablesScheduler {
static void schedule(@NonNull final TextView textView) {
final List<Pair> list = extract(textView, true);
final List<AsyncDrawable> list = extract(textView);
if (list.size() > 0) {
textView.addOnAttachStateChangeListener(new View.OnAttachStateChangeListener() {
@Override
@ -37,22 +36,22 @@ abstract class DrawablesScheduler {
}
});
for (Pair pair : list) {
pair.drawable.setCallback2(new DrawableCallbackImpl(textView, pair.coordinatesProvider, pair.drawable.getBounds()));
for (AsyncDrawable drawable : list) {
drawable.setCallback2(new DrawableCallbackImpl(textView, drawable.getBounds()));
}
}
}
// must be called when text manually changed in TextView
static void unschedule(@NonNull TextView view) {
for (Pair pair : extract(view, false)) {
pair.drawable.setCallback2(null);
for (AsyncDrawable drawable : extract(view)) {
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 int length = cs != null
@ -74,18 +73,14 @@ abstract class DrawablesScheduler {
if (span instanceof AsyncDrawableSpan) {
final AsyncDrawableSpan asyncDrawableSpan = (AsyncDrawableSpan) span;
final CoordinatesProvider provider = coordinates
? new AsyncDrawableSpanCoordinatesProvider(asyncDrawableSpan)
: null;
list.add(new Pair(asyncDrawableSpan.getDrawable(), provider));
list.add(asyncDrawableSpan.getDrawable());
} else if (span instanceof DynamicDrawableSpan) {
// it's really not optimal thing because it stores Drawable in WeakReference...
// which is why it will be most likely already de-referenced...
final Drawable d = ((DynamicDrawableSpan) span).getDrawable();
if (d != null
&& d instanceof AsyncDrawable) {
list.add(new Pair((AsyncDrawable) d, null));
list.add((AsyncDrawable) d);
}
}
}
@ -101,21 +96,13 @@ abstract class DrawablesScheduler {
private DrawablesScheduler() {
}
private interface CoordinatesProvider {
int getX();
int getY();
}
private static class DrawableCallbackImpl implements Drawable.Callback {
private final TextView view;
private final CoordinatesProvider coordinatesProvider;
private Rect previousBounds;
DrawableCallbackImpl(TextView view, CoordinatesProvider provider, Rect initialBounds) {
DrawableCallbackImpl(TextView view, Rect initialBounds) {
this.view = view;
this.coordinatesProvider = provider;
this.previousBounds = new Rect(initialBounds);
}
@ -134,7 +121,7 @@ abstract class DrawablesScheduler {
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...
if (!previousBounds.equals(rect)) {
@ -143,29 +130,7 @@ abstract class DrawablesScheduler {
previousBounds = new Rect(rect);
} 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();
// 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);
}
}
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;
}
}
}

View File

@ -11,7 +11,7 @@ import android.view.View;
import ru.noties.markwon.spans.LinkSpan;
class LinkResolverDef implements LinkSpan.Resolver {
public class LinkResolverDef implements LinkSpan.Resolver {
@Override
public void resolve(View view, @NonNull String link) {
final Uri uri = Uri.parse(link);

View File

@ -121,7 +121,7 @@ public class SpannableConfiguration {
urlProcessor = new UrlProcessorNoOp();
}
if (htmlParser == null) {
htmlParser = SpannableHtmlParser.create(theme, asyncDrawableLoader, urlProcessor);
htmlParser = SpannableHtmlParser.create(theme, asyncDrawableLoader, urlProcessor, linkResolver);
}
return new SpannableConfiguration(this);
}

View File

@ -6,6 +6,7 @@ import android.support.annotation.Nullable;
import java.net.MalformedURLException;
import java.net.URL;
@SuppressWarnings("WeakerAccess")
public class UrlProcessorRelativeToAbsolute implements UrlProcessor {
private final URL base;

View File

@ -33,7 +33,6 @@ import org.commonmark.node.ThematicBreak;
import java.util.ArrayDeque;
import java.util.Deque;
import ru.noties.debug.Debug;
import ru.noties.markwon.SpannableConfiguration;
import ru.noties.markwon.renderer.html.SpannableHtmlParser;
import ru.noties.markwon.spans.AsyncDrawable;
@ -51,8 +50,6 @@ import ru.noties.markwon.spans.ThematicBreakSpan;
@SuppressWarnings("WeakerAccess")
public class SpannableMarkdownVisitor extends AbstractVisitor {
private static final String HTML_CONTENT = "<%1$s>%2$s</%3$s>";
private final SpannableConfiguration configuration;
private final SpannableStringBuilder builder;
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
if (length == builder.length()) {
builder.append(' '); // breakable space
builder.append('\uFFFC');
}
final Node parent = image.getParent();
@ -321,17 +318,23 @@ public class SpannableMarkdownVisitor extends AbstractVisitor {
link
)
);
// todo, maybe, if image is not inside a link, we should make it clickable, so
// user can open it in external viewer?
}
@Override
public void visit(HtmlBlock htmlBlock) {
// http://spec.commonmark.org/0.18/#html-blocks
Debug.i(htmlBlock, htmlBlock.getLiteral());
super.visit(htmlBlock);
final Spanned spanned = configuration.htmlParser().getSpanned(null, htmlBlock.getLiteral());
if (!TextUtils.isEmpty(spanned)) {
builder.append(spanned);
}
}
@Override
public void visit(HtmlInline htmlInline) {
final SpannableHtmlParser htmlParser = configuration.htmlParser();
final SpannableHtmlParser.Tag tag = htmlParser.parseTag(htmlInline.getLiteral());
@ -340,37 +343,25 @@ public class SpannableMarkdownVisitor extends AbstractVisitor {
final boolean voidTag = tag.voidTag();
if (!voidTag && tag.opening()) {
// push in stack
htmlInlineItems.push(new HtmlInlineItem(tag.name(), builder.length()));
htmlInlineItems.push(new HtmlInlineItem(tag, builder.length()));
visitChildren(htmlInline);
} else {
if (!voidTag) {
if (htmlInlineItems.size() > 0) {
final HtmlInlineItem item = htmlInlineItems.pop();
final Object span = htmlParser.handleTag(item.tag);
final int start = item.start;
final Object span = htmlParser.getSpanForTag(item.tag);
if (span != null) {
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 {
final String content = htmlInline.getLiteral();
if (!TextUtils.isEmpty(content)) {
final Spanned html = htmlParser.html(content);
final Spanned html = htmlParser.getSpanned(tag, htmlInline.getLiteral());
if (!TextUtils.isEmpty(html)) {
builder.append(html);
}
}
}
}
} else {
@ -412,10 +403,11 @@ public class SpannableMarkdownVisitor extends AbstractVisitor {
}
private static class HtmlInlineItem {
final String tag;
final SpannableHtmlParser.Tag tag;
final int start;
HtmlInlineItem(String tag, int start) {
HtmlInlineItem(SpannableHtmlParser.Tag tag, int start) {
this.tag = tag;
this.start = start;
}

View File

@ -1,10 +1,13 @@
package ru.noties.markwon.renderer.html;
import android.support.annotation.NonNull;
import ru.noties.markwon.spans.StrongEmphasisSpan;
class BoldProvider implements SpannableHtmlParser.SpanProvider {
@Override
public Object provide() {
public Object provide(@NonNull SpannableHtmlParser.Tag tag) {
return new StrongEmphasisSpan();
}
}

View File

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

View File

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

View File

@ -1,10 +1,13 @@
package ru.noties.markwon.renderer.html;
import android.support.annotation.NonNull;
import ru.noties.markwon.spans.EmphasisSpan;
class ItalicsProvider implements SpannableHtmlParser.SpanProvider {
@Override
public Object provide() {
public Object provide(@NonNull SpannableHtmlParser.Tag tag) {
return new EmphasisSpan();
}
}

View File

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

View File

@ -7,37 +7,35 @@ import android.support.annotation.Nullable;
import android.text.Html;
import android.text.Spanned;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
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.UrlProcessorNoOp;
import ru.noties.markwon.spans.AsyncDrawable;
import ru.noties.markwon.spans.LinkSpan;
import ru.noties.markwon.spans.SpannableTheme;
@SuppressWarnings("WeakerAccess")
public class SpannableHtmlParser {
// we need to handle images independently (in order to parse alt, width, height, etc)
// creates default parser
public static SpannableHtmlParser create(
@NonNull SpannableTheme theme,
@NonNull AsyncDrawable.Loader loader
) {
return builderWithDefaults(theme, loader, null)
return builderWithDefaults(theme, loader, null, null)
.build();
}
public static SpannableHtmlParser create(
@NonNull SpannableTheme theme,
@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();
}
@ -45,151 +43,133 @@ public class SpannableHtmlParser {
return new Builder();
}
public static Builder builderWithDefaults(@NonNull SpannableTheme theme) {
return builderWithDefaults(theme, null, null, null);
}
public static Builder builderWithDefaults(
@NonNull SpannableTheme theme,
@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 ItalicsProvider italicsProvider = new ItalicsProvider();
final StrikeProvider strikeProvider = new StrikeProvider();
final HtmlParser parser;
final ImageProvider imageProvider;
if (asyncDrawableLoader != null) {
parser = DefaultHtmlParser.create(new HtmlImageGetter(asyncDrawableLoader, urlProcessor), null);
imageProvider = new ImageProviderImpl(theme, asyncDrawableLoader, urlProcessor);
} else {
parser = DefaultHtmlParser.create(null, null);
imageProvider = null;
}
return new Builder()
.customTag("b", boldProvider)
.customTag("strong", boldProvider)
.customTag("i", italicsProvider)
.customTag("em", italicsProvider)
.customTag("cite", italicsProvider)
.customTag("dfn", italicsProvider)
.customTag("sup", new SuperScriptProvider(theme))
.customTag("sub", new SubScriptProvider(theme))
.customTag("u", new UnderlineProvider())
.customTag("del", strikeProvider)
.customTag("s", strikeProvider)
.customTag("strike", strikeProvider)
.parser(parser);
.simpleTag("b", boldProvider)
.simpleTag("strong", boldProvider)
.simpleTag("i", italicsProvider)
.simpleTag("em", italicsProvider)
.simpleTag("cite", italicsProvider)
.simpleTag("dfn", italicsProvider)
.simpleTag("sup", new SuperScriptProvider(theme))
.simpleTag("sub", new SubScriptProvider(theme))
.simpleTag("u", new UnderlineProvider())
.simpleTag("del", strikeProvider)
.simpleTag("s", strikeProvider)
.simpleTag("strike", strikeProvider)
.simpleTag("a", new LinkProvider(theme, urlProcessor, resolver))
.imageProvider(imageProvider);
}
// for simple tags without arguments
// <b>, <i>, etc
public interface SpanProvider {
Object provide();
Object provide(@NonNull Tag tag);
}
public interface ImageProvider {
Spanned provide(@NonNull Tag tag);
}
public interface HtmlParser {
Object[] getSpans(@NonNull String html);
// returns span for a simple content
Object getSpan(@NonNull String html);
Spanned parse(@NonNull String html);
}
private static final String LINK_START = "<a ";
private final Map<String, SpanProvider> customTags;
private final Set<String> voidTags;
private final Map<String, SpanProvider> simpleTags;
private final ImageProvider imageProvider;
private final HtmlParser parser;
private final TagParser tagParser;
private SpannableHtmlParser(Builder builder) {
this.customTags = builder.customTags;
this.voidTags = voidTags();
this.simpleTags = builder.simpleTags;
this.imageProvider = builder.imageProvider;
this.parser = builder.parser;
this.tagParser = new TagParser();
}
@Nullable
public Tag parseTag(String 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;
return tagParser.parse(html);
}
@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 SpanProvider provider = customTags.get(tag);
final SpanProvider provider = simpleTags.get(tag.name);
if (provider != null) {
out = provider.provide();
out = provider.provide(tag);
} 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;
}
@Nullable
public Object[] htmlSpans(String html) {
// todo, additional handling of: image & link
Debug.i("html: %s", html);
return parser.getSpans(html);
// if tag is NULL, then it's HtmlBlock... else just a void tag
public Spanned getSpanned(@Nullable Tag tag, String html) {
final Spanned spanned;
if (tag != null && "img".equals(tag.name) && imageProvider != null) {
spanned = imageProvider.provide(tag);
} else {
spanned = parser.parse(html);
}
// 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;
return spanned;
}
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;
public Builder customTag(@NonNull String tag, @NonNull SpanProvider provider) {
customTags.put(tag, provider);
public Builder simpleTag(@NonNull String tag, @NonNull SpanProvider provider) {
simpleTags.put(tag, provider);
return this;
}
public Builder imageProvider(ImageProvider imageProvider) {
this.imageProvider = imageProvider;
return this;
}
@ -200,7 +180,7 @@ public class SpannableHtmlParser {
public SpannableHtmlParser build() {
if (parser == null) {
parser = DefaultHtmlParser.create(null, null);
parser = DefaultHtmlParser.create();
}
return new SpannableHtmlParser(this);
}
@ -208,20 +188,34 @@ public class SpannableHtmlParser {
public static class Tag {
private final String raw;
private final String name;
private final Map<String, String> attributes;
private final boolean opening;
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.attributes = attributes;
this.opening = opening;
this.voidTag = voidTag;
}
public String raw() {
return raw;
}
public String name() {
return name;
}
@NonNull
public Map<String, String> attributes() {
return attributes;
}
public boolean opening() {
return opening;
}
@ -233,7 +227,9 @@ public class SpannableHtmlParser {
@Override
public String toString() {
return "Tag{" +
"name='" + name + '\'' +
"raw='" + raw + '\'' +
", name='" + name + '\'' +
", attributes=" + attributes +
", opening=" + opening +
", voidTag=" + voidTag +
'}';
@ -242,25 +238,20 @@ public class SpannableHtmlParser {
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;
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
parser = new Parser24(imageGetter, tagHandler);
parser = new Parser24();
} else {
parser = new ParserPre24(imageGetter, tagHandler);
parser = new ParserPre24();
}
return parser;
}
final Html.ImageGetter imageGetter;
final Html.TagHandler tagHandler;
Object getSpan(Spanned spanned) {
DefaultHtmlParser(Html.ImageGetter imageGetter, Html.TagHandler tagHandler) {
this.imageGetter = imageGetter;
this.tagHandler = tagHandler;
}
final Object out;
Object[] getSpans(Spanned spanned) {
final Object[] spans;
final int length = spanned != null ? spanned.length() : 0;
if (length == 0) {
@ -268,42 +259,42 @@ public class SpannableHtmlParser {
} else {
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")
private static class ParserPre24 extends DefaultHtmlParser {
ParserPre24(Html.ImageGetter imageGetter, Html.TagHandler tagHandler) {
super(imageGetter, tagHandler);
}
@Override
public Object[] getSpans(@NonNull String html) {
return getSpans(parse(html));
public Object getSpan(@NonNull String html) {
return getSpan(parse(html));
}
@Override
public Spanned parse(@NonNull String html) {
return Html.fromHtml(html, imageGetter, tagHandler);
return Html.fromHtml(html, null, null);
}
}
@TargetApi(Build.VERSION_CODES.N)
private static class Parser24 extends DefaultHtmlParser {
Parser24(Html.ImageGetter imageGetter, Html.TagHandler tagHandler) {
super(imageGetter, tagHandler);
}
@Override
public Object[] getSpans(@NonNull String html) {
return getSpans(parse(html));
public Object getSpan(@NonNull String html) {
return getSpan(parse(html));
}
@Override
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);
}
}
}

View File

@ -1,10 +1,11 @@
package ru.noties.markwon.renderer.html;
import android.support.annotation.NonNull;
import android.text.style.StrikethroughSpan;
class StrikeProvider implements SpannableHtmlParser.SpanProvider {
@Override
public Object provide() {
public Object provide(@NonNull SpannableHtmlParser.Tag tag) {
return new StrikethroughSpan();
}
}

View File

@ -1,5 +1,7 @@
package ru.noties.markwon.renderer.html;
import android.support.annotation.NonNull;
import ru.noties.markwon.spans.SpannableTheme;
import ru.noties.markwon.spans.SubScriptSpan;
@ -12,7 +14,7 @@ class SubScriptProvider implements SpannableHtmlParser.SpanProvider {
}
@Override
public Object provide() {
public Object provide(@NonNull SpannableHtmlParser.Tag tag) {
return new SubScriptSpan(theme);
}
}

View File

@ -1,5 +1,7 @@
package ru.noties.markwon.renderer.html;
import android.support.annotation.NonNull;
import ru.noties.markwon.spans.SpannableTheme;
import ru.noties.markwon.spans.SuperScriptSpan;
@ -12,7 +14,7 @@ class SuperScriptProvider implements SpannableHtmlParser.SpanProvider {
}
@Override
public Object provide() {
public Object provide(@NonNull SpannableHtmlParser.Tag tag) {
return new SuperScriptSpan(theme);
}
}

View File

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

View File

@ -1,11 +1,12 @@
package ru.noties.markwon.renderer.html;
import android.support.annotation.NonNull;
import android.text.style.UnderlineSpan;
class UnderlineProvider implements SpannableHtmlParser.SpanProvider {
@Override
public Object provide() {
public Object provide(@NonNull SpannableHtmlParser.Tag tag) {
return new UnderlineSpan();
}
}

View File

@ -1 +1 @@
include ':app', ':library-renderer'
include ':app', ':library-renderer', ':library-image-loader'