Merge pull request #105 from noties/v3.0.0
# 3.0.0 * Plugins, plugins, plugins * Split basic functionality blocks into standalone modules * Maven artifacts group changed to `ru.noties.markwon` (previously had been `ru.noties`) * removed `markwon`, `markwon-image-loader`, `markwon-html-pareser-api`, `markwon-html-parser-impl`, `markwon-view` modules * new module system: `core`, `ext-latex`, `ext-strikethrough`, `ext-tables`, `ext-tasklist`, `html`, `image-gif`, `image-okhttp`, `image-svg`, `recycler`, `recycler-table`, `syntax-highlight` * Add BufferType option for Markwon configuration * Fix typo in AsyncDrawable waitingForDimensions * New tests format * `Markwon.render` returns `Spanned` instance of generic `CharSequence` * LinkMovementMethod is applied implicitly if not set on a TextView explicitly * Split code and codeBlock spans and factories * Add CustomTypefaceSpan * Add NoCopySpansFactory * Add placeholder to image loading Generally speaking there are a lot of changes. Most of them are not backwards-compatible. The main point of this release is the `Plugin` system that allows more fluent configuration and opens the possibility of extending `Markwon` with 3rd party functionality in a simple and intuitive fashion. Please refer to the [documentation web-site](https://noties.github.io/Markwon) that has information on how to start migration. The shortest excerpt of this release can be expressed like this: ```java // previous v2.x.x way Markwon.setMarkdown(textView, "**Hello there!**"); ``` ```java // 3.x.x Markwon.create(context) .setMarkdown(textView, "**Hello there!**"); ``` But there is much more to it, please visit documentation web-site to get the full picture of latest changes.
| @ -10,7 +10,7 @@ android: | ||||
|       - tools | ||||
| 
 | ||||
|       - build-tools-28.0.3 | ||||
|       - android-27 | ||||
|       - android-28 | ||||
| 
 | ||||
| branches: | ||||
|   except: | ||||
|  | ||||
							
								
								
									
										19
									
								
								README.md
									
									
									
									
									
								
							
							
						
						| @ -2,11 +2,6 @@ | ||||
| 
 | ||||
| # Markwon | ||||
| 
 | ||||
| [](http://search.maven.org/#search|ga|1|g%3A%22ru.noties%22%20AND%20a%3A%22markwon%22) | ||||
| [](http://search.maven.org/#search|ga|1|g%3A%22ru.noties%22%20AND%20a%3A%22markwon-image-loader%22) | ||||
| [](http://search.maven.org/#search|ga|1|g%3A%22ru.noties%22%20AND%20a%3A%22markwon-syntax-highlight%22) | ||||
| [](http://search.maven.org/#search|ga|1|g%3A%22ru.noties%22%20AND%20a%3A%22markwon-view%22) | ||||
| 
 | ||||
| [](https://travis-ci.org/noties/Markwon) | ||||
| 
 | ||||
| **Markwon** is a markdown library for Android. It parses markdown | ||||
| @ -32,15 +27,20 @@ features listed in [commonmark-spec] are supported | ||||
| [sample-apk]: https://github.com/noties/Markwon/releases | ||||
| 
 | ||||
| ## Installation | ||||
| 
 | ||||
|  | ||||
|  | ||||
| 
 | ||||
| ```groovy | ||||
| implementation "ru.noties:markwon:${markwonVersion}" | ||||
| implementation "ru.noties:markwon-image-loader:${markwonVersion}" // optional | ||||
| implementation "ru.noties:markwon-syntax-highlight:${markwonVersion}" // optional | ||||
| implementation "ru.noties:markwon-view:${markwonVersion}" // optional | ||||
| implementation "ru.noties.markwon:core:${markwonVersion}" | ||||
| ``` | ||||
| 
 | ||||
| Please visit [documentation] web-site for further reference | ||||
| 
 | ||||
| 
 | ||||
| > You can find previous version of Markwon in [2.x.x](https://github.com/noties/Markwon/tree/2.x.x) branch | ||||
| 
 | ||||
| 
 | ||||
| ## Supported markdown features: | ||||
| * Emphasis (`*`, `_`) | ||||
| * Strong emphasis (`**`, `__`) | ||||
| @ -55,6 +55,7 @@ Please visit [documentation] web-site for further reference | ||||
| * Code blocks | ||||
| * Tables (*with limitations*) | ||||
| * Syntax highlight | ||||
| * LaTeX formulas | ||||
| * HTML | ||||
|   * Emphasis (`<i>`, `<em>`, `<cite>`, `<dfn>`) | ||||
|   * Strong emphasis (`<b>`, `<strong>`) | ||||
|  | ||||
| @ -11,7 +11,7 @@ android { | ||||
|         targetSdkVersion config['target-sdk'] | ||||
|         versionCode 1 | ||||
|         versionName version | ||||
|         setProperty("archivesBaseName", "markwon-sample-$versionName") | ||||
|         setProperty("archivesBaseName", "markwon-$versionName") | ||||
|     } | ||||
| 
 | ||||
|     lintOptions { | ||||
| @ -28,8 +28,13 @@ android { | ||||
| 
 | ||||
| dependencies { | ||||
| 
 | ||||
|     implementation project(':markwon') | ||||
|     implementation project(':markwon-image-loader') | ||||
|     implementation project(':markwon-core') | ||||
|     implementation project(':markwon-ext-strikethrough') | ||||
|     implementation project(':markwon-ext-tables') | ||||
|     implementation project(':markwon-ext-tasklist') | ||||
|     implementation project(':markwon-html') | ||||
|     implementation project(':markwon-image-gif') | ||||
|     implementation project(':markwon-image-svg') | ||||
|     implementation project(':markwon-syntax-highlight') | ||||
| 
 | ||||
|     deps.with { | ||||
| @ -43,4 +48,4 @@ dependencies { | ||||
|         annotationProcessor it['prism4j-bundler'] | ||||
|         annotationProcessor it['dagger-compiler'] | ||||
|     } | ||||
| } | ||||
| } | ||||
| @ -9,7 +9,7 @@ import android.util.AttributeSet; | ||||
| import android.view.View; | ||||
| 
 | ||||
| import ru.noties.markwon.R; | ||||
| import ru.noties.markwon.spans.TaskListDrawable; | ||||
| import ru.noties.markwon.ext.tasklist.TaskListDrawable; | ||||
| 
 | ||||
| public class DebugCheckboxDrawableView extends View { | ||||
| 
 | ||||
|  | ||||
| @ -1,5 +1,6 @@ | ||||
| <?xml version="1.0" encoding="utf-8"?> | ||||
| <manifest xmlns:android="http://schemas.android.com/apk/res/android" | ||||
|     xmlns:tools="http://schemas.android.com/tools" | ||||
|     package="ru.noties.markwon"> | ||||
| 
 | ||||
|     <uses-permission android:name="android.permission.INTERNET" /> | ||||
| @ -10,8 +11,10 @@ | ||||
|         android:allowBackup="true" | ||||
|         android:icon="@mipmap/ic_launcher" | ||||
|         android:label="@string/app_name" | ||||
|         android:roundIcon="@mipmap/ic_launcher" | ||||
|         android:supportsRtl="true" | ||||
|         android:theme="@style/AppThemeLight"> | ||||
|         android:theme="@style/AppThemeLight" | ||||
|         tools:ignore="AllowBackup"> | ||||
| 
 | ||||
|         <activity android:name=".MainActivity"> | ||||
| 
 | ||||
| @ -38,21 +41,6 @@ | ||||
|                     android:host="*" | ||||
|                     android:scheme="https" /> | ||||
| 
 | ||||
|                 <!--<data--> | ||||
|                 <!--android:host="*"--> | ||||
|                 <!--android:scheme="http"--> | ||||
|                 <!--android:mimeType="text/markdown"/>--> | ||||
| 
 | ||||
|                 <!--<data--> | ||||
|                 <!--android:host="*"--> | ||||
|                 <!--android:scheme="file"--> | ||||
|                 <!--android:mimeType="text/markdown"/>--> | ||||
| 
 | ||||
|                 <!--<data--> | ||||
|                 <!--android:host="*"--> | ||||
|                 <!--android:scheme="https"--> | ||||
|                 <!--android:mimeType="text/markdown"/>--> | ||||
| 
 | ||||
|                 <data android:pathPattern=".*\\.markdown" /> | ||||
|                 <data android:pathPattern=".*\\.mdown" /> | ||||
|                 <data android:pathPattern=".*\\.mkdn" /> | ||||
|  | ||||
| @ -23,8 +23,8 @@ abstract class AppBarItem { | ||||
|         final TextView subtitle; | ||||
| 
 | ||||
|         Renderer(@NonNull View view, @NonNull View.OnClickListener themeChangeClicked) { | ||||
|             this.title = Views.findView(view, R.id.app_bar_title); | ||||
|             this.subtitle = Views.findView(view, R.id.app_bar_subtitle); | ||||
|             this.title = view.findViewById(R.id.app_bar_title); | ||||
|             this.subtitle = view.findViewById(R.id.app_bar_subtitle); | ||||
|             view.findViewById(R.id.app_bar_theme_changer) | ||||
|                     .setOnClickListener(themeChangeClicked); | ||||
|         } | ||||
|  | ||||
| @ -14,11 +14,6 @@ import dagger.Module; | ||||
| import dagger.Provides; | ||||
| import okhttp3.Cache; | ||||
| import okhttp3.OkHttpClient; | ||||
| import ru.noties.markwon.il.AsyncDrawableLoader; | ||||
| import ru.noties.markwon.il.GifMediaDecoder; | ||||
| import ru.noties.markwon.il.ImageMediaDecoder; | ||||
| import ru.noties.markwon.il.SvgMediaDecoder; | ||||
| import ru.noties.markwon.spans.AsyncDrawable; | ||||
| import ru.noties.markwon.syntax.Prism4jThemeDarkula; | ||||
| import ru.noties.markwon.syntax.Prism4jThemeDefault; | ||||
| import ru.noties.prism4j.Prism4j; | ||||
| @ -72,23 +67,6 @@ class AppModule { | ||||
|         return new UriProcessorImpl(); | ||||
|     } | ||||
| 
 | ||||
|     @Provides | ||||
|     AsyncDrawable.Loader asyncDrawableLoader( | ||||
|             OkHttpClient client, | ||||
|             ExecutorService executorService, | ||||
|             Resources resources) { | ||||
|         return AsyncDrawableLoader.builder() | ||||
|                 .client(client) | ||||
|                 .executorService(executorService) | ||||
|                 .resources(resources) | ||||
|                 .mediaDecoders( | ||||
|                         SvgMediaDecoder.create(resources), | ||||
|                         GifMediaDecoder.create(false), | ||||
|                         ImageMediaDecoder.create(resources) | ||||
|                 ) | ||||
|                 .build(); | ||||
|     } | ||||
| 
 | ||||
|     @Provides | ||||
|     @Singleton | ||||
|     Prism4j prism4j() { | ||||
| @ -104,12 +82,12 @@ class AppModule { | ||||
|     @Singleton | ||||
|     @Provides | ||||
|     Prism4jThemeDarkula prism4jThemeDarkula() { | ||||
|         return Prism4jThemeDarkula.create(); | ||||
|     } | ||||
| 
 | ||||
|     @Singleton | ||||
|     @Provides | ||||
|     GifProcessor gifProcessor() { | ||||
|         return GifProcessor.create(); | ||||
|         return Prism4jThemeDarkula.create(0x0Fffffff); | ||||
|     } | ||||
| // | ||||
| //    @Singleton | ||||
| //    @Provides | ||||
| //    GifProcessor gifProcessor() { | ||||
| //        return GifProcessor.create(); | ||||
| //    } | ||||
| } | ||||
|  | ||||
| @ -1,36 +0,0 @@ | ||||
| package ru.noties.markwon; | ||||
| 
 | ||||
| import android.support.annotation.NonNull; | ||||
| import android.support.annotation.Nullable; | ||||
| 
 | ||||
| import ru.noties.markwon.renderer.ImageSize; | ||||
| import ru.noties.markwon.renderer.ImageSizeResolver; | ||||
| import ru.noties.markwon.spans.AsyncDrawable; | ||||
| import ru.noties.markwon.spans.AsyncDrawableSpan; | ||||
| import ru.noties.markwon.spans.SpannableTheme; | ||||
| 
 | ||||
| public class GifAwareSpannableFactory extends SpannableFactoryDef { | ||||
| 
 | ||||
|     private final GifPlaceholder gifPlaceholder; | ||||
| 
 | ||||
|     public GifAwareSpannableFactory(@NonNull GifPlaceholder gifPlaceholder) { | ||||
|         this.gifPlaceholder = gifPlaceholder; | ||||
|     } | ||||
| 
 | ||||
|     @Nullable | ||||
|     @Override | ||||
|     public Object image(@NonNull SpannableTheme theme, @NonNull String destination, @NonNull AsyncDrawable.Loader loader, @NonNull ImageSizeResolver imageSizeResolver, @Nullable ImageSize imageSize, boolean replacementTextIsLink) { | ||||
|         return new AsyncDrawableSpan( | ||||
|                 theme, | ||||
|                 new GifAwareAsyncDrawable( | ||||
|                         gifPlaceholder, | ||||
|                         destination, | ||||
|                         loader, | ||||
|                         imageSizeResolver, | ||||
|                         imageSize | ||||
|                 ), | ||||
|                 AsyncDrawableSpan.ALIGN_BOTTOM, | ||||
|                 replacementTextIsLink | ||||
|         ); | ||||
|     } | ||||
| } | ||||
| @ -6,6 +6,7 @@ import android.net.Uri; | ||||
| import android.os.Bundle; | ||||
| import android.support.annotation.NonNull; | ||||
| import android.support.annotation.Nullable; | ||||
| import android.text.Spanned; | ||||
| import android.view.View; | ||||
| import android.widget.TextView; | ||||
| 
 | ||||
| @ -27,9 +28,6 @@ public class MainActivity extends Activity { | ||||
|     @Inject | ||||
|     UriProcessor uriProcessor; | ||||
| 
 | ||||
|     @Inject | ||||
|     GifProcessor gifProcessor; | ||||
| 
 | ||||
|     @Override | ||||
|     protected void onCreate(final Bundle savedInstanceState) { | ||||
|         super.onCreate(savedInstanceState); | ||||
| @ -40,9 +38,6 @@ public class MainActivity extends Activity { | ||||
| 
 | ||||
|         themes.apply(this); | ||||
| 
 | ||||
|         // how can we obtain SpannableConfiguration after theme was applied? | ||||
|         // as we inject `themes` we won't be able to inject configuration, as it requires theme set | ||||
| 
 | ||||
|         setContentView(R.layout.activity_main); | ||||
| 
 | ||||
|         // we process additionally github urls, as if url has in path `blob`, we won't receive | ||||
| @ -58,7 +53,7 @@ public class MainActivity extends Activity { | ||||
|             } | ||||
|         }); | ||||
| 
 | ||||
|         final TextView textView = Views.findView(this, R.id.text); | ||||
|         final TextView textView = findViewById(R.id.text); | ||||
|         final View progress = findViewById(R.id.progress); | ||||
| 
 | ||||
|         appBarRenderer.render(appBarState()); | ||||
| @ -68,11 +63,9 @@ public class MainActivity extends Activity { | ||||
|             public void apply(final String text) { | ||||
|                 markdownRenderer.render(MainActivity.this, themes.isLight(), uri(), text, new MarkdownRenderer.MarkdownReadyListener() { | ||||
|                     @Override | ||||
|                     public void onMarkdownReady(CharSequence markdown) { | ||||
|                     public void onMarkdownReady(@NonNull Markwon markwon, Spanned markdown) { | ||||
| 
 | ||||
|                         Markwon.setText(textView, markdown); | ||||
| 
 | ||||
|                         gifProcessor.process(textView); | ||||
|                         markwon.setParsedMarkdown(textView, markdown); | ||||
| 
 | ||||
|                         Views.setVisible(progress, false); | ||||
|                     } | ||||
|  | ||||
| @ -6,6 +6,7 @@ import android.os.Handler; | ||||
| import android.os.SystemClock; | ||||
| import android.support.annotation.NonNull; | ||||
| import android.support.annotation.Nullable; | ||||
| import android.text.Spanned; | ||||
| 
 | ||||
| import java.util.concurrent.ExecutorService; | ||||
| import java.util.concurrent.Future; | ||||
| @ -13,24 +14,30 @@ import java.util.concurrent.Future; | ||||
| import javax.inject.Inject; | ||||
| 
 | ||||
| import ru.noties.debug.Debug; | ||||
| import ru.noties.markwon.spans.AsyncDrawable; | ||||
| import ru.noties.markwon.spans.SpannableTheme; | ||||
| import ru.noties.markwon.syntax.Prism4jSyntaxHighlight; | ||||
| import ru.noties.markwon.core.CorePlugin; | ||||
| import ru.noties.markwon.ext.strikethrough.StrikethroughPlugin; | ||||
| import ru.noties.markwon.ext.tables.TablePlugin; | ||||
| import ru.noties.markwon.ext.tasklist.TaskListPlugin; | ||||
| import ru.noties.markwon.gif.GifAwarePlugin; | ||||
| import ru.noties.markwon.html.HtmlPlugin; | ||||
| import ru.noties.markwon.image.ImagesPlugin; | ||||
| import ru.noties.markwon.image.gif.GifPlugin; | ||||
| import ru.noties.markwon.image.svg.SvgPlugin; | ||||
| import ru.noties.markwon.syntax.Prism4jTheme; | ||||
| import ru.noties.markwon.syntax.Prism4jThemeDarkula; | ||||
| import ru.noties.markwon.syntax.Prism4jThemeDefault; | ||||
| import ru.noties.markwon.syntax.SyntaxHighlightPlugin; | ||||
| import ru.noties.markwon.urlprocessor.UrlProcessor; | ||||
| import ru.noties.markwon.urlprocessor.UrlProcessorRelativeToAbsolute; | ||||
| import ru.noties.prism4j.Prism4j; | ||||
| 
 | ||||
| @ActivityScope | ||||
| public class MarkdownRenderer { | ||||
| 
 | ||||
|     interface MarkdownReadyListener { | ||||
|         void onMarkdownReady(CharSequence markdown); | ||||
|         void onMarkdownReady(@NonNull Markwon markwon, Spanned markdown); | ||||
|     } | ||||
| 
 | ||||
|     @Inject | ||||
|     AsyncDrawable.Loader loader; | ||||
| 
 | ||||
|     @Inject | ||||
|     ExecutorService service; | ||||
| 
 | ||||
| @ -64,9 +71,17 @@ public class MarkdownRenderer { | ||||
|         cancel(); | ||||
| 
 | ||||
|         task = service.submit(new Runnable() { | ||||
| 
 | ||||
|             @Override | ||||
|             public void run() { | ||||
|                 try { | ||||
|                     execute(); | ||||
|                 } catch (Throwable t) { | ||||
|                     Debug.e(t); | ||||
|                 } | ||||
|             } | ||||
| 
 | ||||
|             private void execute() { | ||||
|                 final UrlProcessor urlProcessor; | ||||
|                 if (uri == null) { | ||||
|                     urlProcessor = new UrlProcessorInitialReadme(); | ||||
| @ -78,29 +93,28 @@ public class MarkdownRenderer { | ||||
|                         ? prism4jThemeDefault | ||||
|                         : prism4JThemeDarkula; | ||||
| 
 | ||||
|                 final int background = isLightTheme | ||||
|                         ? prism4jTheme.background() | ||||
|                         : 0x0Fffffff; | ||||
| 
 | ||||
|                 final GifPlaceholder gifPlaceholder = new GifPlaceholder( | ||||
|                         context.getResources().getDrawable(R.drawable.ic_play_circle_filled_18dp_white), | ||||
|                         0x20000000 | ||||
|                 ); | ||||
| 
 | ||||
|                 final SpannableConfiguration configuration = SpannableConfiguration.builder(context) | ||||
|                         .asyncDrawableLoader(loader) | ||||
|                         .urlProcessor(urlProcessor) | ||||
|                         .syntaxHighlight(Prism4jSyntaxHighlight.create(prism4j, prism4jTheme)) | ||||
|                         .theme(SpannableTheme.builderWithDefaults(context) | ||||
|                                 .codeBackgroundColor(background) | ||||
|                                 .codeTextColor(prism4jTheme.textColor()) | ||||
|                                 .build()) | ||||
|                         .factory(new GifAwareSpannableFactory(gifPlaceholder)) | ||||
|                 final Markwon markwon = Markwon.builder(context) | ||||
|                         .usePlugin(CorePlugin.create()) | ||||
|                         .usePlugin(ImagesPlugin.createWithAssets(context)) | ||||
|                         .usePlugin(SvgPlugin.create(context.getResources())) | ||||
|                         .usePlugin(GifPlugin.create(false)) | ||||
|                         .usePlugin(SyntaxHighlightPlugin.create(prism4j, prism4jTheme)) | ||||
|                         .usePlugin(GifAwarePlugin.create(context)) | ||||
|                         .usePlugin(TablePlugin.create(context)) | ||||
|                         .usePlugin(TaskListPlugin.create(context)) | ||||
|                         .usePlugin(StrikethroughPlugin.create()) | ||||
|                         .usePlugin(HtmlPlugin.create()) | ||||
|                         .usePlugin(new AbstractMarkwonPlugin() { | ||||
|                             @Override | ||||
|                             public void configureConfiguration(@NonNull MarkwonConfiguration.Builder builder) { | ||||
|                                 builder.urlProcessor(urlProcessor); | ||||
|                             } | ||||
|                         }) | ||||
|                         .build(); | ||||
| 
 | ||||
|                 final long start = SystemClock.uptimeMillis(); | ||||
| 
 | ||||
|                 final CharSequence text = Markwon.markdown(configuration, markdown); | ||||
|                 final Spanned text = markwon.toMarkdown(markdown); | ||||
| 
 | ||||
|                 final long end = SystemClock.uptimeMillis(); | ||||
| 
 | ||||
| @ -111,7 +125,7 @@ public class MarkdownRenderer { | ||||
|                         @Override | ||||
|                         public void run() { | ||||
|                             if (!isCancelled()) { | ||||
|                                 listener.onMarkdownReady(text); | ||||
|                                 listener.onMarkdownReady(markwon, text); | ||||
|                                 task = null; | ||||
|                             } | ||||
|                         } | ||||
|  | ||||
| @ -4,6 +4,9 @@ import android.net.Uri; | ||||
| import android.support.annotation.NonNull; | ||||
| import android.text.TextUtils; | ||||
| 
 | ||||
| import ru.noties.markwon.urlprocessor.UrlProcessor; | ||||
| import ru.noties.markwon.urlprocessor.UrlProcessorRelativeToAbsolute; | ||||
| 
 | ||||
| class UrlProcessorInitialReadme implements UrlProcessor { | ||||
| 
 | ||||
|     private static final String GITHUB_BASE = "https://github.com/noties/Markwon/raw/master/"; | ||||
|  | ||||
| @ -1,7 +1,5 @@ | ||||
| package ru.noties.markwon; | ||||
| 
 | ||||
| import android.app.Activity; | ||||
| import android.support.annotation.IdRes; | ||||
| import android.support.annotation.IntDef; | ||||
| import android.support.annotation.NonNull; | ||||
| import android.view.View; | ||||
| @ -13,16 +11,6 @@ public abstract class Views { | ||||
|     @interface NotVisible { | ||||
|     } | ||||
| 
 | ||||
|     public static <V extends View> V findView(@NonNull View view, @IdRes int id) { | ||||
|         //noinspection unchecked | ||||
|         return (V) view.findViewById(id); | ||||
|     } | ||||
| 
 | ||||
|     public static <V extends View> V findView(@NonNull Activity activity, @IdRes int id) { | ||||
|         //noinspection unchecked | ||||
|         return (V) activity.findViewById(id); | ||||
|     } | ||||
| 
 | ||||
|     public static void setVisible(@NonNull View view, boolean visible) { | ||||
|         setVisible(view, visible, View.GONE); | ||||
|     } | ||||
|  | ||||
| @ -1,4 +1,4 @@ | ||||
| package ru.noties.markwon; | ||||
| package ru.noties.markwon.gif; | ||||
| 
 | ||||
| import android.graphics.Canvas; | ||||
| import android.graphics.drawable.Drawable; | ||||
| @ -6,9 +6,10 @@ import android.support.annotation.NonNull; | ||||
| import android.support.annotation.Nullable; | ||||
| 
 | ||||
| import pl.droidsonroids.gif.GifDrawable; | ||||
| import ru.noties.markwon.renderer.ImageSize; | ||||
| import ru.noties.markwon.renderer.ImageSizeResolver; | ||||
| import ru.noties.markwon.spans.AsyncDrawable; | ||||
| import ru.noties.markwon.image.AsyncDrawableLoader; | ||||
| import ru.noties.markwon.image.ImageSize; | ||||
| import ru.noties.markwon.image.ImageSizeResolver; | ||||
| import ru.noties.markwon.image.AsyncDrawable; | ||||
| 
 | ||||
| public class GifAwareAsyncDrawable extends AsyncDrawable { | ||||
| 
 | ||||
| @ -23,7 +24,7 @@ public class GifAwareAsyncDrawable extends AsyncDrawable { | ||||
|     public GifAwareAsyncDrawable( | ||||
|             @NonNull Drawable gifPlaceholder, | ||||
|             @NonNull String destination, | ||||
|             @NonNull Loader loader, | ||||
|             @NonNull AsyncDrawableLoader loader, | ||||
|             @Nullable ImageSizeResolver imageSizeResolver, | ||||
|             @Nullable ImageSize imageSize) { | ||||
|         super(destination, loader, imageSizeResolver, imageSize); | ||||
							
								
								
									
										72
									
								
								app/src/main/java/ru/noties/markwon/gif/GifAwarePlugin.java
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @ -0,0 +1,72 @@ | ||||
| package ru.noties.markwon.gif; | ||||
| 
 | ||||
| import android.content.Context; | ||||
| import android.support.annotation.NonNull; | ||||
| import android.widget.TextView; | ||||
| 
 | ||||
| import org.commonmark.node.Image; | ||||
| 
 | ||||
| import ru.noties.markwon.AbstractMarkwonPlugin; | ||||
| import ru.noties.markwon.MarkwonConfiguration; | ||||
| import ru.noties.markwon.MarkwonSpansFactory; | ||||
| import ru.noties.markwon.R; | ||||
| import ru.noties.markwon.RenderProps; | ||||
| import ru.noties.markwon.SpanFactory; | ||||
| import ru.noties.markwon.image.AsyncDrawableSpan; | ||||
| import ru.noties.markwon.image.ImageProps; | ||||
| import ru.noties.markwon.image.ImagesPlugin; | ||||
| import ru.noties.markwon.priority.Priority; | ||||
| 
 | ||||
| public class GifAwarePlugin extends AbstractMarkwonPlugin { | ||||
| 
 | ||||
|     @NonNull | ||||
|     public static GifAwarePlugin create(@NonNull Context context) { | ||||
|         return new GifAwarePlugin(context); | ||||
|     } | ||||
| 
 | ||||
|     private final Context context; | ||||
|     private final GifProcessor processor; | ||||
| 
 | ||||
|     GifAwarePlugin(@NonNull Context context) { | ||||
|         this.context = context; | ||||
|         this.processor = GifProcessor.create(); | ||||
|     } | ||||
| 
 | ||||
|     @Override | ||||
|     public void configureSpansFactory(@NonNull MarkwonSpansFactory.Builder builder) { | ||||
| 
 | ||||
|         final GifPlaceholder gifPlaceholder = new GifPlaceholder( | ||||
|                 context.getResources().getDrawable(R.drawable.ic_play_circle_filled_18dp_white), | ||||
|                 0x20000000 | ||||
|         ); | ||||
| 
 | ||||
|         builder.setFactory(Image.class, new SpanFactory() { | ||||
|             @Override | ||||
|             public Object getSpans(@NonNull MarkwonConfiguration configuration, @NonNull RenderProps props) { | ||||
|                 return new AsyncDrawableSpan( | ||||
|                         configuration.theme(), | ||||
|                         new GifAwareAsyncDrawable( | ||||
|                                 gifPlaceholder, | ||||
|                                 ImageProps.DESTINATION.require(props), | ||||
|                                 configuration.asyncDrawableLoader(), | ||||
|                                 configuration.imageSizeResolver(), | ||||
|                                 ImageProps.IMAGE_SIZE.get(props) | ||||
|                         ), | ||||
|                         AsyncDrawableSpan.ALIGN_BOTTOM, | ||||
|                         ImageProps.REPLACEMENT_TEXT_IS_LINK.get(props, false) | ||||
|                 ); | ||||
|             } | ||||
|         }); | ||||
|     } | ||||
| 
 | ||||
|     @NonNull | ||||
|     @Override | ||||
|     public Priority priority() { | ||||
|         return Priority.after(ImagesPlugin.class); | ||||
|     } | ||||
| 
 | ||||
|     @Override | ||||
|     public void afterSetText(@NonNull TextView textView) { | ||||
|         processor.process(textView); | ||||
|     } | ||||
| } | ||||
| @ -1,4 +1,4 @@ | ||||
| package ru.noties.markwon; | ||||
| package ru.noties.markwon.gif; | ||||
| 
 | ||||
| import android.graphics.Canvas; | ||||
| import android.graphics.ColorFilter; | ||||
| @ -1,4 +1,4 @@ | ||||
| package ru.noties.markwon; | ||||
| package ru.noties.markwon.gif; | ||||
| 
 | ||||
| import android.support.annotation.NonNull; | ||||
| import android.support.annotation.Nullable; | ||||
| @ -9,7 +9,7 @@ import android.view.View; | ||||
| import android.widget.TextView; | ||||
| 
 | ||||
| import pl.droidsonroids.gif.GifDrawable; | ||||
| import ru.noties.markwon.spans.AsyncDrawableSpan; | ||||
| import ru.noties.markwon.image.AsyncDrawableSpan; | ||||
| 
 | ||||
| public abstract class GifProcessor { | ||||
| 
 | ||||
| @ -31,6 +31,7 @@ public abstract class GifProcessor { | ||||
|             // if not we apply onGifListener | ||||
| 
 | ||||
|             final Spannable spannable = spannable(textView); | ||||
| 
 | ||||
|             if (spannable == null) { | ||||
|                 return; | ||||
|             } | ||||
| @ -89,6 +90,7 @@ public abstract class GifProcessor { | ||||
|             // as with each `setText()` new spannable is created and keeping reference | ||||
|             // to an older one won't affect textView | ||||
|             final Spannable spannable = spannable(textView); | ||||
| 
 | ||||
|             if (spannable == null) { | ||||
|                 return; | ||||
|             } | ||||
| @ -113,12 +115,13 @@ public abstract class GifProcessor { | ||||
|             } | ||||
| 
 | ||||
|             @Override | ||||
|             public void onClick(View widget) { | ||||
|             public void onClick(@NonNull View widget) { | ||||
|                 if (gifDrawable.isPlaying()) { | ||||
|                     gifDrawable.pause(); | ||||
|                 } else { | ||||
|                     gifDrawable.start(); | ||||
|                 } | ||||
|                 widget.invalidate(); | ||||
|             } | ||||
|         } | ||||
|     } | ||||
							
								
								
									
										24
									
								
								app/src/main/res/drawable-v26/ic_launcher_background.xml
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @ -0,0 +1,24 @@ | ||||
| <vector xmlns:android="http://schemas.android.com/apk/res/android" | ||||
|         android:width="108dp" | ||||
|         android:height="108dp" | ||||
|         android:viewportWidth="512" | ||||
|         android:viewportHeight="512"> | ||||
|   <path | ||||
|       android:pathData="M0,0h512v256h-512z" | ||||
|       android:strokeAlpha="0.94117647" | ||||
|       android:strokeWidth="0.40000001" | ||||
|       android:fillColor="#d7d7d7" | ||||
|       android:strokeColor="#00000000" | ||||
|       android:fillType="nonZero" | ||||
|       android:fillAlpha="1" | ||||
|       android:strokeLineCap="butt"/> | ||||
|   <path | ||||
|       android:pathData="M0,256h512v256h-512z" | ||||
|       android:strokeAlpha="0.94117647" | ||||
|       android:strokeWidth="0.40000001" | ||||
|       android:fillColor="#eeeeee" | ||||
|       android:strokeColor="#00000000" | ||||
|       android:fillType="nonZero" | ||||
|       android:fillAlpha="1" | ||||
|       android:strokeLineCap="butt"/> | ||||
| </vector> | ||||
							
								
								
									
										24
									
								
								app/src/main/res/drawable/ic_launcher_background.xml
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @ -0,0 +1,24 @@ | ||||
| <vector xmlns:android="http://schemas.android.com/apk/res/android" | ||||
|         android:width="108dp" | ||||
|         android:height="108dp" | ||||
|         android:viewportWidth="512" | ||||
|         android:viewportHeight="512"> | ||||
|   <path | ||||
|       android:pathData="M0,0h512v256h-512z" | ||||
|       android:strokeAlpha="0.94117647" | ||||
|       android:strokeWidth="0.40000001" | ||||
|       android:fillColor="#d7d7d7" | ||||
|       android:strokeColor="#00000000" | ||||
|       android:fillType="nonZero" | ||||
|       android:fillAlpha="1" | ||||
|       android:strokeLineCap="butt"/> | ||||
|   <path | ||||
|       android:pathData="M0,256h512v256h-512z" | ||||
|       android:strokeAlpha="0.94117647" | ||||
|       android:strokeWidth="0.40000001" | ||||
|       android:fillColor="#eeeeee" | ||||
|       android:strokeColor="#00000000" | ||||
|       android:fillType="nonZero" | ||||
|       android:fillAlpha="1" | ||||
|       android:strokeLineCap="butt"/> | ||||
| </vector> | ||||
| @ -1,5 +1,5 @@ | ||||
| <?xml version="1.0" encoding="utf-8"?> | ||||
| <adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android"> | ||||
|     <background android:drawable="@drawable/ic_launcher_background" /> | ||||
|     <foreground android:drawable="@drawable/ic_launcher_foreground" /> | ||||
|     <background android:drawable="@drawable/ic_launcher_background"/> | ||||
|     <foreground android:drawable="@mipmap/ic_launcher_foreground"/> | ||||
| </adaptive-icon> | ||||
							
								
								
									
										
											BIN
										
									
								
								app/src/main/res/mipmap-hdpi-v26/ic_launcher_foreground.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| After Width: | Height: | Size: 3.3 KiB | 
| Before Width: | Height: | Size: 2.5 KiB After Width: | Height: | Size: 2.2 KiB | 
| Before Width: | Height: | Size: 1.3 KiB | 
							
								
								
									
										
											BIN
										
									
								
								app/src/main/res/mipmap-xhdpi-v26/ic_launcher_foreground.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| After Width: | Height: | Size: 4.6 KiB | 
| Before Width: | Height: | Size: 3.1 KiB After Width: | Height: | Size: 3.1 KiB | 
							
								
								
									
										
											BIN
										
									
								
								app/src/main/res/mipmap-xxhdpi-v26/ic_launcher_foreground.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| After Width: | Height: | Size: 7.6 KiB | 
| Before Width: | Height: | Size: 5.8 KiB After Width: | Height: | Size: 4.8 KiB | 
							
								
								
									
										
											BIN
										
									
								
								app/src/main/res/mipmap-xxxhdpi-v26/ic_launcher_foreground.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| After Width: | Height: | Size: 11 KiB | 
| Before Width: | Height: | Size: 7.3 KiB After Width: | Height: | Size: 6.5 KiB | 
							
								
								
									
										97
									
								
								art/markwon-icon-foreground.svg
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @ -0,0 +1,97 @@ | ||||
| <?xml version="1.0" encoding="UTF-8" standalone="no"?> | ||||
| <!-- Created with Inkscape (http://www.inkscape.org/) --> | ||||
| 
 | ||||
| <svg | ||||
|    xmlns:dc="http://purl.org/dc/elements/1.1/" | ||||
|    xmlns:cc="http://creativecommons.org/ns#" | ||||
|    xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" | ||||
|    xmlns:svg="http://www.w3.org/2000/svg" | ||||
|    xmlns="http://www.w3.org/2000/svg" | ||||
|    xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd" | ||||
|    xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape" | ||||
|    width="512" | ||||
|    height="512" | ||||
|    viewBox="0 0 512.00001 512.00001" | ||||
|    id="svg2" | ||||
|    version="1.1" | ||||
|    inkscape:version="0.91 r13725" | ||||
|    inkscape:export-filename="/Users/di/text4169.png" | ||||
|    inkscape:export-xdpi="89.93" | ||||
|    inkscape:export-ydpi="89.93" | ||||
|    sodipodi:docname="markwon-icon-foreground.svg"> | ||||
|   <defs | ||||
|      id="defs4" /> | ||||
|   <sodipodi:namedview | ||||
|      id="base" | ||||
|      pagecolor="#ffffff" | ||||
|      bordercolor="#666666" | ||||
|      borderopacity="1.0" | ||||
|      inkscape:pageopacity="0.0" | ||||
|      inkscape:pageshadow="2" | ||||
|      inkscape:zoom="0.92578125" | ||||
|      inkscape:cx="163.7657" | ||||
|      inkscape:cy="186.451" | ||||
|      inkscape:document-units="px" | ||||
|      inkscape:current-layer="layer1" | ||||
|      showgrid="false" | ||||
|      units="px" | ||||
|      inkscape:window-width="1442" | ||||
|      inkscape:window-height="788" | ||||
|      inkscape:window-x="-1" | ||||
|      inkscape:window-y="0" | ||||
|      inkscape:window-maximized="0" /> | ||||
|   <metadata | ||||
|      id="metadata7"> | ||||
|     <rdf:RDF> | ||||
|       <cc:Work | ||||
|          rdf:about=""> | ||||
|         <dc:format>image/svg+xml</dc:format> | ||||
|         <dc:type | ||||
|            rdf:resource="http://purl.org/dc/dcmitype/StillImage" /> | ||||
|         <dc:title></dc:title> | ||||
|       </cc:Work> | ||||
|     </rdf:RDF> | ||||
|   </metadata> | ||||
|   <g | ||||
|      inkscape:label="Layer 1" | ||||
|      inkscape:groupmode="layer" | ||||
|      id="layer1" | ||||
|      transform="translate(0,-540.36216)"> | ||||
|     <text | ||||
|        xml:space="preserve" | ||||
|        style="font-style:normal;font-weight:normal;font-size:34.96873856px;line-height:125%;font-family:sans-serif;letter-spacing:0px;word-spacing:0px;fill:#000000;fill-opacity:1;stroke:none;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" | ||||
|        x="106.24741" | ||||
|        y="908.6958" | ||||
|        id="text4136" | ||||
|        sodipodi:linespacing="125%"><tspan | ||||
|          sodipodi:role="line" | ||||
|          id="tspan4138" | ||||
|          x="106.24741" | ||||
|          y="908.6958" | ||||
|          style="font-style:normal;font-variant:normal;font-weight:bold;font-stretch:normal;font-size:314.71862793px;font-family:'Noto Serif';-inkscape-font-specification:'Noto Serif Bold'">M</tspan></text> | ||||
|     <text | ||||
|        xml:space="preserve" | ||||
|        style="font-style:normal;font-weight:normal;font-size:65.1031189px;line-height:125%;font-family:sans-serif;letter-spacing:0px;word-spacing:0px;fill:#666666;fill-opacity:1;stroke:none;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1;" | ||||
|        x="109.9856" | ||||
|        y="780.45221" | ||||
|        id="text4140" | ||||
|        sodipodi:linespacing="125%"><tspan | ||||
|          sodipodi:role="line" | ||||
|          id="tspan4142" | ||||
|          x="109.9856" | ||||
|          y="780.45221" | ||||
|          style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:293.75px;font-family:'Noto Serif';-inkscape-font-specification:'Noto Serif';fill:#666666;">**</tspan></text> | ||||
|     <text | ||||
|        xml:space="preserve" | ||||
|        style="font-style:normal;font-weight:normal;font-size:40.77807617px;line-height:125%;font-family:sans-serif;letter-spacing:0px;word-spacing:0px;fill:#666666;fill-opacity:1;stroke:none;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1;" | ||||
|        x="109.9856" | ||||
|        y="1150.7955" | ||||
|        id="text4169" | ||||
|        sodipodi:linespacing="125%"><tspan | ||||
|          sodipodi:role="line" | ||||
|          id="tspan4171" | ||||
|          x="109.9856" | ||||
|          y="1150.7955" | ||||
|          style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:293.75px;font-family:'Noto Serif';-inkscape-font-specification:'Noto Serif';fill:#666666;">**</tspan></text> | ||||
|   </g> | ||||
| </svg> | ||||
| After Width: | Height: | Size: 3.8 KiB | 
							
								
								
									
										97
									
								
								art/sample-icon-foreground.svg
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @ -0,0 +1,97 @@ | ||||
| <?xml version="1.0" encoding="UTF-8" standalone="no"?> | ||||
| <!-- Created with Inkscape (http://www.inkscape.org/) --> | ||||
| 
 | ||||
| <svg | ||||
|    xmlns:dc="http://purl.org/dc/elements/1.1/" | ||||
|    xmlns:cc="http://creativecommons.org/ns#" | ||||
|    xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" | ||||
|    xmlns:svg="http://www.w3.org/2000/svg" | ||||
|    xmlns="http://www.w3.org/2000/svg" | ||||
|    xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd" | ||||
|    xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape" | ||||
|    width="512" | ||||
|    height="512" | ||||
|    viewBox="0 0 512.00001 512.00001" | ||||
|    id="svg2" | ||||
|    version="1.1" | ||||
|    inkscape:version="0.91 r13725" | ||||
|    inkscape:export-filename="/Users/di/text4169.png" | ||||
|    inkscape:export-xdpi="89.93" | ||||
|    inkscape:export-ydpi="89.93" | ||||
|    sodipodi:docname="sample-icon-foreground.svg"> | ||||
|   <defs | ||||
|      id="defs4" /> | ||||
|   <sodipodi:namedview | ||||
|      id="base" | ||||
|      pagecolor="#ffffff" | ||||
|      bordercolor="#666666" | ||||
|      borderopacity="1.0" | ||||
|      inkscape:pageopacity="0.0" | ||||
|      inkscape:pageshadow="2" | ||||
|      inkscape:zoom="0.92578125" | ||||
|      inkscape:cx="163.7657" | ||||
|      inkscape:cy="186.451" | ||||
|      inkscape:document-units="px" | ||||
|      inkscape:current-layer="layer1" | ||||
|      showgrid="false" | ||||
|      units="px" | ||||
|      inkscape:window-width="1442" | ||||
|      inkscape:window-height="788" | ||||
|      inkscape:window-x="-1" | ||||
|      inkscape:window-y="0" | ||||
|      inkscape:window-maximized="0" /> | ||||
|   <metadata | ||||
|      id="metadata7"> | ||||
|     <rdf:RDF> | ||||
|       <cc:Work | ||||
|          rdf:about=""> | ||||
|         <dc:format>image/svg+xml</dc:format> | ||||
|         <dc:type | ||||
|            rdf:resource="http://purl.org/dc/dcmitype/StillImage" /> | ||||
|         <dc:title></dc:title> | ||||
|       </cc:Work> | ||||
|     </rdf:RDF> | ||||
|   </metadata> | ||||
|   <g | ||||
|      inkscape:label="Layer 1" | ||||
|      inkscape:groupmode="layer" | ||||
|      id="layer1" | ||||
|      transform="translate(0,-540.36216)"> | ||||
|     <text | ||||
|        xml:space="preserve" | ||||
|        style="font-style:normal;font-weight:normal;font-size:34.96873856px;line-height:125%;font-family:sans-serif;letter-spacing:0px;word-spacing:0px;fill:#ffffff;fill-opacity:1;stroke:none;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1;" | ||||
|        x="106.24741" | ||||
|        y="908.6958" | ||||
|        id="text4136" | ||||
|        sodipodi:linespacing="125%"><tspan | ||||
|          sodipodi:role="line" | ||||
|          id="tspan4138" | ||||
|          x="106.24741" | ||||
|          y="908.6958" | ||||
|          style="font-style:normal;font-variant:normal;font-weight:bold;font-stretch:normal;font-size:314.71862793px;font-family:'Noto Serif';-inkscape-font-specification:'Noto Serif Bold';fill:#ffffff;">M</tspan></text> | ||||
|     <text | ||||
|        xml:space="preserve" | ||||
|        style="font-style:normal;font-weight:normal;font-size:65.1031189px;line-height:125%;font-family:sans-serif;letter-spacing:0px;word-spacing:0px;fill:#999999;fill-opacity:1;stroke:none;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1;" | ||||
|        x="109.9856" | ||||
|        y="780.45221" | ||||
|        id="text4140" | ||||
|        sodipodi:linespacing="125%"><tspan | ||||
|          sodipodi:role="line" | ||||
|          id="tspan4142" | ||||
|          x="109.9856" | ||||
|          y="780.45221" | ||||
|          style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:293.75px;font-family:'Noto Serif';-inkscape-font-specification:'Noto Serif';fill:#999999;">**</tspan></text> | ||||
|     <text | ||||
|        xml:space="preserve" | ||||
|        style="font-style:normal;font-weight:normal;font-size:40.77807617px;line-height:125%;font-family:sans-serif;letter-spacing:0px;word-spacing:0px;fill:#999999;fill-opacity:1;stroke:none;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1;" | ||||
|        x="109.9856" | ||||
|        y="1150.7955" | ||||
|        id="text4169" | ||||
|        sodipodi:linespacing="125%"><tspan | ||||
|          sodipodi:role="line" | ||||
|          id="tspan4171" | ||||
|          x="109.9856" | ||||
|          y="1150.7955" | ||||
|          style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:293.75px;font-family:'Noto Serif';-inkscape-font-specification:'Noto Serif';fill:#999999;">**</tspan></text> | ||||
|   </g> | ||||
| </svg> | ||||
| After Width: | Height: | Size: 3.9 KiB | 
							
								
								
									
										28
									
								
								build.gradle
									
									
									
									
									
								
							
							
						
						| @ -4,7 +4,7 @@ buildscript { | ||||
|         jcenter() | ||||
|     } | ||||
|     dependencies { | ||||
|         classpath 'com.android.tools.build:gradle:3.2.1' | ||||
|         classpath 'com.android.tools.build:gradle:3.3.2' | ||||
|         classpath 'com.github.ben-manes:gradle-versions-plugin:0.20.0' | ||||
|     } | ||||
| } | ||||
| @ -19,6 +19,10 @@ allprojects { | ||||
|     } | ||||
|     version = VERSION_NAME | ||||
|     group = GROUP | ||||
| 
 | ||||
|     tasks.withType(Javadoc) { | ||||
|         options.addStringOption('Xdoclint:none', '-quiet') | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| task clean(type: Delete) { | ||||
| @ -43,27 +47,30 @@ ext { | ||||
|     // NB, updating build-tools or compile-sdk will require updating Travis config (.travis.yml) | ||||
|     config = [ | ||||
|             'build-tools'    : '28.0.3', | ||||
|             'compile-sdk'    : 27, | ||||
|             'target-sdk'     : 27, | ||||
|             'compile-sdk'    : 28, | ||||
|             'target-sdk'     : 28, | ||||
|             'min-sdk'        : 16, | ||||
|             'push-aar-gradle': 'https://raw.githubusercontent.com/noties/gradle-mvn-push/master/gradle-mvn-push-aar.gradle' | ||||
|     ] | ||||
| 
 | ||||
|     final def supportVersion = '27.1.1' | ||||
|     final def supportVersion = '28.0.0' | ||||
|     final def commonMarkVersion = '0.12.1' | ||||
|     final def daggerVersion = '2.10' | ||||
| 
 | ||||
|     deps = [ | ||||
|             'support-annotations'     : "com.android.support:support-annotations:$supportVersion", | ||||
|             'support-app-compat'      : "com.android.support:appcompat-v7:$supportVersion", | ||||
|             'support-recycler-view'   : "com.android.support:recyclerview-v7:$supportVersion", | ||||
|             'commonmark'              : "com.atlassian.commonmark:commonmark:$commonMarkVersion", | ||||
|             'commonmark-strikethrough': "com.atlassian.commonmark:commonmark-ext-gfm-strikethrough:$commonMarkVersion", | ||||
|             'commonmark-table'        : "com.atlassian.commonmark:commonmark-ext-gfm-tables:$commonMarkVersion", | ||||
|             'android-svg'             : 'com.caverock:androidsvg:1.2.1', | ||||
|             'android-gif'             : 'pl.droidsonroids.gif:android-gif-drawable:1.2.14', | ||||
|             'jlatexmath-android'      : 'ru.noties:jlatexmath-android:0.1.0', | ||||
|             'okhttp'                  : 'com.squareup.okhttp3:okhttp:3.9.0', | ||||
|             'prism4j'                 : 'ru.noties:prism4j:1.1.0', | ||||
|             'debug'                   : 'ru.noties:debug:3.0.0@jar', | ||||
|             'adapt'                   : 'ru.noties:adapt:1.1.0', | ||||
|             'dagger'                  : "com.google.dagger:dagger:$daggerVersion" | ||||
|     ] | ||||
| 
 | ||||
| @ -73,14 +80,11 @@ ext { | ||||
|     ] | ||||
| 
 | ||||
|     deps['test'] = [ | ||||
|             'junit'           : 'junit:junit:4.12', | ||||
|             'robolectric'     : 'org.robolectric:robolectric:3.8', | ||||
|             'ix-java'         : 'com.github.akarnokd:ixjava:1.0.0', | ||||
|             'jackson-yaml'    : 'com.fasterxml.jackson.dataformat:jackson-dataformat-yaml:2.9.0', | ||||
|             'jackson-databind': 'com.fasterxml.jackson.core:jackson-databind:2.9.6', | ||||
|             'gson'            : 'com.google.code.gson:gson:2.8.5', | ||||
|             'commons-io'      : 'commons-io:commons-io:2.6', | ||||
|             'mockito'         : 'org.mockito:mockito-core:2.21.0' | ||||
|             'junit'      : 'junit:junit:4.12', | ||||
|             'robolectric': 'org.robolectric:robolectric:3.8', | ||||
|             'ix-java'    : 'com.github.akarnokd:ixjava:1.0.0', | ||||
|             'commons-io' : 'commons-io:commons-io:2.6', | ||||
|             'mockito'    : 'org.mockito:mockito-core:2.21.0' | ||||
|     ] | ||||
| 
 | ||||
|     registerArtifact = this.®isterArtifact | ||||
|  | ||||
							
								
								
									
										4
									
								
								docs/.vuepress/.artifacts.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @ -0,0 +1,4 @@ | ||||
| 
 | ||||
| // this is a generated file, do not modify. To update it run 'collectArtifacts.js' script
 | ||||
| const artifacts = [{"id":"core","name":"Core","group":"ru.noties.markwon","description":"Core Markwon artifact that includes basic markdown parsing and rendering"},{"id":"ext-latex","name":"LaTeX","group":"ru.noties.markwon","description":"Extension to add LaTeX formulas to Markwon markdown"},{"id":"ext-strikethrough","name":"Strikethrough","group":"ru.noties.markwon","description":"Extension to add strikethrough markup to Markwon markdown"},{"id":"ext-tables","name":"Tables","group":"ru.noties.markwon","description":"Extension to add tables markup (GFM) to Markwon markdown"},{"id":"ext-tasklist","name":"Task List","group":"ru.noties.markwon","description":"Extension to add task lists (GFM) to Markwon markdown"},{"id":"html","name":"HTML","group":"ru.noties.markwon","description":"Provides HTML parsing functionality"},{"id":"image-gif","name":"Image GIF","group":"ru.noties.markwon","description":"Adds GIF media support to Markwon markdown"},{"id":"image-okhttp","name":"Image OkHttp","group":"ru.noties.markwon","description":"Adds OkHttp client to retrieve images data from network"},{"id":"image-svg","name":"Image SVG","group":"ru.noties.markwon","description":"Adds SVG media support to Markwon markdown"},{"id":"recycler","name":"Recycler","group":"ru.noties.markwon","description":"Provides RecyclerView.Adapter to display Markwon markdown"},{"id":"recycler-table","name":"Recycler Table","group":"ru.noties.markwon","description":"Provides MarkwonAdapter.Entry to render TableBlocks inside Android-native TableLayout widget"},{"id":"syntax-highlight","name":"Syntax Highlight","group":"ru.noties.markwon","description":"Add syntax highlight to Markwon markdown via Prism4j library"}]; | ||||
| export { artifacts }; | ||||
							
								
								
									
										105
									
								
								docs/.vuepress/components/ArtifactPicker.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @ -0,0 +1,105 @@ | ||||
| <template> | ||||
|   <div> | ||||
|     <div class="artifact-container"> | ||||
|       <div v-for="artifact in artifacts" class="artifact" @click="toggleSelection(artifact)"> | ||||
|         <div class="artifact-header"> | ||||
|           <input type="checkbox" v-model="selected" :value="artifact.id" :id="artifact.id"> | ||||
|           <strong> | ||||
|             <label :for="artifact.id">{{artifact.name}}</label> | ||||
|           </strong> | ||||
|         </div> | ||||
|         <div class="artifact-description" v-if="artifact.description">{{artifact.description}}</div> | ||||
|       </div> | ||||
|     </div> | ||||
|     <div class="extra-class language-gradle selected-artifacts" v-if="selected.length > 0"> | ||||
|       <div class="selected-artifact-script"> | ||||
|         <span class="token keyword">final def</span> | ||||
|         <span> markwon_version = </span> | ||||
|         <span class="token string">'{{latestVersion}}'</span> | ||||
|       </div> | ||||
|       <br> | ||||
|       <div class="selected-artifact-script" v-for="artifact in selectedArtifacts"> | ||||
|         <span>implementation </span> | ||||
|         <span class="token string">"{{artifact.group}}:{{artifact.id}}:</span> | ||||
|         <span>$markwon_version</span> | ||||
|         <span class="token string">"</span> | ||||
|       </div> | ||||
|     </div> | ||||
|   </div> | ||||
| </template> | ||||
| 
 | ||||
| <script> | ||||
| import { artifacts } from "../.artifacts.js"; | ||||
| 
 | ||||
| if (!artifacts) { | ||||
|   throw "Artifacts not found. Use `collectArtifacts.js` script to obtain artifacts metadata."; | ||||
| } | ||||
| 
 | ||||
| export default { | ||||
|   name: "ArtifactPicker", | ||||
|   data() { | ||||
|     return { | ||||
|       artifacts, | ||||
|       selected: ["core"], | ||||
|       latestVersion: "latest_version" | ||||
|     }; | ||||
|   }, | ||||
|   methods: { | ||||
|     toggleSelection(artifact) { | ||||
|       const index = this.selected.indexOf(artifact.id); | ||||
|       if (index < 0) { | ||||
|         this.selected.push(artifact.id); | ||||
|       } else { | ||||
|         this.selected.splice(index, 1); | ||||
|       } | ||||
|     } | ||||
|   }, | ||||
|   computed: { | ||||
|     selectedArtifacts() { | ||||
|       return this.artifacts.filter(a => this.selected.indexOf(a.id) >= 0); | ||||
|     } | ||||
|   } | ||||
| }; | ||||
| </script> | ||||
| 
 | ||||
| <style scoped> | ||||
| .artifact-container { | ||||
|   display: flex; | ||||
|   flex-wrap: wrap; | ||||
|   flex-direction: row; | ||||
|   margin-top: 0.5em; | ||||
| } | ||||
| .artifact { | ||||
|   flex: 1; | ||||
|   border: 1px #ccc solid; | ||||
|   background-color: #fafafa; | ||||
|   padding: 0.5em; | ||||
|   margin: 0.2em; | ||||
|   border-radius: 0.25em; | ||||
|   min-width: 10em; | ||||
|   max-width: 10em; | ||||
| } | ||||
| .artifact-description { | ||||
|   font-size: 0.85em; | ||||
|   margin-top: 0.5em; | ||||
| } | ||||
| .selected-artifacts { | ||||
|   color: white; | ||||
|   font-family: source-code-pro, Menlo, Monaco, Consolas, "Courier New", | ||||
|     monospace; | ||||
|   padding: 16px; | ||||
|   text-align: left; | ||||
|   word-spacing: normal; | ||||
|   word-break: normal; | ||||
|   word-wrap: normal; | ||||
|   line-height: 1.5; | ||||
|   -moz-tab-size: 4; | ||||
|   hyphens: none; | ||||
|   font-size: 0.85em; | ||||
|   margin-top: 0.5em; | ||||
| } | ||||
| .selected-artifact-script { | ||||
|   display: flex; | ||||
|   flex-wrap: wrap; | ||||
| } | ||||
| </style> | ||||
							
								
								
									
										105
									
								
								docs/.vuepress/components/CommonmarkSandbox.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @ -0,0 +1,105 @@ | ||||
| <template> | ||||
|   <div> | ||||
|     <div class="container"> | ||||
|       <div class="container-item"> | ||||
|         <textarea @input="processMarkdown">{{markdownInput}}</textarea> | ||||
|       </div> | ||||
|       <div class="container-item display" v-html="markdownHtml"></div> | ||||
|     </div> | ||||
|     <div class="footer"> | ||||
|       <!-- <p v-if="permalink"> | ||||
|         Permalink: <span v-html="permalink"></span> | ||||
|       </p> --> | ||||
|       <p> | ||||
|         <em> | ||||
|           * Please note that this tool can be used to evaluate how commonmark | ||||
|           will react to certain markdown input. There is no guarantee that results | ||||
|           here and in an Android application that uses Markwon will be the same. | ||||
|           Especially if raw HTML is involved. | ||||
|         </em> | ||||
|       </p> | ||||
|       <p> | ||||
|         <em> | ||||
|           ** For a more sophisticated commonmark sandbox editor | ||||
|           <a | ||||
|             href="https://spec.commonmark.org/dingus/" | ||||
|           >the dingus</a> can be used. | ||||
|         </em> | ||||
|       </p> | ||||
|     </div> | ||||
|   </div> | ||||
| </template> | ||||
| 
 | ||||
| <script> | ||||
| import commonmark from "commonmark"; | ||||
| 
 | ||||
| const parser = new commonmark.Parser(); | ||||
| const writer = new commonmark.HtmlRenderer(); | ||||
| 
 | ||||
| export default { | ||||
|   name: "CommonmarkSandbox", | ||||
|   data() { | ||||
|     return { | ||||
|       markdownInput: this.initialMarkdown() | ||||
|     }; | ||||
|   }, | ||||
|   methods: { | ||||
|     initialMarkdown() { | ||||
|       // const query = this.$route.query; | ||||
|       // if (query) { | ||||
|       //   const md = query.md; | ||||
|       //   if (md) { | ||||
|       //     query.md = null; | ||||
|       //     return md; | ||||
|       //   } | ||||
|       // } | ||||
|       return `# Header 1\n\n*Hello* __there!__`; | ||||
|     }, | ||||
|     processMarkdown(e) { | ||||
|       this.markdownInput = e.target.value; | ||||
|     } | ||||
|   }, | ||||
|   computed: { | ||||
|     markdownHtml() { | ||||
|       return writer.render(parser.parse(this.markdownInput)); | ||||
|     }, | ||||
|     // permalink() { | ||||
|     //   if (!this.markdownInput) { | ||||
|     //       return null; | ||||
|     //   } | ||||
|     //   const url = `${window.location.href}?md=${encodeURIComponent(this.markdownInput)}`; | ||||
|     //   return `<a href="#" title="${url}" onclick="">click to copy</a>`; | ||||
|     // } | ||||
|   } | ||||
| }; | ||||
| </script> | ||||
| 
 | ||||
| <style scoped> | ||||
| .container { | ||||
|   display: flex; | ||||
|   flex-wrap: nowrap; | ||||
|   box-sizing: border-box; | ||||
| } | ||||
| .container-item { | ||||
|   flex: 4; | ||||
|   padding: 0.5em; | ||||
| } | ||||
| .container textarea { | ||||
|   width: 100%; | ||||
|   height: 100%; | ||||
|   resize: vertical; | ||||
|   min-height: 20em; | ||||
|   padding: 0px; | ||||
|   margin: 0px; | ||||
| } | ||||
| .display { | ||||
|   flex: 5; | ||||
|   background-color: rgba(0, 0, 0, 0.05); | ||||
| } | ||||
| .footer { | ||||
|   color: #666; | ||||
|   font-size: 0.85em; | ||||
| } | ||||
| </style> | ||||
| 
 | ||||
| 
 | ||||
							
								
								
									
										13
									
								
								docs/.vuepress/components/LegacyWarning.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @ -0,0 +1,13 @@ | ||||
| <template> | ||||
|     <div class="warning custom-block"> | ||||
|         <p class="custom-block-title">WARNING</p> | ||||
|         <p>This is documentation for <u>legacy 2.x.x</u> versions. For the most current version <a :href="$withBase('/')">click here.</a></p> | ||||
|     </div> | ||||
| </template> | ||||
| 
 | ||||
| <script> | ||||
| export default { | ||||
|     name: 'LegacyWarning' | ||||
| } | ||||
| </script> | ||||
| 
 | ||||
| @ -1,5 +1,8 @@ | ||||
| <template> | ||||
|     <a :href="linkHref()" target="_blank" rel="noopener noreferrer">{{linkText()}}<OutboundLink/></a> | ||||
|   <a :href="linkHref()" target="_blank" rel="noopener noreferrer"> | ||||
|     {{linkText()}} | ||||
|     <OutboundLink/> | ||||
|   </a> | ||||
| </template> | ||||
| 
 | ||||
| <script> | ||||
| @ -9,34 +12,36 @@ var map = { | ||||
|     href: "https://spec.commonmark.org/0.28/" | ||||
|   }, | ||||
|   "commonmark-spec#inline": { | ||||
|       href: "https://spec.commonmark.org/0.28/#raw-html" | ||||
|     href: "https://spec.commonmark.org/0.28/#raw-html" | ||||
|   }, | ||||
|   "commonmark-spec#block": { | ||||
|       href: "https://spec.commonmark.org/0.28/#html-blocks" | ||||
|     href: "https://spec.commonmark.org/0.28/#html-blocks" | ||||
|   }, | ||||
|   "commonmark-spec#soft-break": { | ||||
|       href: "https://spec.commonmark.org/0.28/#soft-line-breaks" | ||||
|     href: "https://spec.commonmark.org/0.28/#soft-line-breaks" | ||||
|   }, | ||||
|   "commonmark-dingus": { | ||||
|       displayName: "commonmark dingus", | ||||
|       href: "https://spec.commonmark.org/dingus/" | ||||
|     displayName: "commonmark dingus", | ||||
|     href: "https://spec.commonmark.org/dingus/" | ||||
|   }, | ||||
|   "html-inlines": { | ||||
|       href: "https://developer.mozilla.org/en-US/docs/Web/HTML/Inline_elements" | ||||
|     href: "https://developer.mozilla.org/en-US/docs/Web/HTML/Inline_elements" | ||||
|   }, | ||||
|   "html-blocks": { | ||||
|       href: "https://developer.mozilla.org/en-US/docs/Web/HTML/Block-level_elements" | ||||
|     href: | ||||
|       "https://developer.mozilla.org/en-US/docs/Web/HTML/Block-level_elements" | ||||
|   }, | ||||
|   "jsoup": { | ||||
|       displayName: "Jsoup", | ||||
|       href: "https://github.com/jhy/jsoup/" | ||||
|   jsoup: { | ||||
|     displayName: "Jsoup", | ||||
|     href: "https://github.com/jhy/jsoup/" | ||||
|   }, | ||||
|   "markwon-jsoup": { | ||||
|       href: "https://github.com/noties/Markwon/tree/master/markwon-html-parser-impl/src/main/java/ru/noties/markwon/html/impl/jsoup" | ||||
|     href: | ||||
|       "https://github.com/noties/Markwon/tree/master/markwon-html-parser-impl/src/main/java/ru/noties/markwon/html/impl/jsoup" | ||||
|   }, | ||||
|   "commonmark-java": { | ||||
|       href: "https://github.com/atlassian/commonmark-java/", | ||||
|       displayName: "commonmark-java" | ||||
|     href: "https://github.com/atlassian/commonmark-java/", | ||||
|     displayName: "commonmark-java" | ||||
|   } | ||||
| }; | ||||
| 
 | ||||
| @ -48,7 +53,7 @@ export default { | ||||
|       return this.href || map[this.name].href; | ||||
|     }, | ||||
|     linkText: function() { | ||||
|         return this.displayName || map[this.name].displayName; | ||||
|       return this.displayName || map[this.name].displayName; | ||||
|     } | ||||
|   } | ||||
| }; | ||||
|  | ||||
| @ -1,17 +1,22 @@ | ||||
| <template> | ||||
|     <a :href="mavenSearchUrl()"><img :src="shieldImgageUrl()" :alt="'' + artifact"></a> | ||||
|     <a :href="mavenSearchUrl()"><img :src="shieldImgageUrl()" :alt="displayLabel"></a> | ||||
| </template> | ||||
| 
 | ||||
| <script> | ||||
| export default { | ||||
|     name: 'MavenBadge', | ||||
|     props: ['artifact'], | ||||
|     props: ['artifact', 'label'], | ||||
|     methods: { | ||||
|         mavenSearchUrl: function() { | ||||
|             return 'http://search.maven.org/#search|ga|1|g%3A%22ru.noties%22%20AND%20a%3A%22' + this.artifact + '%22'; | ||||
|             return `http://search.maven.org/#search|ga|1|g%3A%22ru.noties.markwon%22%20AND%20a%3A%22${this.artifact}%22`; | ||||
|         }, | ||||
|         shieldImgageUrl: function() { | ||||
|             return 'https://img.shields.io/maven-central/v/ru.noties/' + this.artifact +'.svg?label=' + this.artifact; | ||||
|             return `https://img.shields.io/maven-central/v/ru.noties.markwon/${this.artifact}.svg?label=${this.displayLabel}`; | ||||
|         } | ||||
|     }, | ||||
|     computed: { | ||||
|         displayLabel() { | ||||
|             return this.label || this.artifact; | ||||
|         } | ||||
|     } | ||||
| } | ||||
|  | ||||
							
								
								
									
										24
									
								
								docs/.vuepress/components/MavenBadge2xx.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @ -0,0 +1,24 @@ | ||||
| <template> | ||||
|     <a :href="mavenSearchUrl()"><img :src="shieldImgageUrl()" :alt="displayLabel"></a> | ||||
| </template> | ||||
| 
 | ||||
| <script> | ||||
| export default { | ||||
|     name: 'MavenBadge2xx', | ||||
|     props: ['artifact', 'label'], | ||||
|     methods: { | ||||
|         mavenSearchUrl: function() { | ||||
|             return `http://search.maven.org/#search|ga|1|g%3A%22ru.noties%22%20AND%20a%3A%22${this.artifact}%22`; | ||||
|         }, | ||||
|         shieldImgageUrl: function() { | ||||
|             return `https://img.shields.io/maven-central/v/ru.noties/${this.artifact}.svg?label=${this.displayLabel}`; | ||||
|         } | ||||
|     }, | ||||
|     computed: { | ||||
|         displayLabel() { | ||||
|             return this.label || this.artifact; | ||||
|         } | ||||
|     } | ||||
| } | ||||
| </script> | ||||
| 
 | ||||
							
								
								
									
										20
									
								
								docs/.vuepress/components/MavenBadges2xx.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @ -0,0 +1,20 @@ | ||||
| <template> | ||||
|     <div> | ||||
|         <MavenBadge2xx :artifact="'markwon'" /> | ||||
|         <MavenBadge2xx :artifact="'markwon-image-loader'" /> | ||||
|         <MavenBadge2xx :artifact="'markwon-syntax-highlight'"/> | ||||
|         <MavenBadge2xx :artifact="'markwon-view'"/> | ||||
|     </div> | ||||
| </template> | ||||
| 
 | ||||
| <script> | ||||
| import MavenBadge2xx from "./MavenBadge2xx.vue"; | ||||
| export default { | ||||
|   name: "MavenBadges2xx", | ||||
|   components: { | ||||
|     MavenBadge2xx | ||||
|   } | ||||
| }; | ||||
| </script> | ||||
| 
 | ||||
| 
 | ||||
| @ -1,37 +1,74 @@ | ||||
| module.exports = { | ||||
|     base: '/Markwon/', | ||||
|     title: 'Markwon', | ||||
|     description: 'Android markdown library based on commonmark specification', | ||||
|     description: 'Android markdown library based on commonmark specification that renders markdown as system-native Spannables (no WebView)', | ||||
|     head: [ | ||||
|         ['link', {rel: 'apple-touch-icon', sizes: '180x180', href: '/apple-touch-icon.png?v=1'}], | ||||
|         ['link', {rel: 'icon', type: 'image/png', sizes: '16x16', href: '/favicon-16x16.png?v=1'}], | ||||
|         ['link', {rel: 'icon', href: '/favicon.ico?v=1'}], | ||||
|         ['link', {rel: 'icon', type: 'image/png', sizes: '32x32', href: '/favicon-32x32.png?v=1'}], | ||||
|         ['link', {rel: 'manifest', href: '/manifest.json?v=1'}], | ||||
|         ['link', { rel: 'apple-touch-icon', sizes: '180x180', href: '/apple-touch-icon.png?v=1' }], | ||||
|         ['link', { rel: 'icon', type: 'image/png', sizes: '16x16', href: '/favicon-16x16.png?v=1' }], | ||||
|         ['link', { rel: 'icon', href: '/favicon.ico?v=1' }], | ||||
|         ['link', { rel: 'icon', type: 'image/png', sizes: '32x32', href: '/favicon-32x32.png?v=1' }], | ||||
|         ['link', { rel: 'manifest', href: '/manifest.json?v=1' }], | ||||
|         ['meta', { name: 'keywords', content: 'android,markdown,library,spannable,markwon,commonmark' }] | ||||
|     ], | ||||
|     themeConfig: { | ||||
|         nav: [ | ||||
|             { text: 'Install', link: '/docs/install.md' }, | ||||
|             { text: 'Install', link: '/docs/v3/install.md' }, | ||||
|             { text: 'Changelog', link: '/CHANGELOG.md' }, | ||||
|             { | ||||
|                 text: 'API Version', | ||||
|                 items: [ | ||||
|                     { text: 'Current (3.x.x)', link: '/' }, | ||||
|                     { text: 'Legacy (2.x.x)', link: '/docs/v2/' } | ||||
|                 ] | ||||
|             }, | ||||
|             { text: 'Sandbox', link: '/sandbox.md' }, | ||||
|             { text: 'Github', link: 'https://github.com/noties/Markwon' } | ||||
|         ], | ||||
|         sidebar: [ | ||||
|             '/', | ||||
|             '/docs/getting-started.md', | ||||
|             '/docs/configure.md', | ||||
|             '/docs/theme.md', | ||||
|             '/docs/factory.md', | ||||
|             '/docs/image-loader.md', | ||||
|             '/docs/syntax-highlight.md', | ||||
|             '/docs/html.md', | ||||
|             '/docs/view.md' | ||||
|         ], | ||||
|         sidebar: { | ||||
|             '/docs/v2': [ | ||||
|                 '/docs/v2/getting-started.md', | ||||
|                 '/docs/v2/configure.md', | ||||
|                 '/docs/v2/theme.md', | ||||
|                 '/docs/v2/factory.md', | ||||
|                 '/docs/v2/image-loader.md', | ||||
|                 '/docs/v2/syntax-highlight.md', | ||||
|                 '/docs/v2/html.md', | ||||
|                 '/docs/v2/view.md' | ||||
|             ], | ||||
|             '/': [ | ||||
|                 '', | ||||
|                 { | ||||
|                     title: 'Core', | ||||
|                     collapsable: false, | ||||
|                     children: [ | ||||
|                         '/docs/v3/core/getting-started.md', | ||||
|                         '/docs/v3/core/plugins.md', | ||||
|                         '/docs/v3/core/theme.md', | ||||
|                         '/docs/v3/core/images.md', | ||||
|                         '/docs/v3/core/configuration.md', | ||||
|                         '/docs/v3/core/visitor.md', | ||||
|                         '/docs/v3/core/spans-factory.md', | ||||
|                         '/docs/v3/core/html-renderer.md', | ||||
|                         '/docs/v3/core/core-plugin.md', | ||||
|                         '/docs/v3/core/movement-method-plugin.md', | ||||
|                         '/docs/v3/core/render-props.md' | ||||
|                     ] | ||||
|                 }, | ||||
|                 '/docs/v3/ext-latex/', | ||||
|                 '/docs/v3/ext-strikethrough/', | ||||
|                 '/docs/v3/ext-tables/', | ||||
|                 '/docs/v3/ext-tasklist/', | ||||
|                 '/docs/v3/html/', | ||||
|                 '/docs/v3/image/gif.md', | ||||
|                 '/docs/v3/image/okhttp.md', | ||||
|                 '/docs/v3/image/svg.md', | ||||
|                 '/docs/v3/recycler/', | ||||
|                 '/docs/v3/recycler-table/', | ||||
|                 '/docs/v3/syntax-highlight/', | ||||
|                 '/docs/v3/migration-2-3.md' | ||||
|             ] | ||||
|         }, | ||||
|         sidebarDepth: 2, | ||||
|         lastUpdated: true | ||||
|     }, | ||||
|     markdown: { | ||||
|         config: md => { | ||||
|             md.use(require('markdown-it-task-lists')); | ||||
|         } | ||||
|     } | ||||
| } | ||||
| @ -1,2 +1,23 @@ | ||||
| $textColor = #000000 | ||||
| $accentColor = #4CAF50 | ||||
| $accentColor = #4CAF50 | ||||
| 
 | ||||
| a.sidebar-link { | ||||
|     font-weight: 500; | ||||
| } | ||||
| 
 | ||||
| .sidebar-sub-headers a.sidebar-link { | ||||
|     font-weight: normal; | ||||
| } | ||||
| 
 | ||||
| .sidebar-group a.sidebar-link { | ||||
|     font-weight: normal; | ||||
| } | ||||
| 
 | ||||
| .sidebar-heading { | ||||
|     color: $textColor; | ||||
|     font-weight: 600; | ||||
| } | ||||
| 
 | ||||
| .sidebar-heading.open, .sidebar-heading:hover { | ||||
|     color: $accentColor; | ||||
| } | ||||
							
								
								
									
										
											BIN
										
									
								
								docs/.vuepress/public/assets/recycler-table-screenshot.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| After Width: | Height: | Size: 77 KiB | 
| @ -0,0 +1,71 @@ | ||||
| div[class~=language-gradle]:before { | ||||
|     content:"gradle" | ||||
| } | ||||
| 
 | ||||
| div[class~=language-proguard]:before { | ||||
|     content:"proguard" | ||||
| } | ||||
| 
 | ||||
| div[class~=language-groovy]:before { | ||||
|     content:"gradle" | ||||
| } | ||||
| 
 | ||||
| div[class*="language-"] { | ||||
|     background-color: #2d2d2d; | ||||
| } | ||||
| 
 | ||||
| .token.comment, .token.prolog, .token.cdata { | ||||
|     color: #808080; | ||||
| } | ||||
| 
 | ||||
| .token.delimiter, .token.boolean, .token.keyword, .token.selector, .token.important, .token.atrule { | ||||
|     color: #cc7832; | ||||
| } | ||||
|                  | ||||
| .token.operator, .token.punctuation, .token.attr-name { | ||||
|     color: #a9b7c6; | ||||
| } | ||||
| 
 | ||||
| .token.tag, .token.doctype, .token.builtin { | ||||
|     color: #e8bf6a; | ||||
| } | ||||
| 
 | ||||
| .token.entity, .token.number, .token.symbol { | ||||
|     color: #6897bb; | ||||
| } | ||||
| 
 | ||||
| .token.property, .token.constant, .token.variable { | ||||
|     color: #9876aa; | ||||
| } | ||||
|              | ||||
| .token.string, .token.char { | ||||
|     color: #6a8759; | ||||
| } | ||||
| 
 | ||||
| .token.annotation { | ||||
|     color: #bbb438; | ||||
| } | ||||
| 
 | ||||
| .token.attr-value { | ||||
|     color: #a5c261; | ||||
| } | ||||
| 
 | ||||
| .token.url { | ||||
|     color: #287bde; | ||||
| } | ||||
| 
 | ||||
| .token.function { | ||||
|     color: #ffc66d; | ||||
| } | ||||
| 
 | ||||
| .token.regex { | ||||
|     color: #364135; | ||||
| } | ||||
| 
 | ||||
| .token.inserted { | ||||
|     color: #294436; | ||||
| } | ||||
| 
 | ||||
| .token.deleted { | ||||
|     color: #484a4a; | ||||
| } | ||||
| @ -1,17 +1,54 @@ | ||||
| # Changelog | ||||
| 
 | ||||
| # 2.0.1 | ||||
| # 3.0.0 | ||||
| * Plugins, plugins, plugins | ||||
| * Split basic functionality blocks into standalone modules | ||||
| * Maven artifacts group changed to `ru.noties.markwon` (previously had been `ru.noties`) | ||||
| * removed `markwon`, `markwon-image-loader`, `markwon-html-pareser-api`, `markwon-html-parser-impl`, `markwon-view` modules | ||||
| * new module system: `core`, `ext-latex`, `ext-strikethrough`, `ext-tables`, `ext-tasklist`, `html`, `image-gif`, `image-okhttp`, `image-svg`, `recycler`, `recycler-table`, `syntax-highlight` | ||||
| * Add BufferType option for Markwon configuration | ||||
| * Fix typo in AsyncDrawable waitingForDimensions | ||||
| * New tests format | ||||
| * `Markwon.render` returns `Spanned` instance of generic `CharSequence` | ||||
| * LinkMovementMethod is applied implicitly if not set on a TextView explicitly | ||||
| * Split code and codeBlock spans and factories | ||||
| * Add CustomTypefaceSpan  | ||||
| * Add NoCopySpansFactory | ||||
| * Add placeholder to image loading | ||||
| 
 | ||||
| Generally speaking there are a lot of changes. Most of them are not backwards-compatible. | ||||
| The main point of this release is the `Plugin` system that allows more fluent configuration | ||||
| and opens the possibility of extending `Markwon` with 3rd party functionality in a simple | ||||
| and intuitive fashion. Please refer to the [documentation web-site](https://noties.github.io/Markwon) | ||||
| that has information on how to start migration. | ||||
| 
 | ||||
| The shortest excerpt of this release can be expressed like this: | ||||
| 
 | ||||
| ```java | ||||
| // previous v2.x.x way | ||||
| Markwon.setMarkdown(textView, "**Hello there!**"); | ||||
| ``` | ||||
| 
 | ||||
| ```java | ||||
| // 3.x.x | ||||
| Markwon.create(context) | ||||
|         .setMarkdown(textView, "**Hello there!**"); | ||||
| ``` | ||||
| 
 | ||||
| But there is much more to it, please visit documentation web-site | ||||
| to get the full picture of latest changes. | ||||
| 
 | ||||
| ## 2.0.1 | ||||
| * `SpannableMarkdownVisitor` Rename blockQuoteIndent to blockIndent | ||||
| * Fixed block new lines logic for block quote and paragraph (#82) | ||||
| * AsyncDrawable fix no dimensions bug (#81) | ||||
| * Fixed block new lines logic for block quote and paragraph (<GithubIssue id="82" />) | ||||
| * AsyncDrawable fix no dimensions bug (<GithubIssue id="81" />) | ||||
| * Update SpannableTheme to use Px instead of Dimension annotation | ||||
| * Allow TaskListSpan isDone mutation | ||||
| * Updated commonmark-java to 0.12.1 | ||||
| * Add OrderedListItemSpan measure utility method (#78) | ||||
| * Add OrderedListItemSpan measure utility method (<GithubIssue id="78" />) | ||||
| * Add SpannableBuilder#getSpans method | ||||
| * Fix DataUri scheme handler in image-loader (#74) | ||||
| * Introduced a "copy" builder for SpannableThem | ||||
|   Thanks @c-b-h 🙌 | ||||
| * Fix DataUri scheme handler in image-loader (<GithubIssue id="74" />) | ||||
| * Introduced a "copy" builder for SpannableThem <br>Thanks <GithubUser name="c-b-h" /> | ||||
| 
 | ||||
| ## 2.0.0 | ||||
| * Add `html-parser-api` and `html-parser-impl` modules | ||||
|  | ||||
| @ -1,11 +1,12 @@ | ||||
| --- | ||||
| title: 'Overview' | ||||
| title: 'Introduction' | ||||
| --- | ||||
| 
 | ||||
| <img :src="$withBase('./art/markwon_logo.png')" alt="Markwon Logo" width="50%"> | ||||
| <img :src="$withBase('/art/markwon_logo.png')" alt="Markwon Logo" width="50%"> | ||||
| 
 | ||||
| <br><br> | ||||
| <MavenBadges/> | ||||
| [](http://search.maven.org/#search|ga|1|g%3A%22ru.noties.markwon%22%20) | ||||
| [](https://travis-ci.org/noties/Markwon) | ||||
| 
 | ||||
| **Markwon** is a markdown library for Android. It parses markdown following  | ||||
| <Link name="commonmark-spec" /> with the help of amazing <Link name="commonmark-java" /> library | ||||
| @ -20,22 +21,23 @@ but also gives all the means to tweak the appearance if desired. All markdown fe | ||||
| listed in <Link name="commonmark-spec" /> are supported (including support for **inlined/block HTML code**,  | ||||
| **markdown tables**, **images** and **syntax highlight**). | ||||
| 
 | ||||
| ## Supported markdown features: | ||||
| ## Supported markdown features | ||||
| 
 | ||||
| * Emphasis (`*`, `_`) | ||||
| * Strong emphasis (`**`, `__`) | ||||
| * Strike-through (`~~`) | ||||
| * Headers (`#{1,6}`) | ||||
| * Links (`[]()` && `[][]`) | ||||
| * [Images](/docs/image-loader.md) | ||||
| * [Images](/docs/v3/core/images.md) | ||||
| * Thematic break (`---`, `***`, `___`) | ||||
| * Quotes & nested quotes (`>{1,}`) | ||||
| * Ordered & non-ordered lists & nested ones | ||||
| * Inline code | ||||
| * Code blocks | ||||
| * Tables (*with limitations*) | ||||
| * [Syntax highlight](/docs/syntax-highlight.md) | ||||
| * [HTML](/docs/html.md) | ||||
| * [Strike-through](/docs/v3/ext-strikethrough/) (`~~`) | ||||
| * [Tables](/docs/v3/ext-tables/) (*with limitations*) | ||||
| * [Syntax highlight](/docs/v3/syntax-highlight/) | ||||
| * [LaTeX](/docs/v3/ext-latex/) formulas | ||||
| * [HTML](/docs/v3/html/) | ||||
|   * Emphasis (`<i>`, `<em>`, `<cite>`, `<dfn>`) | ||||
|   * Strong emphasis (`<b>`, `<strong>`) | ||||
|   * SuperScript (`<sup>`) | ||||
| @ -48,11 +50,13 @@ listed in <Link name="commonmark-spec" /> are supported (including support for * | ||||
|   * Blockquote (`blockquote`) | ||||
|   * Heading (`h1`, `h2`, `h3`, `h4`, `h5`, `h6`) | ||||
|   * there is support to render any HTML tag, but it will require to create a special `TagHandler`, | ||||
|     more information can be found in [HTML section](/docs/html.md#custom-tag-handler) | ||||
| * Task lists: | ||||
| - [ ] Not _done_ | ||||
|   - [X] **Done** with `X` | ||||
|   - [x] ~~and~~ **or** small `x` | ||||
|     more information can be found in [HTML section](/docs/v3/core/html-renderer.md) | ||||
| * [Task lists](/docs/v3/ext-tasklist/): | ||||
| <ul style="list-style-type: none; margin: 0; padding: 0;"> | ||||
| <li><input type="checkbox" disabled>Not <i>done</i></li> | ||||
| <li><input type="checkbox" disabled checked><strong>Done</strong> with <code>X</code></li> | ||||
| <li><input type="checkbox" disabled checked><del>and</del> <strong>or</strong> small <code>x</code></li>     | ||||
| </ul> | ||||
| 
 | ||||
| ## Screenshots | ||||
| 
 | ||||
| @ -68,3 +72,24 @@ Screenshots are taken from sample application. It is a generic markdown viewer | ||||
| with support to display markdown content via `http`, `https` & `file` schemes  | ||||
| and 2 themes included: Light & Dark. It can be downloaded from [releases](https://github.com/noties/Markwon/releases) | ||||
| ::: | ||||
| 
 | ||||
| 
 | ||||
| ## Awesome Markwon | ||||
| 
 | ||||
| <u>Applications using Markwon</u>: | ||||
| 
 | ||||
| * [Partico](https://partiko.app/) - Partiko is a censorship free social network. | ||||
| * [FairNote](https://play.google.com/store/apps/details?id=com.rgiskard.fairnote) - simple and intuitive notepad app. It gives you speed and efficiency when you write notes, to-do lists, e-mails, or jot down quick ideas. | ||||
| 
 | ||||
| 
 | ||||
| <u>Extension/plugins</u>: | ||||
| 
 | ||||
| * [MarkwonCodeEx](https://github.com/kingideayou/MarkwonCodeEx) - Markwon extension support elegant code background. | ||||
| 
 | ||||
| --- | ||||
| 
 | ||||
| [Help to improve][awesome_link] this section by submitting your application or library | ||||
| that is using `Markwon` | ||||
| 
 | ||||
| 
 | ||||
| [awesome_link]: https://github.com/noties/Markwon/issues/new?labels=awesome&body=Please%20provide%20the%20following%3A%0A*%20Project%20name%0A*%20Project%20URL%20(repository%2C%20store%20listing%2C%20web%20page)%0A*%20Optionally%20_brand_%20image%20URL%0A%0APlease%20make%20sure%20that%20there%20is%20the%20**awesome**%20label%20selected%20for%20this%20issue.%0A%0A%F0%9F%99%8C%20 | ||||
|  | ||||
							
								
								
									
										50
									
								
								docs/collectArtifacts.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @ -0,0 +1,50 @@ | ||||
| const fs = require('fs'); | ||||
| const path = require('path'); | ||||
| 
 | ||||
| const PROPERTIES_FILE_NAME = 'gradle.properties'; | ||||
| const PROP_GROUP = 'GROUP'; | ||||
| const PROP_DESCRIPTION = 'POM_DESCRIPTION'; | ||||
| const PROP_ARTIFACT_NAME = 'POM_NAME'; | ||||
| const PROP_ARTIFACT_ID = 'POM_ARTIFACT_ID'; | ||||
| 
 | ||||
| const readProperties = (file) => fs.readFileSync(file, { encoding: 'utf-8' }, 'string') | ||||
|     .split('\n') | ||||
|     // filter-out empty lines
 | ||||
|     .filter(s => s) | ||||
|     .map(s => s.split('=')) | ||||
|     .reduce((a, s) => { | ||||
|         a[s[0]] = s[1]; | ||||
|         return a; | ||||
|     }, {}); | ||||
| 
 | ||||
| const listDirectories = (folder) => fs.readdirSync(folder) | ||||
|     .map(name => path.join(folder, name)) | ||||
|     .filter(f => fs.lstatSync(f).isDirectory()); | ||||
| 
 | ||||
| const projectDir = path.resolve(__dirname, '../'); | ||||
| 
 | ||||
| const projectProperties = readProperties(path.join(projectDir, PROPERTIES_FILE_NAME)); | ||||
| 
 | ||||
| const projectGroup = projectProperties[PROP_GROUP] | ||||
| 
 | ||||
| const artifacts = listDirectories(projectDir) | ||||
|     .map(dir => path.join(dir, PROPERTIES_FILE_NAME)) | ||||
|     .filter(f => fs.existsSync(f)) | ||||
|     .map(readProperties) | ||||
|     .map(props => { | ||||
|         return { | ||||
|             id: props[PROP_ARTIFACT_ID], | ||||
|             name: props[PROP_ARTIFACT_NAME], | ||||
|             group: projectGroup, | ||||
|             description: props[PROP_DESCRIPTION] | ||||
|         } | ||||
|     }); | ||||
| 
 | ||||
| const artifactsFile = path.join(__dirname, '.vuepress', '.artifacts.js'); | ||||
| const artifactsJs = ` | ||||
| // this is a generated file, do not modify. To update it run 'collectArtifacts.js' script
 | ||||
| const artifacts = ${JSON.stringify(artifacts)}; | ||||
| export { artifacts }; | ||||
| ` | ||||
| 
 | ||||
| fs.writeFileSync(artifactsFile, artifactsJs); | ||||
							
								
								
									
										70
									
								
								docs/docs/v2/README.md
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @ -0,0 +1,70 @@ | ||||
| --- | ||||
| title: 'Overview' | ||||
| --- | ||||
| 
 | ||||
| <img :src="$withBase('/art/markwon_logo.png')" alt="Markwon Logo" width="50%"> | ||||
| 
 | ||||
| <br><br> | ||||
| <MavenBadges2xx/> | ||||
| 
 | ||||
| **Markwon** is a markdown library for Android. It parses markdown following  | ||||
| <Link name="commonmark-spec" /> with the help of amazing <Link name="commonmark-java" /> library | ||||
| and renders result as _Android-native_ Spannables. **No HTML** is involved | ||||
| as an intermediate step. <u>**No WebView** is required</u>. It's extremely fast,  | ||||
| feature-rich and extensible. | ||||
| 
 | ||||
| It gives ability to display markdown in all TextView widgets (**TextView**,  | ||||
| **Button**, **Switch**, **CheckBox**, etc), **Toasts** and all other places that accept | ||||
| **Spanned content**. Library provides reasonable defaults to display style of a markdown content | ||||
| but also gives all the means to tweak the appearance if desired. All markdown features  | ||||
| listed in <Link name="commonmark-spec" /> are supported (including support for **inlined/block HTML code**,  | ||||
| **markdown tables**, **images** and **syntax highlight**). | ||||
| 
 | ||||
| ## Supported markdown features: | ||||
| 
 | ||||
| * Emphasis (`*`, `_`) | ||||
| * Strong emphasis (`**`, `__`) | ||||
| * Strike-through (`~~`) | ||||
| * Headers (`#{1,6}`) | ||||
| * Links (`[]()` && `[][]`) | ||||
| * [Images](/docs/v2/image-loader.md) | ||||
| * Thematic break (`---`, `***`, `___`) | ||||
| * Quotes & nested quotes (`>{1,}`) | ||||
| * Ordered & non-ordered lists & nested ones | ||||
| * Inline code | ||||
| * Code blocks | ||||
| * Tables (*with limitations*) | ||||
| * [Syntax highlight](/docs/v2/syntax-highlight.md) | ||||
| * [HTML](/docs/v2/html.md) | ||||
|   * Emphasis (`<i>`, `<em>`, `<cite>`, `<dfn>`) | ||||
|   * Strong emphasis (`<b>`, `<strong>`) | ||||
|   * SuperScript (`<sup>`) | ||||
|   * SubScript (`<sub>`) | ||||
|   * Underline (`<u>`, `ins`) | ||||
|   * Strike-through (`<s>`, `<strike>`, `<del>`) | ||||
|   * Link (`a`) | ||||
|   * Lists (`ul`, `ol`) | ||||
|   * Images (`img` will require configured image loader) | ||||
|   * Blockquote (`blockquote`) | ||||
|   * Heading (`h1`, `h2`, `h3`, `h4`, `h5`, `h6`) | ||||
|   * there is support to render any HTML tag, but it will require to create a special `TagHandler`, | ||||
|     more information can be found in [HTML section](/docs/v2/html.md#custom-tag-handler) | ||||
| * Task lists: | ||||
| - [ ] Not _done_ | ||||
|   - [X] **Done** with `X` | ||||
|   - [x] ~~and~~ **or** small `x` | ||||
| 
 | ||||
| ## Screenshots | ||||
| 
 | ||||
| <img :src="$withBase('/art/mw_light_01.png')" alt="screenshot light #1" width="30%"> | ||||
| <img :src="$withBase('/art/mw_light_02.png')" alt="screenshot light #2" width="30%"> | ||||
| <img :src="$withBase('/art/mw_light_03.png')" alt="screenshot light #3" width="30%"> | ||||
| <img :src="$withBase('/art/mw_dark_01.png')" alt="screenshot dark #2" width="30%"> | ||||
| 
 | ||||
| By default configuration uses TextView textColor for styling, so changing textColor changes style | ||||
| 
 | ||||
| :::tip Sample application | ||||
| Screenshots are taken from sample application. It is a generic markdown viewer  | ||||
| with support to display markdown content via `http`, `https` & `file` schemes  | ||||
| and 2 themes included: Light & Dark. It can be downloaded from [releases](https://github.com/noties/Markwon/releases) | ||||
| ::: | ||||
| @ -24,13 +24,13 @@ values as they will be applied automatically | ||||
| If you plan on using images inside your markdown/HTML, you will have to **explicitly** | ||||
| register an implementation of `AsyncDrawable.Loader` via `#asyncDrawableLoader` builder method. | ||||
| `Markwon` comes with ready implementation for that and it can be found in | ||||
| `markwon-image-loader` module. Refer to module [documentation](/docs/image-loader.md) | ||||
| `markwon-image-loader` module. Refer to module [documentation](/docs/v2/image-loader.md) | ||||
| ::: | ||||
| 
 | ||||
| ## Theme | ||||
| 
 | ||||
| `SpannableTheme` controls how markdown is rendered. It has pretty extensive number of | ||||
| options that can be found [here](/docs/theme.md) | ||||
| options that can be found [here](/docs/v2/theme.md) | ||||
| 
 | ||||
| ```java | ||||
| SpannableConfiguration.builder(context) | ||||
| @ -56,7 +56,7 @@ If `AsyncDrawable.Loader` is not provided explicitly, default **no-op** implemen | ||||
| 
 | ||||
| :::tip Implementation | ||||
| There are no restrictions on what implementation to use, but `Markwon` has artifact that can | ||||
| answer the most common needs of displaying SVG, GIF and other image formats. It can be found [here](/docs/image-loader.md) | ||||
| answer the most common needs of displaying SVG, GIF and other image formats. It can be found [here](/docs/v2/image-loader.md) | ||||
| ::: | ||||
| 
 | ||||
| ### Size resolver <Badge text="1.0.1" /> | ||||
| @ -107,7 +107,7 @@ If not provided explicitly, default **no-op** implementation will be used. | ||||
| Although `SyntaxHighlight` interface was included with the very first version | ||||
| of `Markwon` there were no ready-to-use implementations. But starting with <Badge text="1.1.0" />  | ||||
| `Markwon` provides one. It can be found in `markwon-syntax-highlight` artifact. Refer | ||||
| to module [documentation](/docs/syntax-highlight.md) | ||||
| to module [documentation](/docs/v2/syntax-highlight.md) | ||||
| ::: | ||||
| 
 | ||||
| ## Link resolver | ||||
| @ -166,7 +166,7 @@ SpannableConfiguration.builder(context) | ||||
| ``` | ||||
| 
 | ||||
| If not provided explicitly, default `SpannableFactoryDef` implementation will be used. It is documented | ||||
| in [this section](/docs/factory.md) | ||||
| in [this section](/docs/v2/factory.md) | ||||
| 
 | ||||
| ## Soft line break <Badge text="1.1.1" /> | ||||
| 
 | ||||
| @ -197,7 +197,7 @@ SpannableConfiguration.builder(context) | ||||
| 
 | ||||
| if not provided explicitly, default `MarkwonHtmlParserImpl` will be used | ||||
| **if** it can be found in classpath, otherwise default **no-op** implementation | ||||
| wiil be used. Refer to [HTML](/docs/html.md#parser) document for more information about this behavior. | ||||
| wiil be used. Refer to [HTML](/docs/v2/html.md#parser) document for more information about this behavior. | ||||
| 
 | ||||
| ### Renderer | ||||
| 
 | ||||
| @ -210,7 +210,7 @@ SpannableConfiguration.builder(context) | ||||
| ``` | ||||
| 
 | ||||
| If not provided explicitly, default `MarkwonHtmlRenderer` implementation will be used. | ||||
| It is documented [here](/docs/html.md#renderer) | ||||
| It is documented [here](/docs/v2/html.md#renderer) | ||||
| 
 | ||||
| ### HTML allow non-closed tags | ||||
| 
 | ||||
| @ -1,10 +1,5 @@ | ||||
| # Getting started | ||||
| 
 | ||||
| :::tip Installation | ||||
| Please follow [installation](/docs/install.md) instructions | ||||
| to learn how to add `Markwon` to your project | ||||
| ::: | ||||
| 
 | ||||
| ## Quick one | ||||
| 
 | ||||
| This is the most simple way to set markdown to a `TextView` or any of its siblings: | ||||
| @ -25,7 +20,7 @@ Toast.makeText(context, markdown, Toast.LENGTH_LONG).show(); | ||||
| 
 | ||||
| ## Longer one | ||||
| 
 | ||||
| When you need to customize markdown parsing/rendering you can use [SpannableConfiguration](/docs/configure.md): | ||||
| When you need to customize markdown parsing/rendering you can use [SpannableConfiguration](/docs/v2/configure.md): | ||||
| 
 | ||||
| ```java | ||||
| final SpannableConfiguration configuration = SpannableConfiguration.builder(context) | ||||
| @ -16,12 +16,12 @@ public interface Loader { | ||||
| 
 | ||||
| ## AsyncDrawableLoader | ||||
| 
 | ||||
| <MavenBadge artifact="markwon-image-loader" /> | ||||
| <MavenBadge2xx artifact="markwon-image-loader" /> | ||||
| 
 | ||||
| `AsyncDrawableLoader` from `markwon-image-loader` artifact can be used. | ||||
| 
 | ||||
| :::tip Install | ||||
| [Learn how to add](/docs/install.md#image-loader) `markwon-image-loader` to your project | ||||
| [Learn how to add](/docs/v2/install.md#image-loader) `markwon-image-loader` to your project | ||||
| ::: | ||||
| 
 | ||||
| Default instance of `AsyncDrawableLoader` can be obtain like this: | ||||
| @ -5,7 +5,7 @@ next: /docs/getting-started.md | ||||
| 
 | ||||
| # Installation | ||||
| 
 | ||||
| <MavenBadges /> | ||||
| <MavenBadges2xx /> | ||||
| 
 | ||||
| In order to start using `Markwon` add this to your dependencies block | ||||
| in your projects `build.gradle`: | ||||
| @ -39,7 +39,7 @@ Provides implementation of `AsyncDrawable.Loader` and comes with support for: | ||||
| * GIF | ||||
| * Other image formats | ||||
| 
 | ||||
| Please refer to documentation for [image loader](/docs/image-loader.md) module | ||||
| Please refer to documentation for [image loader](/docs/v2/image-loader.md) module | ||||
| 
 | ||||
| ### Syntax highlight | ||||
| 
 | ||||
| @ -49,7 +49,7 @@ implementation "ru.noties:markwon-syntax-highlight:${markwonVersion}" | ||||
| 
 | ||||
| Provides implementation of `SyntaxHighlight` and allows various syntax highlighting | ||||
| in your markdown based Android applications. Comes with 2 ready-to-be-used themes: `light` and `dark`. | ||||
| Please refer to documentation for [syntax highlight](/docs/syntax-highlight.md) module | ||||
| Please refer to documentation for [syntax highlight](/docs/v2/syntax-highlight.md) module | ||||
| 
 | ||||
| ### View | ||||
| 
 | ||||
| @ -59,7 +59,7 @@ implementation "ru.noties:markwon-view:${markwonVersion}" | ||||
| 
 | ||||
| Provides 2 widgets to display markdown: `MarkwonView` and `MarkwonViewCompat` (subclasses | ||||
| of `TextView` and `AppCompatTextView` respectively). | ||||
| Please refer to documentation for [view](/docs/view.md) module | ||||
| Please refer to documentation for [view](/docs/v2/view.md) module | ||||
| 
 | ||||
| ## Proguard | ||||
| 
 | ||||
| @ -1,6 +1,6 @@ | ||||
| # Syntax highlight | ||||
| 
 | ||||
| <MavenBadge artifact="markwon-syntax-highlight" /> | ||||
| <MavenBadge2xx artifact="markwon-syntax-highlight" /> | ||||
| 
 | ||||
| This is a simple module to add **syntax highlight** functionality to your markdown rendered with `Markwon` library. It is based on [Prism4j](https://github.com/noties/Prism4j) so lead there to understand how to configure `Prism4j` instance. | ||||
| 
 | ||||
| @ -1,10 +1,19 @@ | ||||
| # Theme | ||||
| 
 | ||||
| Here is the list of properties that can be configured via `SpannableTheme#builder` factory | ||||
| method. If you wish to control what is out of this list, you can use [SpannableFactory](/docs/factory.md) | ||||
| Here is the list of properties that can be configured via `SpannableTheme`. If you wish to control what  | ||||
| is out of this list, you can use [SpannableFactory](/docs/v2/factory.md) | ||||
| abstraction which lets you to gather full control of Spans that are used to display markdown. | ||||
| 
 | ||||
| * factory methods | ||||
| * `SpannableTheme#create(Context)` - creates a **default** instance of `SpannableBuilder (with _defaults_ registered) | ||||
| * `SpannableTheme#builder` - creates **empty** builder with **no defaults registered** | ||||
| * `SpannableTheme#builderWithDefaults(Context)` - create a **default** instance of builder (with default values registered) | ||||
| 
 | ||||
| :::warning | ||||
| `SpannbleTheme#builder` method has an unfortunate naming. It should've been `emptyBuilder` | ||||
| or `builderNoDefaults` because `#builder` method returns a builder with <strong>no default | ||||
| theme values registered</strong>. To create a builder **with** default values registered | ||||
| use `SpannableBuilder#builderWithDefaults(Context)` | ||||
| ::: | ||||
| 
 | ||||
| ## Link color | ||||
| 
 | ||||
| @ -107,7 +116,7 @@ The color of background of code block text | ||||
| 
 | ||||
| Leading margin for the block code content | ||||
| 
 | ||||
| <ThemeProperty name="codeMultilineMargin" type="@Px int" defaults="Width of the space character" /> | ||||
| <ThemeProperty name="codeMultilineMargin" type="@Px int" defaults="8dip" /> | ||||
| 
 | ||||
| ### Code typeface | ||||
| 
 | ||||
| @ -1,6 +1,6 @@ | ||||
| # MarkwonView | ||||
| 
 | ||||
| <MavenBadge artifact="markwon-view" /> | ||||
| <MavenBadge2xx artifact="markwon-view" /> | ||||
| 
 | ||||
| This is simple library containing 2 views that are able to display markdown: | ||||
| * MarkwonView - extends `android.view.TextView` | ||||
| @ -27,7 +27,8 @@ public interface IMarkwonView { | ||||
| 
 | ||||
| Both views support layout-preview in Android Studio (with some exceptions, for example, bold span is not rendered due to some limitations of layout preview). | ||||
| These are XML attributes: | ||||
| ``` | ||||
| 
 | ||||
| ```xml | ||||
| app:mv_markdown="string" | ||||
| app:mv_configurationProvider="string" | ||||
| ``` | ||||
							
								
								
									
										181
									
								
								docs/docs/v3/core/configuration.md
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @ -0,0 +1,181 @@ | ||||
| # Configuration | ||||
| 
 | ||||
| `MarkwonConfiguration` class holds common Markwon functionality. | ||||
| These are _configurable_ properties: | ||||
| * `SyntaxHighlight` | ||||
| * `LinkSpan.Resolver` | ||||
| * `UrlProcessor` | ||||
| * `ImageSizeResolver` | ||||
| * `MarkwonHtmlParser` | ||||
| 
 | ||||
| :::tip | ||||
| Additionally `MarkwonConfiguration` holds: | ||||
| * `MarkwonTheme` | ||||
| * `AsyncDrawableLoader` | ||||
| * `MarkwonHtmlRenderer` | ||||
| * `MarkwonSpansFactory` | ||||
| 
 | ||||
| Please note that these values can be retrieved from `MarkwonConfiguration` | ||||
| instance, but their _configuration_ must be done by a `Plugin` by overriding | ||||
| one of the methods: | ||||
| * `Plugin#configureTheme` | ||||
| * `Plugin#configureImages` | ||||
| * `Plugin#configureHtmlRenderer` | ||||
| * `Plugin#configureSpansFactory` | ||||
| ::: | ||||
| 
 | ||||
| ## SyntaxHighlight | ||||
| 
 | ||||
| ```java | ||||
| final Markwon markwon = Markwon.builder(this) | ||||
|         .usePlugin(new AbstractMarkwonPlugin() { | ||||
|             @Override | ||||
|             public void configureConfiguration(@NonNull MarkwonConfiguration.Builder builder) { | ||||
|                 builder.syntaxHighlight(new SyntaxHighlightNoOp()); | ||||
|             } | ||||
|         }) | ||||
|         .build(); | ||||
| ``` | ||||
| 
 | ||||
| :::tip | ||||
| Use [syntax-highlight](/docs/v3/syntax-highlight/) to add syntax highlighting | ||||
| to your application | ||||
| ::: | ||||
| 
 | ||||
| ## LinkSpan.Resolver | ||||
| 
 | ||||
| React to a link click event. By default `LinkResolverDef` is used, | ||||
| which tries to start an Activity given the `link` argument. If no | ||||
| Activity can handle `link` `LinkResolverDef` silently ignores click event | ||||
| 
 | ||||
| ```java | ||||
| final Markwon markwon = Markwon.builder(this) | ||||
|         .usePlugin(new AbstractMarkwonPlugin() { | ||||
|             @Override | ||||
|             public void configureConfiguration(@NonNull MarkwonConfiguration.Builder builder) { | ||||
|                 builder.linkResolver(new LinkSpan.Resolver() { | ||||
|                     @Override | ||||
|                     public void resolve(View view, @NonNull String link) { | ||||
|                         // react to link click here | ||||
|                     } | ||||
|                 }); | ||||
|             } | ||||
|         }) | ||||
|         .build(); | ||||
| ``` | ||||
| 
 | ||||
| :::tip | ||||
| Please note that `Markwon` will apply `LinkMovementMethod` to a resulting TextView | ||||
| if there is none registered. if you wish to register own instance of a `MovementMethod` | ||||
| apply it directly to a TextView or use [MovementMethodPlugin](/docs/v3/core/movement-method-plugin.md) | ||||
| ::: | ||||
| 
 | ||||
| ## UrlProcessor | ||||
| 
 | ||||
| Process URLs in your markdown (for links and images). If not provided explicitly,  | ||||
| default **no-op** implementation will be used, which does not modify URLs (keeping them as-is). | ||||
| 
 | ||||
| `Markwon` provides 2 implementations of `UrlProcessor`: | ||||
| * `UrlProcessorRelativeToAbsolute` | ||||
| * `UrlProcessorAndroidAssets` | ||||
| 
 | ||||
| ### UrlProcessorRelativeToAbsolute | ||||
| 
 | ||||
| `UrlProcessorRelativeToAbsolute` can be used to make relative URL absolute. For example if an image is | ||||
| defined like this: `` and `UrlProcessorRelativeToAbsolute` | ||||
| is created with `https://github.com/noties/Markwon/raw/master/` as the base:  | ||||
| `new UrlProcessorRelativeToAbsolute("https://github.com/noties/Markwon/raw/master/")`, | ||||
| then final image will have `https://github.com/noties/Markwon/raw/master/art/image.JPG` | ||||
| as the destination. | ||||
| 
 | ||||
| ### UrlProcessorAndroidAssets | ||||
| 
 | ||||
| `UrlProcessorAndroidAssets` can be used to make processed links to point to Android assets folder. | ||||
| So an image: `` will have `file:///android_asset/art/image.JPG` as the | ||||
| destination. | ||||
| 
 | ||||
| :::tip | ||||
| Please note that `UrlProcessorAndroidAssets` will process only URLs that have no `scheme` information, | ||||
| so a `./art/image.png` will become `file:///android_asset/art/image.JPG` whilst `https://so.me/where.png` | ||||
| will be kept as-is. | ||||
| ::: | ||||
| 
 | ||||
| :::warning | ||||
| In order to display an image from assets you still need to register `ImagesPlugin#createWithAssets(Context)` | ||||
| plugin in resulting `Markwon` instance. As `UrlProcessorAndroidAssets` only | ||||
| _processes_ URLs and doesn't take any part in displaying an image. | ||||
| ::: | ||||
| 
 | ||||
| 
 | ||||
| ## ImageSizeResolver | ||||
| 
 | ||||
| `ImageSizeResolver` controls the size of an image to be displayed. Currently it | ||||
| handles only HTML images (specified via `img` tag). | ||||
| 
 | ||||
| ```java | ||||
| final Markwon markwon = Markwon.builder(this) | ||||
|         .usePlugin(new AbstractMarkwonPlugin() { | ||||
|             @Override | ||||
|             public void configureConfiguration(@NonNull MarkwonConfiguration.Builder builder) { | ||||
|                 builder.imageSizeResolver(new ImageSizeResolver() { | ||||
|                     @NonNull | ||||
|                     @Override | ||||
|                     public Rect resolveImageSize( | ||||
|                             @Nullable ImageSize imageSize, | ||||
|                             @NonNull Rect imageBounds, | ||||
|                             int canvasWidth, | ||||
|                             float textSize) { | ||||
|                         return null; | ||||
|                     } | ||||
|                 }); | ||||
|             } | ||||
|         }) | ||||
|         .build(); | ||||
| ``` | ||||
| 
 | ||||
| If not provided explicitly, default `ImageSizeResolverDef` implementation will be used. | ||||
| It handles 3 dimension units: | ||||
| * `%` (percent, relative to Canvas width) | ||||
| * `em` (relative to text size) | ||||
| * `px` (absolute size, every dimension that is not `%` or `em` is considered to be _absolute_) | ||||
| 
 | ||||
| ```html | ||||
| <img width="100%"> | ||||
| <img width="2em" height="10px"> | ||||
| <img style="{width: 100%; height: 8em;}"> | ||||
| ``` | ||||
| 
 | ||||
| `ImageSizeResolverDef` keeps the ratio of original image if one of the dimensions is missing. | ||||
| 
 | ||||
| :::warning Height% | ||||
| There is no support for `%` units for `height` dimension. This is due to the fact that | ||||
| height of an TextView in which markdown is displayed is non-stable and changes with time | ||||
| (for example when image is loaded and applied to a TextView it will _increase_ TextView's height), | ||||
| so we will have no point-of-reference from which to _calculate_ image height. | ||||
| ::: | ||||
| 
 | ||||
| :::tip | ||||
| `ImageSizeResolverDef` also takes care for an image to **not** exceed | ||||
| canvas width. If an image has greater width than a TextView Canvas, then | ||||
| image will be _scaled-down_ to fit the canvas. Please note that this rule | ||||
| applies only if image has no absolute sizes (for example width is specified | ||||
| in pixels). | ||||
| ::: | ||||
| 
 | ||||
| ## MarkwonHtmlParser | ||||
| 
 | ||||
| Specify which HTML parser to use. Default implementation is **no-op**. | ||||
| 
 | ||||
| :::warning | ||||
| One must explicitly use [HtmlPlugin](/docs/v3/html/) in order to display | ||||
| HTML content in markdown. Without specified HTML parser **no HTML content | ||||
| will be rendered**. | ||||
| 
 | ||||
| ```java | ||||
| Markwon.builder(context) | ||||
|     .usePlugin(HtmlPlugin.create()) | ||||
| ``` | ||||
| 
 | ||||
| Please note that adding `HtmlPlugin` will take care of initializing parser, | ||||
| so after `HtmlPlugin` is used, no additional configuration steps are required. | ||||
| ::: | ||||
							
								
								
									
										105
									
								
								docs/docs/v3/core/core-plugin.md
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @ -0,0 +1,105 @@ | ||||
| # Core plugin <Badge text="3.0.0" /> | ||||
| 
 | ||||
| Since <Badge text="3.0.0" /> with introduction of _plugins_, Markwon | ||||
| **core** functionality was moved to a dedicated plugin. | ||||
| 
 | ||||
| ```java | ||||
| CorePlugin.create(); | ||||
| ``` | ||||
| 
 | ||||
| ## Node visitors | ||||
| 
 | ||||
| `CorePlugin` registers these `commonmark-java` node visitors: | ||||
| * `Text` | ||||
| * `StrongEmphasis` | ||||
| * `Emphasis` | ||||
| * `BlockQuote` | ||||
| * `Code` | ||||
| * `FencedCodeBlock` | ||||
| * `IndentedCodeBlock` | ||||
| * `BulletList` | ||||
| * `OrderedList` | ||||
| * `ListItem` | ||||
| * `ThematicBreak` | ||||
| * `Heading` | ||||
| * `SoftLineBreak` | ||||
| * `HardLineBreak` | ||||
| * `Paragraph` | ||||
| * `Link` | ||||
| 
 | ||||
| ## Span factories | ||||
| 
 | ||||
| `CorePlugin` adds these `SpanFactory`s: | ||||
| * `StrongEmphasis` | ||||
| * `Emphasis` | ||||
| * `BlockQuote` | ||||
| * `Code` | ||||
| * `FencedCodeBlock` | ||||
| * `IndentedCodeBlock` | ||||
| * `ListItem` | ||||
| * `Heading` | ||||
| * `Link` | ||||
| * `ThematicBreak` | ||||
| 
 | ||||
| 
 | ||||
| :::tip | ||||
| By default `CorePlugin` does not register a `Paragraph` `SpanFactory` but | ||||
| this can be done in your custom plugin: | ||||
| 
 | ||||
| ```java | ||||
| Markwon.builder(context) | ||||
|         .usePlugin(new AbstractMarkwonPlugin() { | ||||
|             @Override | ||||
|             public void configureSpansFactory(@NonNull MarkwonSpansFactory.Builder builder) { | ||||
|                 builder.setFactory(Paragraph.class, (configuration, props) ->  | ||||
|                         new ForegroundColorSpan(Color.RED)); | ||||
|             } | ||||
|         }) | ||||
| ``` | ||||
| ::: | ||||
| 
 | ||||
| ## Props | ||||
| These props are exported by `CorePlugin` and can be found in `CoreProps`: | ||||
| * `Prop<ListItemType> LIST_ITEM_TYPE` (BULLET | ORDERED) | ||||
| * `Prop<Integer> BULLET_LIST_ITEM_LEVEL` | ||||
| * `Prop<Integer> ORDERED_LIST_ITEM_NUMBER` | ||||
| * `Prop<Integer> HEADING_LEVEL` | ||||
| * `Prop<String> LINK_DESTINATION` | ||||
| * `Prop<Boolean> PARAGRAPH_IS_IN_TIGHT_LIST` | ||||
| 
 | ||||
| :::warning List item type | ||||
| Before <Badge text="3.0.0" /> `Markwon` had 2 distinct lists (bullet and ordered).  | ||||
| Since <Badge text="3.0.0" /> a single `SpanFactory` is used, which internally checks  | ||||
| for `Prop<ListItemType> LIST_ITEM_TYPE`. | ||||
| Beware of this if you would like to override only one of the list types. This is | ||||
| done to correspond to `commonmark-java` implementation. | ||||
| ::: | ||||
| 
 | ||||
| More information about props can be found [here](/docs/v3/core/render-props.md) | ||||
| 
 | ||||
| --- | ||||
| 
 | ||||
| :::tip Soft line break | ||||
| Since <Badge text="3.0.0" /> Markwon core does not give an option to | ||||
| insert a new line when there is a soft line break in markdown. Instead a | ||||
| custom plugin can be used: | ||||
| 
 | ||||
| ```java | ||||
| final Markwon markwon = Markwon.builder(this) | ||||
|         .usePlugin(new AbstractMarkwonPlugin() { | ||||
|             @Override | ||||
|             public void configureVisitor(@NonNull MarkwonVisitor.Builder builder) { | ||||
|                 builder.on(SoftLineBreak.class, (visitor, softLineBreak) -> | ||||
|                         visitor.forceNewLine()); | ||||
|             } | ||||
|         }) | ||||
|         .build(); | ||||
| ``` | ||||
| ::: | ||||
| 
 | ||||
| :::warning | ||||
| Please note that `CorePlugin` will implicitly set a `LinkMovementMethod` on a TextView | ||||
| if one is not present. If you wish to customize a MovementMethod that is used, apply | ||||
| one manually to a TextView (before applying markdown) or use the [MovementMethodPlugin](/docs/v3/core/movement-method-plugin.md) | ||||
| which accepts a MovementMethod as an argument. | ||||
| ::: | ||||
							
								
								
									
										65
									
								
								docs/docs/v3/core/getting-started.md
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @ -0,0 +1,65 @@ | ||||
| # Getting started | ||||
| 
 | ||||
| :::tip Installation | ||||
| Please follow [installation](/docs/v3/install.md) instructions | ||||
| to learn how to add `Markwon` to your project | ||||
| ::: | ||||
| 
 | ||||
| ## Quick one | ||||
| 
 | ||||
| This is the most simple way to set markdown to a `TextView` or any of its siblings: | ||||
| 
 | ||||
| ```java | ||||
| // obtain an instance of Markwon | ||||
| final Markwon markwon = Markwon.create(context); | ||||
| 
 | ||||
| // set markdown | ||||
| markwon.setMarkdown(textView, "**Hello there!**"); | ||||
| ``` | ||||
| 
 | ||||
| The most simple way to obtain markdown to be applied _somewhere_ else: | ||||
| 
 | ||||
| ```java | ||||
| // obtain an instance of Markwon | ||||
| final Markwon markwon = Markwon.create(context); | ||||
| 
 | ||||
| // parse markdown and create styled text | ||||
| final Spanned markdown = markwon.toMarkdown("**Hello there!**"); | ||||
| 
 | ||||
| // use it | ||||
| Toast.makeText(context, markdown, Toast.LENGTH_LONG).show(); | ||||
| ``` | ||||
| 
 | ||||
| :::warning 3.x.x migration | ||||
| Starting with <Badge text="3.0.0" /> version Markwon no longer relies on static | ||||
| utility methods. To learn more about migrating existing applications | ||||
| refer to [migration](/docs/v3/migration-2-3.md) section. | ||||
| ::: | ||||
| 
 | ||||
| ## Longer one | ||||
| 
 | ||||
| With explicit `parse` and `render` methods: | ||||
| 
 | ||||
| ```java | ||||
| // obtain an instance of Markwon | ||||
| final Markwon markwon = Markwon.create(context); | ||||
| 
 | ||||
| // parse markdown to commonmark-java Node | ||||
| final Node node = markwon.parse("Are **you** still there?"); | ||||
| 
 | ||||
| // create styled text from parsed Node | ||||
| final Spanned markdown = markwon.render(node); | ||||
| 
 | ||||
| // use it on a TextView | ||||
| markwon.setParsedMarkdown(textView, markdown); | ||||
| 
 | ||||
| // or a Toast | ||||
| Toast.makeText(context, markdown, Toast.LENGTH_LONG).show(); | ||||
| ``` | ||||
| 
 | ||||
| ## No magic one | ||||
| 
 | ||||
| This section is kept due to historical reasons. Starting with version <Badge text="3.0.0" /> | ||||
| the amount of magic is reduced. To leverage your `Markwon` usage a concept of `Plugin` | ||||
| is introduced which helps to extend default behavior in a simple and _no-breaking-the-flow_ manner. | ||||
| Head to the [next section](/docs/v3/core/plugins.md) to know more. | ||||
							
								
								
									
										101
									
								
								docs/docs/v3/core/html-renderer.md
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @ -0,0 +1,101 @@ | ||||
| # HTML Renderer | ||||
| 
 | ||||
| Starting with <Badge text="3.0.0" /> `MarkwonHtmlRenderer` controls how HTML | ||||
| is rendered: | ||||
| 
 | ||||
| ```java | ||||
| Markwon.builder(context) | ||||
|         .usePlugin(new AbstractMarkwonPlugin() { | ||||
|             @Override | ||||
|             public void configureHtmlRenderer(@NonNull MarkwonHtmlRenderer.Builder builder) { | ||||
|                 builder.setHandler("a", new MyTagHandler()); | ||||
|             } | ||||
|         }); | ||||
| ``` | ||||
| 
 | ||||
| :::danger | ||||
| Customizing `MarkwonHtmlRenderer` is not enough to include HTML content in your application. | ||||
| You must explicitly include [markwon-html](/docs/v3/html/) artifact (includes HtmlParser)  | ||||
| to your project and register `HtmlPlugin`: | ||||
| 
 | ||||
| ```java | ||||
| Markwon.builder(context) | ||||
|         .usePlugin(HtmlPlugin.create()) | ||||
| ``` | ||||
| ::: | ||||
| 
 | ||||
| For example, to create an `<a>` HTML tag handler: | ||||
| 
 | ||||
| ```java | ||||
| builder.setHandler("a", new SimpleTagHandler() { | ||||
|     @Override | ||||
|     public Object getSpans( | ||||
|             @NonNull MarkwonConfiguration configuration, | ||||
|             @NonNull RenderProps renderProps, | ||||
|             @NonNull HtmlTag tag) { | ||||
|         return new LinkSpan( | ||||
|                 configuration.theme(),  | ||||
|                 tag.attributes().get("href"),  | ||||
|                 configuration.linkResolver()); | ||||
|     } | ||||
| }); | ||||
| ``` | ||||
| 
 | ||||
| `SimpleTagHandler` can be used for simple cases when a tag does not require any special | ||||
| handling (like visiting it's children) | ||||
| 
 | ||||
| :::tip | ||||
| One can return `null` a single span or an array of spans from `getSpans` method | ||||
| ::: | ||||
| 
 | ||||
| For a more advanced usage `TagHandler` can be used directly: | ||||
| 
 | ||||
| ```java | ||||
| builder.setHandler("a", new TagHandler() { | ||||
|     @Override | ||||
|     public void handle(@NonNull MarkwonVisitor visitor, @NonNull MarkwonHtmlRenderer renderer, @NonNull HtmlTag tag) { | ||||
|          | ||||
|         // obtain default spanFactory for Link node | ||||
|         final SpanFactory factory = visitor.configuration().spansFactory().get(Link.class); | ||||
|          | ||||
|         if (factory != null) { | ||||
|              | ||||
|             // set destination property | ||||
|             CoreProps.LINK_DESTINATION.set( | ||||
|                     visitor.renderProps(),  | ||||
|                     tag.attributes().get("href")); | ||||
|              | ||||
|             // Obtain spans from the factory | ||||
|             final Object spans = factory.getSpans( | ||||
|                     visitor.configuration(),  | ||||
|                     visitor.renderProps()); | ||||
|              | ||||
|             // apply spans to SpannableBuilder | ||||
|             SpannableBuilder.setSpans( | ||||
|                     visitor.builder(),  | ||||
|                     spans,  | ||||
|                     tag.start(),  | ||||
|                     tag.end()); | ||||
|         } | ||||
|     } | ||||
| }); | ||||
| ``` | ||||
| 
 | ||||
| :::tip | ||||
| Sometimes HTML content might include tags that are not closed (although  | ||||
| they are required to be by the spec, for example a `div`). | ||||
| Markwon by default disallows such tags and ignores them. Still, | ||||
| there is an option to allow them _explicitly_ via builder method: | ||||
| ```java | ||||
| final Markwon markwon = Markwon.builder(context) | ||||
|         .usePlugin(new AbstractMarkwonPlugin() { | ||||
|             @Override | ||||
|             public void configureHtmlRenderer(@NonNull MarkwonHtmlRenderer.Builder builder) { | ||||
|                 builder.allowNonClosedTags(true); | ||||
|             } | ||||
|         }) | ||||
|         .build(); | ||||
| ``` | ||||
| Please note that if `allowNonClosedTags=true` then all non-closed tags will be closed | ||||
| at the end of a document. | ||||
| ::: | ||||
							
								
								
									
										205
									
								
								docs/docs/v3/core/images.md
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @ -0,0 +1,205 @@ | ||||
| # Images | ||||
| 
 | ||||
| Starting with <Badge text="3.0.0" /> `Markwon` comes with `ImagesPlugin` | ||||
| which supports `http(s)`, `file` and `data` schemes and default media | ||||
| decoder (for simple images, no [SVG](/docs/v3/image/svg.md) or [GIF](/docs/v3/image/gif.md) which | ||||
| are defined in standalone modules). | ||||
| 
 | ||||
| ## ImagesPlugin | ||||
| 
 | ||||
| `ImagePlugin` takes care of _obtaining_ image resource, decoding it and displaying it in a `TextView`. | ||||
| 
 | ||||
| :::warning | ||||
| Although `core` artifact contains `ImagesPlugin` one must  | ||||
| still **explicitly** register the `ImagesPlugin` on resulting `Markwon` | ||||
| instance. | ||||
| ```java | ||||
| final Markwon markwon = Markwon.builder(context) | ||||
|         .usePlugin(ImagesPlugin.create()) | ||||
| ``` | ||||
| ::: | ||||
| 
 | ||||
| There are 2 factory methods to obtain `ImagesPlugin`: | ||||
| * `ImagesPlugin#create(Context)` | ||||
| * `ImagesPlugin#createWithAssets(Context)` | ||||
| 
 | ||||
| The first one `#create(Context)` configures: | ||||
| * `FileSchemeHandler` that allows obtaining images from `file://` uris | ||||
| * `DataUriSchemeHandler` that allows _inlining_ images with `data:`  | ||||
|   scheme (`data:image/svg+xml;base64,MTIz`) | ||||
| * `NetworkSchemeHandler` that allows obtaining images from `http://` and `https://` uris | ||||
|   (internally it uses `HttpURLConnection`) | ||||
| * `ImageMediaDecoder` which _tries_ to decode all encountered images as regular ones (png, jpg, etc) | ||||
| 
 | ||||
| The second one `#createWithAssets(Context)` does the same but also adds support | ||||
| for images that reside in `assets` folder of your application and | ||||
| referenced by `file:///android_asset/{path}` uri. | ||||
| 
 | ||||
| `ImagesPlugin` also _prepares_ a TextView to display images. Due to asynchronous | ||||
| nature of image loading, there must be a way to invalidate resulting Spanned  | ||||
| content after an image is loaded. | ||||
| 
 | ||||
| :::warning | ||||
| Images come with few limitations. For of all, they work with a **TextView only**. | ||||
| This is due to the fact that there is no way to invalidate a `Spanned` content | ||||
| by itself (without context in which it is displayed). So, if `Markwon` is used, | ||||
| for example, to display a `Toast` with an image: | ||||
| 
 | ||||
| ```java | ||||
| final Spanned spanned = markwon.toMarkdown("Hello "); | ||||
| Toast.makeText(context, spanned, Toast.LENGTH_LONG).show(); | ||||
| ``` | ||||
| 
 | ||||
| Image _probably_ won't be displayed. As a workaround for `Toast` a custom `View` | ||||
| can be used: | ||||
| 
 | ||||
| ```java | ||||
| final Spanned spanned = markwon.toMarkdown("Hello "); | ||||
| 
 | ||||
| final View view = createToastView(); | ||||
| final TextView textView = view.findViewById(R.id.text_view); | ||||
| markwon.setParsedMarkdown(textView, spanned); | ||||
| 
 | ||||
| final Toast toast = new Toast(context); | ||||
| toast.setView(view); | ||||
| // other Toast configurations | ||||
| toast.show(); | ||||
| ``` | ||||
| ::: | ||||
| 
 | ||||
| ## SchemeHandler | ||||
| 
 | ||||
| To add support for different schemes (or customize provided) a `SchemeHandler` must be used. | ||||
| 
 | ||||
| ```java | ||||
| final Markwon markwon = Markwon.builder(context) | ||||
|         .usePlugin(ImagesPlugin.create(context)) | ||||
|         .usePlugin(new AbstractMarkwonPlugin() { | ||||
|             @Override | ||||
|             public void configureImages(@NonNull AsyncDrawableLoader.Builder builder) { | ||||
|                 // example only, Markwon doesn't come with a ftp scheme handler | ||||
|                 builder.addSchemeHandler("ftp", new FtpSchemeHandler()); | ||||
|             } | ||||
|         }) | ||||
|         .build(); | ||||
| ``` | ||||
| 
 | ||||
| It's a class to _convert_ an URI into an `InputStream`: | ||||
| 
 | ||||
| ```java | ||||
| public abstract class SchemeHandler { | ||||
| 
 | ||||
|     @Nullable | ||||
|     public abstract ImageItem handle(@NonNull String raw, @NonNull Uri uri); | ||||
| } | ||||
| ``` | ||||
| 
 | ||||
| `ImageItem` is a holder class for resulting `InputStream` and (optional) | ||||
| content type: | ||||
| 
 | ||||
| ```java | ||||
| public class ImageItem { | ||||
| 
 | ||||
|     private final String contentType; | ||||
|     private final InputStream inputStream; | ||||
| 
 | ||||
|     /* rest omitted */ | ||||
| } | ||||
| ``` | ||||
| 
 | ||||
| Based on `contentType` returned a corresponding `MediaDecoder` will be matched. | ||||
| If no `MediaDecoder` can handle given `contentType` then a default media decoder will | ||||
| be used. | ||||
| 
 | ||||
| ## MediaDecoder | ||||
| 
 | ||||
| By default `core` artifact comes with _default image decoder_ only. It's called | ||||
| `ImageMediaDecoder` and it can decode all the formats that `BitmapFactory#decodeStream(InputStream)` | ||||
| can. | ||||
| 
 | ||||
| ```java | ||||
| final Markwon markwon = Markwon.builder(this) | ||||
|         .usePlugin(ImagesPlugin.create(this)) | ||||
|         .usePlugin(new AbstractMarkwonPlugin() { | ||||
|             @Override | ||||
|             public void configureImages(@NonNull AsyncDrawableLoader.Builder builder) { | ||||
|                 builder.addMediaDecoder("text/plain", new TextPlainMediaDecoder()); | ||||
|             } | ||||
|         }) | ||||
|         .build(); | ||||
| 
 | ||||
| ``` | ||||
| 
 | ||||
| `MediaDecoder` is a class to turn `InputStream` into a `Drawable`: | ||||
| 
 | ||||
| ```java | ||||
| public abstract class MediaDecoder { | ||||
| 
 | ||||
|     @Nullable | ||||
|     public abstract Drawable decode(@NonNull InputStream inputStream); | ||||
| } | ||||
| ``` | ||||
| 
 | ||||
| :::tip | ||||
| If you want to display GIF or SVG images also, you can use [image-gif](/docs/v3/image/gif.md) | ||||
| and [image-svg](/docs/v3/image/svg.md) modules. | ||||
| ::: | ||||
| 
 | ||||
| :::tip | ||||
| If you are using [html](/docs/v3/html/) you do not have to additionally setup | ||||
| images displayed via `<img>` tag, as `HtmlPlugin` automatically uses configured | ||||
| image loader. But images referenced in HTML come with additional support for | ||||
| sizes, which is not supported natively by markdown, allowing absolute or relative sizes: | ||||
| 
 | ||||
| ```html | ||||
| <img src="./assets/my-image" width="100%"> | ||||
| ``` | ||||
| ::: | ||||
| 
 | ||||
| ## Placeholder drawable <Badge text="3.0.0" /> | ||||
| 
 | ||||
| It's possible to provide a custom placeholder for an image (whilst it's loading). | ||||
| 
 | ||||
| ```java | ||||
| final Markwon markwon = Markwon.builder(context) | ||||
|         .usePlugin(ImagesPlugin.create(context)) | ||||
|         .usePlugin(new AbstractMarkwonPlugin() { | ||||
|             @Override | ||||
|             public void configureImages(@NonNull AsyncDrawableLoader.Builder builder) { | ||||
|                 builder.placeholderDrawableProvider(new AsyncDrawableLoader.DrawableProvider() { | ||||
|                     @Override | ||||
|                     public Drawable provide() { | ||||
|                         // your custom placeholder drawable | ||||
|                         return new PlaceholderDrawable(); | ||||
|                     } | ||||
|                 }); | ||||
|             } | ||||
|         }); | ||||
| ``` | ||||
| 
 | ||||
| ## Error drawable <Badge text="3.0.0" /> | ||||
| 
 | ||||
| To fallback in case of error whilst loading an image, an `error drawable` can be used: | ||||
| 
 | ||||
| 
 | ||||
| ```java | ||||
| final Markwon markwon = Markwon.builder(context) | ||||
|         .usePlugin(ImagesPlugin.create(context)) | ||||
|         .usePlugin(new AbstractMarkwonPlugin() { | ||||
|             @Override | ||||
|             public void configureImages(@NonNull AsyncDrawableLoader.Builder builder) { | ||||
|                 builder.errorDrawableProvider(new AsyncDrawableLoader.DrawableProvider() { | ||||
|                     @Override | ||||
|                     public Drawable provide() { | ||||
|                         // your custom error drawable | ||||
|                         return new MyErrorDrawable(); | ||||
|                     } | ||||
|                 }); | ||||
|             } | ||||
|         }); | ||||
| ``` | ||||
| 
 | ||||
| :::warning | ||||
| Before `3.0.0` `AsyncDrawableLoader` accepted a simple `Drawable` as error drawable | ||||
| argument. Starting `3.0.0` it accepts a `DrawableProvider` instead. | ||||
| ::: | ||||
							
								
								
									
										17
									
								
								docs/docs/v3/core/movement-method-plugin.md
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @ -0,0 +1,17 @@ | ||||
| # Movement method plugin | ||||
| 
 | ||||
| `MovementMethodPlugin` can be used to apply a `MovementMethod` to a TextView | ||||
| (important if you have links inside your markdown). By default `CorePlugin` | ||||
| will set a `LinkMovementMethod` on a TextView if one is missing. If you have | ||||
| specific needs for a `MovementMethod` and `LinkMovementMethod` doesn't answer | ||||
| your needs use `MovementMethodPlugin`: | ||||
| 
 | ||||
| ```java | ||||
| Markwon.builder(context) | ||||
|         .usePlugin(MovementMethodPlugin.create(ScrollingMovementMethod.getInstance())) | ||||
| ``` | ||||
| 
 | ||||
| :::tip | ||||
| If you are having trouble with system `LinkMovementMethod` as an alternative | ||||
| [BetterLinkMovementMethod](https://github.com/saket/Better-Link-Movement-Method) library can be used. | ||||
| ::: | ||||
							
								
								
									
										467
									
								
								docs/docs/v3/core/plugins.md
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @ -0,0 +1,467 @@ | ||||
| # Plugins <Badge text="3.0.0" /> | ||||
| 
 | ||||
| Since <Badge text="3.0.0" /> `MarkwonPlugin` takes the key role in | ||||
| processing and rendering markdown. Even **core** functionaly is abstracted | ||||
| into a `CorePlugin`. So it's still possible to use `Markwon` with a completely | ||||
| own set of plugins. | ||||
| 
 | ||||
| To register a plugin `Markwon.Builder` must be used: | ||||
| 
 | ||||
| ```java | ||||
| Markwon.builder(context) | ||||
|     .usePlugin(CorePlugin.create()) | ||||
|     .build(); | ||||
| ``` | ||||
| 
 | ||||
| All the process of transforming _raw_ markdown into a styled text (Spanned) | ||||
| will go through plugins. A plugin can: | ||||
| 
 | ||||
| * [configure commonmark-java `Parser`](#parser) | ||||
| * [configure `MarkwonTheme`](#markwontheme) | ||||
| * [configure `AsyncDrawableLoader` (used to display images in markdown)](#images) | ||||
| * [configure `MarkwonConfiguration`](#configuration) | ||||
| * [configure `MarkwonVisitor` (extensible commonmark-java Node visitor)](#visitor) | ||||
| * [configure `MarkwonSpansFactory` (factory to hold spans information for each Node)](#spans-factory) | ||||
| * [configure `MarkwonHtmlRenderer` (utility to properly display HTML in markdown)](#html-renderer) | ||||
| 
 | ||||
| --- | ||||
| 
 | ||||
| * [declare a dependency on another plugin (will be used as a runtime validator)](#priority) | ||||
| 
 | ||||
| --- | ||||
| 
 | ||||
| * [process raw input markdown before parsing it](#process-markdown) | ||||
| * [inspect/modify commonmark-java Node after it's been parsed, but before rendering](#inspect-modify-node) | ||||
| * [inspect commonmark-java Node after it's been rendered](#inspect-node-after-render) | ||||
| * [prepare TextView to display markdown _before_ markdown is applied to a TextView](#prepare-textview) | ||||
| * [post-process TextView _after_ markdown was applied](#textview-after-markdown-applied) | ||||
| 
 | ||||
| :::tip | ||||
| if you need to override only few methods of `MarkwonPlugin` (since it is an interface), | ||||
| `AbstractMarkwonPlugin` can be used. | ||||
| ::: | ||||
| 
 | ||||
| ## Parser | ||||
| 
 | ||||
| For example, let's register a new commonmark-java Parser extension: | ||||
| 
 | ||||
| ```java | ||||
| final Markwon markwon = Markwon.builder(context) | ||||
|         .usePlugin(CorePlugin.create()) | ||||
|         .usePlugin(new AbstractMarkwonPlugin() { | ||||
|             @Override | ||||
|             public void configureParser(@NonNull Parser.Builder builder) { | ||||
|                 // no need to call `super.configureParser(builder)` | ||||
|                 builder.extensions(Collections.singleton(StrikethroughExtension.create())); | ||||
|             } | ||||
|         }) | ||||
|         .build(); | ||||
| ``` | ||||
| 
 | ||||
| There are no limitations on what to do with commonmark-java Parser. For more info | ||||
| _what_ can be done please refer to <Link name="commonmark-java" displayName="commonmark-java documentation" />. | ||||
| 
 | ||||
| ## MarkwonTheme | ||||
| 
 | ||||
| Starting <Badge text="3.0.0" /> `MarkwonTheme` represents _core_ theme. Aka theme for | ||||
| things core module knows of. For example it doesn't know anything about `strikethrough` | ||||
| or `tables` (as they belong to different modules). | ||||
| 
 | ||||
| ```java | ||||
| final Markwon markwon = Markwon.builder(context) | ||||
|         .usePlugin(new AbstractMarkwonPlugin() { | ||||
|             @Override | ||||
|             public void configureTheme(@NonNull MarkwonTheme.Builder builder) { | ||||
|                 builder | ||||
|                         .codeTextColor(Color.BLACK) | ||||
|                         .codeBackgroundColor(Color.GREEN); | ||||
|             } | ||||
|         }) | ||||
|         .build(); | ||||
| ``` | ||||
| 
 | ||||
| :::warning | ||||
| `CorePlugin` has special handling - it will be **implicitly** added | ||||
| if a plugin declares dependency on it. This is why in previous example we haven't | ||||
| added CorePlugin _explicitly_ as `AbstractMarkwonPlugin` declares a dependency on it. | ||||
| If it's not desireable override `AbstractMarkwonPlugin#priority` method to specify own rules. | ||||
| ::: | ||||
| 
 | ||||
| More information about `MarkwonTheme` can be found [here](/docs/v3/core/theme.md). | ||||
| 
 | ||||
| 
 | ||||
| ## Images | ||||
| 
 | ||||
| Since <Badge text="3.0.0" /> core images functionality moved to the `core` module. | ||||
| Now `Markwon` comes bundled with support for regular images (no `SVG` or `GIF`, they | ||||
| defined in standalone modules now). And 3(4) schemes supported by default: | ||||
| * http (+https; using system built-in `HttpURLConnection`) | ||||
| * file (including Android assets) | ||||
| * data (image inline, `data:image/svg+xml;base64,!@#$%^&*(`) | ||||
| 
 | ||||
| ```java | ||||
| final Markwon markwon = Markwon.builder(context) | ||||
|         .usePlugin(ImagesPlugin.create()) | ||||
|         .usePlugin(new AbstractMarkwonPlugin() { | ||||
|             @Override | ||||
|             public void configureImages(@NonNull AsyncDrawableLoader.Builder builder) { | ||||
|                 // sorry, these are not bundled with the library | ||||
|                 builder | ||||
|                         .addSchemeHandler("ftp", new FtpSchemeHandler("root", "")) | ||||
|                         .addMediaDecoder("text/plain", new AnsiiMediaDecoder()); | ||||
|             } | ||||
|         }) | ||||
|         .build(); | ||||
| ``` | ||||
| 
 | ||||
| :::warning | ||||
| Although `ImagesPlugin` is bundled with the `core` artifact, it is **not** used by default | ||||
| and one must **explicitly** add it: | ||||
| 
 | ||||
| ```java | ||||
| Markwon.builder(context) | ||||
|         .usePlugin(ImagesPlugin.create(context)); | ||||
| ``` | ||||
| 
 | ||||
| Without explicit usage of `ImagesPlugin` all image configuration will be ignored (no-op'ed) | ||||
| ::: | ||||
| 
 | ||||
| More information about dealing with images can be found [here](/docs/v3/core/images.md) | ||||
| 
 | ||||
| 
 | ||||
| ## Configuration | ||||
| 
 | ||||
| `MarkwonConfiguration` is a set of common tools that are used by different parts | ||||
| of `Markwon`. It allows configurations of these: | ||||
| 
 | ||||
| * `SyntaxHighlight` (highlighting code blocks) | ||||
| * `LinkResolver` (opens links in markdown) | ||||
| * `UrlProcessor` (process URLs in markdown for both links and images) | ||||
| * `MarkwonHtmlParser` (HTML parser) | ||||
| * `ImageSizeResolver` (resolve image sizes, like `fit-to-canvas`, etc) | ||||
| 
 | ||||
| ```java | ||||
| final Markwon markwon = Markwon.builder(context) | ||||
|         .usePlugin(new AbstractMarkwonPlugin() { | ||||
|             @Override | ||||
|             public void configureConfiguration(@NonNull MarkwonConfiguration.Builder builder) { | ||||
|                 // MarkwonHtmlParserImpl is defined in `markwon-html` artifact | ||||
|                 builder.htmlParser(MarkwonHtmlParserImpl.create()); | ||||
|             } | ||||
|         }) | ||||
|         .build(); | ||||
| ``` | ||||
| 
 | ||||
| More information about `MarkwonConfiguration` can be found [here](/docs/v3/core/configuration.md) | ||||
| 
 | ||||
| 
 | ||||
| ## Visitor | ||||
| 
 | ||||
| `MarkwonVisitor` <Badge text="3.0.0" /> is commonmark-java Visitor that allows | ||||
| configuration of how each Node is visited. There is no longer need to create | ||||
| own subclass of Visitor and override required methods (like in `2.x.x` versions). | ||||
| `MarkwonVisitor` also allows registration of Nodes, that `core` module knows | ||||
| nothing about (instead of relying on `visit(CustomNode)` method)). | ||||
| 
 | ||||
| For example, let's add `strikethrough` Node visitor: | ||||
| 
 | ||||
| ```java | ||||
| final Markwon markwon = Markwon.builder(context) | ||||
|         .usePlugin(new AbstractMarkwonPlugin() { | ||||
|             @Override | ||||
|             public void configureVisitor(@NonNull MarkwonVisitor.Builder builder) { | ||||
|                 builder | ||||
|                         .on(Strikethrough.class, new MarkwonVisitor.NodeVisitor<Strikethrough>() { | ||||
|                             @Override | ||||
|                             public void visit(@NonNull MarkwonVisitor visitor, @NonNull Strikethrough strikethrough) { | ||||
|                                 final int length = visitor.length(); | ||||
|                                 visitor.visitChildren(strikethrough); | ||||
|                                 visitor.setSpansForNodeOptional(strikethrough, length); | ||||
|                             } | ||||
|                         }); | ||||
|             } | ||||
|         }) | ||||
|         .build(); | ||||
| ``` | ||||
| 
 | ||||
| :::tip | ||||
| `MarkwonVisitor` also allows _overriding_ already registered nodes. For example, | ||||
| we can disable `Heading` Node rendering: | ||||
| 
 | ||||
| ```java | ||||
| builder.on(Heading.class, null); | ||||
| ``` | ||||
| 
 | ||||
| Please note that `Priority` plays nicely here to ensure that your | ||||
| custom Node override/disable happens _after_ some plugin defines it. | ||||
| ::: | ||||
| 
 | ||||
| More information about `MarkwonVisitor` can be found [here](/docs/v3/core/visitor.md) | ||||
| 
 | ||||
| 
 | ||||
| ## Spans Factory | ||||
| 
 | ||||
| `MarkwonSpansFactory` <Badge text="3.0.0" /> is an abstract factory (factory that produces other factories) | ||||
| for spans that `Markwon` uses. It controls what spans to use for certain Nodes. | ||||
| 
 | ||||
| ```java | ||||
| final Markwon markwon = Markwon.builder(context) | ||||
|         .usePlugin(new AbstractMarkwonPlugin() { | ||||
|             @Override | ||||
|             public void configureSpansFactory(@NonNull MarkwonSpansFactory.Builder builder) { | ||||
|                 // override emphasis factory to make all emphasis nodes underlined | ||||
|                 builder.setFactory(Emphasis.class, new SpanFactory() { | ||||
|                     @Override | ||||
|                     public Object getSpans(@NonNull MarkwonConfiguration configuration, @NonNull RenderProps props) { | ||||
|                         return new UnderlineSpan(); | ||||
|                     } | ||||
|                 }); | ||||
|             } | ||||
|         }) | ||||
|         .build(); | ||||
| ``` | ||||
| 
 | ||||
| :::tip | ||||
| `SpanFactory` allows to return an _array_ of spans to apply multiple spans | ||||
| for a Node: | ||||
| 
 | ||||
| ```java | ||||
| @Override | ||||
| public Object getSpans(@NonNull MarkwonConfiguration configuration, @NonNull RenderProps props) { | ||||
|     // make underlined and set text color to red | ||||
|     return new Object[]{ | ||||
|             new UnderlineSpan(), | ||||
|             new ForegroundColorSpan(Color.RED) | ||||
|     }; | ||||
| } | ||||
| ``` | ||||
| ::: | ||||
| 
 | ||||
| More information about spans factory can be found [here](/docs/v3/core/spans-factory.md) | ||||
| 
 | ||||
| 
 | ||||
| ## HTML Renderer | ||||
| 
 | ||||
| `MarkwonHtmlRenderer` controls how HTML is rendered in markdown. | ||||
| 
 | ||||
| ```java | ||||
| final Markwon markwon = Markwon.builder(context) | ||||
|         .usePlugin(HtmlPlugin.create()) | ||||
|         .usePlugin(new AbstractMarkwonPlugin() { | ||||
|             @Override | ||||
|             public void configureHtmlRenderer(@NonNull MarkwonHtmlRenderer.Builder builder) { | ||||
|                 // <center> tag handling (deprecated but valid in our case) | ||||
|                 // can be any tag name, there is no connection with _real_ HTML tags, | ||||
|                 // <just-try-to-not-go-crazy-and-remember-about-portability> | ||||
|                 builder.addHandler("center", new SimpleTagHandler() { | ||||
|                     @Override | ||||
|                     public Object getSpans(@NonNull MarkwonConfiguration configuration, @NonNull RenderProps renderProps, @NonNull HtmlTag tag) { | ||||
|                         return new AlignmentSpan() { | ||||
|                             @Override | ||||
|                             public Layout.Alignment getAlignment() { | ||||
|                                 return Layout.Alignment.ALIGN_CENTER; | ||||
|                             } | ||||
|                         }; | ||||
|                     } | ||||
|                 }); | ||||
|             } | ||||
|         }) | ||||
|         .build(); | ||||
| ``` | ||||
| 
 | ||||
| :::danger | ||||
| Although `MarkwonHtmlRenderer` is bundled with `core` artifact, actual | ||||
| HTML parser is placed in a standalone artifact and must be added to your | ||||
| project **explicitly** and then registered via `Markwon.Builder#usePlugin(HtmlPlugin.create())`. | ||||
| If not done so, no HTML will be parsed nor rendered. | ||||
| ::: | ||||
| 
 | ||||
| More information about HTML rendering can be found [here](/docs/v3/core/html-renderer.md) | ||||
| 
 | ||||
| 
 | ||||
| ## Priority | ||||
| 
 | ||||
| `Priority` is an abstraction to _state_ dependency connection between plugins. It is | ||||
| also used as a runtime graph validator. If a plugin defines a dependency on other, but | ||||
| _other_ is not in resulting `Markwon` instance, then a runtime exception will be thrown. | ||||
| `Priority` is also defines the order in which plugins will be placed. So, if a plugin `A` | ||||
| states a plugin `B` as a dependency, then plugin `A` will come **after** plugin `B`. | ||||
| 
 | ||||
| ```java | ||||
| final Markwon markwon = Markwon.builder(context) | ||||
|         .usePlugin(new AbstractMarkwonPlugin() { | ||||
|             @NonNull | ||||
|             @Override | ||||
|             public Priority priority() { | ||||
|                 return Priority.after(CorePlugin.class); | ||||
|             } | ||||
|         }) | ||||
|         .build(); | ||||
| ``` | ||||
| 
 | ||||
| :::warning | ||||
| Please note that `AbstractMarkwonPlugin` _implicitly_ defines `CorePlugin` | ||||
| as a dependency (`return Priority.after(CorePlugin.class);`). This will | ||||
| also add `CorePlugin` to a `Markwon` instance, because it will be added | ||||
| _implicitly_ if a plugin defines it as a dependency.  | ||||
| ::: | ||||
| 
 | ||||
| Use one of the factory methods to create a `Priority` instance: | ||||
| 
 | ||||
| ```java | ||||
| // none | ||||
| Priority.none(); | ||||
| 
 | ||||
| // single dependency | ||||
| Priority.after(CorePlugin.class); | ||||
| 
 | ||||
| // 2 dependencies | ||||
| Priority.after(CorePlugin.class, ImagesPlugin.class); | ||||
| 
 | ||||
| // for a number >2, use #builder | ||||
| Priority.builder() | ||||
|         .after(CorePlugin.class) | ||||
|         .after(ImagesPlugin.class) | ||||
|         .after(StrikethroughPlugin.class) | ||||
|         .build(); | ||||
| ``` | ||||
| 
 | ||||
| ## Process markdown | ||||
| 
 | ||||
| A plugin can be used to _pre-process_ input markdown (this will be called before _parsing_): | ||||
| 
 | ||||
| ```java | ||||
| final Markwon markwon = Markwon.builder(context) | ||||
|         .usePlugin(new AbstractMarkwonPlugin() { | ||||
|             @NonNull | ||||
|             @Override | ||||
|             public String processMarkdown(@NonNull String markdown) { | ||||
|                 return markdown.replaceAll("foo", "bar"); | ||||
|             } | ||||
|         }) | ||||
|         .build(); | ||||
| ``` | ||||
| 
 | ||||
| ## Inspect/modify Node | ||||
| 
 | ||||
| A plugin can inspect/modify commonmark-java Node _before_ it's being rendered. | ||||
| 
 | ||||
| ```java | ||||
| final Markwon markwon = Markwon.builder(context) | ||||
|         .usePlugin(new AbstractMarkwonPlugin() { | ||||
|             @Override | ||||
|             public void beforeRender(@NonNull Node node) { | ||||
| 
 | ||||
|                 // for example inspect it with custom visitor | ||||
|                 node.accept(new MyVisitor()); | ||||
| 
 | ||||
|                 // or modify (you know what you are doing, right?) | ||||
|                 node.appendChild(new Text("Appended")); | ||||
|             } | ||||
|         }) | ||||
|         .build(); | ||||
| ``` | ||||
| 
 | ||||
| ## Inspect Node after render | ||||
| 
 | ||||
| A plugin can inspect commonmark-java Node after it's been rendered. | ||||
| Modifying Node at this point makes not much sense (it's already been | ||||
| rendered and all modifications won't change anything). But this method can be used, | ||||
| for example, to clean-up some internal state (after rendering). Generally | ||||
| speaking, a plugin must be stateless, but if it cannot, then this method is | ||||
| the best place to clean-up. | ||||
| 
 | ||||
| ```java | ||||
| final Markwon markwon = Markwon.builder(context) | ||||
|         .usePlugin(new AbstractMarkwonPlugin() { | ||||
|             @Override | ||||
|             public void afterRender(@NonNull Node node, @NonNull MarkwonVisitor visitor) { | ||||
|                 cleanUp(); | ||||
|             } | ||||
|         }) | ||||
|         .build(); | ||||
| ``` | ||||
| 
 | ||||
| ## Prepare TextView | ||||
| 
 | ||||
| A plugin can _prepare_ a TextView before markdown is applied. For example `images` | ||||
| unschedules all previously scheduled `AsyncDrawableSpans` (if any) here. This way | ||||
| when new markdown (and set of Spannables) arrives, previous set won't be kept in | ||||
| memory and could be garbage-collected. | ||||
| 
 | ||||
| ```java | ||||
| final Markwon markwon = Markwon.builder(context) | ||||
|         .usePlugin(new AbstractMarkwonPlugin() { | ||||
|             @Override | ||||
|             public void beforeSetText(@NonNull TextView textView, @NonNull Spanned markdown) { | ||||
|                 // clean-up previous | ||||
|                 AsyncDrawableScheduler.unschedule(textView); | ||||
|             } | ||||
|         }) | ||||
|         .build(); | ||||
| ``` | ||||
| 
 | ||||
| ## TextView after markdown applied | ||||
| 
 | ||||
| A plugin will receive a callback _after_ markdown is applied to a TextView. | ||||
| For example `images` uses this callback to schedule new set of Spannables. | ||||
| 
 | ||||
| ```java | ||||
| final Markwon markwon = Markwon.builder(context) | ||||
|         .usePlugin(new AbstractMarkwonPlugin() { | ||||
|             @Override | ||||
|             public void afterSetText(@NonNull TextView textView) { | ||||
|                 AsyncDrawableScheduler.schedule(textView); | ||||
|             } | ||||
|         }) | ||||
|         .build(); | ||||
| ``` | ||||
| 
 | ||||
| :::tip | ||||
| Please note that unlike `#beforeSetText`, `#afterSetText` won't receive | ||||
| `Spanned` markdown. This happens because at this point spans must be | ||||
| queried directly from a TextView. | ||||
| ::: | ||||
| 
 | ||||
| ## What happens underneath | ||||
| 
 | ||||
| Here is what happens inside `Markwon` when `setMarkdown` method is called: | ||||
| 
 | ||||
| ```java | ||||
| // `Markwon#create` implicitly uses CorePlugin | ||||
| final Markwon markwon = Markwon.builder(context) | ||||
|         .usePlugin(CorePlugin.create()) | ||||
|         .build(); | ||||
| 
 | ||||
| // warning: pseudo-code | ||||
| 
 | ||||
| // 0. each plugin will be called to _pre-process_ raw input markdown | ||||
| rawInput = plugins.reduce(rawInput, (input, plugin) -> plugin.processMarkdown(input)); | ||||
| 
 | ||||
| // 1. after input is processed it's being parsed to a Node | ||||
| node = parser.parse(rawInput); | ||||
| 
 | ||||
| // 2. each plugin will be able to inspect or manipulate resulting Node | ||||
| //  before rendering | ||||
| plugins.forEach(plugin -> plugin.beforeRender(node)); | ||||
| 
 | ||||
| // 3. node is being visited by a visitor | ||||
| node.accept(visitor); | ||||
| 
 | ||||
| // 4. each plugin will be called after node is being visited (aka rendered) | ||||
| plugins.forEach(plugin -> plugin.afterRender(node, visitor)); | ||||
| 
 | ||||
| // 5. styled markdown ready at this point | ||||
| final Spanned markdown = visitor.markdown(); | ||||
| 
 | ||||
| // NB, points 6-8 are applied **only** if markdown is set to a TextView | ||||
| 
 | ||||
| // 6. each plugin will be called before styled markdown is applied to a TextView | ||||
| plugins.forEach(plugin -> plugin.beforeSetText(textView, markdown)); | ||||
| 
 | ||||
| // 7. markdown is applied to a TextView | ||||
| textView.setText(markdown); | ||||
| 
 | ||||
| // 8. each plugin will be called after markdown is applied to a TextView | ||||
| plugins.forEach(plugin -> plugin.afterSetText(textView)); | ||||
| ``` | ||||
							
								
								
									
										75
									
								
								docs/docs/v3/core/render-props.md
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @ -0,0 +1,75 @@ | ||||
| # RenderProps <Badge text="3.0.0" /> | ||||
| 
 | ||||
| `RenderProps` encapsulates passing arguments from a node visitor to a node renderer. | ||||
| Without hardcoding arguments into an API method calls. | ||||
| 
 | ||||
| `RenderProps` is the state collection for `Props` that are set by a node visitor and | ||||
| retrieved by a node renderer. | ||||
| 
 | ||||
| ```java | ||||
| public class Prop<T> { | ||||
| 
 | ||||
|     @NonNull | ||||
|     public static <T> Prop<T> of(@NonNull String name) { | ||||
|         return new Prop<>(name); | ||||
|     } | ||||
| 
 | ||||
|     /* ... */ | ||||
| } | ||||
| ``` | ||||
| 
 | ||||
| For example `CorePlugin` defines a _Heading level_ prop (inside `CoreProps` class): | ||||
| 
 | ||||
| ```java | ||||
| public static final Prop<Integer> HEADING_LEVEL = Prop.of("heading-level"); | ||||
| ``` | ||||
| 
 | ||||
| Then CorePlugin registers a `Heading` node visitor and applies heading value: | ||||
| 
 | ||||
| ```java | ||||
| @Override | ||||
| public void configureVisitor(@NonNull MarkwonVisitor.Builder builder) { | ||||
|     builder.on(Heading.class, new MarkwonVisitor.NodeVisitor<Heading>() { | ||||
|         @Override | ||||
|         public void visit(@NonNull MarkwonVisitor visitor, @NonNull Heading heading) { | ||||
|              | ||||
|             /* Heading node handling logic */ | ||||
| 
 | ||||
|             // set heading level | ||||
|             CoreProps.HEADING_LEVEL.set(visitor.renderProps(), heading.getLevel()); | ||||
|              | ||||
|             // a helper method to apply span(s) for a node  | ||||
|             // (internally obtains a SpanFactory for Heading or silently ignores | ||||
|             // this call if no factory for a Heading is registered) | ||||
|             visitor.setSpansForNodeOptional(heading, start); | ||||
| 
 | ||||
|             /* Heading node handling logic */ | ||||
|         } | ||||
|     }); | ||||
| } | ||||
| ``` | ||||
| 
 | ||||
| And finally `HeadingSpanFactory` (which is also registered by `CorePlugin`): | ||||
| 
 | ||||
| ```java | ||||
| public class HeadingSpanFactory implements SpanFactory { | ||||
|     @Nullable | ||||
|     @Override | ||||
|     public Object getSpans(@NonNull MarkwonConfiguration configuration, @NonNull RenderProps props) { | ||||
|         return new HeadingSpan( | ||||
|                 configuration.theme(), | ||||
|                 CoreProps.HEADING_LEVEL.require(props) | ||||
|         ); | ||||
|     } | ||||
| } | ||||
| ``` | ||||
| 
 | ||||
| --- | ||||
| 
 | ||||
| `Prop<T>` has these methods: | ||||
| 
 | ||||
| * `@Nullable T get(RenderProps)` - returns value stored in RenderProps or `null` if none is present | ||||
| * `@NonNull T get(RenderProps, @NonNull T defValue)` - returns value stored in RenderProps or default value (this method always return non-null value) | ||||
| * `@NonNull T require(RenderProps)` - returns value stored in RenderProps or _throws an exception_ if none is present | ||||
| * `void set(RenderProps, @Nullable T value)` - updates value stored in RenderProps, passing `null` as value is the same as calling `clear` | ||||
| * `void clear(RenderProps)` - clears value stored in RenderProps | ||||
							
								
								
									
										61
									
								
								docs/docs/v3/core/spans-factory.md
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @ -0,0 +1,61 @@ | ||||
| # Spans Factory | ||||
| 
 | ||||
| Starting with <Badge text="3.0.0" /> `MarkwonSpansFactory` controls what spans are displayed | ||||
| for markdown nodes. | ||||
| 
 | ||||
| ```java | ||||
| Markwon.builder(context) | ||||
|         .usePlugin(new AbstractMarkwonPlugin() { | ||||
|             @Override | ||||
|             public void configureSpansFactory(@NonNull MarkwonSpansFactory.Builder builder) { | ||||
|                 // passing null as second argument will remove previously added  | ||||
|                 // factory for the Link node | ||||
|                 builder.setFactory(Link.class, null); | ||||
|             } | ||||
|         }); | ||||
| ``` | ||||
| 
 | ||||
| ## SpanFactory | ||||
| 
 | ||||
| In order to create a _generic_ interface for all possible Nodes, a `SpanFactory` | ||||
| was added: | ||||
| 
 | ||||
| ```java | ||||
| builder.setFactory(Link.class, new SpanFactory() { | ||||
|     @Nullable | ||||
|     @Override | ||||
|     public Object getSpans(@NonNull MarkwonConfiguration configuration, @NonNull RenderProps props) { | ||||
|         return null; | ||||
|     } | ||||
| }); | ||||
| ``` | ||||
| 
 | ||||
| All possible arguments are passed via [RenderProps](/docs/v3/core/render-props.md): | ||||
| 
 | ||||
| ```java | ||||
| builder.setFactory(Link.class, new SpanFactory() { | ||||
|     @Override | ||||
|     public Object getSpans(@NonNull MarkwonConfiguration configuration, @NonNull RenderProps props) { | ||||
|         final String href = CoreProps.LINK_DESTINATION.require(props); | ||||
|         return new LinkSpan(configuration.theme(), href, configuration.linkResolver()); | ||||
|     } | ||||
| }); | ||||
| ``` | ||||
| 
 | ||||
| `SpanFactory` allows returning `null` for a certain span (no span will be applied). | ||||
| Or an array of spans: | ||||
| 
 | ||||
| ```java | ||||
| builder.setFactory(Link.class, new SpanFactory() { | ||||
|     @Override | ||||
|     public Object getSpans(@NonNull MarkwonConfiguration configuration, @NonNull RenderProps props) { | ||||
|         return new Object[]{ | ||||
|                 new LinkSpan( | ||||
|                         configuration.theme(), | ||||
|                         CoreProps.LINK_DESTINATION.require(props), | ||||
|                         configuration.linkResolver()), | ||||
|                 new ForegroundColorSpan(Color.RED) | ||||
|         }; | ||||
|     } | ||||
| }); | ||||
| ``` | ||||
							
								
								
									
										187
									
								
								docs/docs/v3/core/theme.md
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @ -0,0 +1,187 @@ | ||||
| # Theme | ||||
| 
 | ||||
| Here is the list of properties that can be configured via `MarkwonTheme.Builder` class.  | ||||
| 
 | ||||
| :::tip | ||||
| Starting with <Badge text="3.0.0" /> there is no need to manually construct a `MarkwonTheme`. | ||||
| Instead a `Plugin` should be used: | ||||
| ```java | ||||
| final Markwon markwon = Markwon.builder(context) | ||||
|         .usePlugin(new AbstractMarkwonPlugin() { | ||||
|             @Override | ||||
|             public void configureTheme(@NonNull MarkwonTheme.Builder builder) { | ||||
|                 builder | ||||
|                         .codeTextColor(Color.BLACK) | ||||
|                         .codeBackgroundColor(Color.GREEN); | ||||
|             } | ||||
|         }) | ||||
|         .build(); | ||||
| ``` | ||||
| ::: | ||||
| 
 | ||||
| ## Link color | ||||
| 
 | ||||
| Controls the color of a [link](#) | ||||
| 
 | ||||
| <ThemeProperty name="linkColor" type="@ColorInt int" defaults="Default link color of a context where markdown is displayed <sup>*</sup>" /> | ||||
| 
 | ||||
| <sup>*</sup> `TextPaint#linkColor` will be used to determine linkColor of a context | ||||
| 
 | ||||
| ## Block margin | ||||
| 
 | ||||
| Starting margin before text content for the: | ||||
| * lists | ||||
| * blockquotes | ||||
| * task lists | ||||
| 
 | ||||
| <ThemeProperty name="blockMargin" type="@Px int" defaults="24dp" /> | ||||
| 
 | ||||
| ## Block quote | ||||
| 
 | ||||
| Customizations for the `blockquote` stripe | ||||
| 
 | ||||
| > Quote | ||||
| 
 | ||||
| ### Stripe width | ||||
| 
 | ||||
| Width of a blockquote stripe | ||||
| 
 | ||||
| <ThemeProperty name="blockQuoteWidth" type="@Px int" defaults="1/4 of the <a href='#block-margin'>block margin</a>" /> | ||||
| 
 | ||||
| ### Stripe color | ||||
| 
 | ||||
| Color of a blockquote stripe | ||||
| 
 | ||||
| <ThemeProperty name="blockQuoteColor" type="@ColorInt int" defaults="textColor with <code>25</code> (0-255) alpha value" /> | ||||
| 
 | ||||
| ## List | ||||
| 
 | ||||
| ### List item color | ||||
| 
 | ||||
| Controls the color of a list item. For ordered list: leading number, | ||||
| for unordered list: bullet. | ||||
| 
 | ||||
| * UL | ||||
| 1. OL | ||||
| 
 | ||||
| <ThemeProperty name="listItemColor" type="@ColorInt int" defaults="Text color" /> | ||||
| 
 | ||||
| ### Bullet item stroke width | ||||
| 
 | ||||
| Border width of a bullet list item (level 2) | ||||
| 
 | ||||
| * First | ||||
| * * Second | ||||
| * * * Third | ||||
| 
 | ||||
| <ThemeProperty name="bulletListItemStrokeWidth" type="@Px int" defaults="Stroke width of TextPaint" /> | ||||
| 
 | ||||
| ### Bullet width | ||||
| 
 | ||||
| The width of the bullet item | ||||
| 
 | ||||
| * First | ||||
|   * Second | ||||
|     * Third | ||||
| 
 | ||||
| <ThemeProperty name="bulletWidth" type="@Px int" defaults="min(<a href='#block-margin'>blockMargin</a>, lineHeight) / 2" /> | ||||
| 
 | ||||
| ## Code | ||||
| 
 | ||||
| ### Inline code text color | ||||
| 
 | ||||
| The color of the `code` content | ||||
| 
 | ||||
| <ThemeProperty name="codeTextColor" type="@ColorInt int" defaults="Content text color" /> | ||||
| 
 | ||||
| ### Inline code background color | ||||
| 
 | ||||
| The color of `background` of a code content | ||||
| 
 | ||||
| <ThemeProperty name="codeBackgroundColor" type="@ColorInt int" defaults="<a href='#inline-code-text-color'>inline code text color</a> with 25 (0-255) alpha" /> | ||||
| 
 | ||||
| ### Block code text color | ||||
| 
 | ||||
| ``` | ||||
| The color of code block text | ||||
| ``` | ||||
| 
 | ||||
| <ThemeProperty name="codeBlockTextColor" type="@ColorInt int" defaults="<a href='#inline-code-text-color'>inline code text color</a>" /> | ||||
| 
 | ||||
| ### Block code background color | ||||
| 
 | ||||
| ``` | ||||
| The color of background of code block text | ||||
| ``` | ||||
| 
 | ||||
| <ThemeProperty name="codeBlockBackgroundColor" type="@ColorInt int" defaults="<a href='#inline-code-background-color'>inline code background color</a>" /> | ||||
| 
 | ||||
| ### Block code leading margin | ||||
| 
 | ||||
| Leading margin for the block code content | ||||
| 
 | ||||
| <ThemeProperty name="codeMultilineMargin" type="@Px int" defaults="8dip" /> | ||||
| 
 | ||||
| ### Code typeface | ||||
| 
 | ||||
| Typeface of code content | ||||
| 
 | ||||
| <ThemeProperty name="codeTypeface" type="android.graphics.Typeface" defaults="Typeface.MONOSPACE" /> | ||||
| 
 | ||||
| ### Block code typeface <Badge text="3.0.0" /> | ||||
| 
 | ||||
| Typeface of block code content | ||||
| 
 | ||||
| <ThemeProperty name="codeBlockTypeface" type="android.graphics.Typeface" defaults="<code>codeTypeface</code> if set or Typeface.MONOSPACE" /> | ||||
| 
 | ||||
| ### Code text size | ||||
| 
 | ||||
| Text size of code content | ||||
| 
 | ||||
| <ThemeProperty name="codeTextSize" type="@Px int" defaults="(Content text size) * 0.87 if no custom <a href='#code-typeface'>Typeface</a> was set, otherwise (content text size)" /> | ||||
| 
 | ||||
| ### Block code text size <Badge text="3.0.0" /> | ||||
| 
 | ||||
| Text size of block code content | ||||
| 
 | ||||
| <ThemeProperty name="codeBlockTextSize" type="@Px int" defaults="<code>codeTextSize</code> if set or (content text size) * 0.87 if no custom <a href='#code-typeface'>Typeface</a> was set, otherwise (content text size)" /> | ||||
| 
 | ||||
| ## Heading | ||||
| 
 | ||||
| ### Break height | ||||
| 
 | ||||
| The height of a brake under H1 & H2 | ||||
| 
 | ||||
| <ThemeProperty name="headingBreakHeight" type="@Px int" defaults="Stroke width of context TextPaint" /> | ||||
| 
 | ||||
| ### Break color | ||||
| 
 | ||||
| The color of a brake under H1 & H2 | ||||
| 
 | ||||
| <ThemeProperty name="headingBreakColor" type="@ColorInt int" defaults="(text color) with 75 (0-255) alpha" /> | ||||
| 
 | ||||
| ### Typeface <Badge text="1.1.0" /> | ||||
| 
 | ||||
| The typeface of heading elements | ||||
| 
 | ||||
| <ThemeProperty name="headingTypeface" type="android.graphics.Typeface" defaults="default text Typeface" /> | ||||
| 
 | ||||
| ### Text size <Badge text="1.1.0" /> | ||||
| 
 | ||||
| Array of heading text sizes _ratio_ that is applied to text size | ||||
| 
 | ||||
| <ThemeProperty name="headingTextSizeMultipliers" type="float[]" defaults="<code>{2.F, 1.5F, 1.17F, 1.F, .83F, .67F}</code> (HTML spec)" /> | ||||
| 
 | ||||
| ## Thematic break | ||||
| 
 | ||||
| ### Color | ||||
| 
 | ||||
| Color of a thematic break | ||||
| 
 | ||||
| <ThemeProperty name="thematicBreakColor" type="@ColorInt int" defaults="(text color) with 25 (0-255) alpha" /> | ||||
| 
 | ||||
| ### Height | ||||
| 
 | ||||
| Height of a thematic break | ||||
| 
 | ||||
| <ThemeProperty name="thematicBreakHeight" type="@Px int" defaults="Stroke width of context TextPaint" /> | ||||
							
								
								
									
										73
									
								
								docs/docs/v3/core/visitor.md
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @ -0,0 +1,73 @@ | ||||
| # Visitor | ||||
| 
 | ||||
| Starting with <Badge text="3.0.0" /> _visiting_ of parsed markdown | ||||
| nodes does not require creating own instance of commonmark-java `Visitor`, | ||||
| instead a composable/configurable `MarkwonVisitor` is used. | ||||
| 
 | ||||
| ## Visitor.Builder | ||||
| There is no need to create own instance of `MarkwonVisitor.Builder` as | ||||
| it is done by `Markwon` itself. One still can configure it as one wishes: | ||||
| 
 | ||||
| ```java | ||||
| final Markwon markwon = Markwon.builder(contex) | ||||
|         .usePlugin(new AbstractMarkwonPlugin() { | ||||
|             @Override | ||||
|             public void configureVisitor(@NonNull MarkwonVisitor.Builder builder) { | ||||
|                 builder.on(SoftLineBreak.class, new MarkwonVisitor.NodeVisitor<SoftLineBreak>() { | ||||
|                     @Override | ||||
|                     public void visit(@NonNull MarkwonVisitor visitor, @NonNull SoftLineBreak softLineBreak) { | ||||
|                         visitor.forceNewLine(); | ||||
|                     } | ||||
|                 }); | ||||
|             } | ||||
|         }); | ||||
| ``` | ||||
| 
 | ||||
| --- | ||||
| 
 | ||||
| `MarkwonVisitor` encapsulates most of the functionality of rendering parsed markdown. | ||||
| 
 | ||||
| It holds rendering configuration: | ||||
| * `MarkwonVisitor#configuration` - getter for current [MarkwonConfiguration](/docs/v3/core/configuration.md) | ||||
| * `MarkwonVisitor#renderProps` - getter for current [RenderProps](/docs/v3/core/render-props.md) | ||||
| * `MarkwonVisitor#builder` - getter for current `SpannableBuilder` | ||||
| 
 | ||||
| It contains also a number of utility functions: | ||||
| * `visitChildren(Node)` - will visit all children of supplied Node | ||||
| * `hasNext(Node)` - utility function to check if supplied Node has a Node after it (useful for white-space management, so there should be no blank new line after last BlockNode) | ||||
| * `ensureNewLine` - will insert a new line at current `SpannableBuilder` position only if current (last) character is not a new-line | ||||
| * `forceNewLine` - will insert a new line character without any condition checking | ||||
| * `length` - helper function to call `visitor.builder().length()`, returns current length of `SpannableBuilder` | ||||
| * `clear` - will clear state for `RenderProps` and `SpannableBuilder`, this is done by `Markwon` automatically after each render call | ||||
| 
 | ||||
| And some utility functions to control the spans: | ||||
| * `setSpans(int start, Object spans)` - will apply supplied `spans` on `SpannableBuilder` starting at `start` position and ending at `SpannableBuilder#length`. `spans` can be `null` (no spans will be applied) or an array of spans (each span of this array will be applied) | ||||
| * `setSpansForNodeOptional(N node, int start)` - helper method to set spans for specified `node` (internally obtains `SpanFactory` for that node and uses it to apply spans) | ||||
| * `setSpansForNode(N node, int start)` - almost the same as `setSpansForNodeOptional` but instead of silently ignoring call if none `SpanFactory` is registered, this method will throw an exception. | ||||
| 
 | ||||
| ```java | ||||
| @Override | ||||
| public void configureVisitor(@NonNull MarkwonVisitor.Builder builder) { | ||||
|     builder.on(Heading.class, new MarkwonVisitor.NodeVisitor<Heading>() { | ||||
|         @Override | ||||
|         public void visit(@NonNull MarkwonVisitor visitor, @NonNull Heading heading) { | ||||
| 
 | ||||
|             // or just `visitor.length()` | ||||
|             final int start = visitor.builder().length(); | ||||
| 
 | ||||
|             visitor.visitChildren(heading); | ||||
| 
 | ||||
|             // or just `visitor.setSpansForNodeOptional(heading, start)` | ||||
|             final SpanFactory factory = visitor.configuration().spansFactory().get(heading.getClass()); | ||||
|             if (factory != null) { | ||||
|                 visitor.setSpans(start, factory.getSpans(visitor.configuration(), visitor.renderProps())); | ||||
|             } | ||||
|              | ||||
|             if (visitor.hasNext(heading)) { | ||||
|                 visitor.ensureNewLine(); | ||||
|                 visitor.forceNewLine(); | ||||
|             } | ||||
|         } | ||||
|     }); | ||||
| } | ||||
| ``` | ||||
							
								
								
									
										46
									
								
								docs/docs/v3/ext-latex/README.md
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @ -0,0 +1,46 @@ | ||||
| # LaTeX extension | ||||
| 
 | ||||
| <MavenBadge :artifact="'ext-latex'" /> | ||||
| 
 | ||||
| This is an extension that will help you display LaTeX formulas in your markdown. | ||||
| Syntax is pretty simple: pre-fix and post-fix your latex with `$$` (double dollar sign). | ||||
| `$$` should be the first characters in a line. | ||||
| 
 | ||||
| ```markdown | ||||
| $$ | ||||
| \\text{A long division \\longdiv{12345}{13} | ||||
| $$ | ||||
| ``` | ||||
| 
 | ||||
| ```markdown | ||||
| $$\\text{A long division \\longdiv{12345}{13}$$ | ||||
| ``` | ||||
| 
 | ||||
| ```java | ||||
| Markwon.builder(context) | ||||
|     .use(ImagesPlugin.create(context)) | ||||
|     .use(JLatexMathPlugin.create(textSize)) | ||||
|     .build(); | ||||
| ``` | ||||
| 
 | ||||
| This extension uses [jlatexmath-android](https://github.com/noties/jlatexmath-android) artifact to create LaTeX drawable. Then it | ||||
| registers special `latex` image scheme handler and uses `AsyncDrawableLoader` to display | ||||
| final result | ||||
| 
 | ||||
| ## Config | ||||
| 
 | ||||
| ```java | ||||
| final Markwon markwon = Markwon.builder(context) | ||||
|         .usePlugin(ImagesPlugin.create(context)) | ||||
|         .usePlugin(JLatexMathPlugin.create(textSize, new BuilderConfigure() { | ||||
|             @Override | ||||
|             public void configureBuilder(@NonNull Builder builder) { | ||||
|                 builder | ||||
|                         .background(backgroundDrawable) | ||||
|                         .align(JLatexMathDrawable.ALIGN_CENTER) | ||||
|                         .fitCanvas(true) | ||||
|                         .padding(paddingPx); | ||||
|             } | ||||
|         })) | ||||
|         .build(); | ||||
| ``` | ||||
							
								
								
									
										29
									
								
								docs/docs/v3/ext-strikethrough/README.md
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @ -0,0 +1,29 @@ | ||||
| # Strikethrough extension | ||||
| 
 | ||||
| <MavenBadge :artifact="'ext-strikethrough'" /> | ||||
| 
 | ||||
| This module adds `strikethrough` functionality to `Markwon` via `StrikethroughPlugin`: | ||||
| 
 | ||||
| ```java | ||||
| Markwon.builder(context) | ||||
|     .usePlugin(StrikethroughPlugin.create()) | ||||
| ``` | ||||
| 
 | ||||
| This plugin registers `SpanFactory` for `Strikethrough` node, so it's possible to customize Strikethrough Span that is used in rendering: | ||||
| 
 | ||||
| ```java | ||||
| Markwon.builder(context) | ||||
|         .usePlugin(StrikethroughPlugin.create()) | ||||
|         .usePlugin(new AbstractMarkwonPlugin() { | ||||
|             @Override | ||||
|             public void configureSpansFactory(@NonNull MarkwonSpansFactory.Builder builder) { | ||||
|                 builder.setFactory(Strikethrough.class, new SpanFactory() { | ||||
|                     @Override | ||||
|                     public Object getSpans(@NonNull MarkwonConfiguration configuration, @NonNull RenderProps props) { | ||||
|                         // will use Underline span instead of Strikethrough | ||||
|                         return new UnderlineSpan(); | ||||
|                     } | ||||
|                 }); | ||||
|             } | ||||
|         }) | ||||
| ``` | ||||
							
								
								
									
										99
									
								
								docs/docs/v3/ext-tables/README.md
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @ -0,0 +1,99 @@ | ||||
| # Tables extension | ||||
| 
 | ||||
| <MavenBadge :artifact="'ext-tables'" /> | ||||
| 
 | ||||
| This extension adds support for GFM tables. | ||||
| 
 | ||||
| ```java | ||||
| final Markwon markwon = Markwon.builder(context) | ||||
|         // create default instance of TablePlugin | ||||
|         .usePlugin(TablePlugin.create(context)) | ||||
| ``` | ||||
| 
 | ||||
| ```java | ||||
| final TableTheme tableTheme = TableTheme.builder() | ||||
|         .tableBorderColor(Color.RED) | ||||
|         .tableBorderWidth(0) | ||||
|         .tableCellPadding(0) | ||||
|         .tableHeaderRowBackgroundColor(Color.BLACK) | ||||
|         .tableEvenRowBackgroundColor(Color.GREEN) | ||||
|         .tableOddRowBackgroundColor(Color.YELLOW) | ||||
|         .build(); | ||||
| 
 | ||||
| final Markwon markwon = Markwon.builder(context) | ||||
|         .usePlugin(TablePlugin.create(tableTheme)) | ||||
| ``` | ||||
| 
 | ||||
| ```java | ||||
| Markwon.builder(context) | ||||
|         .usePlugin(TablePlugin.create(builder -> | ||||
|                 builder | ||||
|                         .tableBorderColor(Color.RED) | ||||
|                         .tableBorderWidth(0) | ||||
|                         .tableCellPadding(0) | ||||
|                         .tableHeaderRowBackgroundColor(Color.BLACK) | ||||
|                         .tableEvenRowBackgroundColor(Color.GREEN) | ||||
|                         .tableOddRowBackgroundColor(Color.YELLOW) | ||||
| )) | ||||
| ``` | ||||
| 
 | ||||
| Please note, that _by default_ tables have limitations. For example, there is no support | ||||
| for images inside table cells. And table contents won't be copied to clipboard if a TextView | ||||
| has such functionality. Table will always take full width of a TextView in which it is displayed. | ||||
| All columns will always be the of the same width. So, _default_ implementation provides basic | ||||
| functionality which can answer some needs. These all come from the limited nature of the TextView | ||||
| to display such content. | ||||
| 
 | ||||
| In order to provide full-fledged experience, tables must be displayed in a special widget. | ||||
| Since version `3.0.0` Markwon provides a special artifact `markwon-recycler` that allows | ||||
| to render markdown in a set of widgets in a RecyclerView. It also gives ability to change | ||||
| display widget form TextView to any other. | ||||
| 
 | ||||
| ```java | ||||
| final Table table = Table.parse(Markwon, TableBlock); | ||||
| myTableWidget.setTable(table); | ||||
| ``` | ||||
| 
 | ||||
| :::tip | ||||
| To take advantage of this functionality and render tables without limitations (including | ||||
| horizontally scrollable layout when its contents exceed screen width), refer to [recycler-table](/docs/v3/recycler-table/) | ||||
| module documentation that adds support for rendering `TableBlock` markdown node inside Android-native `TableLayout` widget. | ||||
| ::: | ||||
| 
 | ||||
| ## Theme | ||||
| 
 | ||||
| ### Cell padding | ||||
| 
 | ||||
| Padding inside a table cell | ||||
| 
 | ||||
| <ThemeProperty name="tableCellPadding" type="@Px int" defaults="0" /> | ||||
| 
 | ||||
| ### Border color | ||||
| 
 | ||||
| The color of table borders | ||||
| 
 | ||||
| <ThemeProperty name="tableBorderColor" type="@ColorInt int" defaults="(text color) with 75 (0-255) alpha" /> | ||||
| 
 | ||||
| ### Border width | ||||
| 
 | ||||
| The width of table borders | ||||
| 
 | ||||
| <ThemeProperty name="tableBorderWidth" type="@Px int" defaults="Stroke with of context TextPaint" /> | ||||
| 
 | ||||
| ### Odd row background | ||||
| 
 | ||||
| Background of an odd table row | ||||
| 
 | ||||
| <ThemeProperty name="tableOddRowBackgroundColor" type="@ColorInt int" defaults="(text color) with 22 (0-255) alpha" /> | ||||
| 
 | ||||
| ### Even row background <Badge text="1.1.1" /> | ||||
| 
 | ||||
| Background of an even table row | ||||
| 
 | ||||
| <ThemeProperty name="tableEventRowBackgroundColor" type="@ColorInt int" defaults="0" /> | ||||
| 
 | ||||
| ### Header row background <Badge text="1.1.1" /> | ||||
| 
 | ||||
| Background of header table row | ||||
| 
 | ||||
| <ThemeProperty name="tableHeaderRowBackgroundColor" type="@ColorInt int" defaults="0" /> | ||||
							
								
								
									
										146
									
								
								docs/docs/v3/ext-tasklist/README.md
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @ -0,0 +1,146 @@ | ||||
| # Task list extension | ||||
| 
 | ||||
| <MavenBadge :artifact="'ext-tasklist'" /> | ||||
| 
 | ||||
| Adds support for GFM (Github-flavored markdown) task-lists: | ||||
| 
 | ||||
| ```java | ||||
| Markwon.builder(context) | ||||
|         .usePlugin(TaskListPlugin.create(context)); | ||||
| ``` | ||||
| 
 | ||||
| --- | ||||
| 
 | ||||
| Create a default instance of `TaskListPlugin` with `TaskListDrawable` initialized to use | ||||
| `android.R.attr.textColorLink` as primary color and `android.R.attr.colorBackground` as background | ||||
| ```java | ||||
| TaskListPlugin.create(context); | ||||
| ``` | ||||
| 
 | ||||
| --- | ||||
| 
 | ||||
| Create an instance of `TaskListPlugin` with exact color values to use: | ||||
| ```java | ||||
| // obtain color values | ||||
| final int checkedFillColor = /* */; | ||||
| final int normalOutlineColor = /* */; | ||||
| final int checkMarkColor = /* */; | ||||
| 
 | ||||
| TaskListPlugin.create(checkedFillColor, normalOutlineColor, checkMarkColor); | ||||
| ``` | ||||
| 
 | ||||
| --- | ||||
| 
 | ||||
| Specify own drawable for a task list item: | ||||
| 
 | ||||
| ```java | ||||
| // obtain drawable | ||||
| final Drawable drawable = /* */; | ||||
| 
 | ||||
| TaskListPlugin.create(drawable); | ||||
| ``` | ||||
| 
 | ||||
| :::warning | ||||
| Please note that custom drawable for a task list item must correctly handle state | ||||
| in order to display done/not-done: | ||||
| 
 | ||||
| ```java | ||||
| public class MyTaskListDrawable extends Drawable { | ||||
| 
 | ||||
|     private boolean isChecked; | ||||
| 
 | ||||
|     @Override | ||||
|     public void draw(@NonNull Canvas canvas) { | ||||
|         // draw accordingly to the isChecked value | ||||
|     } | ||||
|      | ||||
|     /* implementation omitted */ | ||||
| 
 | ||||
|     @Override | ||||
|     protected boolean onStateChange(int[] state) { | ||||
|         final boolean isChecked = contains(state, android.R.attr.state_checked); | ||||
|         final boolean result = this.isChecked != isChecked; | ||||
|         if (result) { | ||||
|             this.isChecked = isChecked; | ||||
|         } | ||||
|         return result; | ||||
|     } | ||||
| 
 | ||||
|     private static boolean contains(@Nullable int[] states, int value) { | ||||
|         if (states != null) { | ||||
|             for (int state : states) { | ||||
|                 if (state == value) { | ||||
|                     // NB return here | ||||
|                     return true; | ||||
|                 } | ||||
|             } | ||||
|         } | ||||
|         return false; | ||||
|     } | ||||
| } | ||||
| ``` | ||||
| ::: | ||||
| 
 | ||||
| ## Task list mutation | ||||
| 
 | ||||
| It is possible to mutate task list item state (toggle done/not-done). But note | ||||
| that `Markwon` won't handle state change internally by any means and this change | ||||
| is merely a visual one. If you need to persist state of a task list | ||||
| item change you have to implement it yourself. This should get your started: | ||||
| 
 | ||||
| ```java | ||||
| final Markwon markwon = Markwon.builder(context) | ||||
|         .usePlugin(TaskListPlugin.create(context)) | ||||
|         .usePlugin(new AbstractMarkwonPlugin() { | ||||
|             @Override | ||||
|             public void configureSpansFactory(@NonNull MarkwonSpansFactory.Builder builder) { | ||||
| 
 | ||||
|                 // obtain original SpanFactory set by TaskListPlugin | ||||
|                 final SpanFactory origin = builder.getFactory(TaskListItem.class); | ||||
|                 if (origin == null) { | ||||
|                     // or throw, as it's a bit weird state and we expect | ||||
|                     // this factory to be present | ||||
|                     return; | ||||
|                 } | ||||
| 
 | ||||
|                 builder.setFactory(TaskListItem.class, new SpanFactory() { | ||||
|                     @Override | ||||
|                     public Object getSpans(@NonNull MarkwonConfiguration configuration, @NonNull RenderProps props) { | ||||
|                         // it's a bit non-secure behavior and we should validate | ||||
|                         // the type of returned span first, but for the sake of brevity | ||||
|                         // we skip this step | ||||
|                         final TaskListSpan span = (TaskListSpan) origin.getSpans(configuration, props); | ||||
| 
 | ||||
|                         if (span == null) { | ||||
|                             // or throw | ||||
|                             return null; | ||||
|                         } | ||||
| 
 | ||||
|                         // return an array of spans | ||||
|                         return new Object[]{ | ||||
|                                 span, | ||||
|                                 new ClickableSpan() { | ||||
|                                     @Override | ||||
|                                     public void onClick(@NonNull View widget) { | ||||
|                                         // toggle VISUAL state | ||||
|                                         span.setDone(!span.isDone()); | ||||
| 
 | ||||
|                                         // do not forget to invalidate widget | ||||
|                                         widget.invalidate(); | ||||
| 
 | ||||
|                                         // execute your persistence logic | ||||
|                                     } | ||||
| 
 | ||||
|                                     @Override | ||||
|                                     public void updateDrawState(@NonNull TextPaint ds) { | ||||
|                                         // no-op, so appearance is not changed (otherwise | ||||
|                                         // task list item will look like a link) | ||||
|                                     } | ||||
|                                 } | ||||
|                         }; | ||||
|                     } | ||||
|                 }); | ||||
|             } | ||||
|         }) | ||||
|         .build(); | ||||
| ``` | ||||
							
								
								
									
										63
									
								
								docs/docs/v3/html/README.md
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @ -0,0 +1,63 @@ | ||||
| # HTML | ||||
| 
 | ||||
| This artifact encapsulates HTML parsing from the core artifact and provides | ||||
| few predefined `TagHandlers` | ||||
| 
 | ||||
| ```java | ||||
| final Markwon markwon = Markwon.builder(context) | ||||
|         .usePlugin(HtmlPlugin.create()) | ||||
|         .build(); | ||||
| ``` | ||||
| 
 | ||||
| As this artifact brings modified [jsoup](https://github.com/jhy/jsoup) library  | ||||
| it was moved to a standalone module in order to minimize dependencies and unused code | ||||
| in applications that does not require HTML render capabilities. | ||||
| 
 | ||||
| Before <Badge text="2.0.0" /> `Markwon` used android `Html` class for parsing and | ||||
| rendering. Unfortunately, according to markdown specification, markdown can contain | ||||
| HTML in _unpredictable_ way if rendered _outside_ of browser. For example: | ||||
| 
 | ||||
| ```markdown{4} | ||||
| <i> | ||||
| Hello from italics tag | ||||
| 
 | ||||
| </i><b>bold></b> | ||||
| ``` | ||||
| 
 | ||||
| This snippet could be represented as: | ||||
| * HtmlBlock (`<i>\nHello from italics tag`) | ||||
| * HtmlInline (`<i>`) | ||||
| * HtmlInline (`<b>`) | ||||
| * Text (`bold`) | ||||
| * HtmlInline (`</b>`) | ||||
| 
 | ||||
| :::tip A bit of background | ||||
| <br> | ||||
| <GithubIssue id="52" displayName="This issue" /> had brought attention to differences between HTML & commonmark implementations. <br><br> | ||||
| ::: | ||||
| 
 | ||||
| Unfortunately Android `HTML` class cannot parse a _fragment_ of HTML to later | ||||
| be included in a bigger set of content. This is why the decision was made to bring | ||||
| HTML parsing _in-markwon-house_ | ||||
| 
 | ||||
| ## Predefined TagHandlers | ||||
| * `<img>` | ||||
| * `<a>` | ||||
| * `<blockquote>` | ||||
| * `<sub>` | ||||
| * `<sup>` | ||||
| * `<b>, <strong>` | ||||
| * `<s>, <del>` | ||||
| * `<u>, <ins>` | ||||
| * `<ul>, <ol>` | ||||
| * `<i>, <cite>, <em>, <dfn>` | ||||
| * `<h1>, <h2>, <h3>, <h4>, <h5>, <h6>` | ||||
| 
 | ||||
| :::tip | ||||
| All predefined tag handlers will use styling spans for native markdown content. | ||||
| So, if your `Markwon` instance was configured to, for example, render Emphasis | ||||
| nodes as a <span style="color: #FF0000">red text</span> then HTML tag handler will | ||||
| use the same span. This includes images, links, UrlResolver, LinkProcessor, etc | ||||
| ::: | ||||
| 
 | ||||
| To learn more about defining own TagHandlers, please refer to [html-renderer docs](/docs/v3/core/html-renderer.md) | ||||
							
								
								
									
										15
									
								
								docs/docs/v3/image/gif.md
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @ -0,0 +1,15 @@ | ||||
| # Image GIF | ||||
| 
 | ||||
| <MavenBadge :artifact="'image-gif'" /> | ||||
| 
 | ||||
| Adds support for GIF images inside markdown.  | ||||
| Relies on [android-gif-drawable library](https://github.com/koral--/android-gif-drawable) | ||||
| 
 | ||||
| ```java | ||||
| final Markwon markwon = Markwon.builder(context) | ||||
|         // it's required to register ImagesPlugin | ||||
|         .usePlugin(ImagesPlugin.create(context)) | ||||
|         // add GIF support for images | ||||
|         .usePlugin(GifPlugin.create()) | ||||
|         .build(); | ||||
| ``` | ||||
							
								
								
									
										28
									
								
								docs/docs/v3/image/okhttp.md
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @ -0,0 +1,28 @@ | ||||
| # Image OkHttp | ||||
| 
 | ||||
| <MavenBadge :artifact="'image-okhttp'" /> | ||||
| 
 | ||||
| Uses [okhttp library](https://github.com/square/okhttp) as the network transport fro images. Since <Badge text="3.0.0" /> | ||||
| `Markwon` uses a system-native `HttpUrlConnection` and does not rely on any | ||||
| 3rd-party tool to download resources from network. It can answer the most common needs, | ||||
| but if you would like to have a custom redirect policy or add an explicit caching | ||||
| of downloaded resources OkHttp might be a better option. | ||||
| 
 | ||||
| ```java | ||||
| final Markwon markwon = Markwon.builder(context) | ||||
|         // it's required to register ImagesPlugin | ||||
|         .usePlugin(ImagesPlugin.create(context)) | ||||
|          | ||||
|         // will create default instance of OkHttpClient | ||||
|         .usePlugin(OkHttpImagesPlugin.create()) | ||||
|          | ||||
|         // or accept a configured client | ||||
|         .usePlugin(OkHttpImagesPlugin.create(new OkHttpClient())) | ||||
|         .build(); | ||||
| ``` | ||||
| 
 | ||||
| ## Proguard | ||||
| ```proguard | ||||
| -dontwarn okhttp3.** | ||||
| -dontwarn okio.** | ||||
| ``` | ||||
							
								
								
									
										25
									
								
								docs/docs/v3/image/svg.md
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @ -0,0 +1,25 @@ | ||||
| # Image SVG | ||||
| 
 | ||||
| <MavenBadge :artifact="'image-svg'" /> | ||||
| 
 | ||||
| Adds support for SVG images inside markdown.  | ||||
| Relies on [androidsvg library](https://github.com/BigBadaboom/androidsvg) | ||||
| 
 | ||||
| ```java | ||||
| final Markwon markwon = Markwon.builder(context) | ||||
|         // it's required to register ImagesPlugin | ||||
|         .usePlugin(ImagesPlugin.create(context)) | ||||
|         .usePlugin(SvgPlugin.create(context.getResources())) | ||||
|         .build(); | ||||
| ``` | ||||
| 
 | ||||
| :::tip | ||||
| `SvgPlugin` requires `Resources` in order to scale SVG media based on display density | ||||
| ::: | ||||
| 
 | ||||
| ## Proguard | ||||
| 
 | ||||
| ```proguard | ||||
| -keep class com.caverock.androidsvg.** { *; } | ||||
| -dontwarn com.caverock.androidsvg.** | ||||
| ``` | ||||
							
								
								
									
										34
									
								
								docs/docs/v3/install.md
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @ -0,0 +1,34 @@ | ||||
| --- | ||||
| prev: false | ||||
| next: /docs/v3/core/getting-started.md | ||||
| --- | ||||
| 
 | ||||
| # Installation | ||||
| 
 | ||||
|  | ||||
|  | ||||
| 
 | ||||
| <ArtifactPicker /> | ||||
| 
 | ||||
| ## Snapshot | ||||
| 
 | ||||
| In order to use latest `SNAPSHOT` version add snapshot repository  | ||||
| to your root project's `build.gradle` file: | ||||
| 
 | ||||
| ```groovy | ||||
| allprojects { | ||||
|     repositories { | ||||
|         jcenter() | ||||
|         google() | ||||
|         // this one 👇 | ||||
|         maven { url 'https://oss.sonatype.org/content/repositories/snapshots/' } // 👈 this one | ||||
|         // this one 👆 | ||||
|     } | ||||
| } | ||||
| ``` | ||||
| 
 | ||||
| :::tip Info | ||||
| All official artifacts share the same version number and all  | ||||
| are uploaded to **release** and **snapshot** repositories | ||||
| ::: | ||||
| 
 | ||||
							
								
								
									
										12
									
								
								docs/docs/v3/migration-2-3.md
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @ -0,0 +1,12 @@ | ||||
| # Migration 2.x.x -> 3.x.x | ||||
| 
 | ||||
| * strikethrough moved to standalone module | ||||
| * tables moved to standalone module | ||||
| * core functionality of `AsyncDrawableLoader` moved to `core` module | ||||
| * * Handling of GIF and SVG media moved to standalone modules (`gif` and `svg` respectively) | ||||
| * * OkHttpClient to download images moved to standalone module | ||||
| * HTML no longer _implicitly_ added to core functionality, it must be specified __explicitly__ (as an artifact) | ||||
| * removed `markwon-view` module | ||||
| * changed Maven artifacts group to `ru.noties.markwon` | ||||
| * removed `errorDrawable` in AsyncDrawableLoader in favor of a drawable provider | ||||
| * added placeholder for AsyncDrawableProvider | ||||
							
								
								
									
										92
									
								
								docs/docs/v3/recycler-table/README.md
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @ -0,0 +1,92 @@ | ||||
| # Recycler Table <Badge text="3.0.0" /> | ||||
| 
 | ||||
| <MavenBadge :artifact="'recycler-table'" /> | ||||
| 
 | ||||
| Artifact that provides [MarkwonAdapter.Entry](/docs/v3/recycler/) to render `TableBlock` inside  | ||||
| Android-native `TableLayout` widget. | ||||
| 
 | ||||
| <img :src="$withBase('/assets/recycler-table-screenshot.png')" alt="screenshot" width="45%"> | ||||
| <br> | ||||
| <small><em><sup>*</sup> It's possible to wrap `TableLayout` inside a `HorizontalScrollView` to include all table content</em></small> | ||||
| 
 | ||||
| --- | ||||
| 
 | ||||
| Register instance of `TableEntry` with `MarkwonAdapter` to render TableBlocks: | ||||
| ```java | ||||
| final MarkwonAdapter adapter = MarkwonAdapter.builder(R.layout.adapter_default_entry, R.id.text) | ||||
|         .include(TableBlock.class, TableEntry.create(builder -> builder | ||||
|                 .tableLayout(R.layout.adapter_table_block, R.id.table_layout) | ||||
|                 .textLayoutIsRoot(R.layout.view_table_entry_cell))) | ||||
|         .build(); | ||||
| ``` | ||||
| 
 | ||||
| `TableEntry` requires at least 2 arguments: | ||||
| * `tableLayout` - layout with `TableLayout` inside | ||||
| * `textLayout` - layout with `TextView` inside (represents independent table cell) | ||||
| 
 | ||||
| In case when required view is the root of layout specific builder methods can be used: | ||||
| * `tableLayoutIsRoot(int)` | ||||
| * `textLayoutIsRoot(int)` | ||||
| 
 | ||||
| If your layouts have different structure (for example wrap a `TableView` inside a `HorizontalScrollView`) | ||||
| then you should use methods that accept ID of required view inside layout: | ||||
| * `tableLayout(int, int)` | ||||
| * `textLayout(int, int)` | ||||
| 
 | ||||
| --- | ||||
| 
 | ||||
| To display `TableBlock` as a `TableLayout` specific `MarkwonPlugin` must be used: `TableEntryPlugin`. | ||||
| 
 | ||||
| :::warning | ||||
| Do not use `TablePlugin` if you wish to display markdown tables via `TableEntry`. Use **TableEntryPlugin** instead | ||||
| ::: | ||||
| 
 | ||||
| `TableEntryPlugin` can reuse existing `TablePlugin` to make appearance of tables the same in both contexts: | ||||
| when rendering _natively_ in a TextView and when rendering in RecyclerView with TableEntry. | ||||
| 
 | ||||
| * `TableEntryPlugin.create(Context)` - creates plugin with default `TableTheme` | ||||
| * `TableEntryPlugin.create(TableTheme)` - creates plugin with provided `TableTheme` | ||||
| * `TableEntryPlugin.create(TablePlugin.ThemeConfigure)` - creates plugin with theme configured by `ThemeConfigure` | ||||
| * `TableEntryPlugin.create(TablePlugin)` - creates plugin with `TableTheme` used in provided `TablePlugin` | ||||
| 
 | ||||
| ```java | ||||
| final Markwon markwon = Markwon.builder(context) | ||||
|         .usePlugin(TableEntryPlugin.create(context)) | ||||
|         // other plugins | ||||
|         .build(); | ||||
| ``` | ||||
| 
 | ||||
| ```java | ||||
| final Markwon markwon = Markwon.builder(context) | ||||
|         .usePlugin(TableEntryPlugin.create(builder -> builder | ||||
|                 .tableBorderWidth(0) | ||||
|                 .tableHeaderRowBackgroundColor(Color.RED))) | ||||
|         // other plugins | ||||
|         .build(); | ||||
| ``` | ||||
| 
 | ||||
| ## Table with scrollable content | ||||
| 
 | ||||
| To stretch table columns to fit the width of screen or to make table scrollable when content exceeds screen width | ||||
| this layout can be used: | ||||
| 
 | ||||
| ```xml | ||||
| <HorizontalScrollView xmlns:android="http://schemas.android.com/apk/res/android" | ||||
|     android:layout_width="match_parent" | ||||
|     android:layout_height="wrap_content" | ||||
|     android:clipChildren="false" | ||||
|     android:clipToPadding="false" | ||||
|     android:paddingLeft="16dip" | ||||
|     android:paddingTop="8dip" | ||||
|     android:paddingRight="16dip" | ||||
|     android:paddingBottom="8dip" | ||||
|     android:scrollbarStyle="outsideInset"> | ||||
| 
 | ||||
|     <TableLayout | ||||
|         android:id="@+id/table_layout" | ||||
|         android:layout_width="wrap_content" | ||||
|         android:layout_height="wrap_content" | ||||
|         android:stretchColumns="*" /> | ||||
| 
 | ||||
| </HorizontalScrollView> | ||||
| ``` | ||||
							
								
								
									
										153
									
								
								docs/docs/v3/recycler/README.md
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @ -0,0 +1,153 @@ | ||||
| # Recycler <Badge text="3.0.0" /> | ||||
| 
 | ||||
| <MavenBadge :artifact="'recycler'" /> | ||||
| 
 | ||||
| This artifact allows displaying markdown in a set of Android widgets | ||||
| inside a RecyclerView. Can be useful when displaying lengthy markdown | ||||
| content or **displaying certain markdown blocks inside specific widgets**. | ||||
| 
 | ||||
| ```java | ||||
| // create an adapter that will use a TextView for each block of markdown | ||||
| // `createTextViewIsRoot` accepts a layout in which TextView is the root view | ||||
| final MarkwonAdapter adapter =  | ||||
|         MarkwonAdapter.createTextViewIsRoot(R.layout.adapter_default_entry); | ||||
| ``` | ||||
| 
 | ||||
| ```java | ||||
| // `create` method accepts a layout with TextView and ID of a TextView | ||||
| // which allows wrapping a TextView inside another widget or combine with other widgets | ||||
| final MarkwonAdapter adapter =  | ||||
|         MarkwonAdapter.create(R.layout.adapter_default_entry, R.id.text_view); | ||||
| 
 | ||||
| // initialize RecyclerView (LayoutManager, Decorations, etc) | ||||
| final RecyclerView recyclerView = obtainRecyclerView(); | ||||
| 
 | ||||
| // set adapter | ||||
| recyclerView.setAdapter(adapter); | ||||
| 
 | ||||
| // obtain an instance of Markwon (register all required plugins) | ||||
| final Markwon markwon = obtainMarkwon(); | ||||
| 
 | ||||
| // set markdown to be displayed | ||||
| adapter.setMarkdown(markwon, "# This is markdown!"); | ||||
| 
 | ||||
| // NB, adapter does not handle updates on its own, please use | ||||
| // whatever method appropriate for you. | ||||
| adapter.notifyDataSetChanged(); | ||||
| ``` | ||||
| 
 | ||||
| Initialized adapter above will use a TextView for each markdown block. | ||||
| In order to tell adapter to render certain blocks differently a `builder` can be used. | ||||
| For example, let's render `FencedCodeBlock` inside a `HorizontalScrollView`: | ||||
| 
 | ||||
| ```java | ||||
| // we still need to have a _default_ entry | ||||
| final MarkwonAdapter adapter = | ||||
|         MarkwonAdapter.builderTextViewIsRoot(R.layout.adapter_default_entry) | ||||
|                 .include(FencedCodeBlock.class, new FencedCodeBlockEntry()) | ||||
|                 .build(); | ||||
| ``` | ||||
| 
 | ||||
| where `FencedCodeBlockEntry` is: | ||||
| 
 | ||||
| ```java | ||||
| public class FencedCodeBlockEntry extends MarkwonAdapter.Entry<FencedCodeBlock, FencedCodeBlockEntry.Holder> { | ||||
| 
 | ||||
|     @NonNull | ||||
|     @Override | ||||
|     public Holder createHolder(@NonNull LayoutInflater inflater, @NonNull ViewGroup parent) { | ||||
|         return new Holder(inflater.inflate(R.layout.adapter_fenced_code_block, parent, false)); | ||||
|     } | ||||
| 
 | ||||
|     @Override | ||||
|     public void bindHolder(@NonNull Markwon markwon, @NonNull Holder holder, @NonNull FencedCodeBlock node) { | ||||
|         markwon.setParsedMarkdown(holder.textView, markwon.render(node)); | ||||
|     } | ||||
| 
 | ||||
|     public static class Holder extends MarkwonAdapter.Holder { | ||||
| 
 | ||||
|         final TextView textView; | ||||
| 
 | ||||
|         public Holder(@NonNull View itemView) { | ||||
|             super(itemView); | ||||
| 
 | ||||
|             this.textView = requireView(R.id.text_view); | ||||
|         } | ||||
|     } | ||||
| } | ||||
| ``` | ||||
| 
 | ||||
| and its layout (`R.layout.adapter_fenced_code_block`): | ||||
| 
 | ||||
| ```xml | ||||
| <?xml version="1.0" encoding="utf-8"?> | ||||
| <HorizontalScrollView xmlns:android="http://schemas.android.com/apk/res/android" | ||||
|     android:layout_width="match_parent" | ||||
|     android:layout_height="wrap_content" | ||||
|     android:clipChildren="false" | ||||
|     android:clipToPadding="false" | ||||
|     android:fillViewport="true" | ||||
|     android:paddingLeft="16dip" | ||||
|     android:paddingRight="16dip" | ||||
|     android:scrollbarStyle="outsideInset"> | ||||
| 
 | ||||
|     <TextView | ||||
|         android:id="@+id/text" | ||||
|         android:layout_width="wrap_content" | ||||
|         android:layout_height="wrap_content" | ||||
|         android:background="#0f000000" | ||||
|         android:fontFamily="monospace" | ||||
|         android:lineSpacingExtra="2dip" | ||||
|         android:paddingLeft="16dip" | ||||
|         android:paddingTop="8dip" | ||||
|         android:paddingRight="16dip" | ||||
|         android:paddingBottom="8dip" | ||||
|         android:textAppearance="?android:attr/textAppearanceMedium" | ||||
|         android:textSize="14sp" /> | ||||
| 
 | ||||
| </HorizontalScrollView> | ||||
| ``` | ||||
| 
 | ||||
| As we apply styling to `FencedCodeBlock` _manually_, we no longer need | ||||
| `Markwon` to apply styling spans for us, so `Markwon` initialization could be: | ||||
| 
 | ||||
| ```java | ||||
| final Markwon markwon = Markwon.builder(context) | ||||
|         // your other plugins | ||||
|         .usePlugin(new AbstractMarkwonPlugin() { | ||||
|             @Override | ||||
|             public void configureVisitor(@NonNull MarkwonVisitor.Builder builder) { | ||||
|                 builder.on(FencedCodeBlock.class, (visitor, fencedCodeBlock) -> { | ||||
|                     // we actually won't be applying code spans here, as our custom view will | ||||
|                     // draw background and apply mono typeface | ||||
|                     // | ||||
|                     // NB the `trim` operation on literal (as code will have a new line at the end) | ||||
|                     final CharSequence code = visitor.configuration() | ||||
|                             .syntaxHighlight() | ||||
|                             .highlight(fencedCodeBlock.getInfo(), fencedCodeBlock.getLiteral().trim()); | ||||
|                     visitor.builder().append(code); | ||||
|                 }); | ||||
|             } | ||||
|         }) | ||||
|         .build(); | ||||
| ``` | ||||
| 
 | ||||
| Previously we have created a `FencedCodeBlockEntry` but all it does is apply markdown to a TextView. | ||||
| For such a case there is a `SimpleEntry` that could be used instead: | ||||
| 
 | ||||
| ```java | ||||
| final MarkwonAdapter adapter = | ||||
|         MarkwonAdapter.builderTextViewIsRoot(R.layout.adapter_default_entry) | ||||
|                 .include(FencedCodeBlock.class, SimpleEntry.create(R.layout.adapter_fenced_code_block, R.id.text_view)) | ||||
|                 .build(); | ||||
| ``` | ||||
| 
 | ||||
| :::tip | ||||
| `SimpleEntry` also takes care of _caching_ parsed markdown. So each node will be | ||||
| parsed only once and each subsequent adapter binding call will reuse previously cached markdown. | ||||
| ::: | ||||
| 
 | ||||
| :::tip Tables | ||||
| There is a standalone artifact that adds support for displaying markdown tables | ||||
| natively via `TableLayout`. Please refer to its [documentation](/docs/v3/recycler-table/) | ||||
| ::: | ||||
							
								
								
									
										69
									
								
								docs/docs/v3/syntax-highlight/README.md
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @ -0,0 +1,69 @@ | ||||
| # Syntax highlight | ||||
| 
 | ||||
| <MavenBadge :artifact="'syntax-highlight'" /> | ||||
| 
 | ||||
| This is a simple module to add **syntax highlight** functionality to your markdown rendered with `Markwon` library. It is based on [Prism4j](https://github.com/noties/Prism4j) so lead there to understand how to configure `Prism4j` instance. | ||||
| 
 | ||||
| <img :src="$withBase('/art/markwon-syntax-default.png')" alt="theme-default" width="80%"> | ||||
| 
 | ||||
| 
 | ||||
| <img :src="$withBase('/art/markwon-syntax-darkula.png')" alt="theme-darkula" width="80%"> | ||||
| 
 | ||||
| --- | ||||
| 
 | ||||
| First, we need to obtain an instance of `Prism4jSyntaxHighlight` which implements Markwon's `SyntaxHighlight`: | ||||
| 
 | ||||
| ```java | ||||
| final SyntaxHighlight highlight =  | ||||
|     Prism4jSyntaxHighlight.create(Prism4j, Prism4jTheme); | ||||
| ``` | ||||
| 
 | ||||
| we also can obtain an instance of `Prism4jSyntaxHighlight` that has a _fallback_ option (if a language is not defined in `Prism4j` instance, fallback language can be used): | ||||
| 
 | ||||
| ```java | ||||
| final SyntaxHighlight highlight =  | ||||
|     Prism4jSyntaxHighlight.create(Prism4j, Prism4jTheme, String); | ||||
| ``` | ||||
| 
 | ||||
| Generally obtaining a `Prism4j` instance is pretty easy: | ||||
| 
 | ||||
| ```java | ||||
| final Prism4j prism4j = new Prism4j(new GrammarLocatorDef()); | ||||
| ``` | ||||
| 
 | ||||
| Where `GrammarLocatorDef` is a generated grammar locator (if you use `prism4j-bundler` annotation processor) | ||||
| 
 | ||||
| `Prism4jTheme` is a specific type that is defined in this module (`prism4j` doesn't know anything about rendering). It has 2 implementations: | ||||
| 
 | ||||
| * `Prism4jThemeDefault` | ||||
| * `Prism4jThemeDarkula` | ||||
| 
 | ||||
| Both of them can be obtained via factory method `create`: | ||||
| 
 | ||||
| * `Prism4jThemeDefault.create()` | ||||
| * `Prism4jThemeDarkula.create()` | ||||
| 
 | ||||
| But of cause nothing is stopping you from defining your own theme: | ||||
| 
 | ||||
| ```java | ||||
| public interface Prism4jTheme { | ||||
| 
 | ||||
|     @ColorInt | ||||
|     int background(); | ||||
| 
 | ||||
|     @ColorInt | ||||
|     int textColor(); | ||||
| 
 | ||||
|     void apply( | ||||
|             @NonNull String language, | ||||
|             @NonNull Prism4j.Syntax syntax, | ||||
|             @NonNull SpannableStringBuilder builder, | ||||
|             int start, | ||||
|             int end | ||||
|     ); | ||||
| } | ||||
| ``` | ||||
| 
 | ||||
| :::tip | ||||
| You can extend `Prism4jThemeBase` which has some helper methods | ||||
| ::: | ||||
							
								
								
									
										34
									
								
								docs/package-lock.json
									
									
									
										generated
									
									
									
								
							
							
						
						| @ -2471,6 +2471,24 @@ | ||||
|       "resolved": "https://registry.npmjs.org/commondir/-/commondir-1.0.1.tgz", | ||||
|       "integrity": "sha1-3dgA2gxmEnOTzKWVDqloo6rxJTs=" | ||||
|     }, | ||||
|     "commonmark": { | ||||
|       "version": "0.28.1", | ||||
|       "resolved": "https://registry.npmjs.org/commonmark/-/commonmark-0.28.1.tgz", | ||||
|       "integrity": "sha1-Buq41SM4uDn6Gi11rwCF7tGxvq4=", | ||||
|       "requires": { | ||||
|         "entities": "~ 1.1.1", | ||||
|         "mdurl": "~ 1.0.1", | ||||
|         "minimist": "~ 1.2.0", | ||||
|         "string.prototype.repeat": "^0.2.0" | ||||
|       }, | ||||
|       "dependencies": { | ||||
|         "minimist": { | ||||
|           "version": "1.2.0", | ||||
|           "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.0.tgz", | ||||
|           "integrity": "sha1-o1AIsg9BOD7sH7kU9M1d95omQoQ=" | ||||
|         } | ||||
|       } | ||||
|     }, | ||||
|     "component-emitter": { | ||||
|       "version": "1.2.1", | ||||
|       "resolved": "https://registry.npmjs.org/component-emitter/-/component-emitter-1.2.1.tgz", | ||||
| @ -5465,9 +5483,9 @@ | ||||
|       } | ||||
|     }, | ||||
|     "linkify-it": { | ||||
|       "version": "2.0.3", | ||||
|       "resolved": "https://registry.npmjs.org/linkify-it/-/linkify-it-2.0.3.tgz", | ||||
|       "integrity": "sha1-2UpGSPmxwXnWT6lykSaL22zpQ08=", | ||||
|       "version": "2.1.0", | ||||
|       "resolved": "https://registry.npmjs.org/linkify-it/-/linkify-it-2.1.0.tgz", | ||||
|       "integrity": "sha512-4REs8/062kV2DSHxNfq5183zrqXMl7WP0WzABH9IeJI+NLm429FgE1PDecltYfnOoFDFlZGh2T8PfZn0r+GTRg==", | ||||
|       "requires": { | ||||
|         "uc.micro": "^1.0.1" | ||||
|       } | ||||
| @ -5700,11 +5718,6 @@ | ||||
|       "resolved": "https://registry.npmjs.org/markdown-it-table-of-contents/-/markdown-it-table-of-contents-0.4.3.tgz", | ||||
|       "integrity": "sha512-x/OdaRzLYxAjmB+jIVlXuE3nX7tZTLDQxm58RkgjTLyQ+I290jYQvPS9cJjVN6SM3U6K6CHKYNgUtPNZmLblYQ==" | ||||
|     }, | ||||
|     "markdown-it-task-lists": { | ||||
|       "version": "2.1.1", | ||||
|       "resolved": "https://registry.npmjs.org/markdown-it-task-lists/-/markdown-it-task-lists-2.1.1.tgz", | ||||
|       "integrity": "sha512-TxFAc76Jnhb2OUu+n3yz9RMu4CwGfaT788br6HhEDlvWfdeJcLUsxk1Hgw2yJio0OXsxv7pyIPmvECY7bMbluA==" | ||||
|     }, | ||||
|     "math-expression-evaluator": { | ||||
|       "version": "1.2.17", | ||||
|       "resolved": "https://registry.npmjs.org/math-expression-evaluator/-/math-expression-evaluator-1.2.17.tgz", | ||||
| @ -9060,6 +9073,11 @@ | ||||
|         } | ||||
|       } | ||||
|     }, | ||||
|     "string.prototype.repeat": { | ||||
|       "version": "0.2.0", | ||||
|       "resolved": "https://registry.npmjs.org/string.prototype.repeat/-/string.prototype.repeat-0.2.0.tgz", | ||||
|       "integrity": "sha1-q6Nt4I3O5qWjN9SbLqHaGyj8Ds8=" | ||||
|     }, | ||||
|     "string_decoder": { | ||||
|       "version": "1.1.1", | ||||
|       "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", | ||||
|  | ||||
| @ -1,9 +1,9 @@ | ||||
| { | ||||
|   "scripts": { | ||||
|     "docs:build": "vuepress build" | ||||
|     "docs:build": "node ./collectArtifacts.js && vuepress build" | ||||
|   }, | ||||
|   "dependencies": { | ||||
|     "markdown-it-task-lists": "^2.1.1", | ||||
|     "commonmark": "^0.28.1", | ||||
|     "vuepress": "^0.14.2" | ||||
|   } | ||||
| } | ||||
|  | ||||
							
								
								
									
										3
									
								
								docs/sandbox.md
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @ -0,0 +1,3 @@ | ||||
| # Commonmark Sandbox | ||||
| 
 | ||||
| <CommonmarkSandbox /> | ||||
| @ -6,10 +6,10 @@ org.gradle.configureondemand=true | ||||
| android.enableBuildCache=true | ||||
| android.buildCacheDir=build/pre-dex-cache | ||||
| 
 | ||||
| VERSION_NAME=2.0.1 | ||||
| VERSION_NAME=3.0.0 | ||||
| 
 | ||||
| GROUP=ru.noties | ||||
| POM_DESCRIPTION=Markwon | ||||
| GROUP=ru.noties.markwon | ||||
| POM_DESCRIPTION=Markwon markdown for Android | ||||
| POM_URL=https://github.com/noties/Markwon | ||||
| POM_SCM_URL=https://github.com/noties/Markwon | ||||
| POM_SCM_CONNECTION=scm:git:git://github.com/noties/Markwon.git | ||||
|  | ||||
| @ -15,17 +15,21 @@ android { | ||||
| 
 | ||||
| dependencies { | ||||
| 
 | ||||
|     api project(':markwon-html-parser-api') | ||||
| 
 | ||||
|     deps.with { | ||||
|         api it['support-annotations'] | ||||
|         api it['commonmark'] | ||||
|     } | ||||
| 
 | ||||
|     deps.test.with { | ||||
|     deps['test'].with { | ||||
| 
 | ||||
|         testImplementation project(':markwon-test-span') | ||||
| 
 | ||||
|         testImplementation it['junit'] | ||||
|         testImplementation it['robolectric'] | ||||
|         testImplementation it['mockito'] | ||||
| 
 | ||||
|         testImplementation it['commons-io'] | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| registerArtifact(this) | ||||
| registerArtifact(this) | ||||
							
								
								
									
										4
									
								
								markwon-core/gradle.properties
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @ -0,0 +1,4 @@ | ||||
| POM_NAME=Core | ||||
| POM_ARTIFACT_ID=core | ||||
| POM_DESCRIPTION=Core Markwon artifact that includes basic markdown parsing and rendering | ||||
| POM_PACKAGING=aar | ||||
| @ -0,0 +1,92 @@ | ||||
| package ru.noties.markwon; | ||||
| 
 | ||||
| import android.support.annotation.NonNull; | ||||
| import android.text.Spanned; | ||||
| import android.widget.TextView; | ||||
| 
 | ||||
| import org.commonmark.node.Node; | ||||
| import org.commonmark.parser.Parser; | ||||
| 
 | ||||
| import ru.noties.markwon.core.CorePlugin; | ||||
| import ru.noties.markwon.core.MarkwonTheme; | ||||
| import ru.noties.markwon.html.MarkwonHtmlRenderer; | ||||
| import ru.noties.markwon.image.AsyncDrawableLoader; | ||||
| import ru.noties.markwon.priority.Priority; | ||||
| 
 | ||||
| /** | ||||
|  * Class that extends {@link MarkwonPlugin} with all methods implemented (empty body) | ||||
|  * for easier plugin implementation. Only required methods can be overriden | ||||
|  * | ||||
|  * @see MarkwonPlugin | ||||
|  * @since 3.0.0 | ||||
|  */ | ||||
| public abstract class AbstractMarkwonPlugin implements MarkwonPlugin { | ||||
| 
 | ||||
|     @Override | ||||
|     public void configureParser(@NonNull Parser.Builder builder) { | ||||
| 
 | ||||
|     } | ||||
| 
 | ||||
|     @Override | ||||
|     public void configureTheme(@NonNull MarkwonTheme.Builder builder) { | ||||
| 
 | ||||
|     } | ||||
| 
 | ||||
|     @Override | ||||
|     public void configureImages(@NonNull AsyncDrawableLoader.Builder builder) { | ||||
| 
 | ||||
|     } | ||||
| 
 | ||||
|     @Override | ||||
|     public void configureConfiguration(@NonNull MarkwonConfiguration.Builder builder) { | ||||
| 
 | ||||
|     } | ||||
| 
 | ||||
|     @Override | ||||
|     public void configureVisitor(@NonNull MarkwonVisitor.Builder builder) { | ||||
| 
 | ||||
|     } | ||||
| 
 | ||||
|     @Override | ||||
|     public void configureSpansFactory(@NonNull MarkwonSpansFactory.Builder builder) { | ||||
| 
 | ||||
|     } | ||||
| 
 | ||||
|     @Override | ||||
|     public void configureHtmlRenderer(@NonNull MarkwonHtmlRenderer.Builder builder) { | ||||
| 
 | ||||
|     } | ||||
| 
 | ||||
|     @NonNull | ||||
|     @Override | ||||
|     public Priority priority() { | ||||
|         // by default all come after CorePlugin | ||||
|         return Priority.after(CorePlugin.class); | ||||
|     } | ||||
| 
 | ||||
|     @NonNull | ||||
|     @Override | ||||
|     public String processMarkdown(@NonNull String markdown) { | ||||
|         return markdown; | ||||
|     } | ||||
| 
 | ||||
|     @Override | ||||
|     public void beforeRender(@NonNull Node node) { | ||||
| 
 | ||||
|     } | ||||
| 
 | ||||
|     @Override | ||||
|     public void afterRender(@NonNull Node node, @NonNull MarkwonVisitor visitor) { | ||||
| 
 | ||||
|     } | ||||
| 
 | ||||
|     @Override | ||||
|     public void beforeSetText(@NonNull TextView textView, @NonNull Spanned markdown) { | ||||
| 
 | ||||
|     } | ||||
| 
 | ||||
|     @Override | ||||
|     public void afterSetText(@NonNull TextView textView) { | ||||
| 
 | ||||
|     } | ||||
| } | ||||
| @ -9,7 +9,7 @@ import android.support.annotation.NonNull; | ||||
| import android.util.Log; | ||||
| import android.view.View; | ||||
| 
 | ||||
| import ru.noties.markwon.spans.LinkSpan; | ||||
| import ru.noties.markwon.core.spans.LinkSpan; | ||||
| 
 | ||||
| public class LinkResolverDef implements LinkSpan.Resolver { | ||||
|     @Override | ||||
							
								
								
									
										136
									
								
								markwon-core/src/main/java/ru/noties/markwon/Markwon.java
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @ -0,0 +1,136 @@ | ||||
| package ru.noties.markwon; | ||||
| 
 | ||||
| import android.content.Context; | ||||
| import android.support.annotation.NonNull; | ||||
| import android.support.annotation.Nullable; | ||||
| import android.text.Spanned; | ||||
| import android.widget.TextView; | ||||
| 
 | ||||
| import org.commonmark.node.Node; | ||||
| 
 | ||||
| import ru.noties.markwon.core.CorePlugin; | ||||
| 
 | ||||
| /** | ||||
|  * Class to parse and render markdown. Since version 3.0.0 instance specific (previously consisted | ||||
|  * of static stateless methods). An instance of builder can be obtained via {@link #builder(Context)} | ||||
|  * method. | ||||
|  * | ||||
|  * @see #create(Context) | ||||
|  * @see #builder(Context) | ||||
|  * @see Builder | ||||
|  */ | ||||
| public abstract class Markwon { | ||||
| 
 | ||||
|     /** | ||||
|      * Factory method to create a <em>minimally</em> functional {@link Markwon} instance. This | ||||
|      * instance will have <strong>only</strong> {@link CorePlugin} registered. If you wish | ||||
|      * to configure this instance more consider using {@link #builder(Context)} method. | ||||
|      * | ||||
|      * @return {@link Markwon} instance with only CorePlugin registered | ||||
|      * @since 3.0.0 | ||||
|      */ | ||||
|     @NonNull | ||||
|     public static Markwon create(@NonNull Context context) { | ||||
|         return builder(context) | ||||
|                 .usePlugin(CorePlugin.create()) | ||||
|                 .build(); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Factory method to obtain an instance of {@link Builder}. | ||||
|      * | ||||
|      * @see Builder | ||||
|      * @since 3.0.0 | ||||
|      */ | ||||
|     @NonNull | ||||
|     public static Builder builder(@NonNull Context context) { | ||||
|         return new MarkwonBuilderImpl(context); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Method to parse markdown (without rendering) | ||||
|      * | ||||
|      * @param input markdown input to parse | ||||
|      * @return parsed via commonmark-java <code>org.commonmark.node.Node</code> | ||||
|      * @see #render(Node) | ||||
|      * @since 3.0.0 | ||||
|      */ | ||||
|     @NonNull | ||||
|     public abstract Node parse(@NonNull String input); | ||||
| 
 | ||||
|     /** | ||||
|      * Create Spanned markdown from parsed Node (via {@link #parse(String)} call). | ||||
|      * <p> | ||||
|      * Please note that returned Spanned has few limitations. For example, images, tables | ||||
|      * and ordered lists require TextView to be properly displayed. This is why images and tables | ||||
|      * most likely won\'t work in this case. Ordered lists might have mis-measurements. Whenever | ||||
|      * possible use {@link #setMarkdown(TextView, String)} or {@link #setParsedMarkdown(TextView, Spanned)} | ||||
|      * as these methods will additionally call specific {@link MarkwonPlugin} methods to <em>prepare</em> | ||||
|      * proper display. | ||||
|      * | ||||
|      * @since 3.0.0 | ||||
|      */ | ||||
|     @NonNull | ||||
|     public abstract Spanned render(@NonNull Node node); | ||||
| 
 | ||||
|     /** | ||||
|      * This method will {@link #parse(String)} and {@link #render(Node)} supplied markdown. Returned | ||||
|      * Spanned has the same limitations as from {@link #render(Node)} method. | ||||
|      * | ||||
|      * @param input markdown input | ||||
|      * @see #parse(String) | ||||
|      * @see #render(Node) | ||||
|      * @since 3.0.0 | ||||
|      */ | ||||
|     @NonNull | ||||
|     public abstract Spanned toMarkdown(@NonNull String input); | ||||
| 
 | ||||
|     public abstract void setMarkdown(@NonNull TextView textView, @NonNull String markdown); | ||||
| 
 | ||||
|     public abstract void setParsedMarkdown(@NonNull TextView textView, @NonNull Spanned markdown); | ||||
| 
 | ||||
|     /** | ||||
|      * Requests information if certain plugin has been registered. Please note that this | ||||
|      * method will check for super classes also, so if supplied with {@code markwon.hasPlugin(MarkwonPlugin.class)} | ||||
|      * this method (if has at least one plugin) will return true. If for example a custom | ||||
|      * (subclassed) version of a {@link CorePlugin} has been registered and given name | ||||
|      * {@code CorePlugin2}, then both {@code markwon.hasPlugin(CorePlugin2.class)} and | ||||
|      * {@code markwon.hasPlugin(CorePlugin.class)} will return true. | ||||
|      * | ||||
|      * @param plugin type to query | ||||
|      * @return true if a plugin is used when configuring this {@link Markwon} instance | ||||
|      */ | ||||
|     public abstract boolean hasPlugin(@NonNull Class<? extends MarkwonPlugin> plugin); | ||||
| 
 | ||||
|     @Nullable | ||||
|     public abstract <P extends MarkwonPlugin> P getPlugin(@NonNull Class<P> type); | ||||
| 
 | ||||
|     /** | ||||
|      * Builder for {@link Markwon}. | ||||
|      * <p> | ||||
|      * Please note that the order in which plugins are supplied is important as this order will be | ||||
|      * used through the whole usage of built Markwon instance | ||||
|      * | ||||
|      * @since 3.0.0 | ||||
|      */ | ||||
|     public interface Builder { | ||||
| 
 | ||||
|         /** | ||||
|          * Specify bufferType when applying text to a TextView {@code textView.setText(CharSequence,BufferType)}. | ||||
|          * By default `BufferType.SPANNABLE` is used | ||||
|          * | ||||
|          * @param bufferType BufferType | ||||
|          */ | ||||
|         @NonNull | ||||
|         Builder bufferType(@NonNull TextView.BufferType bufferType); | ||||
| 
 | ||||
|         @NonNull | ||||
|         Builder usePlugin(@NonNull MarkwonPlugin plugin); | ||||
| 
 | ||||
|         @NonNull | ||||
|         Builder usePlugins(@NonNull Iterable<? extends MarkwonPlugin> plugins); | ||||
| 
 | ||||
|         @NonNull | ||||
|         Markwon build(); | ||||
|     } | ||||
| } | ||||
| @ -0,0 +1,194 @@ | ||||
| package ru.noties.markwon; | ||||
| 
 | ||||
| import android.content.Context; | ||||
| import android.support.annotation.NonNull; | ||||
| import android.support.annotation.VisibleForTesting; | ||||
| import android.widget.TextView; | ||||
| 
 | ||||
| import org.commonmark.parser.Parser; | ||||
| 
 | ||||
| import java.util.ArrayList; | ||||
| import java.util.Collections; | ||||
| import java.util.Iterator; | ||||
| import java.util.List; | ||||
| 
 | ||||
| import ru.noties.markwon.core.CorePlugin; | ||||
| import ru.noties.markwon.core.MarkwonTheme; | ||||
| import ru.noties.markwon.html.MarkwonHtmlRenderer; | ||||
| import ru.noties.markwon.image.AsyncDrawableLoader; | ||||
| import ru.noties.markwon.priority.PriorityProcessor; | ||||
| 
 | ||||
| /** | ||||
|  * @since 3.0.0 | ||||
|  */ | ||||
| @SuppressWarnings("WeakerAccess") | ||||
| class MarkwonBuilderImpl implements Markwon.Builder { | ||||
| 
 | ||||
|     private final Context context; | ||||
| 
 | ||||
|     private final List<MarkwonPlugin> plugins = new ArrayList<>(3); | ||||
| 
 | ||||
|     private TextView.BufferType bufferType = TextView.BufferType.SPANNABLE; | ||||
| 
 | ||||
|     private PriorityProcessor priorityProcessor; | ||||
| 
 | ||||
|     MarkwonBuilderImpl(@NonNull Context context) { | ||||
|         this.context = context; | ||||
|     } | ||||
| 
 | ||||
|     @NonNull | ||||
|     @Override | ||||
|     public Markwon.Builder bufferType(@NonNull TextView.BufferType bufferType) { | ||||
|         this.bufferType = bufferType; | ||||
|         return this; | ||||
|     } | ||||
| 
 | ||||
|     @NonNull | ||||
|     @Override | ||||
|     public Markwon.Builder usePlugin(@NonNull MarkwonPlugin plugin) { | ||||
|         plugins.add(plugin); | ||||
|         return this; | ||||
|     } | ||||
| 
 | ||||
|     @NonNull | ||||
|     @Override | ||||
|     public Markwon.Builder usePlugins(@NonNull Iterable<? extends MarkwonPlugin> plugins) { | ||||
| 
 | ||||
|         final Iterator<? extends MarkwonPlugin> iterator = plugins.iterator(); | ||||
| 
 | ||||
|         MarkwonPlugin plugin; | ||||
| 
 | ||||
|         while (iterator.hasNext()) { | ||||
|             plugin = iterator.next(); | ||||
|             if (plugin == null) { | ||||
|                 throw new NullPointerException(); | ||||
|             } | ||||
|             this.plugins.add(plugin); | ||||
|         } | ||||
| 
 | ||||
|         return this; | ||||
|     } | ||||
| 
 | ||||
|     @SuppressWarnings("UnusedReturnValue") | ||||
|     @NonNull | ||||
|     public MarkwonBuilderImpl priorityProcessor(@NonNull PriorityProcessor priorityProcessor) { | ||||
|         this.priorityProcessor = priorityProcessor; | ||||
|         return this; | ||||
|     } | ||||
| 
 | ||||
|     @NonNull | ||||
|     @Override | ||||
|     public Markwon build() { | ||||
| 
 | ||||
|         if (plugins.isEmpty()) { | ||||
|             throw new IllegalStateException("No plugins were added to this builder. Use #usePlugin " + | ||||
|                     "method to add them"); | ||||
|         } | ||||
| 
 | ||||
|         // this class will sort plugins to match a priority/dependency graph that we have | ||||
|         PriorityProcessor priorityProcessor = this.priorityProcessor; | ||||
|         if (priorityProcessor == null) { | ||||
|             // strictly speaking we do not need updating this field | ||||
|             // as we are not building this class to be reused between multiple `build` calls | ||||
|             priorityProcessor = this.priorityProcessor = PriorityProcessor.create(); | ||||
|         } | ||||
| 
 | ||||
|         // please note that this method must not modify supplied collection | ||||
|         // if nothing should be done -> the same collection can be returned | ||||
|         final List<MarkwonPlugin> plugins = preparePlugins(priorityProcessor, this.plugins); | ||||
| 
 | ||||
|         final Parser.Builder parserBuilder = new Parser.Builder(); | ||||
|         final MarkwonTheme.Builder themeBuilder = MarkwonTheme.builderWithDefaults(context); | ||||
|         final AsyncDrawableLoader.Builder asyncDrawableLoaderBuilder = new AsyncDrawableLoader.Builder(); | ||||
|         final MarkwonConfiguration.Builder configurationBuilder = new MarkwonConfiguration.Builder(); | ||||
|         final MarkwonVisitor.Builder visitorBuilder = new MarkwonVisitorImpl.BuilderImpl(); | ||||
|         final MarkwonSpansFactory.Builder spanFactoryBuilder = new MarkwonSpansFactoryImpl.BuilderImpl(); | ||||
|         final MarkwonHtmlRenderer.Builder htmlRendererBuilder = MarkwonHtmlRenderer.builder(); | ||||
| 
 | ||||
|         for (MarkwonPlugin plugin : plugins) { | ||||
|             plugin.configureParser(parserBuilder); | ||||
|             plugin.configureTheme(themeBuilder); | ||||
|             plugin.configureImages(asyncDrawableLoaderBuilder); | ||||
|             plugin.configureConfiguration(configurationBuilder); | ||||
|             plugin.configureVisitor(visitorBuilder); | ||||
|             plugin.configureSpansFactory(spanFactoryBuilder); | ||||
|             plugin.configureHtmlRenderer(htmlRendererBuilder); | ||||
|         } | ||||
| 
 | ||||
|         final MarkwonConfiguration configuration = configurationBuilder.build( | ||||
|                 themeBuilder.build(), | ||||
|                 asyncDrawableLoaderBuilder.build(), | ||||
|                 htmlRendererBuilder.build(), | ||||
|                 spanFactoryBuilder.build()); | ||||
| 
 | ||||
|         final RenderProps renderProps = new RenderPropsImpl(); | ||||
| 
 | ||||
|         return new MarkwonImpl( | ||||
|                 bufferType, | ||||
|                 parserBuilder.build(), | ||||
|                 visitorBuilder.build(configuration, renderProps), | ||||
|                 Collections.unmodifiableList(plugins) | ||||
|         ); | ||||
|     } | ||||
| 
 | ||||
|     @VisibleForTesting | ||||
|     @NonNull | ||||
|     static List<MarkwonPlugin> preparePlugins( | ||||
|             @NonNull PriorityProcessor priorityProcessor, | ||||
|             @NonNull List<MarkwonPlugin> plugins) { | ||||
| 
 | ||||
|         // with this method we will ensure that CorePlugin is added IF and ONLY IF | ||||
|         // there are plugins that depend on it. If CorePlugin is added, or there are | ||||
|         // no plugins that require it, CorePlugin won't be added | ||||
|         final List<MarkwonPlugin> out = ensureImplicitCoreIfHasDependents(plugins); | ||||
| 
 | ||||
|         return priorityProcessor.process(out); | ||||
|     } | ||||
| 
 | ||||
|     // this method will _implicitly_ add CorePlugin if there is at least one plugin | ||||
|     // that depends on CorePlugin | ||||
|     @VisibleForTesting | ||||
|     @NonNull | ||||
|     static List<MarkwonPlugin> ensureImplicitCoreIfHasDependents(@NonNull List<MarkwonPlugin> plugins) { | ||||
|         // loop over plugins -> if CorePlugin is found -> break; | ||||
|         // iterate over all plugins and check if CorePlugin is requested | ||||
| 
 | ||||
|         boolean hasCore = false; | ||||
|         boolean hasCoreDependents = false; | ||||
| 
 | ||||
|         for (MarkwonPlugin plugin : plugins) { | ||||
| 
 | ||||
|             // here we do not check for exact match (a user could've subclasses CorePlugin | ||||
|             // and supplied it. In this case we DO NOT implicitly add CorePlugin | ||||
|             // | ||||
|             // if core is present already we do not need to iterate anymore -> as nothing | ||||
|             // will be changed (and we actually do not care if there are any dependents of Core | ||||
|             // as it's present anyway) | ||||
|             if (CorePlugin.class.isAssignableFrom(plugin.getClass())) { | ||||
|                 hasCore = true; | ||||
|                 break; | ||||
|             } | ||||
| 
 | ||||
|             // if plugin has CorePlugin in dependencies -> mark for addition | ||||
|             if (!hasCoreDependents) { | ||||
|                 // here we check for direct CorePlugin, if it's not CorePlugin (exact, not a subclass | ||||
|                 // or something -> ignore) | ||||
|                 if (plugin.priority().after().contains(CorePlugin.class)) { | ||||
|                     hasCoreDependents = true; | ||||
|                 } | ||||
|             } | ||||
|         } | ||||
| 
 | ||||
|         // important thing here is to check if corePlugin is added | ||||
|         // add it _only_ if it's not present | ||||
|         if (hasCoreDependents && !hasCore) { | ||||
|             final List<MarkwonPlugin> out = new ArrayList<>(plugins.size() + 1); | ||||
|             // add default instance of CorePlugin | ||||
|             out.add(CorePlugin.create()); | ||||
|             out.addAll(plugins); | ||||
|             return out; | ||||
|         } | ||||
| 
 | ||||
|         return plugins; | ||||
|     } | ||||
| } | ||||
| @ -0,0 +1,185 @@ | ||||
| package ru.noties.markwon; | ||||
| 
 | ||||
| import android.support.annotation.NonNull; | ||||
| 
 | ||||
| import ru.noties.markwon.core.MarkwonTheme; | ||||
| import ru.noties.markwon.core.spans.LinkSpan; | ||||
| import ru.noties.markwon.html.MarkwonHtmlParser; | ||||
| import ru.noties.markwon.html.MarkwonHtmlRenderer; | ||||
| import ru.noties.markwon.image.AsyncDrawableLoader; | ||||
| import ru.noties.markwon.image.ImageSizeResolver; | ||||
| import ru.noties.markwon.image.ImageSizeResolverDef; | ||||
| import ru.noties.markwon.syntax.SyntaxHighlight; | ||||
| import ru.noties.markwon.syntax.SyntaxHighlightNoOp; | ||||
| import ru.noties.markwon.urlprocessor.UrlProcessor; | ||||
| import ru.noties.markwon.urlprocessor.UrlProcessorNoOp; | ||||
| 
 | ||||
| /** | ||||
|  * since 3.0.0 renamed `SpannableConfiguration` -> `MarkwonConfiguration` | ||||
|  */ | ||||
| @SuppressWarnings("WeakerAccess") | ||||
| public class MarkwonConfiguration { | ||||
| 
 | ||||
|     @NonNull | ||||
|     public static Builder builder() { | ||||
|         return new Builder(); | ||||
|     } | ||||
| 
 | ||||
|     private final MarkwonTheme theme; | ||||
|     private final AsyncDrawableLoader asyncDrawableLoader; | ||||
|     private final SyntaxHighlight syntaxHighlight; | ||||
|     private final LinkSpan.Resolver linkResolver; | ||||
|     private final UrlProcessor urlProcessor; | ||||
|     private final ImageSizeResolver imageSizeResolver; | ||||
|     private final MarkwonHtmlParser htmlParser; | ||||
|     private final MarkwonHtmlRenderer htmlRenderer; | ||||
| 
 | ||||
|     // @since 3.0.0 | ||||
|     private final MarkwonSpansFactory spansFactory; | ||||
| 
 | ||||
|     private MarkwonConfiguration(@NonNull Builder builder) { | ||||
|         this.theme = builder.theme; | ||||
|         this.asyncDrawableLoader = builder.asyncDrawableLoader; | ||||
|         this.syntaxHighlight = builder.syntaxHighlight; | ||||
|         this.linkResolver = builder.linkResolver; | ||||
|         this.urlProcessor = builder.urlProcessor; | ||||
|         this.imageSizeResolver = builder.imageSizeResolver; | ||||
|         this.spansFactory = builder.spansFactory; | ||||
|         this.htmlParser = builder.htmlParser; | ||||
|         this.htmlRenderer = builder.htmlRenderer; | ||||
|     } | ||||
| 
 | ||||
|     @NonNull | ||||
|     public MarkwonTheme theme() { | ||||
|         return theme; | ||||
|     } | ||||
| 
 | ||||
|     @NonNull | ||||
|     public AsyncDrawableLoader asyncDrawableLoader() { | ||||
|         return asyncDrawableLoader; | ||||
|     } | ||||
| 
 | ||||
|     @NonNull | ||||
|     public SyntaxHighlight syntaxHighlight() { | ||||
|         return syntaxHighlight; | ||||
|     } | ||||
| 
 | ||||
|     @NonNull | ||||
|     public LinkSpan.Resolver linkResolver() { | ||||
|         return linkResolver; | ||||
|     } | ||||
| 
 | ||||
|     @NonNull | ||||
|     public UrlProcessor urlProcessor() { | ||||
|         return urlProcessor; | ||||
|     } | ||||
| 
 | ||||
|     @NonNull | ||||
|     public ImageSizeResolver imageSizeResolver() { | ||||
|         return imageSizeResolver; | ||||
|     } | ||||
| 
 | ||||
|     @NonNull | ||||
|     public MarkwonHtmlParser htmlParser() { | ||||
|         return htmlParser; | ||||
|     } | ||||
| 
 | ||||
|     @NonNull | ||||
|     public MarkwonHtmlRenderer htmlRenderer() { | ||||
|         return htmlRenderer; | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * @since 3.0.0 | ||||
|      */ | ||||
|     @NonNull | ||||
|     public MarkwonSpansFactory spansFactory() { | ||||
|         return spansFactory; | ||||
|     } | ||||
| 
 | ||||
|     @SuppressWarnings({"unused", "UnusedReturnValue"}) | ||||
|     public static class Builder { | ||||
| 
 | ||||
|         private MarkwonTheme theme; | ||||
|         private AsyncDrawableLoader asyncDrawableLoader; | ||||
|         private SyntaxHighlight syntaxHighlight; | ||||
|         private LinkSpan.Resolver linkResolver; | ||||
|         private UrlProcessor urlProcessor; | ||||
|         private ImageSizeResolver imageSizeResolver; | ||||
|         private MarkwonHtmlParser htmlParser; | ||||
|         private MarkwonHtmlRenderer htmlRenderer; | ||||
|         private MarkwonSpansFactory spansFactory; | ||||
| 
 | ||||
|         Builder() { | ||||
|         } | ||||
| 
 | ||||
|         @NonNull | ||||
|         public Builder syntaxHighlight(@NonNull SyntaxHighlight syntaxHighlight) { | ||||
|             this.syntaxHighlight = syntaxHighlight; | ||||
|             return this; | ||||
|         } | ||||
| 
 | ||||
|         @NonNull | ||||
|         public Builder linkResolver(@NonNull LinkSpan.Resolver linkResolver) { | ||||
|             this.linkResolver = linkResolver; | ||||
|             return this; | ||||
|         } | ||||
| 
 | ||||
|         @NonNull | ||||
|         public Builder urlProcessor(@NonNull UrlProcessor urlProcessor) { | ||||
|             this.urlProcessor = urlProcessor; | ||||
|             return this; | ||||
|         } | ||||
| 
 | ||||
|         @NonNull | ||||
|         public Builder htmlParser(@NonNull MarkwonHtmlParser htmlParser) { | ||||
|             this.htmlParser = htmlParser; | ||||
|             return this; | ||||
|         } | ||||
| 
 | ||||
|         /** | ||||
|          * @since 1.0.1 | ||||
|          */ | ||||
|         @NonNull | ||||
|         public Builder imageSizeResolver(@NonNull ImageSizeResolver imageSizeResolver) { | ||||
|             this.imageSizeResolver = imageSizeResolver; | ||||
|             return this; | ||||
|         } | ||||
| 
 | ||||
|         @NonNull | ||||
|         public MarkwonConfiguration build( | ||||
|                 @NonNull MarkwonTheme theme, | ||||
|                 @NonNull AsyncDrawableLoader asyncDrawableLoader, | ||||
|                 @NonNull MarkwonHtmlRenderer htmlRenderer, | ||||
|                 @NonNull MarkwonSpansFactory spansFactory) { | ||||
| 
 | ||||
|             this.theme = theme; | ||||
|             this.asyncDrawableLoader = asyncDrawableLoader; | ||||
|             this.htmlRenderer = htmlRenderer; | ||||
|             this.spansFactory = spansFactory; | ||||
| 
 | ||||
|             if (syntaxHighlight == null) { | ||||
|                 syntaxHighlight = new SyntaxHighlightNoOp(); | ||||
|             } | ||||
| 
 | ||||
|             if (linkResolver == null) { | ||||
|                 linkResolver = new LinkResolverDef(); | ||||
|             } | ||||
| 
 | ||||
|             if (urlProcessor == null) { | ||||
|                 urlProcessor = new UrlProcessorNoOp(); | ||||
|             } | ||||
| 
 | ||||
|             if (imageSizeResolver == null) { | ||||
|                 imageSizeResolver = new ImageSizeResolverDef(); | ||||
|             } | ||||
| 
 | ||||
|             if (htmlParser == null) { | ||||
|                 htmlParser = MarkwonHtmlParser.noOp(); | ||||
|             } | ||||
| 
 | ||||
|             return new MarkwonConfiguration(this); | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
| } | ||||
							
								
								
									
										110
									
								
								markwon-core/src/main/java/ru/noties/markwon/MarkwonImpl.java
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @ -0,0 +1,110 @@ | ||||
| package ru.noties.markwon; | ||||
| 
 | ||||
| import android.support.annotation.NonNull; | ||||
| import android.support.annotation.Nullable; | ||||
| import android.text.Spanned; | ||||
| import android.widget.TextView; | ||||
| 
 | ||||
| import org.commonmark.node.Node; | ||||
| import org.commonmark.parser.Parser; | ||||
| 
 | ||||
| import java.util.List; | ||||
| 
 | ||||
| /** | ||||
|  * @since 3.0.0 | ||||
|  */ | ||||
| class MarkwonImpl extends Markwon { | ||||
| 
 | ||||
|     private final TextView.BufferType bufferType; | ||||
|     private final Parser parser; | ||||
|     private final MarkwonVisitor visitor; | ||||
|     private final List<MarkwonPlugin> plugins; | ||||
| 
 | ||||
|     MarkwonImpl( | ||||
|             @NonNull TextView.BufferType bufferType, | ||||
|             @NonNull Parser parser, | ||||
|             @NonNull MarkwonVisitor visitor, | ||||
|             @NonNull List<MarkwonPlugin> plugins) { | ||||
|         this.bufferType = bufferType; | ||||
|         this.parser = parser; | ||||
|         this.visitor = visitor; | ||||
|         this.plugins = plugins; | ||||
|     } | ||||
| 
 | ||||
|     @NonNull | ||||
|     @Override | ||||
|     public Node parse(@NonNull String input) { | ||||
| 
 | ||||
|         // make sure that all plugins are called `processMarkdown` before parsing | ||||
|         for (MarkwonPlugin plugin : plugins) { | ||||
|             input = plugin.processMarkdown(input); | ||||
|         } | ||||
| 
 | ||||
|         return parser.parse(input); | ||||
|     } | ||||
| 
 | ||||
|     @NonNull | ||||
|     @Override | ||||
|     public Spanned render(@NonNull Node node) { | ||||
| 
 | ||||
|         for (MarkwonPlugin plugin : plugins) { | ||||
|             plugin.beforeRender(node); | ||||
|         } | ||||
| 
 | ||||
|         node.accept(visitor); | ||||
| 
 | ||||
|         for (MarkwonPlugin plugin : plugins) { | ||||
|             plugin.afterRender(node, visitor); | ||||
|         } | ||||
| 
 | ||||
|         final Spanned spanned = visitor.builder().spannableStringBuilder(); | ||||
| 
 | ||||
|         // clear render props and builder after rendering | ||||
|         visitor.clear(); | ||||
| 
 | ||||
|         return spanned; | ||||
|     } | ||||
| 
 | ||||
|     @NonNull | ||||
|     @Override | ||||
|     public Spanned toMarkdown(@NonNull String input) { | ||||
|         return render(parse(input)); | ||||
|     } | ||||
| 
 | ||||
|     @Override | ||||
|     public void setMarkdown(@NonNull TextView textView, @NonNull String markdown) { | ||||
|         setParsedMarkdown(textView, toMarkdown(markdown)); | ||||
|     } | ||||
| 
 | ||||
|     @Override | ||||
|     public void setParsedMarkdown(@NonNull TextView textView, @NonNull Spanned markdown) { | ||||
| 
 | ||||
|         for (MarkwonPlugin plugin : plugins) { | ||||
|             plugin.beforeSetText(textView, markdown); | ||||
|         } | ||||
| 
 | ||||
|         textView.setText(markdown, bufferType); | ||||
| 
 | ||||
|         for (MarkwonPlugin plugin : plugins) { | ||||
|             plugin.afterSetText(textView); | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     @Override | ||||
|     public boolean hasPlugin(@NonNull Class<? extends MarkwonPlugin> type) { | ||||
|         return getPlugin(type) != null; | ||||
|     } | ||||
| 
 | ||||
|     @Nullable | ||||
|     @Override | ||||
|     public <P extends MarkwonPlugin> P getPlugin(@NonNull Class<P> type) { | ||||
|         MarkwonPlugin out = null; | ||||
|         for (MarkwonPlugin plugin : plugins) { | ||||
|             if (type.isAssignableFrom(plugin.getClass())) { | ||||
|                 out = plugin; | ||||
|             } | ||||
|         } | ||||
|         //noinspection unchecked | ||||
|         return (P) out; | ||||
|     } | ||||
| } | ||||
| @ -0,0 +1,184 @@ | ||||
| package ru.noties.markwon; | ||||
| 
 | ||||
| import android.content.Context; | ||||
| import android.support.annotation.IdRes; | ||||
| import android.support.annotation.LayoutRes; | ||||
| import android.support.annotation.NonNull; | ||||
| import android.view.LayoutInflater; | ||||
| import android.view.View; | ||||
| import android.view.ViewGroup; | ||||
| import android.widget.TextView; | ||||
| 
 | ||||
| import org.commonmark.node.Node; | ||||
| 
 | ||||
| import java.util.HashMap; | ||||
| import java.util.Map; | ||||
| 
 | ||||
| /** | ||||
|  * @since 3.0.0 | ||||
|  */ | ||||
| public abstract class MarkwonNodeRenderer { | ||||
| 
 | ||||
|     public interface ViewProvider<N extends Node> { | ||||
| 
 | ||||
|         /** | ||||
|          * Please note that you should not attach created View to specified group. It will be done | ||||
|          * automatically. | ||||
|          */ | ||||
|         @NonNull | ||||
|         View provide( | ||||
|                 @NonNull LayoutInflater inflater, | ||||
|                 @NonNull ViewGroup group, | ||||
|                 @NonNull Markwon markwon, | ||||
|                 @NonNull N n); | ||||
|     } | ||||
| 
 | ||||
|     @NonNull | ||||
|     public static Builder builder(@NonNull ViewProvider<Node> defaultViewProvider) { | ||||
|         return new Builder(defaultViewProvider); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * @param defaultViewProviderLayoutResId layout resource id to be used in default view provider | ||||
|      * @param defaultViewProviderTextViewId  id of a TextView in specified layout | ||||
|      * @return Builder | ||||
|      * @see SimpleTextViewProvider | ||||
|      */ | ||||
|     @NonNull | ||||
|     public static Builder builder( | ||||
|             @LayoutRes int defaultViewProviderLayoutResId, | ||||
|             @IdRes int defaultViewProviderTextViewId) { | ||||
|         return new Builder(new SimpleTextViewProvider( | ||||
|                 defaultViewProviderLayoutResId, | ||||
|                 defaultViewProviderTextViewId)); | ||||
|     } | ||||
| 
 | ||||
|     public abstract void render(@NonNull ViewGroup group, @NonNull Markwon markwon, @NonNull String markdown); | ||||
| 
 | ||||
|     public abstract void render(@NonNull ViewGroup group, @NonNull Markwon markwon, @NonNull Node root); | ||||
| 
 | ||||
| 
 | ||||
|     public static class Builder { | ||||
| 
 | ||||
|         private final ViewProvider<Node> defaultViewProvider; | ||||
| 
 | ||||
|         private MarkwonReducer reducer; | ||||
|         private Map<Class<? extends Node>, ViewProvider<Node>> viewProviders; | ||||
|         private LayoutInflater inflater; | ||||
| 
 | ||||
|         public Builder(@NonNull ViewProvider<Node> defaultViewProvider) { | ||||
|             this.defaultViewProvider = defaultViewProvider; | ||||
|             this.viewProviders = new HashMap<>(3); | ||||
|         } | ||||
| 
 | ||||
|         @NonNull | ||||
|         public Builder reducer(@NonNull MarkwonReducer reducer) { | ||||
|             this.reducer = reducer; | ||||
|             return this; | ||||
|         } | ||||
| 
 | ||||
|         @NonNull | ||||
|         public <N extends Node> Builder viewProvider( | ||||
|                 @NonNull Class<N> type, | ||||
|                 @NonNull ViewProvider<? super N> viewProvider) { | ||||
|             //noinspection unchecked | ||||
|             viewProviders.put(type, (ViewProvider<Node>) viewProvider); | ||||
|             return this; | ||||
|         } | ||||
| 
 | ||||
|         @NonNull | ||||
|         public Builder inflater(@NonNull LayoutInflater inflater) { | ||||
|             this.inflater = inflater; | ||||
|             return this; | ||||
|         } | ||||
| 
 | ||||
|         @NonNull | ||||
|         public MarkwonNodeRenderer build() { | ||||
|             if (reducer == null) { | ||||
|                 reducer = MarkwonReducer.directChildren(); | ||||
|             } | ||||
|             return new Impl(this); | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     public static class SimpleTextViewProvider implements ViewProvider<Node> { | ||||
| 
 | ||||
|         private final int layoutResId; | ||||
|         private final int textViewId; | ||||
| 
 | ||||
|         public SimpleTextViewProvider(@LayoutRes int layoutResId, @IdRes int textViewId) { | ||||
|             this.layoutResId = layoutResId; | ||||
|             this.textViewId = textViewId; | ||||
|         } | ||||
| 
 | ||||
|         @NonNull | ||||
|         @Override | ||||
|         public View provide( | ||||
|                 @NonNull LayoutInflater inflater, | ||||
|                 @NonNull ViewGroup group, | ||||
|                 @NonNull Markwon markwon, | ||||
|                 @NonNull Node node) { | ||||
|             final View view = inflater.inflate(layoutResId, group, false); | ||||
|             final TextView textView = view.findViewById(textViewId); | ||||
|             markwon.setParsedMarkdown(textView, markwon.render(node)); | ||||
|             return view; | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     static class Impl extends MarkwonNodeRenderer { | ||||
| 
 | ||||
|         private final MarkwonReducer reducer; | ||||
|         private final Map<Class<? extends Node>, ViewProvider<Node>> viewProviders; | ||||
|         private final ViewProvider<Node> defaultViewProvider; | ||||
| 
 | ||||
|         private LayoutInflater inflater; | ||||
| 
 | ||||
|         Impl(@NonNull Builder builder) { | ||||
|             this.reducer = builder.reducer; | ||||
|             this.viewProviders = builder.viewProviders; | ||||
|             this.defaultViewProvider = builder.defaultViewProvider; | ||||
|             this.inflater = builder.inflater; | ||||
|         } | ||||
| 
 | ||||
|         @Override | ||||
|         public void render(@NonNull ViewGroup group, @NonNull Markwon markwon, @NonNull String markdown) { | ||||
|             render(group, markwon, markwon.parse(markdown)); | ||||
|         } | ||||
| 
 | ||||
|         @Override | ||||
|         public void render(@NonNull ViewGroup group, @NonNull Markwon markwon, @NonNull Node root) { | ||||
| 
 | ||||
|             final LayoutInflater inflater = ensureLayoutInflater(group.getContext()); | ||||
| 
 | ||||
|             ViewProvider<Node> viewProvider; | ||||
| 
 | ||||
|             for (Node node : reducer.reduce(root)) { | ||||
|                 viewProvider = viewProvider(node); | ||||
|                 group.addView(viewProvider.provide(inflater, group, markwon, node)); | ||||
|             } | ||||
|         } | ||||
| 
 | ||||
|         @NonNull | ||||
|         private LayoutInflater ensureLayoutInflater(@NonNull Context context) { | ||||
|             LayoutInflater inflater = this.inflater; | ||||
|             if (inflater == null) { | ||||
|                 inflater = this.inflater = LayoutInflater.from(context); | ||||
|             } | ||||
|             return inflater; | ||||
|         } | ||||
| 
 | ||||
|         @NonNull | ||||
|         private ViewProvider<Node> viewProvider(@NonNull Node node) { | ||||
| 
 | ||||
|             // check for specific node view provider | ||||
|             final ViewProvider<Node> provider = viewProviders.get(node.getClass()); | ||||
|             if (provider != null) { | ||||
|                 return provider; | ||||
|             } | ||||
| 
 | ||||
|             // if it's not present, then we can return a default one | ||||
|             return defaultViewProvider; | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
| } | ||||
							
								
								
									
										143
									
								
								markwon-core/src/main/java/ru/noties/markwon/MarkwonPlugin.java
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @ -0,0 +1,143 @@ | ||||
| package ru.noties.markwon; | ||||
| 
 | ||||
| import android.support.annotation.NonNull; | ||||
| import android.text.Spanned; | ||||
| import android.widget.TextView; | ||||
| 
 | ||||
| import org.commonmark.node.Node; | ||||
| import org.commonmark.parser.Parser; | ||||
| 
 | ||||
| import ru.noties.markwon.core.MarkwonTheme; | ||||
| import ru.noties.markwon.html.MarkwonHtmlRenderer; | ||||
| import ru.noties.markwon.image.AsyncDrawableLoader; | ||||
| import ru.noties.markwon.image.MediaDecoder; | ||||
| import ru.noties.markwon.image.SchemeHandler; | ||||
| import ru.noties.markwon.priority.Priority; | ||||
| 
 | ||||
| /** | ||||
|  * Class represents a plugin (extension) to Markwon to configure how parsing and rendering | ||||
|  * of markdown is carried on. | ||||
|  * | ||||
|  * @see AbstractMarkwonPlugin | ||||
|  * @see ru.noties.markwon.core.CorePlugin | ||||
|  * @see ru.noties.markwon.image.ImagesPlugin | ||||
|  * @since 3.0.0 | ||||
|  */ | ||||
| public interface MarkwonPlugin { | ||||
| 
 | ||||
|     /** | ||||
|      * Method to configure <code>org.commonmark.parser.Parser</code> (for example register custom | ||||
|      * extension, etc). | ||||
|      */ | ||||
|     void configureParser(@NonNull Parser.Builder builder); | ||||
| 
 | ||||
|     /** | ||||
|      * Modify {@link MarkwonTheme} that is used for rendering of markdown. | ||||
|      * | ||||
|      * @see MarkwonTheme | ||||
|      * @see MarkwonTheme.Builder | ||||
|      */ | ||||
|     void configureTheme(@NonNull MarkwonTheme.Builder builder); | ||||
| 
 | ||||
|     /** | ||||
|      * Configure image loading functionality. For example add new content-types | ||||
|      * {@link AsyncDrawableLoader.Builder#addMediaDecoder(String, MediaDecoder)}, a transport | ||||
|      * layer (network, file, etc) {@link AsyncDrawableLoader.Builder#addSchemeHandler(String, SchemeHandler)} | ||||
|      * or modify existing properties. | ||||
|      * | ||||
|      * @see AsyncDrawableLoader | ||||
|      * @see AsyncDrawableLoader.Builder | ||||
|      */ | ||||
|     void configureImages(@NonNull AsyncDrawableLoader.Builder builder); | ||||
| 
 | ||||
|     /** | ||||
|      * Configure {@link MarkwonConfiguration} | ||||
|      * | ||||
|      * @see MarkwonConfiguration | ||||
|      * @see MarkwonConfiguration.Builder | ||||
|      */ | ||||
|     void configureConfiguration(@NonNull MarkwonConfiguration.Builder builder); | ||||
| 
 | ||||
|     /** | ||||
|      * Configure {@link MarkwonVisitor} to accept new node types or override already registered nodes. | ||||
|      * | ||||
|      * @see MarkwonVisitor | ||||
|      * @see MarkwonVisitor.Builder | ||||
|      */ | ||||
|     void configureVisitor(@NonNull MarkwonVisitor.Builder builder); | ||||
| 
 | ||||
|     /** | ||||
|      * Configure {@link MarkwonSpansFactory} to change what spans are used for certain node types. | ||||
|      * | ||||
|      * @see MarkwonSpansFactory | ||||
|      * @see MarkwonSpansFactory.Builder | ||||
|      */ | ||||
|     void configureSpansFactory(@NonNull MarkwonSpansFactory.Builder builder); | ||||
| 
 | ||||
|     /** | ||||
|      * Configure {@link MarkwonHtmlRenderer} to add or remove HTML {@link ru.noties.markwon.html.TagHandler}s | ||||
|      * | ||||
|      * @see MarkwonHtmlRenderer | ||||
|      * @see MarkwonHtmlRenderer.Builder | ||||
|      */ | ||||
|     void configureHtmlRenderer(@NonNull MarkwonHtmlRenderer.Builder builder); | ||||
| 
 | ||||
|     @NonNull | ||||
|     Priority priority(); | ||||
| 
 | ||||
|     /** | ||||
|      * Process input markdown and return new string to be used in parsing stage further. | ||||
|      * Can be described as <code>pre-processing</code> of markdown String. | ||||
|      * | ||||
|      * @param markdown String to process | ||||
|      * @return processed markdown String | ||||
|      */ | ||||
|     @NonNull | ||||
|     String processMarkdown(@NonNull String markdown); | ||||
| 
 | ||||
|     /** | ||||
|      * This method will be called <strong>before</strong> rendering will occur thus making possible | ||||
|      * to <code>post-process</code> parsed node (make changes for example). | ||||
|      * | ||||
|      * @param node root parsed org.commonmark.node.Node | ||||
|      */ | ||||
|     void beforeRender(@NonNull Node node); | ||||
| 
 | ||||
|     /** | ||||
|      * This method will be called <strong>after</strong> rendering (but before applying markdown to a | ||||
|      * TextView, if such action will happen). It can be used to clean some | ||||
|      * internal state, or trigger certain action. Please note that modifying <code>node</code> won\'t | ||||
|      * have any effect as it has been already <i>visited</i> at this stage. | ||||
|      * | ||||
|      * @param node    root parsed org.commonmark.node.Node | ||||
|      * @param visitor {@link MarkwonVisitor} instance used to render markdown | ||||
|      */ | ||||
|     void afterRender(@NonNull Node node, @NonNull MarkwonVisitor visitor); | ||||
| 
 | ||||
|     /** | ||||
|      * This method will be called <strong>before</strong> calling <code>TextView#setText</code>. | ||||
|      * <p> | ||||
|      * It can be useful to prepare a TextView for markdown. For example {@link ru.noties.markwon.image.ImagesPlugin} | ||||
|      * uses this method to unregister previously registered {@link ru.noties.markwon.image.AsyncDrawableSpan} | ||||
|      * (if there are such spans in this TextView at this point). Or {@link ru.noties.markwon.core.CorePlugin} | ||||
|      * which measures ordered list numbers | ||||
|      * | ||||
|      * @param textView TextView to which <code>markdown</code> will be applied | ||||
|      * @param markdown Parsed markdown | ||||
|      */ | ||||
|     void beforeSetText(@NonNull TextView textView, @NonNull Spanned markdown); | ||||
| 
 | ||||
|     /** | ||||
|      * This method will be called <strong>after</strong> markdown was applied. | ||||
|      * <p> | ||||
|      * It can be useful to trigger certain action on spans/textView. For example {@link ru.noties.markwon.image.ImagesPlugin} | ||||
|      * uses this method to register {@link ru.noties.markwon.image.AsyncDrawableSpan} and start | ||||
|      * asynchronously loading images. | ||||
|      * <p> | ||||
|      * Unlike {@link #beforeSetText(TextView, Spanned)} this method does not receive parsed markdown | ||||
|      * as at this point spans must be queried by calling <code>TextView#getText#getSpans</code>. | ||||
|      * | ||||
|      * @param textView TextView to which markdown was applied | ||||
|      */ | ||||
|     void afterSetText(@NonNull TextView textView); | ||||
| } | ||||
| @ -0,0 +1,61 @@ | ||||
| package ru.noties.markwon; | ||||
| 
 | ||||
| import android.support.annotation.NonNull; | ||||
| 
 | ||||
| import org.commonmark.node.Node; | ||||
| 
 | ||||
| import java.util.ArrayList; | ||||
| import java.util.Collections; | ||||
| import java.util.List; | ||||
| 
 | ||||
| /** | ||||
|  * @since 3.0.0 | ||||
|  */ | ||||
| public abstract class MarkwonReducer { | ||||
| 
 | ||||
|     /** | ||||
|      * @return direct children of supplied Node. In the most usual case | ||||
|      * will return all BlockNodes of a Document | ||||
|      */ | ||||
|     @NonNull | ||||
|     public static MarkwonReducer directChildren() { | ||||
|         return new DirectChildren(); | ||||
|     } | ||||
| 
 | ||||
|     @NonNull | ||||
|     public abstract List<Node> reduce(@NonNull Node node); | ||||
| 
 | ||||
| 
 | ||||
|     static class DirectChildren extends MarkwonReducer { | ||||
| 
 | ||||
|         @NonNull | ||||
|         @Override | ||||
|         public List<Node> reduce(@NonNull Node root) { | ||||
| 
 | ||||
|             final List<Node> list; | ||||
| 
 | ||||
|             // we will extract all blocks that are direct children of Document | ||||
|             Node node = root.getFirstChild(); | ||||
| 
 | ||||
|             // please note, that if there are no children -> we will return a list with | ||||
|             // single element (which was supplied) | ||||
|             if (node == null) { | ||||
|                 list = Collections.singletonList(root); | ||||
|             } else { | ||||
| 
 | ||||
|                 list = new ArrayList<>(); | ||||
| 
 | ||||
|                 Node temp; | ||||
| 
 | ||||
|                 while (node != null) { | ||||
|                     list.add(node); | ||||
|                     temp = node.getNext(); | ||||
|                     node.unlink(); | ||||
|                     node = temp; | ||||
|                 } | ||||
|             } | ||||
| 
 | ||||
|             return list; | ||||
|         } | ||||
|     } | ||||
| } | ||||
| @ -0,0 +1,46 @@ | ||||
| package ru.noties.markwon; | ||||
| 
 | ||||
| import android.support.annotation.NonNull; | ||||
| import android.support.annotation.Nullable; | ||||
| 
 | ||||
| import org.commonmark.node.Node; | ||||
| 
 | ||||
| /** | ||||
|  * Class that controls what spans are used for certain Nodes. | ||||
|  * | ||||
|  * @see SpanFactory | ||||
|  * @since 3.0.0 | ||||
|  */ | ||||
| public interface MarkwonSpansFactory { | ||||
| 
 | ||||
|     /** | ||||
|      * Returns registered {@link SpanFactory} or <code>null</code> if a factory for this node type | ||||
|      * is not registered. There is {@link #require(Class)} method that will throw an exception | ||||
|      * if required {@link SpanFactory} is not registered, thus making return type <code>non-null</code> | ||||
|      * | ||||
|      * @param node type of the node | ||||
|      * @return registered {@link SpanFactory} or null if it\'s not registered | ||||
|      * @see #require(Class) | ||||
|      */ | ||||
|     @Nullable | ||||
|     <N extends Node> SpanFactory get(@NonNull Class<N> node); | ||||
| 
 | ||||
|     @NonNull | ||||
|     <N extends Node> SpanFactory require(@NonNull Class<N> node); | ||||
| 
 | ||||
| 
 | ||||
|     interface Builder { | ||||
| 
 | ||||
|         @NonNull | ||||
|         <N extends Node> Builder setFactory(@NonNull Class<N> node, @Nullable SpanFactory factory); | ||||
| 
 | ||||
|         /** | ||||
|          * Can be useful when <em>enhancing</em> an already defined SpanFactory with another one. | ||||
|          */ | ||||
|         @Nullable | ||||
|         <N extends Node> SpanFactory getFactory(@NonNull Class<N> node); | ||||
| 
 | ||||
|         @NonNull | ||||
|         MarkwonSpansFactory build(); | ||||
|     } | ||||
| } | ||||
| @ -0,0 +1,67 @@ | ||||
| package ru.noties.markwon; | ||||
| 
 | ||||
| import android.support.annotation.NonNull; | ||||
| import android.support.annotation.Nullable; | ||||
| 
 | ||||
| import org.commonmark.node.Node; | ||||
| 
 | ||||
| import java.util.Collections; | ||||
| import java.util.HashMap; | ||||
| import java.util.Map; | ||||
| 
 | ||||
| /** | ||||
|  * @since 3.0.0 | ||||
|  */ | ||||
| class MarkwonSpansFactoryImpl implements MarkwonSpansFactory { | ||||
| 
 | ||||
|     private final Map<Class<? extends Node>, SpanFactory> factories; | ||||
| 
 | ||||
|     MarkwonSpansFactoryImpl(@NonNull Map<Class<? extends Node>, SpanFactory> factories) { | ||||
|         this.factories = factories; | ||||
|     } | ||||
| 
 | ||||
|     @Nullable | ||||
|     @Override | ||||
|     public <N extends Node> SpanFactory get(@NonNull Class<N> node) { | ||||
|         return factories.get(node); | ||||
|     } | ||||
| 
 | ||||
|     @NonNull | ||||
|     @Override | ||||
|     public <N extends Node> SpanFactory require(@NonNull Class<N> node) { | ||||
|         final SpanFactory f = get(node); | ||||
|         if (f == null) { | ||||
|             throw new NullPointerException(node.getName()); | ||||
|         } | ||||
|         return f; | ||||
|     } | ||||
| 
 | ||||
|     static class BuilderImpl implements Builder { | ||||
| 
 | ||||
|         private final Map<Class<? extends Node>, SpanFactory> factories = | ||||
|                 new HashMap<>(3); | ||||
| 
 | ||||
|         @NonNull | ||||
|         @Override | ||||
|         public <N extends Node> Builder setFactory(@NonNull Class<N> node, @Nullable SpanFactory factory) { | ||||
|             if (factory == null) { | ||||
|                 factories.remove(node); | ||||
|             } else { | ||||
|                 factories.put(node, factory); | ||||
|             } | ||||
|             return this; | ||||
|         } | ||||
| 
 | ||||
|         @Nullable | ||||
|         @Override | ||||
|         public <N extends Node> SpanFactory getFactory(@NonNull Class<N> node) { | ||||
|             return factories.get(node); | ||||
|         } | ||||
| 
 | ||||
|         @NonNull | ||||
|         @Override | ||||
|         public MarkwonSpansFactory build() { | ||||
|             return new MarkwonSpansFactoryImpl(Collections.unmodifiableMap(factories)); | ||||
|         } | ||||
|     } | ||||
| } | ||||
							
								
								
									
										136
									
								
								markwon-core/src/main/java/ru/noties/markwon/MarkwonVisitor.java
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @ -0,0 +1,136 @@ | ||||
| package ru.noties.markwon; | ||||
| 
 | ||||
| import android.support.annotation.NonNull; | ||||
| import android.support.annotation.Nullable; | ||||
| 
 | ||||
| import org.commonmark.node.Node; | ||||
| import org.commonmark.node.Visitor; | ||||
| 
 | ||||
| /** | ||||
|  * Configurable visitor of parsed markdown. Allows visiting certain (registered) nodes without | ||||
|  * need to create own instance of this class. | ||||
|  * | ||||
|  * @see Builder#on(Class, NodeVisitor) | ||||
|  * @see MarkwonPlugin#configureVisitor(Builder) | ||||
|  * @since 3.0.0 | ||||
|  */ | ||||
| public interface MarkwonVisitor extends Visitor { | ||||
| 
 | ||||
|     /** | ||||
|      * @see Builder#on(Class, NodeVisitor) | ||||
|      */ | ||||
|     interface NodeVisitor<N extends Node> { | ||||
|         void visit(@NonNull MarkwonVisitor visitor, @NonNull N n); | ||||
|     } | ||||
| 
 | ||||
|     interface Builder { | ||||
| 
 | ||||
|         /** | ||||
|          * @param node        to register | ||||
|          * @param nodeVisitor {@link NodeVisitor} to be used or null to ignore previously registered | ||||
|          *                    visitor for this node | ||||
|          */ | ||||
|         @NonNull | ||||
|         <N extends Node> Builder on(@NonNull Class<N> node, @Nullable NodeVisitor<? super N> nodeVisitor); | ||||
| 
 | ||||
|         @NonNull | ||||
|         MarkwonVisitor build(@NonNull MarkwonConfiguration configuration, @NonNull RenderProps renderProps); | ||||
|     } | ||||
| 
 | ||||
|     @NonNull | ||||
|     MarkwonConfiguration configuration(); | ||||
| 
 | ||||
|     @NonNull | ||||
|     RenderProps renderProps(); | ||||
| 
 | ||||
|     @NonNull | ||||
|     SpannableBuilder builder(); | ||||
| 
 | ||||
|     /** | ||||
|      * Visits all children of supplied node. | ||||
|      * | ||||
|      * @param node to visit | ||||
|      */ | ||||
|     void visitChildren(@NonNull Node node); | ||||
| 
 | ||||
|     /** | ||||
|      * Executes a check if there is further content available. | ||||
|      * | ||||
|      * @param node to check | ||||
|      * @return boolean indicating if there are more nodes after supplied one | ||||
|      */ | ||||
|     boolean hasNext(@NonNull Node node); | ||||
| 
 | ||||
|     /** | ||||
|      * This method <strong>ensures</strong> that further content will start at a new line. If current | ||||
|      * last character is already a new line, then it won\'t do anything. | ||||
|      */ | ||||
|     void ensureNewLine(); | ||||
| 
 | ||||
|     /** | ||||
|      * This method inserts a new line without any condition checking (unlike {@link #ensureNewLine()}). | ||||
|      */ | ||||
|     void forceNewLine(); | ||||
| 
 | ||||
|     /** | ||||
|      * Helper method to call <code>builder().length()</code> | ||||
|      * | ||||
|      * @return current length of underlying {@link SpannableBuilder} | ||||
|      */ | ||||
|     int length(); | ||||
| 
 | ||||
|     /** | ||||
|      * Clears state of visitor (both {@link RenderProps} and {@link SpannableBuilder} will be cleared | ||||
|      */ | ||||
|     void clear(); | ||||
| 
 | ||||
|     /** | ||||
|      * Sets <code>spans</code> to underlying {@link SpannableBuilder} from <em>start</em> | ||||
|      * to <em>{@link SpannableBuilder#length()}</em>. | ||||
|      * | ||||
|      * @param start start position of spans | ||||
|      * @param spans to apply | ||||
|      */ | ||||
|     void setSpans(int start, @Nullable Object spans); | ||||
| 
 | ||||
|     /** | ||||
|      * Helper method to obtain and apply spans for supplied Node. Internally queries {@link SpanFactory} | ||||
|      * for the node (via {@link MarkwonSpansFactory#require(Class)} thus throwing an exception | ||||
|      * if there is no {@link SpanFactory} registered for the node). | ||||
|      * | ||||
|      * @param node  to retrieve {@link SpanFactory} for | ||||
|      * @param start start position for further {@link #setSpans(int, Object)} call | ||||
|      * @see #setSpansForNodeOptional(Node, int) | ||||
|      */ | ||||
|     <N extends Node> void setSpansForNode(@NonNull N node, int start); | ||||
| 
 | ||||
|     /** | ||||
|      * The same as {@link #setSpansForNode(Node, int)} but can be used in situations when there is | ||||
|      * no access to a Node instance (for example in HTML rendering which doesn\'t have markdown Nodes). | ||||
|      * | ||||
|      * @see #setSpansForNode(Node, int) | ||||
|      */ | ||||
|     <N extends Node> void setSpansForNode(@NonNull Class<N> node, int start); | ||||
| 
 | ||||
|     // does not throw if there is no SpanFactory registered for this node | ||||
| 
 | ||||
|     /** | ||||
|      * Helper method to apply spans from a {@link SpanFactory} <b>if</b> it\'s registered in | ||||
|      * {@link MarkwonSpansFactory} instance. Otherwise ignores this call (no spans will be applied). | ||||
|      * If there is a need to ensure that specified <code>node</code> has a {@link SpanFactory} registered, | ||||
|      * then {@link #setSpansForNode(Node, int)} can be used. {@link #setSpansForNode(Node, int)} internally | ||||
|      * uses {@link MarkwonSpansFactory#require(Class)}. This method uses {@link MarkwonSpansFactory#get(Class)}. | ||||
|      * | ||||
|      * @see #setSpansForNode(Node, int) | ||||
|      */ | ||||
|     <N extends Node> void setSpansForNodeOptional(@NonNull N node, int start); | ||||
| 
 | ||||
|     /** | ||||
|      * The same as {@link #setSpansForNodeOptional(Node, int)} but can be used in situations when | ||||
|      * there is no access to a Node instance (for example in HTML rendering). | ||||
|      * | ||||
|      * @see #setSpansForNodeOptional(Node, int) | ||||
|      */ | ||||
|     @SuppressWarnings("unused") | ||||
|     <N extends Node> void setSpansForNodeOptional(@NonNull Class<N> node, int start); | ||||
| } | ||||
| @ -0,0 +1,292 @@ | ||||
| package ru.noties.markwon; | ||||
| 
 | ||||
| import android.support.annotation.NonNull; | ||||
| import android.support.annotation.Nullable; | ||||
| 
 | ||||
| import org.commonmark.node.BlockQuote; | ||||
| import org.commonmark.node.BulletList; | ||||
| import org.commonmark.node.Code; | ||||
| import org.commonmark.node.CustomBlock; | ||||
| import org.commonmark.node.CustomNode; | ||||
| import org.commonmark.node.Document; | ||||
| import org.commonmark.node.Emphasis; | ||||
| import org.commonmark.node.FencedCodeBlock; | ||||
| import org.commonmark.node.HardLineBreak; | ||||
| import org.commonmark.node.Heading; | ||||
| import org.commonmark.node.HtmlBlock; | ||||
| import org.commonmark.node.HtmlInline; | ||||
| import org.commonmark.node.Image; | ||||
| import org.commonmark.node.IndentedCodeBlock; | ||||
| import org.commonmark.node.Link; | ||||
| import org.commonmark.node.ListItem; | ||||
| import org.commonmark.node.Node; | ||||
| import org.commonmark.node.OrderedList; | ||||
| import org.commonmark.node.Paragraph; | ||||
| import org.commonmark.node.SoftLineBreak; | ||||
| import org.commonmark.node.StrongEmphasis; | ||||
| import org.commonmark.node.Text; | ||||
| import org.commonmark.node.ThematicBreak; | ||||
| 
 | ||||
| import java.util.Collections; | ||||
| import java.util.HashMap; | ||||
| import java.util.Map; | ||||
| 
 | ||||
| /** | ||||
|  * @since 3.0.0 | ||||
|  */ | ||||
| class MarkwonVisitorImpl implements MarkwonVisitor { | ||||
| 
 | ||||
|     private final MarkwonConfiguration configuration; | ||||
| 
 | ||||
|     private final RenderProps renderProps; | ||||
| 
 | ||||
|     private final SpannableBuilder builder; | ||||
| 
 | ||||
|     private final Map<Class<? extends Node>, NodeVisitor<? extends Node>> nodes; | ||||
| 
 | ||||
|     MarkwonVisitorImpl( | ||||
|             @NonNull MarkwonConfiguration configuration, | ||||
|             @NonNull RenderProps renderProps, | ||||
|             @NonNull SpannableBuilder builder, | ||||
|             @NonNull Map<Class<? extends Node>, NodeVisitor<? extends Node>> nodes) { | ||||
|         this.configuration = configuration; | ||||
|         this.renderProps = renderProps; | ||||
|         this.builder = builder; | ||||
|         this.nodes = nodes; | ||||
|     } | ||||
| 
 | ||||
|     @Override | ||||
|     public void visit(BlockQuote blockQuote) { | ||||
|         visit((Node) blockQuote); | ||||
|     } | ||||
| 
 | ||||
|     @Override | ||||
|     public void visit(BulletList bulletList) { | ||||
|         visit((Node) bulletList); | ||||
|     } | ||||
| 
 | ||||
|     @Override | ||||
|     public void visit(Code code) { | ||||
|         visit((Node) code); | ||||
|     } | ||||
| 
 | ||||
|     @Override | ||||
|     public void visit(Document document) { | ||||
|         visit((Node) document); | ||||
|     } | ||||
| 
 | ||||
|     @Override | ||||
|     public void visit(Emphasis emphasis) { | ||||
|         visit((Node) emphasis); | ||||
|     } | ||||
| 
 | ||||
|     @Override | ||||
|     public void visit(FencedCodeBlock fencedCodeBlock) { | ||||
|         visit((Node) fencedCodeBlock); | ||||
|     } | ||||
| 
 | ||||
|     @Override | ||||
|     public void visit(HardLineBreak hardLineBreak) { | ||||
|         visit((Node) hardLineBreak); | ||||
|     } | ||||
| 
 | ||||
|     @Override | ||||
|     public void visit(Heading heading) { | ||||
|         visit((Node) heading); | ||||
|     } | ||||
| 
 | ||||
|     @Override | ||||
|     public void visit(ThematicBreak thematicBreak) { | ||||
|         visit((Node) thematicBreak); | ||||
|     } | ||||
| 
 | ||||
|     @Override | ||||
|     public void visit(HtmlInline htmlInline) { | ||||
|         visit((Node) htmlInline); | ||||
|     } | ||||
| 
 | ||||
|     @Override | ||||
|     public void visit(HtmlBlock htmlBlock) { | ||||
|         visit((Node) htmlBlock); | ||||
|     } | ||||
| 
 | ||||
|     @Override | ||||
|     public void visit(Image image) { | ||||
|         visit((Node) image); | ||||
|     } | ||||
| 
 | ||||
|     @Override | ||||
|     public void visit(IndentedCodeBlock indentedCodeBlock) { | ||||
|         visit((Node) indentedCodeBlock); | ||||
|     } | ||||
| 
 | ||||
|     @Override | ||||
|     public void visit(Link link) { | ||||
|         visit((Node) link); | ||||
|     } | ||||
| 
 | ||||
|     @Override | ||||
|     public void visit(ListItem listItem) { | ||||
|         visit((Node) listItem); | ||||
|     } | ||||
| 
 | ||||
|     @Override | ||||
|     public void visit(OrderedList orderedList) { | ||||
|         visit((Node) orderedList); | ||||
|     } | ||||
| 
 | ||||
|     @Override | ||||
|     public void visit(Paragraph paragraph) { | ||||
|         visit((Node) paragraph); | ||||
|     } | ||||
| 
 | ||||
|     @Override | ||||
|     public void visit(SoftLineBreak softLineBreak) { | ||||
|         visit((Node) softLineBreak); | ||||
|     } | ||||
| 
 | ||||
|     @Override | ||||
|     public void visit(StrongEmphasis strongEmphasis) { | ||||
|         visit((Node) strongEmphasis); | ||||
|     } | ||||
| 
 | ||||
|     @Override | ||||
|     public void visit(Text text) { | ||||
|         visit((Node) text); | ||||
|     } | ||||
| 
 | ||||
|     @Override | ||||
|     public void visit(CustomBlock customBlock) { | ||||
|         visit((Node) customBlock); | ||||
|     } | ||||
| 
 | ||||
|     @Override | ||||
|     public void visit(CustomNode customNode) { | ||||
|         visit((Node) customNode); | ||||
|     } | ||||
| 
 | ||||
|     private void visit(@NonNull Node node) { | ||||
|         //noinspection unchecked | ||||
|         final NodeVisitor<Node> nodeVisitor = (NodeVisitor<Node>) nodes.get(node.getClass()); | ||||
|         if (nodeVisitor != null) { | ||||
|             nodeVisitor.visit(this, node); | ||||
|         } else { | ||||
|             visitChildren(node); | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     @NonNull | ||||
|     @Override | ||||
|     public MarkwonConfiguration configuration() { | ||||
|         return configuration; | ||||
|     } | ||||
| 
 | ||||
|     @NonNull | ||||
|     @Override | ||||
|     public RenderProps renderProps() { | ||||
|         return renderProps; | ||||
|     } | ||||
| 
 | ||||
|     @NonNull | ||||
|     @Override | ||||
|     public SpannableBuilder builder() { | ||||
|         return builder; | ||||
|     } | ||||
| 
 | ||||
|     @Override | ||||
|     public void visitChildren(@NonNull Node parent) { | ||||
|         Node node = parent.getFirstChild(); | ||||
|         while (node != null) { | ||||
|             // A subclass of this visitor might modify the node, resulting in getNext returning a different node or no | ||||
|             // node after visiting it. So get the next node before visiting. | ||||
|             Node next = node.getNext(); | ||||
|             node.accept(this); | ||||
|             node = next; | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     @Override | ||||
|     public boolean hasNext(@NonNull Node node) { | ||||
|         return node.getNext() != null; | ||||
|     } | ||||
| 
 | ||||
|     @Override | ||||
|     public void ensureNewLine() { | ||||
|         if (builder.length() > 0 | ||||
|                 && '\n' != builder.lastChar()) { | ||||
|             builder.append('\n'); | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     @Override | ||||
|     public void forceNewLine() { | ||||
|         builder.append('\n'); | ||||
|     } | ||||
| 
 | ||||
|     @Override | ||||
|     public int length() { | ||||
|         return builder.length(); | ||||
|     } | ||||
| 
 | ||||
|     @Override | ||||
|     public void setSpans(int start, @Nullable Object spans) { | ||||
|         SpannableBuilder.setSpans(builder, spans, start, builder.length()); | ||||
|     } | ||||
| 
 | ||||
|     @Override | ||||
|     public void clear() { | ||||
|         renderProps.clearAll(); | ||||
|         builder.clear(); | ||||
|     } | ||||
| 
 | ||||
|     @Override | ||||
|     public <N extends Node> void setSpansForNode(@NonNull N node, int start) { | ||||
|         setSpansForNode(node.getClass(), start); | ||||
|     } | ||||
| 
 | ||||
|     @Override | ||||
|     public <N extends Node> void setSpansForNode(@NonNull Class<N> node, int start) { | ||||
|         setSpans(start, configuration.spansFactory().require(node).getSpans(configuration, renderProps)); | ||||
|     } | ||||
| 
 | ||||
|     @Override | ||||
|     public <N extends Node> void setSpansForNodeOptional(@NonNull N node, int start) { | ||||
|         setSpansForNodeOptional(node.getClass(), start); | ||||
|     } | ||||
| 
 | ||||
|     @Override | ||||
|     public <N extends Node> void setSpansForNodeOptional(@NonNull Class<N> node, int start) { | ||||
|         final SpanFactory factory = configuration.spansFactory().get(node); | ||||
|         if (factory != null) { | ||||
|             setSpans(start, factory.getSpans(configuration, renderProps)); | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     static class BuilderImpl implements Builder { | ||||
| 
 | ||||
|         private final Map<Class<? extends Node>, NodeVisitor<? extends Node>> nodes = new HashMap<>(); | ||||
| 
 | ||||
|         @NonNull | ||||
|         @Override | ||||
|         public <N extends Node> Builder on(@NonNull Class<N> node, @Nullable NodeVisitor<? super N> nodeVisitor) { | ||||
|             // we should allow `null` to exclude node from being visited (for example to disable | ||||
|             // some functionality) | ||||
|             if (nodeVisitor == null) { | ||||
|                 nodes.remove(node); | ||||
|             } else { | ||||
|                 nodes.put(node, nodeVisitor); | ||||
|             } | ||||
|             return this; | ||||
|         } | ||||
| 
 | ||||
|         @NonNull | ||||
|         @Override | ||||
|         public MarkwonVisitor build(@NonNull MarkwonConfiguration configuration, @NonNull RenderProps renderProps) { | ||||
|             return new MarkwonVisitorImpl( | ||||
|                     configuration, | ||||
|                     renderProps, | ||||
|                     new SpannableBuilder(), | ||||
|                     Collections.unmodifiableMap(nodes)); | ||||
|         } | ||||
|     } | ||||
| } | ||||
 Dimitry
						Dimitry