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 | ||||
| ``` | ||||
| 
 | ||||
| ### 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 | ||||
|  | ||||
| @ -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' | ||||
| } | ||||
|  | ||||
| @ -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" | ||||
| @ -38,19 +39,19 @@ | ||||
|                     android:scheme="https" /> | ||||
| 
 | ||||
|                 <!--<data--> | ||||
|                     <!--android:host="*"--> | ||||
|                     <!--android:scheme="http"--> | ||||
|                     <!--android:mimeType="text/markdown"/>--> | ||||
|                 <!--android:host="*"--> | ||||
|                 <!--android:scheme="http"--> | ||||
|                 <!--android:mimeType="text/markdown"/>--> | ||||
| 
 | ||||
|                 <!--<data--> | ||||
|                     <!--android:host="*"--> | ||||
|                     <!--android:scheme="file"--> | ||||
|                     <!--android:mimeType="text/markdown"/>--> | ||||
|                 <!--android:host="*"--> | ||||
|                 <!--android:scheme="file"--> | ||||
|                 <!--android:mimeType="text/markdown"/>--> | ||||
| 
 | ||||
|                 <!--<data--> | ||||
|                     <!--android:host="*"--> | ||||
|                     <!--android:scheme="https"--> | ||||
|                     <!--android:mimeType="text/markdown"/>--> | ||||
|                 <!--android:host="*"--> | ||||
|                 <!--android:scheme="https"--> | ||||
|                 <!--android:mimeType="text/markdown"/>--> | ||||
| 
 | ||||
|                 <data android:pathPattern=".*\\.markdown" /> | ||||
|                 <data android:pathPattern=".*\\.mdown" /> | ||||
|  | ||||
| @ -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(); | ||||
|  | ||||
| @ -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(); | ||||
|     } | ||||
| } | ||||
|  | ||||
| @ -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 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(); | ||||
|  | ||||
| @ -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() { | ||||
|                     loaded.apply(text); | ||||
|                     // as this call is async, we need to check again if we are cancelled | ||||
|                     if (!isCancelled()) { | ||||
|                         loaded.apply(text); | ||||
|                         task = null; | ||||
|                     } | ||||
|                 } | ||||
|             }); | ||||
|         } | ||||
|  | ||||
| @ -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); | ||||
|                 handler.post(new Runnable() { | ||||
|                     @Override | ||||
|                     public void run() { | ||||
|                         listener.onMarkdownReady(text); | ||||
|                     } | ||||
|                 }); | ||||
|                 task = null; | ||||
|                 if (!isCancelled()) { | ||||
|                     handler.post(new Runnable() { | ||||
|                         @Override | ||||
|                         public void run() { | ||||
|                             if (!isCancelled()) { | ||||
|                                 listener.onMarkdownReady(text); | ||||
|                                 task = null; | ||||
|                             } | ||||
|                         } | ||||
|                     }); | ||||
|                 } | ||||
|             } | ||||
|         }); | ||||
|     } | ||||
| @ -88,4 +86,8 @@ public class MarkdownRenderer { | ||||
|             task = null; | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     private boolean isCancelled() { | ||||
|         return task == null || task.isCancelled(); | ||||
|     } | ||||
| } | ||||
|  | ||||
| @ -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; | ||||
|  | ||||
| @ -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() { | ||||
|     } | ||||
| } | ||||
|  | ||||
| @ -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> | ||||
| 
 | ||||
|  | ||||
| @ -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' | ||||
| } | ||||
|  | ||||
							
								
								
									
										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.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(); | ||||
|                 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; | ||||
|         } | ||||
|     } | ||||
| } | ||||
|  | ||||
| @ -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); | ||||
|  | ||||
| @ -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); | ||||
|         } | ||||
|  | ||||
| @ -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; | ||||
|  | ||||
| @ -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); | ||||
|                         if (!TextUtils.isEmpty(html)) { | ||||
|                             builder.append(html); | ||||
|                         } | ||||
| 
 | ||||
|                     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; | ||||
|         } | ||||
|  | ||||
| @ -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(); | ||||
|     } | ||||
| } | ||||
|  | ||||
| @ -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; | ||||
| 
 | ||||
| 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(); | ||||
|     } | ||||
| } | ||||
|  | ||||
| @ -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.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); | ||||
|     } | ||||
| 
 | ||||
|     // 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; | ||||
|     // 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); | ||||
|         } | ||||
|         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); | ||||
|             } | ||||
|         } | ||||
|     } | ||||
|  | ||||
| @ -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(); | ||||
|     } | ||||
| } | ||||
|  | ||||
| @ -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); | ||||
|     } | ||||
| } | ||||
|  | ||||
| @ -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); | ||||
|     } | ||||
| } | ||||
|  | ||||
| @ -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; | ||||
| 
 | ||||
| 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(); | ||||
|     } | ||||
| } | ||||
|  | ||||
| @ -1 +1 @@ | ||||
| include ':app', ':library-renderer' | ||||
| include ':app', ':library-renderer', ':library-image-loader' | ||||
|  | ||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user
	 Dimitry Ivanov
						Dimitry Ivanov