commit
						3ab015175b
					
				
							
								
								
									
										19
									
								
								CHANGELOG.md
									
									
									
									
									
								
							
							
						
						
									
										19
									
								
								CHANGELOG.md
									
									
									
									
									
								
							| @ -1,5 +1,24 @@ | |||||||
| # Changelog | # Changelog | ||||||
| 
 | 
 | ||||||
|  | # 4.4.0 | ||||||
|  | * `TextViewSpan` to obtain `TextView` in which markdown is displayed (applied by `CorePlugin`) | ||||||
|  | * `TextLayoutSpan` to obtain `Layout` in which markdown is displayed (applied by `TablePlugin`, more specifically `TableRowSpan` to propagate layout in which cell content is displayed) | ||||||
|  | * `HtmlEmptyTagReplacement` now is configurable by `HtmlPlugin`, `iframe` handling ([#235]) | ||||||
|  | * `AsyncDrawable` now uses `TextView` width without padding instead of width of canvas | ||||||
|  | * Support for images inside table cells (`ext-tables` module) | ||||||
|  | * Expose `enabledBlockTypes` in `CorePlugin` | ||||||
|  | * Update `jlatexmath-android` dependency ([#225]) | ||||||
|  | * Update `image-coil` module (Coil version `0.10.1`) ([#244])<br>Thanks to [@tylerbwong] | ||||||
|  | * Rename `UrlProcessor` to `ImageDestinationProcessor` (`io.noties.markwon.urlprocessor` -> `io.noties.markwon.image.destination`) and limit its usage to process **only** destination URL of images (was used to also process links before) | ||||||
|  | * `fallbackToRawInputWhenEmpty` `Markwon.Builder` configuration to fallback to raw input if rendered markdown is empty ([#242])  | ||||||
|  | 
 | ||||||
|  | [#235]: https://github.com/noties/Markwon/issues/235 | ||||||
|  | [#225]: https://github.com/noties/Markwon/issues/225 | ||||||
|  | [#244]: https://github.com/noties/Markwon/pull/244 | ||||||
|  | [#242]: https://github.com/noties/Markwon/issues/242 | ||||||
|  | [@tylerbwong]: https://github.com/tylerbwong | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
| # 4.3.1 | # 4.3.1 | ||||||
| * Fix DexGuard optimization issue ([#216])<br>Thanks [@francescocervone] | * Fix DexGuard optimization issue ([#216])<br>Thanks [@francescocervone] | ||||||
| * module `images`: `GifSupport` and `SvgSupport` use `Class.forName` instead access to full qualified class name | * module `images`: `GifSupport` and `SvgSupport` use `Class.forName` instead access to full qualified class name | ||||||
|  | |||||||
| @ -5,15 +5,15 @@ import android.text.TextUtils; | |||||||
| 
 | 
 | ||||||
| import androidx.annotation.NonNull; | import androidx.annotation.NonNull; | ||||||
| 
 | 
 | ||||||
| import io.noties.markwon.urlprocessor.UrlProcessor; | import io.noties.markwon.image.destination.ImageDestinationProcessor; | ||||||
| import io.noties.markwon.urlprocessor.UrlProcessorRelativeToAbsolute; | import io.noties.markwon.image.destination.ImageDestinationProcessorRelativeToAbsolute; | ||||||
| 
 | 
 | ||||||
| class UrlProcessorInitialReadme implements UrlProcessor { | class ImageDestinationProcessorInitialReadme extends ImageDestinationProcessor { | ||||||
| 
 | 
 | ||||||
|     private static final String GITHUB_BASE = "https://github.com/noties/Markwon/raw/master/"; |     private static final String GITHUB_BASE = "https://github.com/noties/Markwon/raw/master/"; | ||||||
| 
 | 
 | ||||||
|     private final UrlProcessorRelativeToAbsolute processor |     private final ImageDestinationProcessorRelativeToAbsolute processor | ||||||
|             = new UrlProcessorRelativeToAbsolute(GITHUB_BASE); |             = new ImageDestinationProcessorRelativeToAbsolute(GITHUB_BASE); | ||||||
| 
 | 
 | ||||||
|     @NonNull |     @NonNull | ||||||
|     @Override |     @Override | ||||||
| @ -24,6 +24,8 @@ import io.noties.markwon.ext.tables.TablePlugin; | |||||||
| import io.noties.markwon.ext.tasklist.TaskListPlugin; | import io.noties.markwon.ext.tasklist.TaskListPlugin; | ||||||
| import io.noties.markwon.html.HtmlPlugin; | import io.noties.markwon.html.HtmlPlugin; | ||||||
| import io.noties.markwon.image.ImagesPlugin; | import io.noties.markwon.image.ImagesPlugin; | ||||||
|  | import io.noties.markwon.image.destination.ImageDestinationProcessor; | ||||||
|  | import io.noties.markwon.image.destination.ImageDestinationProcessorRelativeToAbsolute; | ||||||
| import io.noties.markwon.image.file.FileSchemeHandler; | import io.noties.markwon.image.file.FileSchemeHandler; | ||||||
| import io.noties.markwon.image.gif.GifMediaDecoder; | import io.noties.markwon.image.gif.GifMediaDecoder; | ||||||
| import io.noties.markwon.image.network.OkHttpNetworkSchemeHandler; | import io.noties.markwon.image.network.OkHttpNetworkSchemeHandler; | ||||||
| @ -31,8 +33,6 @@ import io.noties.markwon.syntax.Prism4jTheme; | |||||||
| import io.noties.markwon.syntax.Prism4jThemeDarkula; | import io.noties.markwon.syntax.Prism4jThemeDarkula; | ||||||
| import io.noties.markwon.syntax.Prism4jThemeDefault; | import io.noties.markwon.syntax.Prism4jThemeDefault; | ||||||
| import io.noties.markwon.syntax.SyntaxHighlightPlugin; | import io.noties.markwon.syntax.SyntaxHighlightPlugin; | ||||||
| import io.noties.markwon.urlprocessor.UrlProcessor; |  | ||||||
| import io.noties.markwon.urlprocessor.UrlProcessorRelativeToAbsolute; |  | ||||||
| import io.noties.prism4j.Prism4j; | import io.noties.prism4j.Prism4j; | ||||||
| 
 | 
 | ||||||
| @ActivityScope | @ActivityScope | ||||||
| @ -86,11 +86,11 @@ public class MarkdownRenderer { | |||||||
|             } |             } | ||||||
| 
 | 
 | ||||||
|             private void execute() { |             private void execute() { | ||||||
|                 final UrlProcessor urlProcessor; |                 final ImageDestinationProcessor imageDestinationProcessor; | ||||||
|                 if (uri == null) { |                 if (uri == null) { | ||||||
|                     urlProcessor = new UrlProcessorInitialReadme(); |                     imageDestinationProcessor = new ImageDestinationProcessorInitialReadme(); | ||||||
|                 } else { |                 } else { | ||||||
|                     urlProcessor = new UrlProcessorRelativeToAbsolute(uri.toString()); |                     imageDestinationProcessor = new ImageDestinationProcessorRelativeToAbsolute(uri.toString()); | ||||||
|                 } |                 } | ||||||
| 
 | 
 | ||||||
|                 final Prism4jTheme prism4jTheme = isLightTheme |                 final Prism4jTheme prism4jTheme = isLightTheme | ||||||
| @ -119,7 +119,7 @@ public class MarkdownRenderer { | |||||||
|                         .usePlugin(new AbstractMarkwonPlugin() { |                         .usePlugin(new AbstractMarkwonPlugin() { | ||||||
|                             @Override |                             @Override | ||||||
|                             public void configureConfiguration(@NonNull MarkwonConfiguration.Builder builder) { |                             public void configureConfiguration(@NonNull MarkwonConfiguration.Builder builder) { | ||||||
|                                 builder.urlProcessor(urlProcessor); |                                 builder.imageDestinationProcessor(imageDestinationProcessor); | ||||||
|                             } |                             } | ||||||
|                         }) |                         }) | ||||||
|                         .build(); |                         .build(); | ||||||
|  | |||||||
| @ -72,7 +72,7 @@ ext { | |||||||
|             'commonmark-table'        : "com.atlassian.commonmark:commonmark-ext-gfm-tables:$commonMarkVersion", |             'commonmark-table'        : "com.atlassian.commonmark:commonmark-ext-gfm-tables:$commonMarkVersion", | ||||||
|             'android-svg'             : 'com.caverock:androidsvg:1.4', |             'android-svg'             : 'com.caverock:androidsvg:1.4', | ||||||
|             'android-gif'             : 'pl.droidsonroids.gif:android-gif-drawable:1.2.15', |             'android-gif'             : 'pl.droidsonroids.gif:android-gif-drawable:1.2.15', | ||||||
|             'jlatexmath-android'      : 'ru.noties:jlatexmath-android:0.1.1', |             'jlatexmath-android'      : 'ru.noties:jlatexmath-android:0.2.0', | ||||||
|             'okhttp'                  : 'com.squareup.okhttp3:okhttp:3.9.0', |             'okhttp'                  : 'com.squareup.okhttp3:okhttp:3.9.0', | ||||||
|             'prism4j'                 : 'io.noties:prism4j:2.0.0', |             'prism4j'                 : 'io.noties:prism4j:2.0.0', | ||||||
|             'debug'                   : 'io.noties:debug:5.0.0@jar', |             'debug'                   : 'io.noties:debug:5.0.0@jar', | ||||||
| @ -80,7 +80,7 @@ ext { | |||||||
|             'dagger'                  : "com.google.dagger:dagger:$daggerVersion", |             'dagger'                  : "com.google.dagger:dagger:$daggerVersion", | ||||||
|             'picasso'                 : 'com.squareup.picasso:picasso:2.71828', |             'picasso'                 : 'com.squareup.picasso:picasso:2.71828', | ||||||
|             'glide'                   : 'com.github.bumptech.glide:glide:4.9.0', |             'glide'                   : 'com.github.bumptech.glide:glide:4.9.0', | ||||||
|             'coil'                    : 'io.coil-kt:coil:0.8.0' |             'coil'                    : 'io.coil-kt:coil:0.10.1' | ||||||
|     ] |     ] | ||||||
| 
 | 
 | ||||||
|     deps['annotationProcessor'] = [ |     deps['annotationProcessor'] = [ | ||||||
|  | |||||||
| @ -99,12 +99,7 @@ and 2 themes included: Light & Dark. It can be downloaded from [releases](ht | |||||||
| ## # Awesome Markwon | ## # Awesome Markwon | ||||||
| 
 | 
 | ||||||
| </div> | </div> | ||||||
| 
 | <br> | ||||||
| <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. |  | ||||||
| * [Boxcryptor](https://www.boxcryptor.com) - A software that adds AES-256 and RSA encryption to Dropbox, Google Drive, OneDrive and many other clouds. |  | ||||||
| 
 | 
 | ||||||
| <AwesomeGroup :apps="[ | <AwesomeGroup :apps="[ | ||||||
|     {name: 'Nextcloud', image: $withBase(`/assets/apps/nextcloud.png`), link: 'https://github.com/nextcloud/android', description: 'A safe home for all your data. Access & share your files, calendars, contacts, mail & more from any device, on your terms.'}, |     {name: 'Nextcloud', image: $withBase(`/assets/apps/nextcloud.png`), link: 'https://github.com/nextcloud/android', description: 'A safe home for all your data. Access & share your files, calendars, contacts, mail & more from any device, on your terms.'}, | ||||||
| @ -113,6 +108,11 @@ and 2 themes included: Light & Dark. It can be downloaded from [releases](ht | |||||||
|     {name: 'Pure Writer', image: $withBase(`/assets/apps/purewriter.png`), link: 'https://play.google.com/store/apps/details?id=com.drakeet.purewriter', description: 'Never lose content editor & Markdown'} |     {name: 'Pure Writer', image: $withBase(`/assets/apps/purewriter.png`), link: 'https://play.google.com/store/apps/details?id=com.drakeet.purewriter', description: 'Never lose content editor & Markdown'} | ||||||
| ]" /> | ]" /> | ||||||
| 
 | 
 | ||||||
|  | * [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. | ||||||
|  | * [Boxcryptor](https://www.boxcryptor.com) - A software that adds AES-256 and RSA encryption to Dropbox, Google Drive, OneDrive and many other clouds. | ||||||
|  | * [Senstone Portable Voice Assistant](https://play.google.com/store/apps/details?id=com.senstone) - Senstone is a tiny wearable personal assistant powered by this App. It lets you capture your ideas, notes and reminders handsfree without pulling out your phone.  | ||||||
|  | 
 | ||||||
| 
 | 
 | ||||||
| <u>Extension/plugins</u>: | <u>Extension/plugins</u>: | ||||||
| 
 | 
 | ||||||
|  | |||||||
| @ -5,7 +5,7 @@ These are _configurable_ properties: | |||||||
| * `AsyncDrawableLoader` (back here since <Badge text="4.0.0" />) | * `AsyncDrawableLoader` (back here since <Badge text="4.0.0" />) | ||||||
| * `SyntaxHighlight` | * `SyntaxHighlight` | ||||||
| * `LinkResolver` (since <Badge text="4.0.0" />, before — `LinkSpan.Resolver`) | * `LinkResolver` (since <Badge text="4.0.0" />, before — `LinkSpan.Resolver`) | ||||||
| * `UrlProcessor` | * `ImageDestinationProcessor` (since <Badge text="4.4.0" />, before — `UrlProcessor`) | ||||||
| * `ImageSizeResolver` | * `ImageSizeResolver` | ||||||
| 
 | 
 | ||||||
| :::tip | :::tip | ||||||
| @ -36,10 +36,11 @@ final Markwon markwon = Markwon.builder(context) | |||||||
|         .build(); |         .build(); | ||||||
| ``` | ``` | ||||||
| 
 | 
 | ||||||
| Currently `Markwon` provides 3 implementations for loading images: | Currently `Markwon` provides 4 implementations for loading images: | ||||||
| * [markwon implementation](/docs/v4/image/) with SVG, GIF, data uri and android_assets support | * [markwon implementation](/docs/v4/image/) with SVG, GIF, data uri and android_assets support | ||||||
| * [based on Picasso](/docs/v4/image-picasso/) | * [based on Picasso](/docs/v4/image-picasso/) | ||||||
| * [based on Glide](/docs/v4/image-glide/) | * [based on Glide](/docs/v4/image-glide/) | ||||||
|  | * [base on Coil](/docs/v4/image-coil/) | ||||||
| 
 | 
 | ||||||
| ## SyntaxHighlight | ## SyntaxHighlight | ||||||
| 
 | 
 | ||||||
| @ -87,32 +88,32 @@ if there is none registered. if you wish to register own instance of a `Movement | |||||||
| apply it directly to a TextView or use [MovementMethodPlugin](/docs/v4/core/movement-method-plugin.md) | apply it directly to a TextView or use [MovementMethodPlugin](/docs/v4/core/movement-method-plugin.md) | ||||||
| ::: | ::: | ||||||
| 
 | 
 | ||||||
| ## UrlProcessor | ## ImageDestinationProcessor | ||||||
| 
 | 
 | ||||||
| Process URLs in your markdown (for links and images). If not provided explicitly,  | Process destinations (URLs) of images in your markdown. If not provided explicitly,  | ||||||
| default **no-op** implementation will be used, which does not modify URLs (keeping them as-is). | default **no-op** implementation will be used, which does not modify URLs (keeping them as-is). | ||||||
| 
 | 
 | ||||||
| `Markwon` provides 2 implementations of `UrlProcessor`: | `Markwon` provides 2 implementations of `UrlProcessor`: | ||||||
| * `UrlProcessorRelativeToAbsolute` | * `ImageDestinationProcessorRelativeToAbsolute` | ||||||
| * `UrlProcessorAndroidAssets` | * `ImageDestinationProcessorAssets` | ||||||
| 
 | 
 | ||||||
| ### UrlProcessorRelativeToAbsolute | ### ImageDestinationProcessorRelativeToAbsolute | ||||||
| 
 | 
 | ||||||
| `UrlProcessorRelativeToAbsolute` can be used to make relative URL absolute. For example if an image is | `ImageDestinationProcessorRelativeToAbsolute` can be used to make relative URL absolute. For example if an image is | ||||||
| defined like this: `` and `UrlProcessorRelativeToAbsolute` | defined like this: `` and `ImageDestinationProcessorRelativeToAbsolute` | ||||||
| is created with `https://github.com/noties/Markwon/raw/master/` as the base:  | is created with `https://github.com/noties/Markwon/raw/master/` as the base:  | ||||||
| `new UrlProcessorRelativeToAbsolute("https://github.com/noties/Markwon/raw/master/")`, | `new ImageDestinationProcessorRelativeToAbsolute("https://github.com/noties/Markwon/raw/master/")`, | ||||||
| then final image will have `https://github.com/noties/Markwon/raw/master/art/image.JPG` | then final image will have `https://github.com/noties/Markwon/raw/master/art/image.JPG` | ||||||
| as the destination. | as the destination. | ||||||
| 
 | 
 | ||||||
| ### UrlProcessorAndroidAssets | ### ImageDestinationProcessorAssets | ||||||
| 
 | 
 | ||||||
| `UrlProcessorAndroidAssets` can be used to make processed links to point to Android assets folder. | `ImageDestinationProcessorAssets` can be used to make processed destinations to point to Android assets folder. | ||||||
| So an image: `` will have `file:///android_asset/art/image.JPG` as the | So an image: `` will have `file:///android_asset/art/image.JPG` as the | ||||||
| destination. | destination. | ||||||
| 
 | 
 | ||||||
| :::tip | :::tip | ||||||
| Please note that `UrlProcessorAndroidAssets` will process only URLs that have no `scheme` information, | Please note that `ImageDestinationProcessorAssets` 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` | so a `./art/image.png` will become `file:///android_asset/art/image.JPG` whilst `https://so.me/where.png` | ||||||
| will be kept as-is. | will be kept as-is. | ||||||
| ::: | ::: | ||||||
|  | |||||||
| @ -8,7 +8,7 @@ android.enableJetifier=true | |||||||
| android.enableBuildCache=true | android.enableBuildCache=true | ||||||
| android.buildCacheDir=build/pre-dex-cache | android.buildCacheDir=build/pre-dex-cache | ||||||
| 
 | 
 | ||||||
| VERSION_NAME=4.3.1 | VERSION_NAME=4.4.0 | ||||||
| 
 | 
 | ||||||
| GROUP=io.noties.markwon | GROUP=io.noties.markwon | ||||||
| POM_DESCRIPTION=Markwon markdown for Android | POM_DESCRIPTION=Markwon markdown for Android | ||||||
|  | |||||||
| @ -192,6 +192,17 @@ public abstract class Markwon { | |||||||
|         @NonNull |         @NonNull | ||||||
|         Builder usePlugins(@NonNull Iterable<? extends MarkwonPlugin> plugins); |         Builder usePlugins(@NonNull Iterable<? extends MarkwonPlugin> plugins); | ||||||
| 
 | 
 | ||||||
|  |         /** | ||||||
|  |          * Control if small chunks of non-finished markdown sentences (for example, a single `*` character) | ||||||
|  |          * should be displayed/rendered as raw input instead of an empty string. | ||||||
|  |          * <p> | ||||||
|  |          * Since 4.4.0 {@code true} by default, versions prior - {@code false} | ||||||
|  |          * | ||||||
|  |          * @since 4.4.0 | ||||||
|  |          */ | ||||||
|  |         @NonNull | ||||||
|  |         Builder fallbackToRawInputWhenEmpty(boolean fallbackToRawInputWhenEmpty); | ||||||
|  | 
 | ||||||
|         @NonNull |         @NonNull | ||||||
|         Markwon build(); |         Markwon build(); | ||||||
|     } |     } | ||||||
|  | |||||||
| @ -27,6 +27,9 @@ class MarkwonBuilderImpl implements Markwon.Builder { | |||||||
| 
 | 
 | ||||||
|     private Markwon.TextSetter textSetter; |     private Markwon.TextSetter textSetter; | ||||||
| 
 | 
 | ||||||
|  |     // @since 4.4.0 | ||||||
|  |     private boolean fallbackToRawInputWhenEmpty = true; | ||||||
|  | 
 | ||||||
|     MarkwonBuilderImpl(@NonNull Context context) { |     MarkwonBuilderImpl(@NonNull Context context) { | ||||||
|         this.context = context; |         this.context = context; | ||||||
|     } |     } | ||||||
| @ -71,6 +74,13 @@ class MarkwonBuilderImpl implements Markwon.Builder { | |||||||
|         return this; |         return this; | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|  |     @NonNull | ||||||
|  |     @Override | ||||||
|  |     public Markwon.Builder fallbackToRawInputWhenEmpty(boolean fallbackToRawInputWhenEmpty) { | ||||||
|  |         this.fallbackToRawInputWhenEmpty = fallbackToRawInputWhenEmpty; | ||||||
|  |         return this; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|     @NonNull |     @NonNull | ||||||
|     @Override |     @Override | ||||||
|     public Markwon build() { |     public Markwon build() { | ||||||
| @ -114,7 +124,8 @@ class MarkwonBuilderImpl implements Markwon.Builder { | |||||||
|                 parserBuilder.build(), |                 parserBuilder.build(), | ||||||
|                 visitorFactory, |                 visitorFactory, | ||||||
|                 configuration, |                 configuration, | ||||||
|                 Collections.unmodifiableList(plugins) |                 Collections.unmodifiableList(plugins), | ||||||
|  |                 fallbackToRawInputWhenEmpty | ||||||
|         ); |         ); | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|  | |||||||
| @ -6,15 +6,13 @@ import io.noties.markwon.core.MarkwonTheme; | |||||||
| import io.noties.markwon.image.AsyncDrawableLoader; | import io.noties.markwon.image.AsyncDrawableLoader; | ||||||
| import io.noties.markwon.image.ImageSizeResolver; | import io.noties.markwon.image.ImageSizeResolver; | ||||||
| import io.noties.markwon.image.ImageSizeResolverDef; | import io.noties.markwon.image.ImageSizeResolverDef; | ||||||
|  | import io.noties.markwon.image.destination.ImageDestinationProcessor; | ||||||
| import io.noties.markwon.syntax.SyntaxHighlight; | import io.noties.markwon.syntax.SyntaxHighlight; | ||||||
| import io.noties.markwon.syntax.SyntaxHighlightNoOp; | import io.noties.markwon.syntax.SyntaxHighlightNoOp; | ||||||
| import io.noties.markwon.urlprocessor.UrlProcessor; |  | ||||||
| import io.noties.markwon.urlprocessor.UrlProcessorNoOp; |  | ||||||
| 
 | 
 | ||||||
| /** | /** | ||||||
|  * since 3.0.0 renamed `SpannableConfiguration` -> `MarkwonConfiguration` |  * since 3.0.0 renamed `SpannableConfiguration` -> `MarkwonConfiguration` | ||||||
|  */ |  */ | ||||||
| @SuppressWarnings("WeakerAccess") |  | ||||||
| public class MarkwonConfiguration { | public class MarkwonConfiguration { | ||||||
| 
 | 
 | ||||||
|     @NonNull |     @NonNull | ||||||
| @ -26,7 +24,8 @@ public class MarkwonConfiguration { | |||||||
|     private final AsyncDrawableLoader asyncDrawableLoader; |     private final AsyncDrawableLoader asyncDrawableLoader; | ||||||
|     private final SyntaxHighlight syntaxHighlight; |     private final SyntaxHighlight syntaxHighlight; | ||||||
|     private final LinkResolver linkResolver; |     private final LinkResolver linkResolver; | ||||||
|     private final UrlProcessor urlProcessor; |     // @since 4.4.0 | ||||||
|  |     private final ImageDestinationProcessor imageDestinationProcessor; | ||||||
|     private final ImageSizeResolver imageSizeResolver; |     private final ImageSizeResolver imageSizeResolver; | ||||||
| 
 | 
 | ||||||
|     // @since 3.0.0 |     // @since 3.0.0 | ||||||
| @ -37,7 +36,7 @@ public class MarkwonConfiguration { | |||||||
|         this.asyncDrawableLoader = builder.asyncDrawableLoader; |         this.asyncDrawableLoader = builder.asyncDrawableLoader; | ||||||
|         this.syntaxHighlight = builder.syntaxHighlight; |         this.syntaxHighlight = builder.syntaxHighlight; | ||||||
|         this.linkResolver = builder.linkResolver; |         this.linkResolver = builder.linkResolver; | ||||||
|         this.urlProcessor = builder.urlProcessor; |         this.imageDestinationProcessor = builder.imageDestinationProcessor; | ||||||
|         this.imageSizeResolver = builder.imageSizeResolver; |         this.imageSizeResolver = builder.imageSizeResolver; | ||||||
|         this.spansFactory = builder.spansFactory; |         this.spansFactory = builder.spansFactory; | ||||||
|     } |     } | ||||||
| @ -62,9 +61,12 @@ public class MarkwonConfiguration { | |||||||
|         return linkResolver; |         return linkResolver; | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|  |     /** | ||||||
|  |      * @since 4.4.0 | ||||||
|  |      */ | ||||||
|     @NonNull |     @NonNull | ||||||
|     public UrlProcessor urlProcessor() { |     public ImageDestinationProcessor imageDestinationProcessor() { | ||||||
|         return urlProcessor; |         return imageDestinationProcessor; | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     @NonNull |     @NonNull | ||||||
| @ -87,7 +89,8 @@ public class MarkwonConfiguration { | |||||||
|         private AsyncDrawableLoader asyncDrawableLoader; |         private AsyncDrawableLoader asyncDrawableLoader; | ||||||
|         private SyntaxHighlight syntaxHighlight; |         private SyntaxHighlight syntaxHighlight; | ||||||
|         private LinkResolver linkResolver; |         private LinkResolver linkResolver; | ||||||
|         private UrlProcessor urlProcessor; |         // @since 4.4.0 | ||||||
|  |         private ImageDestinationProcessor imageDestinationProcessor; | ||||||
|         private ImageSizeResolver imageSizeResolver; |         private ImageSizeResolver imageSizeResolver; | ||||||
|         private MarkwonSpansFactory spansFactory; |         private MarkwonSpansFactory spansFactory; | ||||||
| 
 | 
 | ||||||
| @ -115,9 +118,12 @@ public class MarkwonConfiguration { | |||||||
|             return this; |             return this; | ||||||
|         } |         } | ||||||
| 
 | 
 | ||||||
|  |         /** | ||||||
|  |          * @since 4.4.0 | ||||||
|  |          */ | ||||||
|         @NonNull |         @NonNull | ||||||
|         public Builder urlProcessor(@NonNull UrlProcessor urlProcessor) { |         public Builder imageDestinationProcessor(@NonNull ImageDestinationProcessor imageDestinationProcessor) { | ||||||
|             this.urlProcessor = urlProcessor; |             this.imageDestinationProcessor = imageDestinationProcessor; | ||||||
|             return this; |             return this; | ||||||
|         } |         } | ||||||
| 
 | 
 | ||||||
| @ -151,8 +157,9 @@ public class MarkwonConfiguration { | |||||||
|                 linkResolver = new LinkResolverDef(); |                 linkResolver = new LinkResolverDef(); | ||||||
|             } |             } | ||||||
| 
 | 
 | ||||||
|             if (urlProcessor == null) { |             // @since 4.4.0 | ||||||
|                 urlProcessor = new UrlProcessorNoOp(); |             if (imageDestinationProcessor == null) { | ||||||
|  |                 imageDestinationProcessor = ImageDestinationProcessor.noOp(); | ||||||
|             } |             } | ||||||
| 
 | 
 | ||||||
|             if (imageSizeResolver == null) { |             if (imageSizeResolver == null) { | ||||||
|  | |||||||
| @ -1,6 +1,8 @@ | |||||||
| package io.noties.markwon; | package io.noties.markwon; | ||||||
| 
 | 
 | ||||||
|  | import android.text.SpannableStringBuilder; | ||||||
| import android.text.Spanned; | import android.text.Spanned; | ||||||
|  | import android.text.TextUtils; | ||||||
| import android.widget.TextView; | import android.widget.TextView; | ||||||
| 
 | 
 | ||||||
| import androidx.annotation.NonNull; | import androidx.annotation.NonNull; | ||||||
| @ -28,19 +30,25 @@ class MarkwonImpl extends Markwon { | |||||||
|     @Nullable |     @Nullable | ||||||
|     private final TextSetter textSetter; |     private final TextSetter textSetter; | ||||||
| 
 | 
 | ||||||
|  |     // @since 4.4.0 | ||||||
|  |     private final boolean fallbackToRawInputWhenEmpty; | ||||||
|  | 
 | ||||||
|     MarkwonImpl( |     MarkwonImpl( | ||||||
|             @NonNull TextView.BufferType bufferType, |             @NonNull TextView.BufferType bufferType, | ||||||
|             @Nullable TextSetter textSetter, |             @Nullable TextSetter textSetter, | ||||||
|             @NonNull Parser parser, |             @NonNull Parser parser, | ||||||
|             @NonNull MarkwonVisitorFactory visitorFactory, |             @NonNull MarkwonVisitorFactory visitorFactory, | ||||||
|             @NonNull MarkwonConfiguration configuration, |             @NonNull MarkwonConfiguration configuration, | ||||||
|             @NonNull List<MarkwonPlugin> plugins) { |             @NonNull List<MarkwonPlugin> plugins, | ||||||
|  |             boolean fallbackToRawInputWhenEmpty | ||||||
|  |     ) { | ||||||
|         this.bufferType = bufferType; |         this.bufferType = bufferType; | ||||||
|         this.textSetter = textSetter; |         this.textSetter = textSetter; | ||||||
|         this.parser = parser; |         this.parser = parser; | ||||||
|         this.visitorFactory = visitorFactory; |         this.visitorFactory = visitorFactory; | ||||||
|         this.configuration = configuration; |         this.configuration = configuration; | ||||||
|         this.plugins = plugins; |         this.plugins = plugins; | ||||||
|  |         this.fallbackToRawInputWhenEmpty = fallbackToRawInputWhenEmpty; | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     @NonNull |     @NonNull | ||||||
| @ -86,7 +94,18 @@ class MarkwonImpl extends Markwon { | |||||||
|     @NonNull |     @NonNull | ||||||
|     @Override |     @Override | ||||||
|     public Spanned toMarkdown(@NonNull String input) { |     public Spanned toMarkdown(@NonNull String input) { | ||||||
|         return render(parse(input)); |         final Spanned spanned = render(parse(input)); | ||||||
|  | 
 | ||||||
|  |         // @since 4.4.0 | ||||||
|  |         // if spanned is empty, we are configured to use raw input and input is not empty | ||||||
|  |         if (TextUtils.isEmpty(spanned) | ||||||
|  |                 && fallbackToRawInputWhenEmpty | ||||||
|  |                 && !TextUtils.isEmpty(input)) { | ||||||
|  |             // let's use SpannableStringBuilder in order to keep backward-compatibility | ||||||
|  |             return new SpannableStringBuilder(input); | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         return spanned; | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     @Override |     @Override | ||||||
|  | |||||||
| @ -1,5 +1,6 @@ | |||||||
| package io.noties.markwon.core; | package io.noties.markwon.core; | ||||||
| 
 | 
 | ||||||
|  | import android.text.Spannable; | ||||||
| import android.text.Spanned; | import android.text.Spanned; | ||||||
| import android.text.method.LinkMovementMethod; | import android.text.method.LinkMovementMethod; | ||||||
| import android.widget.TextView; | import android.widget.TextView; | ||||||
| @ -8,6 +9,7 @@ import androidx.annotation.NonNull; | |||||||
| import androidx.annotation.Nullable; | import androidx.annotation.Nullable; | ||||||
| import androidx.annotation.VisibleForTesting; | import androidx.annotation.VisibleForTesting; | ||||||
| 
 | 
 | ||||||
|  | import org.commonmark.node.Block; | ||||||
| import org.commonmark.node.BlockQuote; | import org.commonmark.node.BlockQuote; | ||||||
| import org.commonmark.node.BulletList; | import org.commonmark.node.BulletList; | ||||||
| import org.commonmark.node.Code; | import org.commonmark.node.Code; | ||||||
| @ -15,6 +17,7 @@ import org.commonmark.node.Emphasis; | |||||||
| import org.commonmark.node.FencedCodeBlock; | import org.commonmark.node.FencedCodeBlock; | ||||||
| import org.commonmark.node.HardLineBreak; | import org.commonmark.node.HardLineBreak; | ||||||
| import org.commonmark.node.Heading; | import org.commonmark.node.Heading; | ||||||
|  | import org.commonmark.node.HtmlBlock; | ||||||
| import org.commonmark.node.Image; | import org.commonmark.node.Image; | ||||||
| import org.commonmark.node.IndentedCodeBlock; | import org.commonmark.node.IndentedCodeBlock; | ||||||
| import org.commonmark.node.Link; | import org.commonmark.node.Link; | ||||||
| @ -29,7 +32,10 @@ import org.commonmark.node.Text; | |||||||
| import org.commonmark.node.ThematicBreak; | import org.commonmark.node.ThematicBreak; | ||||||
| 
 | 
 | ||||||
| import java.util.ArrayList; | import java.util.ArrayList; | ||||||
|  | import java.util.Arrays; | ||||||
|  | import java.util.HashSet; | ||||||
| import java.util.List; | import java.util.List; | ||||||
|  | import java.util.Set; | ||||||
| 
 | 
 | ||||||
| import io.noties.markwon.AbstractMarkwonPlugin; | import io.noties.markwon.AbstractMarkwonPlugin; | ||||||
| import io.noties.markwon.MarkwonConfiguration; | import io.noties.markwon.MarkwonConfiguration; | ||||||
| @ -48,6 +54,7 @@ import io.noties.markwon.core.factory.ListItemSpanFactory; | |||||||
| import io.noties.markwon.core.factory.StrongEmphasisSpanFactory; | import io.noties.markwon.core.factory.StrongEmphasisSpanFactory; | ||||||
| import io.noties.markwon.core.factory.ThematicBreakSpanFactory; | import io.noties.markwon.core.factory.ThematicBreakSpanFactory; | ||||||
| import io.noties.markwon.core.spans.OrderedListItemSpan; | import io.noties.markwon.core.spans.OrderedListItemSpan; | ||||||
|  | import io.noties.markwon.core.spans.TextViewSpan; | ||||||
| import io.noties.markwon.image.ImageProps; | import io.noties.markwon.image.ImageProps; | ||||||
| 
 | 
 | ||||||
| /** | /** | ||||||
| @ -88,6 +95,23 @@ public class CorePlugin extends AbstractMarkwonPlugin { | |||||||
|         return new CorePlugin(); |         return new CorePlugin(); | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|  |     /** | ||||||
|  |      * @return a set with enabled by default block types | ||||||
|  |      * @since 4.4.0 | ||||||
|  |      */ | ||||||
|  |     @NonNull | ||||||
|  |     public static Set<Class<? extends Block>> enabledBlockTypes() { | ||||||
|  |         return new HashSet<>(Arrays.asList( | ||||||
|  |                 BlockQuote.class, | ||||||
|  |                 Heading.class, | ||||||
|  |                 FencedCodeBlock.class, | ||||||
|  |                 HtmlBlock.class, | ||||||
|  |                 ThematicBreak.class, | ||||||
|  |                 ListBlock.class, | ||||||
|  |                 IndentedCodeBlock.class | ||||||
|  |         )); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|     // @since 4.0.0 |     // @since 4.0.0 | ||||||
|     private final List<OnTextAddedListener> onTextAddedListeners = new ArrayList<>(0); |     private final List<OnTextAddedListener> onTextAddedListeners = new ArrayList<>(0); | ||||||
| 
 | 
 | ||||||
| @ -150,6 +174,13 @@ public class CorePlugin extends AbstractMarkwonPlugin { | |||||||
|     @Override |     @Override | ||||||
|     public void beforeSetText(@NonNull TextView textView, @NonNull Spanned markdown) { |     public void beforeSetText(@NonNull TextView textView, @NonNull Spanned markdown) { | ||||||
|         OrderedListItemSpan.measure(textView, markdown); |         OrderedListItemSpan.measure(textView, markdown); | ||||||
|  | 
 | ||||||
|  |         // @since 4.4.0 | ||||||
|  |         // we do not break API compatibility, instead we introduce the `instance of` check | ||||||
|  |         if (markdown instanceof Spannable) { | ||||||
|  |             final Spannable spannable = (Spannable) markdown; | ||||||
|  |             TextViewSpan.applyTo(spannable, textView); | ||||||
|  |         } | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     @Override |     @Override | ||||||
| @ -289,7 +320,7 @@ public class CorePlugin extends AbstractMarkwonPlugin { | |||||||
|                 final boolean link = parent instanceof Link; |                 final boolean link = parent instanceof Link; | ||||||
| 
 | 
 | ||||||
|                 final String destination = configuration |                 final String destination = configuration | ||||||
|                         .urlProcessor() |                         .imageDestinationProcessor() | ||||||
|                         .process(image.getDestination()); |                         .process(image.getDestination()); | ||||||
| 
 | 
 | ||||||
|                 final RenderProps props = visitor.renderProps(); |                 final RenderProps props = visitor.renderProps(); | ||||||
| @ -493,8 +524,7 @@ public class CorePlugin extends AbstractMarkwonPlugin { | |||||||
|                 final int length = visitor.length(); |                 final int length = visitor.length(); | ||||||
|                 visitor.visitChildren(link); |                 visitor.visitChildren(link); | ||||||
| 
 | 
 | ||||||
|                 final MarkwonConfiguration configuration = visitor.configuration(); |                 final String destination = link.getDestination(); | ||||||
|                 final String destination = configuration.urlProcessor().process(link.getDestination()); |  | ||||||
| 
 | 
 | ||||||
|                 CoreProps.LINK_DESTINATION.set(visitor.renderProps(), destination); |                 CoreProps.LINK_DESTINATION.set(visitor.renderProps(), destination); | ||||||
| 
 | 
 | ||||||
|  | |||||||
| @ -0,0 +1,70 @@ | |||||||
|  | package io.noties.markwon.core.spans; | ||||||
|  | 
 | ||||||
|  | import android.text.Layout; | ||||||
|  | import android.text.Spannable; | ||||||
|  | import android.text.Spanned; | ||||||
|  | 
 | ||||||
|  | import androidx.annotation.NonNull; | ||||||
|  | import androidx.annotation.Nullable; | ||||||
|  | 
 | ||||||
|  | import java.lang.ref.WeakReference; | ||||||
|  | 
 | ||||||
|  | /** | ||||||
|  |  * @since 4.4.0 | ||||||
|  |  */ | ||||||
|  | public class TextLayoutSpan { | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * @see #applyTo(Spannable, Layout) | ||||||
|  |      */ | ||||||
|  |     @Nullable | ||||||
|  |     public static Layout layoutOf(@NonNull CharSequence cs) { | ||||||
|  |         if (cs instanceof Spanned) { | ||||||
|  |             return layoutOf((Spanned) cs); | ||||||
|  |         } | ||||||
|  |         return null; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     @Nullable | ||||||
|  |     public static Layout layoutOf(@NonNull Spanned spanned) { | ||||||
|  |         final TextLayoutSpan[] spans = spanned.getSpans( | ||||||
|  |                 0, | ||||||
|  |                 spanned.length(), | ||||||
|  |                 TextLayoutSpan.class | ||||||
|  |         ); | ||||||
|  |         return spans != null && spans.length > 0 | ||||||
|  |                 ? spans[0].layout() | ||||||
|  |                 : null; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     public static void applyTo(@NonNull Spannable spannable, @NonNull Layout layout) { | ||||||
|  | 
 | ||||||
|  |         // remove all current ones (only one should be present) | ||||||
|  |         final TextLayoutSpan[] spans = spannable.getSpans(0, spannable.length(), TextLayoutSpan.class); | ||||||
|  |         if (spans != null) { | ||||||
|  |             for (TextLayoutSpan span : spans) { | ||||||
|  |                 spannable.removeSpan(span); | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         final TextLayoutSpan span = new TextLayoutSpan(layout); | ||||||
|  |         spannable.setSpan( | ||||||
|  |                 span, | ||||||
|  |                 0, | ||||||
|  |                 spannable.length(), | ||||||
|  |                 Spanned.SPAN_INCLUSIVE_INCLUSIVE | ||||||
|  |         ); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     private final WeakReference<Layout> reference; | ||||||
|  | 
 | ||||||
|  |     @SuppressWarnings("WeakerAccess") | ||||||
|  |     TextLayoutSpan(@NonNull Layout layout) { | ||||||
|  |         this.reference = new WeakReference<>(layout); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     @Nullable | ||||||
|  |     public Layout layout() { | ||||||
|  |         return reference.get(); | ||||||
|  |     } | ||||||
|  | } | ||||||
| @ -0,0 +1,64 @@ | |||||||
|  | package io.noties.markwon.core.spans; | ||||||
|  | 
 | ||||||
|  | import android.text.Spannable; | ||||||
|  | import android.text.Spanned; | ||||||
|  | import android.widget.TextView; | ||||||
|  | 
 | ||||||
|  | import androidx.annotation.NonNull; | ||||||
|  | import androidx.annotation.Nullable; | ||||||
|  | 
 | ||||||
|  | import java.lang.ref.WeakReference; | ||||||
|  | 
 | ||||||
|  | /** | ||||||
|  |  * A special span that allows to obtain {@code TextView} in which spans are displayed | ||||||
|  |  * | ||||||
|  |  * @since 4.4.0 | ||||||
|  |  */ | ||||||
|  | public class TextViewSpan { | ||||||
|  | 
 | ||||||
|  |     @Nullable | ||||||
|  |     public static TextView textViewOf(@NonNull CharSequence cs) { | ||||||
|  |         if (cs instanceof Spanned) { | ||||||
|  |             return textViewOf((Spanned) cs); | ||||||
|  |         } | ||||||
|  |         return null; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     @Nullable | ||||||
|  |     public static TextView textViewOf(@NonNull Spanned spanned) { | ||||||
|  |         final TextViewSpan[] spans = spanned.getSpans(0, spanned.length(), TextViewSpan.class); | ||||||
|  |         return spans != null && spans.length > 0 | ||||||
|  |                 ? spans[0].textView() | ||||||
|  |                 : null; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     public static void applyTo(@NonNull Spannable spannable, @NonNull TextView textView) { | ||||||
|  | 
 | ||||||
|  |         final TextViewSpan[] spans = spannable.getSpans(0, spannable.length(), TextViewSpan.class); | ||||||
|  |         if (spans != null) { | ||||||
|  |             for (TextViewSpan span : spans) { | ||||||
|  |                 spannable.removeSpan(span); | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         final TextViewSpan span = new TextViewSpan(textView); | ||||||
|  |         // `SPAN_INCLUSIVE_INCLUSIVE` to persist in case of possible text change (deletion, etc) | ||||||
|  |         spannable.setSpan( | ||||||
|  |                 span, | ||||||
|  |                 0, | ||||||
|  |                 spannable.length(), | ||||||
|  |                 Spanned.SPAN_INCLUSIVE_INCLUSIVE | ||||||
|  |         ); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     private final WeakReference<TextView> reference; | ||||||
|  | 
 | ||||||
|  |     public TextViewSpan(@NonNull TextView textView) { | ||||||
|  |         this.reference = new WeakReference<>(textView); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     @Nullable | ||||||
|  |     public TextView textView() { | ||||||
|  |         return reference.get(); | ||||||
|  |     } | ||||||
|  | } | ||||||
| @ -14,6 +14,7 @@ import java.lang.annotation.Retention; | |||||||
| import java.lang.annotation.RetentionPolicy; | import java.lang.annotation.RetentionPolicy; | ||||||
| 
 | 
 | ||||||
| import io.noties.markwon.core.MarkwonTheme; | import io.noties.markwon.core.MarkwonTheme; | ||||||
|  | import io.noties.markwon.utils.SpanUtils; | ||||||
| 
 | 
 | ||||||
| @SuppressWarnings("WeakerAccess") | @SuppressWarnings("WeakerAccess") | ||||||
| public class AsyncDrawableSpan extends ReplacementSpan { | public class AsyncDrawableSpan extends ReplacementSpan { | ||||||
| @ -99,7 +100,11 @@ public class AsyncDrawableSpan extends ReplacementSpan { | |||||||
|             int bottom, |             int bottom, | ||||||
|             @NonNull Paint paint) { |             @NonNull Paint paint) { | ||||||
| 
 | 
 | ||||||
|         drawable.initWithKnownDimensions(canvas.getWidth(), paint.getTextSize()); |         // @since 4.4.0 use SpanUtils instead of `canvas.getWidth` | ||||||
|  |         drawable.initWithKnownDimensions( | ||||||
|  |                 SpanUtils.width(canvas, text), | ||||||
|  |                 paint.getTextSize() | ||||||
|  |         ); | ||||||
| 
 | 
 | ||||||
|         final AsyncDrawable drawable = this.drawable; |         final AsyncDrawable drawable = this.drawable; | ||||||
| 
 | 
 | ||||||
|  | |||||||
| @ -0,0 +1,27 @@ | |||||||
|  | package io.noties.markwon.image.destination; | ||||||
|  | 
 | ||||||
|  | import androidx.annotation.NonNull; | ||||||
|  | 
 | ||||||
|  | /** | ||||||
|  |  * Process destination of image nodes | ||||||
|  |  * | ||||||
|  |  * @since 4.4.0 | ||||||
|  |  */ | ||||||
|  | public abstract class ImageDestinationProcessor { | ||||||
|  |     @NonNull | ||||||
|  |     public abstract String process(@NonNull String destination); | ||||||
|  | 
 | ||||||
|  |     @NonNull | ||||||
|  |     public static ImageDestinationProcessor noOp() { | ||||||
|  |         return new NoOp(); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     private static class NoOp extends ImageDestinationProcessor { | ||||||
|  | 
 | ||||||
|  |         @NonNull | ||||||
|  |         @Override | ||||||
|  |         public String process(@NonNull String destination) { | ||||||
|  |             return destination; | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | } | ||||||
| @ -0,0 +1,59 @@ | |||||||
|  | package io.noties.markwon.image.destination; | ||||||
|  | 
 | ||||||
|  | import android.net.Uri; | ||||||
|  | import android.text.TextUtils; | ||||||
|  | 
 | ||||||
|  | import androidx.annotation.NonNull; | ||||||
|  | import androidx.annotation.Nullable; | ||||||
|  | 
 | ||||||
|  | /** | ||||||
|  |  * {@link ImageDestinationProcessor} that treats all destinations <strong>without scheme</strong> | ||||||
|  |  * information as pointing to the {@code assets} folder of an application. Please note that this | ||||||
|  |  * processor only adds required {@code file:///android_asset/} prefix to destinations and | ||||||
|  |  * actual image loading must take that into account (implement this functionality). | ||||||
|  |  * <p> | ||||||
|  |  * {@code FileSchemeHandler} from the {@code image} module supports asset images when created with | ||||||
|  |  * {@code createWithAssets} factory method | ||||||
|  |  * | ||||||
|  |  * @since 4.4.0 | ||||||
|  |  */ | ||||||
|  | public class ImageDestinationProcessorAssets extends ImageDestinationProcessor { | ||||||
|  | 
 | ||||||
|  |     @NonNull | ||||||
|  |     public static ImageDestinationProcessorAssets create(@Nullable ImageDestinationProcessor parent) { | ||||||
|  |         return new ImageDestinationProcessorAssets(parent); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     static final String MOCK = "https://android.asset/"; | ||||||
|  |     static final String BASE = "file:///android_asset/"; | ||||||
|  | 
 | ||||||
|  |     private final ImageDestinationProcessorRelativeToAbsolute assetsProcessor | ||||||
|  |             = new ImageDestinationProcessorRelativeToAbsolute(MOCK); | ||||||
|  | 
 | ||||||
|  |     private final ImageDestinationProcessor processor; | ||||||
|  | 
 | ||||||
|  |     public ImageDestinationProcessorAssets() { | ||||||
|  |         this(null); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     public ImageDestinationProcessorAssets(@Nullable ImageDestinationProcessor parent) { | ||||||
|  |         this.processor = parent; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     @NonNull | ||||||
|  |     @Override | ||||||
|  |     public String process(@NonNull String destination) { | ||||||
|  |         final String out; | ||||||
|  |         final Uri uri = Uri.parse(destination); | ||||||
|  |         if (TextUtils.isEmpty(uri.getScheme())) { | ||||||
|  |             out = assetsProcessor.process(destination).replace(MOCK, BASE); | ||||||
|  |         } else { | ||||||
|  |             if (processor != null) { | ||||||
|  |                 out = processor.process(destination); | ||||||
|  |             } else { | ||||||
|  |                 out = destination; | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |         return out; | ||||||
|  |     } | ||||||
|  | } | ||||||
| @ -1,4 +1,4 @@ | |||||||
| package io.noties.markwon.urlprocessor; | package io.noties.markwon.image.destination; | ||||||
| 
 | 
 | ||||||
| import androidx.annotation.NonNull; | import androidx.annotation.NonNull; | ||||||
| import androidx.annotation.Nullable; | import androidx.annotation.Nullable; | ||||||
| @ -6,15 +6,30 @@ import androidx.annotation.Nullable; | |||||||
| import java.net.MalformedURLException; | import java.net.MalformedURLException; | ||||||
| import java.net.URL; | import java.net.URL; | ||||||
| 
 | 
 | ||||||
| @SuppressWarnings("WeakerAccess") | /** | ||||||
| public class UrlProcessorRelativeToAbsolute implements UrlProcessor { |  * @since 4.4.0 | ||||||
|  |  */ | ||||||
|  | public class ImageDestinationProcessorRelativeToAbsolute extends ImageDestinationProcessor { | ||||||
|  | 
 | ||||||
|  |     @NonNull | ||||||
|  |     public static ImageDestinationProcessorRelativeToAbsolute create(@NonNull String base) { | ||||||
|  |         return new ImageDestinationProcessorRelativeToAbsolute(base); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     public static ImageDestinationProcessorRelativeToAbsolute create(@NonNull URL base) { | ||||||
|  |         return new ImageDestinationProcessorRelativeToAbsolute(base); | ||||||
|  |     } | ||||||
| 
 | 
 | ||||||
|     private final URL base; |     private final URL base; | ||||||
| 
 | 
 | ||||||
|     public UrlProcessorRelativeToAbsolute(@NonNull String base) { |     public ImageDestinationProcessorRelativeToAbsolute(@NonNull String base) { | ||||||
|         this.base = obtain(base); |         this.base = obtain(base); | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|  |     public ImageDestinationProcessorRelativeToAbsolute(@NonNull URL base) { | ||||||
|  |         this.base = base; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|     @NonNull |     @NonNull | ||||||
|     @Override |     @Override | ||||||
|     public String process(@NonNull String destination) { |     public String process(@NonNull String destination) { | ||||||
| @ -1,8 +0,0 @@ | |||||||
| package io.noties.markwon.urlprocessor; |  | ||||||
| 
 |  | ||||||
| import androidx.annotation.NonNull; |  | ||||||
| 
 |  | ||||||
| public interface UrlProcessor { |  | ||||||
|     @NonNull |  | ||||||
|     String process(@NonNull String destination); |  | ||||||
| } |  | ||||||
| @ -1,49 +0,0 @@ | |||||||
| package io.noties.markwon.urlprocessor; |  | ||||||
| 
 |  | ||||||
| import android.net.Uri; |  | ||||||
| import android.text.TextUtils; |  | ||||||
| 
 |  | ||||||
| import androidx.annotation.NonNull; |  | ||||||
| import androidx.annotation.Nullable; |  | ||||||
| 
 |  | ||||||
| /** |  | ||||||
|  * Processor that will <em>assume</em> that an URL without scheme points to android assets folder. |  | ||||||
|  * URL with a scheme will be processed by {@link #processor} (if it is specified) or returned `as-is`. |  | ||||||
|  */ |  | ||||||
| @SuppressWarnings({"unused", "WeakerAccess"}) |  | ||||||
| public class UrlProcessorAndroidAssets implements UrlProcessor { |  | ||||||
| 
 |  | ||||||
| 
 |  | ||||||
|     static final String MOCK = "https://android.asset/"; |  | ||||||
|     static final String BASE = "file:///android_asset/"; |  | ||||||
| 
 |  | ||||||
|     private final UrlProcessorRelativeToAbsolute assetsProcessor |  | ||||||
|             = new UrlProcessorRelativeToAbsolute(MOCK); |  | ||||||
| 
 |  | ||||||
|     private final UrlProcessor processor; |  | ||||||
| 
 |  | ||||||
|     public UrlProcessorAndroidAssets() { |  | ||||||
|         this(null); |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     public UrlProcessorAndroidAssets(@Nullable UrlProcessor parent) { |  | ||||||
|         this.processor = parent; |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     @NonNull |  | ||||||
|     @Override |  | ||||||
|     public String process(@NonNull String destination) { |  | ||||||
|         final String out; |  | ||||||
|         final Uri uri = Uri.parse(destination); |  | ||||||
|         if (TextUtils.isEmpty(uri.getScheme())) { |  | ||||||
|             out = assetsProcessor.process(destination).replace(MOCK, BASE); |  | ||||||
|         } else { |  | ||||||
|             if (processor != null) { |  | ||||||
|                 out = processor.process(destination); |  | ||||||
|             } else { |  | ||||||
|                 out = destination; |  | ||||||
|             } |  | ||||||
|         } |  | ||||||
|         return out; |  | ||||||
|     } |  | ||||||
| } |  | ||||||
| @ -1,11 +0,0 @@ | |||||||
| package io.noties.markwon.urlprocessor; |  | ||||||
| 
 |  | ||||||
| import androidx.annotation.NonNull; |  | ||||||
| 
 |  | ||||||
| public class UrlProcessorNoOp implements UrlProcessor { |  | ||||||
|     @NonNull |  | ||||||
|     @Override |  | ||||||
|     public String process(@NonNull String destination) { |  | ||||||
|         return destination; |  | ||||||
|     } |  | ||||||
| } |  | ||||||
| @ -18,6 +18,7 @@ public class Dip { | |||||||
| 
 | 
 | ||||||
|     private final float density; |     private final float density; | ||||||
| 
 | 
 | ||||||
|  |     @SuppressWarnings("WeakerAccess") | ||||||
|     public Dip(float density) { |     public Dip(float density) { | ||||||
|         this.density = density; |         this.density = density; | ||||||
|     } |     } | ||||||
|  | |||||||
| @ -12,6 +12,7 @@ import java.lang.reflect.Method; | |||||||
| import java.lang.reflect.Proxy; | import java.lang.reflect.Proxy; | ||||||
| 
 | 
 | ||||||
| // utility class to print parsed Nodes hierarchy | // utility class to print parsed Nodes hierarchy | ||||||
|  | @SuppressWarnings({"unused", "WeakerAccess"}) | ||||||
| public abstract class DumpNodes { | public abstract class DumpNodes { | ||||||
| 
 | 
 | ||||||
|     public interface NodeProcessor { |     public interface NodeProcessor { | ||||||
|  | |||||||
| @ -0,0 +1,72 @@ | |||||||
|  | package io.noties.markwon.utils; | ||||||
|  | 
 | ||||||
|  | import android.os.Build; | ||||||
|  | import android.text.Layout; | ||||||
|  | 
 | ||||||
|  | import androidx.annotation.NonNull; | ||||||
|  | 
 | ||||||
|  | /** | ||||||
|  |  * @since 4.4.0 | ||||||
|  |  */ | ||||||
|  | public abstract class LayoutUtils { | ||||||
|  | 
 | ||||||
|  |     private static final float DEFAULT_EXTRA = 0F; | ||||||
|  |     private static final float DEFAULT_MULTIPLIER = 1F; | ||||||
|  | 
 | ||||||
|  |     public static int getLineBottomWithoutPaddingAndSpacing( | ||||||
|  |             @NonNull Layout layout, | ||||||
|  |             int line | ||||||
|  |     ) { | ||||||
|  | 
 | ||||||
|  |         final int bottom = layout.getLineBottom(line); | ||||||
|  |         final boolean lastLineSpacingNotAdded = Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT; | ||||||
|  |         final boolean isSpanLastLine = line == (layout.getLineCount() - 1); | ||||||
|  | 
 | ||||||
|  |         final int lineBottom; | ||||||
|  |         final float lineSpacingExtra = layout.getSpacingAdd(); | ||||||
|  |         final float lineSpacingMultiplier = layout.getSpacingMultiplier(); | ||||||
|  | 
 | ||||||
|  |         // simplified check | ||||||
|  |         final boolean hasLineSpacing = lineSpacingExtra != DEFAULT_EXTRA | ||||||
|  |                 || lineSpacingMultiplier != DEFAULT_MULTIPLIER; | ||||||
|  | 
 | ||||||
|  |         if (!hasLineSpacing | ||||||
|  |                 || (isSpanLastLine && lastLineSpacingNotAdded)) { | ||||||
|  |             lineBottom = bottom; | ||||||
|  |         } else { | ||||||
|  |             final float extra; | ||||||
|  |             if (Float.compare(DEFAULT_MULTIPLIER, lineSpacingMultiplier) != 0) { | ||||||
|  |                 final int lineHeight = getLineHeight(layout, line); | ||||||
|  |                 extra = lineHeight - | ||||||
|  |                         ((lineHeight - lineSpacingExtra) / lineSpacingMultiplier); | ||||||
|  |             } else { | ||||||
|  |                 extra = lineSpacingExtra; | ||||||
|  |             } | ||||||
|  |             lineBottom = (int) (bottom - extra + .5F); | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         // check if it is the last line that span is occupying **and** that this line is the last | ||||||
|  |         //  one in TextView | ||||||
|  |         if (isSpanLastLine | ||||||
|  |                 && (line == layout.getLineCount() - 1)) { | ||||||
|  |             return lineBottom - layout.getBottomPadding(); | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         return lineBottom; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     public static int getLineTopWithoutPadding(@NonNull Layout layout, int line) { | ||||||
|  |         final int top = layout.getLineTop(line); | ||||||
|  |         if (line == 0) { | ||||||
|  |             return top - layout.getTopPadding(); | ||||||
|  |         } | ||||||
|  |         return top; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     public static int getLineHeight(@NonNull Layout layout, int line) { | ||||||
|  |         return layout.getLineTop(line + 1) - layout.getLineTop(line); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     private LayoutUtils() { | ||||||
|  |     } | ||||||
|  | } | ||||||
| @ -4,7 +4,6 @@ import android.text.Spanned; | |||||||
| 
 | 
 | ||||||
| public abstract class LeadingMarginUtils { | public abstract class LeadingMarginUtils { | ||||||
| 
 | 
 | ||||||
|     @SuppressWarnings("BooleanMethodIsAlwaysInverted") |  | ||||||
|     public static boolean selfStart(int start, CharSequence text, Object span) { |     public static boolean selfStart(int start, CharSequence text, Object span) { | ||||||
|         return text instanceof Spanned && ((Spanned) text).getSpanStart(span) == start; |         return text instanceof Spanned && ((Spanned) text).getSpanStart(span) == start; | ||||||
|     } |     } | ||||||
|  | |||||||
| @ -0,0 +1,42 @@ | |||||||
|  | package io.noties.markwon.utils; | ||||||
|  | 
 | ||||||
|  | import android.graphics.Canvas; | ||||||
|  | import android.text.Layout; | ||||||
|  | import android.text.Spanned; | ||||||
|  | import android.widget.TextView; | ||||||
|  | 
 | ||||||
|  | import androidx.annotation.NonNull; | ||||||
|  | 
 | ||||||
|  | import io.noties.markwon.core.spans.TextLayoutSpan; | ||||||
|  | import io.noties.markwon.core.spans.TextViewSpan; | ||||||
|  | 
 | ||||||
|  | /** | ||||||
|  |  * @since 4.4.0 | ||||||
|  |  */ | ||||||
|  | public abstract class SpanUtils { | ||||||
|  | 
 | ||||||
|  |     public static int width(@NonNull Canvas canvas, @NonNull CharSequence cs) { | ||||||
|  |         // Layout | ||||||
|  |         // TextView | ||||||
|  |         // canvas | ||||||
|  | 
 | ||||||
|  |         if (cs instanceof Spanned) { | ||||||
|  |             final Spanned spanned = (Spanned) cs; | ||||||
|  | 
 | ||||||
|  |             // if we are displayed with layout information -> use it | ||||||
|  |             final Layout layout = TextLayoutSpan.layoutOf(spanned); | ||||||
|  |             if (layout != null) { | ||||||
|  |                 return layout.getWidth(); | ||||||
|  |             } | ||||||
|  | 
 | ||||||
|  |             // if we have TextView -> obtain width from it (exclude padding) | ||||||
|  |             final TextView textView = TextViewSpan.textViewOf(spanned); | ||||||
|  |             if (textView != null) { | ||||||
|  |                 return textView.getWidth() - textView.getPaddingLeft() - textView.getPaddingRight(); | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         // else just use canvas width | ||||||
|  |         return canvas.getWidth(); | ||||||
|  |     } | ||||||
|  | } | ||||||
| @ -1,6 +1,7 @@ | |||||||
| package io.noties.markwon; | package io.noties.markwon; | ||||||
| 
 | 
 | ||||||
| import android.text.Spanned; | import android.text.Spanned; | ||||||
|  | import android.text.TextUtils; | ||||||
| import android.widget.TextView; | import android.widget.TextView; | ||||||
| 
 | 
 | ||||||
| import org.commonmark.node.Node; | import org.commonmark.node.Node; | ||||||
| @ -50,7 +51,9 @@ public class MarkwonImplTest { | |||||||
|                 mock(Parser.class), |                 mock(Parser.class), | ||||||
|                 mock(MarkwonVisitorFactory.class), |                 mock(MarkwonVisitorFactory.class), | ||||||
|                 mock(MarkwonConfiguration.class), |                 mock(MarkwonConfiguration.class), | ||||||
|                 Collections.singletonList(plugin)); |                 Collections.singletonList(plugin), | ||||||
|  |                 true | ||||||
|  |         ); | ||||||
| 
 | 
 | ||||||
|         impl.parse("whatever"); |         impl.parse("whatever"); | ||||||
| 
 | 
 | ||||||
| @ -74,7 +77,9 @@ public class MarkwonImplTest { | |||||||
|                 parser, |                 parser, | ||||||
|                 mock(MarkwonVisitorFactory.class), |                 mock(MarkwonVisitorFactory.class), | ||||||
|                 mock(MarkwonConfiguration.class), |                 mock(MarkwonConfiguration.class), | ||||||
|                 Arrays.asList(first, second)); |                 Arrays.asList(first, second), | ||||||
|  |                 true | ||||||
|  |         ); | ||||||
| 
 | 
 | ||||||
|         impl.parse("zero"); |         impl.parse("zero"); | ||||||
| 
 | 
 | ||||||
| @ -102,7 +107,9 @@ public class MarkwonImplTest { | |||||||
|                 mock(Parser.class), |                 mock(Parser.class), | ||||||
|                 visitorFactory, |                 visitorFactory, | ||||||
|                 mock(MarkwonConfiguration.class), |                 mock(MarkwonConfiguration.class), | ||||||
|                 Collections.singletonList(plugin)); |                 Collections.singletonList(plugin), | ||||||
|  |                 true | ||||||
|  |         ); | ||||||
| 
 | 
 | ||||||
|         when(visitorFactory.create()).thenReturn(visitor); |         when(visitorFactory.create()).thenReturn(visitor); | ||||||
|         when(visitor.builder()).thenReturn(builder); |         when(visitor.builder()).thenReturn(builder); | ||||||
| @ -149,7 +156,9 @@ public class MarkwonImplTest { | |||||||
|                 mock(Parser.class), |                 mock(Parser.class), | ||||||
|                 visitorFactory, |                 visitorFactory, | ||||||
|                 mock(MarkwonConfiguration.class), |                 mock(MarkwonConfiguration.class), | ||||||
|                 Collections.<MarkwonPlugin>emptyList()); |                 Collections.<MarkwonPlugin>emptyList(), | ||||||
|  |                 true | ||||||
|  |         ); | ||||||
| 
 | 
 | ||||||
|         impl.render(mock(Node.class)); |         impl.render(mock(Node.class)); | ||||||
| 
 | 
 | ||||||
| @ -185,7 +194,9 @@ public class MarkwonImplTest { | |||||||
|                 mock(Parser.class), |                 mock(Parser.class), | ||||||
|                 visitorFactory, |                 visitorFactory, | ||||||
|                 mock(MarkwonConfiguration.class), |                 mock(MarkwonConfiguration.class), | ||||||
|                 Collections.singletonList(plugin)); |                 Collections.singletonList(plugin), | ||||||
|  |                 true | ||||||
|  |         ); | ||||||
| 
 | 
 | ||||||
|         final AtomicBoolean flag = new AtomicBoolean(false); |         final AtomicBoolean flag = new AtomicBoolean(false); | ||||||
|         final Node node = mock(Node.class); |         final Node node = mock(Node.class); | ||||||
| @ -224,7 +235,9 @@ public class MarkwonImplTest { | |||||||
|                 mock(Parser.class), |                 mock(Parser.class), | ||||||
|                 mock(MarkwonVisitorFactory.class, RETURNS_MOCKS), |                 mock(MarkwonVisitorFactory.class, RETURNS_MOCKS), | ||||||
|                 mock(MarkwonConfiguration.class), |                 mock(MarkwonConfiguration.class), | ||||||
|                 Collections.singletonList(plugin)); |                 Collections.singletonList(plugin), | ||||||
|  |                 true | ||||||
|  |         ); | ||||||
| 
 | 
 | ||||||
|         final TextView textView = mock(TextView.class); |         final TextView textView = mock(TextView.class); | ||||||
|         final AtomicBoolean flag = new AtomicBoolean(false); |         final AtomicBoolean flag = new AtomicBoolean(false); | ||||||
| @ -272,7 +285,9 @@ public class MarkwonImplTest { | |||||||
|                 mock(Parser.class), |                 mock(Parser.class), | ||||||
|                 mock(MarkwonVisitorFactory.class), |                 mock(MarkwonVisitorFactory.class), | ||||||
|                 mock(MarkwonConfiguration.class), |                 mock(MarkwonConfiguration.class), | ||||||
|                 plugins); |                 plugins, | ||||||
|  |                 true | ||||||
|  |         ); | ||||||
| 
 | 
 | ||||||
|         assertTrue("First", impl.hasPlugin(First.class)); |         assertTrue("First", impl.hasPlugin(First.class)); | ||||||
|         assertFalse("Second", impl.hasPlugin(Second.class)); |         assertFalse("Second", impl.hasPlugin(Second.class)); | ||||||
| @ -295,7 +310,9 @@ public class MarkwonImplTest { | |||||||
|                 mock(Parser.class), |                 mock(Parser.class), | ||||||
|                 mock(MarkwonVisitorFactory.class), |                 mock(MarkwonVisitorFactory.class), | ||||||
|                 mock(MarkwonConfiguration.class), |                 mock(MarkwonConfiguration.class), | ||||||
|                 Collections.singletonList(plugin)); |                 Collections.singletonList(plugin), | ||||||
|  |                 true | ||||||
|  |         ); | ||||||
| 
 | 
 | ||||||
|         final TextView textView = mock(TextView.class); |         final TextView textView = mock(TextView.class); | ||||||
|         final Spanned spanned = mock(Spanned.class); |         final Spanned spanned = mock(Spanned.class); | ||||||
| @ -339,7 +356,9 @@ public class MarkwonImplTest { | |||||||
|                 mock(Parser.class), |                 mock(Parser.class), | ||||||
|                 mock(MarkwonVisitorFactory.class), |                 mock(MarkwonVisitorFactory.class), | ||||||
|                 mock(MarkwonConfiguration.class), |                 mock(MarkwonConfiguration.class), | ||||||
|                 plugins); |                 plugins, | ||||||
|  |                 true | ||||||
|  |         ); | ||||||
| 
 | 
 | ||||||
|         // should be returned |         // should be returned | ||||||
|         assertNotNull(impl.requirePlugin(MarkwonPlugin.class)); |         assertNotNull(impl.requirePlugin(MarkwonPlugin.class)); | ||||||
| @ -370,7 +389,9 @@ public class MarkwonImplTest { | |||||||
|                 mock(Parser.class), |                 mock(Parser.class), | ||||||
|                 mock(MarkwonVisitorFactory.class), |                 mock(MarkwonVisitorFactory.class), | ||||||
|                 mock(MarkwonConfiguration.class), |                 mock(MarkwonConfiguration.class), | ||||||
|                 plugins); |                 plugins, | ||||||
|  |                 true | ||||||
|  |         ); | ||||||
| 
 | 
 | ||||||
|         final List<? extends MarkwonPlugin> list = impl.getPlugins(); |         final List<? extends MarkwonPlugin> list = impl.getPlugins(); | ||||||
| 
 | 
 | ||||||
| @ -385,4 +406,42 @@ public class MarkwonImplTest { | |||||||
|             assertTrue(e.getMessage(), true); |             assertTrue(e.getMessage(), true); | ||||||
|         } |         } | ||||||
|     } |     } | ||||||
|  | 
 | ||||||
|  |     @Test | ||||||
|  |     public void fallback_to_raw() { | ||||||
|  |         final String md = "*"; | ||||||
|  | 
 | ||||||
|  |         final MarkwonImpl impl = new MarkwonImpl( | ||||||
|  |                 TextView.BufferType.SPANNABLE, | ||||||
|  |                 null, | ||||||
|  |                 mock(Parser.class, RETURNS_MOCKS), | ||||||
|  |                 // it must be sufficient to just return mocks and thus empty rendering result | ||||||
|  |                 mock(MarkwonVisitorFactory.class, RETURNS_MOCKS), | ||||||
|  |                 mock(MarkwonConfiguration.class), | ||||||
|  |                 Collections.<MarkwonPlugin>emptyList(), | ||||||
|  |                 true | ||||||
|  |         ); | ||||||
|  | 
 | ||||||
|  |         final Spanned spanned = impl.toMarkdown(md); | ||||||
|  |         assertEquals(md, spanned.toString()); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     @Test | ||||||
|  |     public void fallback_to_raw_false() { | ||||||
|  |         final String md = "*"; | ||||||
|  | 
 | ||||||
|  |         final MarkwonImpl impl = new MarkwonImpl( | ||||||
|  |                 TextView.BufferType.SPANNABLE, | ||||||
|  |                 null, | ||||||
|  |                 mock(Parser.class, RETURNS_MOCKS), | ||||||
|  |                 // it must be sufficient to just return mocks and thus empty rendering result | ||||||
|  |                 mock(MarkwonVisitorFactory.class, RETURNS_MOCKS), | ||||||
|  |                 mock(MarkwonConfiguration.class), | ||||||
|  |                 Collections.<MarkwonPlugin>emptyList(), | ||||||
|  |                 false | ||||||
|  |         ); | ||||||
|  | 
 | ||||||
|  |         final Spanned spanned = impl.toMarkdown(md); | ||||||
|  |         assertTrue(spanned.toString(), TextUtils.isEmpty(spanned)); | ||||||
|  |     } | ||||||
| } | } | ||||||
| @ -1,4 +1,4 @@ | |||||||
| package io.noties.markwon.urlprocessor; | package io.noties.markwon.image.destination; | ||||||
| 
 | 
 | ||||||
| import org.junit.Before; | import org.junit.Before; | ||||||
| import org.junit.Test; | import org.junit.Test; | ||||||
| @ -6,18 +6,18 @@ import org.junit.runner.RunWith; | |||||||
| import org.robolectric.RobolectricTestRunner; | import org.robolectric.RobolectricTestRunner; | ||||||
| import org.robolectric.annotation.Config; | import org.robolectric.annotation.Config; | ||||||
| 
 | 
 | ||||||
|  | import static io.noties.markwon.image.destination.ImageDestinationProcessorAssets.BASE; | ||||||
| import static org.junit.Assert.assertEquals; | import static org.junit.Assert.assertEquals; | ||||||
| import static io.noties.markwon.urlprocessor.UrlProcessorAndroidAssets.BASE; |  | ||||||
| 
 | 
 | ||||||
| @RunWith(RobolectricTestRunner.class) | @RunWith(RobolectricTestRunner.class) | ||||||
| @Config(manifest = Config.NONE) | @Config(manifest = Config.NONE) | ||||||
| public class UrlProcessorAndroidAssetsTest { | public class ImageDestinationProcessorAssetsTest { | ||||||
| 
 | 
 | ||||||
|     private UrlProcessorAndroidAssets processor; |     private ImageDestinationProcessorAssets processor; | ||||||
| 
 | 
 | ||||||
|     @Before |     @Before | ||||||
|     public void before() { |     public void before() { | ||||||
|         processor = new UrlProcessorAndroidAssets(); |         processor = new ImageDestinationProcessorAssets(); | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     @Test |     @Test | ||||||
| @ -1,4 +1,4 @@ | |||||||
| package io.noties.markwon.urlprocessor; | package io.noties.markwon.image.destination; | ||||||
| 
 | 
 | ||||||
| import org.junit.Test; | import org.junit.Test; | ||||||
| import org.junit.runner.RunWith; | import org.junit.runner.RunWith; | ||||||
| @ -9,39 +9,39 @@ import static org.junit.Assert.*; | |||||||
| 
 | 
 | ||||||
| @RunWith(RobolectricTestRunner.class) | @RunWith(RobolectricTestRunner.class) | ||||||
| @Config(manifest = Config.NONE) | @Config(manifest = Config.NONE) | ||||||
| public class UrlProcessorRelativeToAbsoluteTest { | public class ImageDestinationProcessorRelativeToAbsoluteTest { | ||||||
| 
 | 
 | ||||||
|     @Test |     @Test | ||||||
|     public void malformed_base_do_not_process() { |     public void malformed_base_do_not_process() { | ||||||
|         final UrlProcessorRelativeToAbsolute processor = new UrlProcessorRelativeToAbsolute("!@#$%^&*("); |         final ImageDestinationProcessorRelativeToAbsolute processor = new ImageDestinationProcessorRelativeToAbsolute("!@#$%^&*("); | ||||||
|         final String destination = "../hey.there.html"; |         final String destination = "../hey.there.html"; | ||||||
|         assertEquals(destination, processor.process(destination)); |         assertEquals(destination, processor.process(destination)); | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     @Test |     @Test | ||||||
|     public void access_root() { |     public void access_root() { | ||||||
|         final UrlProcessorRelativeToAbsolute processor = new UrlProcessorRelativeToAbsolute("https://ro.ot/hello/"); |         final ImageDestinationProcessorRelativeToAbsolute processor = new ImageDestinationProcessorRelativeToAbsolute("https://ro.ot/hello/"); | ||||||
|         final String url = "/index.html"; |         final String url = "/index.html"; | ||||||
|         assertEquals("https://ro.ot/index.html", processor.process(url)); |         assertEquals("https://ro.ot/index.html", processor.process(url)); | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     @Test |     @Test | ||||||
|     public void access_same_directory() { |     public void access_same_directory() { | ||||||
|         final UrlProcessorRelativeToAbsolute processor = new UrlProcessorRelativeToAbsolute("https://ro.ot/hello/"); |         final ImageDestinationProcessorRelativeToAbsolute processor = new ImageDestinationProcessorRelativeToAbsolute("https://ro.ot/hello/"); | ||||||
|         final String url = "./.htaccess"; |         final String url = "./.htaccess"; | ||||||
|         assertEquals("https://ro.ot/hello/.htaccess", processor.process(url)); |         assertEquals("https://ro.ot/hello/.htaccess", processor.process(url)); | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     @Test |     @Test | ||||||
|     public void asset_directory_up() { |     public void asset_directory_up() { | ||||||
|         final UrlProcessorRelativeToAbsolute processor = new UrlProcessorRelativeToAbsolute("http://ro.ot/first/second/"); |         final ImageDestinationProcessorRelativeToAbsolute processor = new ImageDestinationProcessorRelativeToAbsolute("http://ro.ot/first/second/"); | ||||||
|         final String url = "../cat.JPG"; |         final String url = "../cat.JPG"; | ||||||
|         assertEquals("http://ro.ot/first/cat.JPG", processor.process(url)); |         assertEquals("http://ro.ot/first/cat.JPG", processor.process(url)); | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     @Test |     @Test | ||||||
|     public void change_directory_inside_destination() { |     public void change_directory_inside_destination() { | ||||||
|         final UrlProcessorRelativeToAbsolute processor = new UrlProcessorRelativeToAbsolute("http://ro.ot/first/"); |         final ImageDestinationProcessorRelativeToAbsolute processor = new ImageDestinationProcessorRelativeToAbsolute("http://ro.ot/first/"); | ||||||
|         final String url = "../first/../second/./thi.rd"; |         final String url = "../first/../second/./thi.rd"; | ||||||
|         assertEquals( |         assertEquals( | ||||||
|                 "http://ro.ot/second/thi.rd", |                 "http://ro.ot/second/thi.rd", | ||||||
| @ -51,7 +51,7 @@ public class UrlProcessorRelativeToAbsoluteTest { | |||||||
| 
 | 
 | ||||||
|     @Test |     @Test | ||||||
|     public void with_query_arguments() { |     public void with_query_arguments() { | ||||||
|         final UrlProcessorRelativeToAbsolute processor = new UrlProcessorRelativeToAbsolute("http://ro.ot/first/"); |         final ImageDestinationProcessorRelativeToAbsolute processor = new ImageDestinationProcessorRelativeToAbsolute("http://ro.ot/first/"); | ||||||
|         final String url = "../index.php?ROOT=1"; |         final String url = "../index.php?ROOT=1"; | ||||||
|         assertEquals( |         assertEquals( | ||||||
|                 "http://ro.ot/index.php?ROOT=1", |                 "http://ro.ot/index.php?ROOT=1", | ||||||
| @ -458,8 +458,7 @@ public class JLatexMathPlugin extends AbstractMarkwonPlugin { | |||||||
| 
 | 
 | ||||||
|             final JLatexMathDrawable.Builder builder = JLatexMathDrawable.builder(latex) |             final JLatexMathDrawable.Builder builder = JLatexMathDrawable.builder(latex) | ||||||
|                     .textSize(theme.blockTextSize()) |                     .textSize(theme.blockTextSize()) | ||||||
|                     .align(theme.blockHorizontalAlignment()) |                     .align(theme.blockHorizontalAlignment()); | ||||||
|                     .fitCanvas(theme.blockFitCanvas()); |  | ||||||
| 
 | 
 | ||||||
|             if (backgroundProvider != null) { |             if (backgroundProvider != null) { | ||||||
|                 builder.background(backgroundProvider.provide()); |                 builder.background(backgroundProvider.provide()); | ||||||
| @ -489,8 +488,7 @@ public class JLatexMathPlugin extends AbstractMarkwonPlugin { | |||||||
|             final int color = theme.inlineTextColor(); |             final int color = theme.inlineTextColor(); | ||||||
| 
 | 
 | ||||||
|             final JLatexMathDrawable.Builder builder = JLatexMathDrawable.builder(latex) |             final JLatexMathDrawable.Builder builder = JLatexMathDrawable.builder(latex) | ||||||
|                     .textSize(theme.inlineTextSize()) |                     .textSize(theme.inlineTextSize()); | ||||||
|                     .fitCanvas(false); |  | ||||||
| 
 | 
 | ||||||
|             if (backgroundProvider != null) { |             if (backgroundProvider != null) { | ||||||
|                 builder.background(backgroundProvider.provide()); |                 builder.background(backgroundProvider.provide()); | ||||||
| @ -530,7 +528,20 @@ public class JLatexMathPlugin extends AbstractMarkwonPlugin { | |||||||
|         @NonNull |         @NonNull | ||||||
|         @Override |         @Override | ||||||
|         public Rect resolveImageSize(@NonNull AsyncDrawable drawable) { |         public Rect resolveImageSize(@NonNull AsyncDrawable drawable) { | ||||||
|             return drawable.getResult().getBounds(); | 
 | ||||||
|  |             // @since 4.4.0 resolve inline size (scale down if exceed available width) | ||||||
|  |             final Rect imageBounds = drawable.getResult().getBounds(); | ||||||
|  |             final int canvasWidth = drawable.getLastKnownCanvasWidth(); | ||||||
|  |             final int w = imageBounds.width(); | ||||||
|  | 
 | ||||||
|  |             if (w > canvasWidth) { | ||||||
|  |                 // here we must scale it down (keeping the ratio) | ||||||
|  |                 final float ratio = (float) w / imageBounds.height(); | ||||||
|  |                 final int h = (int) (canvasWidth / ratio + .5F); | ||||||
|  |                 return new Rect(0, 0, canvasWidth, h); | ||||||
|  |             } | ||||||
|  | 
 | ||||||
|  |             return imageBounds; | ||||||
|         } |         } | ||||||
|     } |     } | ||||||
| } | } | ||||||
|  | |||||||
| @ -4,7 +4,10 @@ import android.annotation.SuppressLint; | |||||||
| import android.graphics.Canvas; | import android.graphics.Canvas; | ||||||
| import android.graphics.Paint; | import android.graphics.Paint; | ||||||
| import android.graphics.Rect; | import android.graphics.Rect; | ||||||
|  | import android.graphics.drawable.Drawable; | ||||||
| import android.text.Layout; | import android.text.Layout; | ||||||
|  | import android.text.Spannable; | ||||||
|  | import android.text.SpannableString; | ||||||
| import android.text.Spanned; | import android.text.Spanned; | ||||||
| import android.text.StaticLayout; | import android.text.StaticLayout; | ||||||
| import android.text.TextPaint; | import android.text.TextPaint; | ||||||
| @ -20,7 +23,11 @@ import java.lang.annotation.RetentionPolicy; | |||||||
| import java.util.ArrayList; | import java.util.ArrayList; | ||||||
| import java.util.List; | import java.util.List; | ||||||
| 
 | 
 | ||||||
|  | import io.noties.markwon.core.spans.TextLayoutSpan; | ||||||
|  | import io.noties.markwon.image.AsyncDrawable; | ||||||
|  | import io.noties.markwon.image.AsyncDrawableSpan; | ||||||
| import io.noties.markwon.utils.LeadingMarginUtils; | import io.noties.markwon.utils.LeadingMarginUtils; | ||||||
|  | import io.noties.markwon.utils.SpanUtils; | ||||||
| 
 | 
 | ||||||
| public class TableRowSpan extends ReplacementSpan { | public class TableRowSpan extends ReplacementSpan { | ||||||
| 
 | 
 | ||||||
| @ -67,7 +74,7 @@ public class TableRowSpan extends ReplacementSpan { | |||||||
| 
 | 
 | ||||||
|     private final TableTheme theme; |     private final TableTheme theme; | ||||||
|     private final List<Cell> cells; |     private final List<Cell> cells; | ||||||
|     private final List<StaticLayout> layouts; |     private final List<Layout> layouts; | ||||||
|     private final TextPaint textPaint; |     private final TextPaint textPaint; | ||||||
|     private final boolean header; |     private final boolean header; | ||||||
|     private final boolean odd; |     private final boolean odd; | ||||||
| @ -108,7 +115,7 @@ public class TableRowSpan extends ReplacementSpan { | |||||||
|             if (fm != null) { |             if (fm != null) { | ||||||
| 
 | 
 | ||||||
|                 int max = 0; |                 int max = 0; | ||||||
|                 for (StaticLayout layout : layouts) { |                 for (Layout layout : layouts) { | ||||||
|                     final int height = layout.getHeight(); |                     final int height = layout.getHeight(); | ||||||
|                     if (height > max) { |                     if (height > max) { | ||||||
|                         max = height; |                         max = height; | ||||||
| @ -144,8 +151,9 @@ public class TableRowSpan extends ReplacementSpan { | |||||||
|             int bottom, |             int bottom, | ||||||
|             @NonNull Paint p) { |             @NonNull Paint p) { | ||||||
| 
 | 
 | ||||||
|         if (recreateLayouts(canvas.getWidth())) { |         final int spanWidth = SpanUtils.width(canvas, text); | ||||||
|             width = canvas.getWidth(); |         if (recreateLayouts(spanWidth)) { | ||||||
|  |             width = spanWidth; | ||||||
|             // @since 4.3.1 it's important to cast to TextPaint in order to display links, etc |             // @since 4.3.1 it's important to cast to TextPaint in order to display links, etc | ||||||
|             if (p instanceof TextPaint) { |             if (p instanceof TextPaint) { | ||||||
|                 // there must be a reason why this method receives Paint instead of TextPaint... |                 // there must be a reason why this method receives Paint instead of TextPaint... | ||||||
| @ -236,7 +244,7 @@ public class TableRowSpan extends ReplacementSpan { | |||||||
|         final int borderTop = isFirstTableRow ? borderWidth : 0; |         final int borderTop = isFirstTableRow ? borderWidth : 0; | ||||||
|         final int borderBottom = bottom - top - borderWidth; |         final int borderBottom = bottom - top - borderWidth; | ||||||
| 
 | 
 | ||||||
|         StaticLayout layout; |         Layout layout; | ||||||
|         for (int i = 0; i < size; i++) { |         for (int i = 0; i < size; i++) { | ||||||
|             layout = layouts.get(i); |             layout = layouts.get(i); | ||||||
|             final int save = canvas.save(); |             final int save = canvas.save(); | ||||||
| @ -293,20 +301,76 @@ public class TableRowSpan extends ReplacementSpan { | |||||||
|         final int w = (width / columns) - padding; |         final int w = (width / columns) - padding; | ||||||
| 
 | 
 | ||||||
|         this.layouts.clear(); |         this.layouts.clear(); | ||||||
|         Cell cell; | 
 | ||||||
|         StaticLayout layout; |  | ||||||
|         for (int i = 0, size = cells.size(); i < size; i++) { |         for (int i = 0, size = cells.size(); i < size; i++) { | ||||||
|             cell = cells.get(i); |             makeLayout(i, w, cells.get(i)); | ||||||
|             layout = new StaticLayout( |         } | ||||||
|                     cell.text, |     } | ||||||
|                     textPaint, | 
 | ||||||
|                     w, |     private void makeLayout(final int index, final int width, @NonNull final Cell cell) { | ||||||
|                     alignment(cell.alignment), | 
 | ||||||
|                     1.F, |         final Runnable recreate = new Runnable() { | ||||||
|                     .0F, |             @Override | ||||||
|                     false |             public void run() { | ||||||
|             ); |                 final Invalidator invalidator = TableRowSpan.this.invalidator; | ||||||
|             layouts.add(layout); |                 if (invalidator != null) { | ||||||
|  |                     layouts.remove(index); | ||||||
|  |                     makeLayout(index, width, cell); | ||||||
|  |                     invalidator.invalidate(); | ||||||
|  |                 } | ||||||
|  |             } | ||||||
|  |         }; | ||||||
|  | 
 | ||||||
|  |         final Spannable spannable; | ||||||
|  | 
 | ||||||
|  |         if (cell.text instanceof Spannable) { | ||||||
|  |             spannable = (Spannable) cell.text; | ||||||
|  |         } else { | ||||||
|  |             spannable = new SpannableString(cell.text); | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         final Layout layout = new StaticLayout( | ||||||
|  |                 spannable, | ||||||
|  |                 textPaint, | ||||||
|  |                 width, | ||||||
|  |                 alignment(cell.alignment), | ||||||
|  |                 1.0F, | ||||||
|  |                 0.0F, | ||||||
|  |                 false | ||||||
|  |         ); | ||||||
|  | 
 | ||||||
|  |         // @since 4.4.0 | ||||||
|  |         TextLayoutSpan.applyTo(spannable, layout); | ||||||
|  | 
 | ||||||
|  |         // @since 4.4.0 | ||||||
|  |         scheduleAsyncDrawables(spannable, recreate); | ||||||
|  | 
 | ||||||
|  |         layouts.add(index, layout); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     private void scheduleAsyncDrawables(@NonNull Spannable spannable, @NonNull final Runnable recreate) { | ||||||
|  | 
 | ||||||
|  |         final AsyncDrawableSpan[] spans = spannable.getSpans(0, spannable.length(), AsyncDrawableSpan.class); | ||||||
|  |         if (spans != null | ||||||
|  |                 && spans.length > 0) { | ||||||
|  | 
 | ||||||
|  |             for (AsyncDrawableSpan span : spans) { | ||||||
|  | 
 | ||||||
|  |                 final AsyncDrawable drawable = span.getDrawable(); | ||||||
|  | 
 | ||||||
|  |                 // it is absolutely crucial to check if drawable is already attached, | ||||||
|  |                 //  otherwise we would end up with a loop | ||||||
|  |                 if (drawable.isAttached()) { | ||||||
|  |                     continue; | ||||||
|  |                 } | ||||||
|  | 
 | ||||||
|  |                 drawable.setCallback2(new CallbackAdapter() { | ||||||
|  |                     @Override | ||||||
|  |                     public void invalidateDrawable(@NonNull Drawable who) { | ||||||
|  |                         recreate.run(); | ||||||
|  |                     } | ||||||
|  |                 }); | ||||||
|  |             } | ||||||
|         } |         } | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
| @ -330,4 +394,21 @@ public class TableRowSpan extends ReplacementSpan { | |||||||
|     public void invalidator(@Nullable Invalidator invalidator) { |     public void invalidator(@Nullable Invalidator invalidator) { | ||||||
|         this.invalidator = invalidator; |         this.invalidator = invalidator; | ||||||
|     } |     } | ||||||
|  | 
 | ||||||
|  |     private static abstract class CallbackAdapter implements Drawable.Callback { | ||||||
|  |         @Override | ||||||
|  |         public void invalidateDrawable(@NonNull Drawable who) { | ||||||
|  | 
 | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         @Override | ||||||
|  |         public void scheduleDrawable(@NonNull Drawable who, @NonNull Runnable what, long when) { | ||||||
|  | 
 | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         @Override | ||||||
|  |         public void unscheduleDrawable(@NonNull Drawable who, @NonNull Runnable what) { | ||||||
|  | 
 | ||||||
|  |         } | ||||||
|  |     } | ||||||
| } | } | ||||||
|  | |||||||
| @ -21,6 +21,7 @@ public class HtmlEmptyTagReplacement { | |||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     private static final String IMG_REPLACEMENT = "\uFFFC"; |     private static final String IMG_REPLACEMENT = "\uFFFC"; | ||||||
|  |     private static final String IFRAME_REPLACEMENT = "\u00a0"; // non-breakable space | ||||||
| 
 | 
 | ||||||
|     /** |     /** | ||||||
|      * @return replacement for supplied startTag or null if no replacement should occur (which will |      * @return replacement for supplied startTag or null if no replacement should occur (which will | ||||||
| @ -44,6 +45,9 @@ public class HtmlEmptyTagReplacement { | |||||||
|             } else { |             } else { | ||||||
|                 replacement = alt; |                 replacement = alt; | ||||||
|             } |             } | ||||||
|  |         } else if ("iframe".equals(name)) { | ||||||
|  |             // @since 4.4.0 make iframe non-empty | ||||||
|  |             replacement = IFRAME_REPLACEMENT; | ||||||
|         } else { |         } else { | ||||||
|             replacement = null; |             replacement = null; | ||||||
|         } |         } | ||||||
|  | |||||||
| @ -53,13 +53,16 @@ public class HtmlPlugin extends AbstractMarkwonPlugin { | |||||||
|     public static final float SCRIPT_DEF_TEXT_SIZE_RATIO = .75F; |     public static final float SCRIPT_DEF_TEXT_SIZE_RATIO = .75F; | ||||||
| 
 | 
 | ||||||
|     private final MarkwonHtmlRendererImpl.Builder builder; |     private final MarkwonHtmlRendererImpl.Builder builder; | ||||||
|     private final MarkwonHtmlParser htmlParser; | 
 | ||||||
|  |     private MarkwonHtmlParser htmlParser; | ||||||
|     private MarkwonHtmlRenderer htmlRenderer; |     private MarkwonHtmlRenderer htmlRenderer; | ||||||
| 
 | 
 | ||||||
|  |     // @since 4.4.0 | ||||||
|  |     private HtmlEmptyTagReplacement emptyTagReplacement = new HtmlEmptyTagReplacement(); | ||||||
|  | 
 | ||||||
|     @SuppressWarnings("WeakerAccess") |     @SuppressWarnings("WeakerAccess") | ||||||
|     HtmlPlugin() { |     HtmlPlugin() { | ||||||
|         this.builder = new MarkwonHtmlRendererImpl.Builder(); |         this.builder = new MarkwonHtmlRendererImpl.Builder(); | ||||||
|         this.htmlParser = MarkwonHtmlParserImpl.create(); |  | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     /** |     /** | ||||||
| @ -104,6 +107,16 @@ public class HtmlPlugin extends AbstractMarkwonPlugin { | |||||||
|         return this; |         return this; | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|  |     /** | ||||||
|  |      * @param emptyTagReplacement {@link HtmlEmptyTagReplacement} | ||||||
|  |      * @since 4.4.0 | ||||||
|  |      */ | ||||||
|  |     @NonNull | ||||||
|  |     public HtmlPlugin emptyTagReplacement(@NonNull HtmlEmptyTagReplacement emptyTagReplacement) { | ||||||
|  |         this.emptyTagReplacement = emptyTagReplacement; | ||||||
|  |         return this; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|     @Override |     @Override | ||||||
|     public void configureConfiguration(@NonNull MarkwonConfiguration.Builder configurationBuilder) { |     public void configureConfiguration(@NonNull MarkwonConfiguration.Builder configurationBuilder) { | ||||||
| 
 | 
 | ||||||
| @ -128,6 +141,7 @@ public class HtmlPlugin extends AbstractMarkwonPlugin { | |||||||
|             builder.addDefaultTagHandler(new HeadingHandler()); |             builder.addDefaultTagHandler(new HeadingHandler()); | ||||||
|         } |         } | ||||||
| 
 | 
 | ||||||
|  |         htmlParser = MarkwonHtmlParserImpl.create(emptyTagReplacement); | ||||||
|         htmlRenderer = builder.build(); |         htmlRenderer = builder.build(); | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|  | |||||||
| @ -62,7 +62,7 @@ public class ImageHandler extends SimpleTagHandler { | |||||||
|             return null; |             return null; | ||||||
|         } |         } | ||||||
| 
 | 
 | ||||||
|         final String destination = configuration.urlProcessor().process(src); |         final String destination = configuration.imageDestinationProcessor().process(src); | ||||||
|         final ImageSize imageSize = imageSizeParser.parse(tag.attributes()); |         final ImageSize imageSize = imageSizeParser.parse(tag.attributes()); | ||||||
| 
 | 
 | ||||||
|         // todo: replacement text is link... as we are not at block level |         // todo: replacement text is link... as we are not at block level | ||||||
|  | |||||||
| @ -27,7 +27,8 @@ public class LinkHandler extends SimpleTagHandler { | |||||||
| 
 | 
 | ||||||
|                 CoreProps.LINK_DESTINATION.set( |                 CoreProps.LINK_DESTINATION.set( | ||||||
|                         renderProps, |                         renderProps, | ||||||
|                         configuration.urlProcessor().process(destination)); |                         destination | ||||||
|  |                 ); | ||||||
| 
 | 
 | ||||||
|                 return spanFactory.getSpans(configuration, renderProps); |                 return spanFactory.getSpans(configuration, renderProps); | ||||||
|             } |             } | ||||||
|  | |||||||
| @ -15,7 +15,6 @@ import java.util.Map; | |||||||
| 
 | 
 | ||||||
| import coil.Coil; | import coil.Coil; | ||||||
| import coil.ImageLoader; | import coil.ImageLoader; | ||||||
| import coil.api.ImageLoaders; |  | ||||||
| import coil.request.LoadRequest; | import coil.request.LoadRequest; | ||||||
| import coil.request.RequestDisposable; | import coil.request.RequestDisposable; | ||||||
| import coil.target.Target; | import coil.target.Target; | ||||||
| @ -48,7 +47,7 @@ public class CoilImagesPlugin extends AbstractMarkwonPlugin { | |||||||
|             @NonNull |             @NonNull | ||||||
|             @Override |             @Override | ||||||
|             public LoadRequest load(@NonNull AsyncDrawable drawable) { |             public LoadRequest load(@NonNull AsyncDrawable drawable) { | ||||||
|                 return ImageLoaders.newLoadBuilder(Coil.loader(), context) |                 return LoadRequest.builder(context) | ||||||
|                         .data(drawable.getDestination()) |                         .data(drawable.getDestination()) | ||||||
|                         .build(); |                         .build(); | ||||||
|             } |             } | ||||||
| @ -57,7 +56,7 @@ public class CoilImagesPlugin extends AbstractMarkwonPlugin { | |||||||
|             public void cancel(@NonNull RequestDisposable disposable) { |             public void cancel(@NonNull RequestDisposable disposable) { | ||||||
|                 disposable.dispose(); |                 disposable.dispose(); | ||||||
|             } |             } | ||||||
|         }, Coil.loader()); |         }, Coil.imageLoader(context)); | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     @NonNull |     @NonNull | ||||||
| @ -67,7 +66,7 @@ public class CoilImagesPlugin extends AbstractMarkwonPlugin { | |||||||
|             @NonNull |             @NonNull | ||||||
|             @Override |             @Override | ||||||
|             public LoadRequest load(@NonNull AsyncDrawable drawable) { |             public LoadRequest load(@NonNull AsyncDrawable drawable) { | ||||||
|                 return ImageLoaders.newLoadBuilder(imageLoader, context) |                 return LoadRequest.builder(context) | ||||||
|                         .data(drawable.getDestination()) |                         .data(drawable.getDestination()) | ||||||
|                         .build(); |                         .build(); | ||||||
|             } |             } | ||||||
| @ -129,7 +128,7 @@ public class CoilImagesPlugin extends AbstractMarkwonPlugin { | |||||||
|             LoadRequest request = coilStore.load(drawable).newBuilder() |             LoadRequest request = coilStore.load(drawable).newBuilder() | ||||||
|                     .target(target) |                     .target(target) | ||||||
|                     .build(); |                     .build(); | ||||||
|             RequestDisposable disposable = imageLoader.load(request); |             RequestDisposable disposable = imageLoader.execute(request); | ||||||
|             cache.put(drawable, disposable); |             cache.put(drawable, disposable); | ||||||
|         } |         } | ||||||
| 
 | 
 | ||||||
|  | |||||||
| @ -3,11 +3,12 @@ package io.noties.markwon.image.file; | |||||||
| import android.content.Context; | import android.content.Context; | ||||||
| import android.content.res.AssetManager; | import android.content.res.AssetManager; | ||||||
| import android.net.Uri; | import android.net.Uri; | ||||||
| import androidx.annotation.NonNull; |  | ||||||
| import androidx.annotation.Nullable; |  | ||||||
| import android.text.TextUtils; | import android.text.TextUtils; | ||||||
| import android.webkit.MimeTypeMap; | import android.webkit.MimeTypeMap; | ||||||
| 
 | 
 | ||||||
|  | import androidx.annotation.NonNull; | ||||||
|  | import androidx.annotation.Nullable; | ||||||
|  | 
 | ||||||
| import java.io.BufferedInputStream; | import java.io.BufferedInputStream; | ||||||
| import java.io.File; | import java.io.File; | ||||||
| import java.io.FileInputStream; | import java.io.FileInputStream; | ||||||
| @ -18,7 +19,6 @@ import java.util.Collection; | |||||||
| import java.util.Collections; | import java.util.Collections; | ||||||
| import java.util.List; | import java.util.List; | ||||||
| 
 | 
 | ||||||
| import io.noties.markwon.urlprocessor.UrlProcessorAndroidAssets; |  | ||||||
| import io.noties.markwon.image.ImageItem; | import io.noties.markwon.image.ImageItem; | ||||||
| import io.noties.markwon.image.SchemeHandler; | import io.noties.markwon.image.SchemeHandler; | ||||||
| 
 | 
 | ||||||
| @ -30,7 +30,7 @@ public class FileSchemeHandler extends SchemeHandler { | |||||||
|     public static final String SCHEME = "file"; |     public static final String SCHEME = "file"; | ||||||
| 
 | 
 | ||||||
|     /** |     /** | ||||||
|      * @see UrlProcessorAndroidAssets |      * @see io.noties.markwon.image.destination.ImageDestinationProcessorAssets | ||||||
|      */ |      */ | ||||||
|     @NonNull |     @NonNull | ||||||
|     public static FileSchemeHandler createWithAssets(@NonNull AssetManager assetManager) { |     public static FileSchemeHandler createWithAssets(@NonNull AssetManager assetManager) { | ||||||
| @ -39,7 +39,7 @@ public class FileSchemeHandler extends SchemeHandler { | |||||||
| 
 | 
 | ||||||
|     /** |     /** | ||||||
|      * @see #createWithAssets(AssetManager) |      * @see #createWithAssets(AssetManager) | ||||||
|      * @see UrlProcessorAndroidAssets |      * @see io.noties.markwon.image.destination.ImageDestinationProcessorAssets | ||||||
|      * @since 4.0.0 |      * @since 4.0.0 | ||||||
|      */ |      */ | ||||||
|     @NonNull |     @NonNull | ||||||
|  | |||||||
| @ -38,7 +38,8 @@ For example, `@since $nap` seems like a good candidate. For this a live template | |||||||
| whenever a new API method/field/functionality-change is introduced (`snc`): | whenever a new API method/field/functionality-change is introduced (`snc`): | ||||||
| 
 | 
 | ||||||
| ``` | ``` | ||||||
| @since $nap; | // semicolon with a space so this one is not accedentally replaced with release version | ||||||
|  | @since $nap ; | ||||||
| ``` | ``` | ||||||
| 
 | 
 | ||||||
| This live template would be possible to use in both inline comment and javadoc comment. | This live template would be possible to use in both inline comment and javadoc comment. | ||||||
|  | |||||||
| @ -24,7 +24,11 @@ | |||||||
|         <activity android:name="io.noties.markwon.sample.basicplugins.BasicPluginsActivity" /> |         <activity android:name="io.noties.markwon.sample.basicplugins.BasicPluginsActivity" /> | ||||||
|         <activity android:name="io.noties.markwon.sample.recycler.RecyclerActivity" /> |         <activity android:name="io.noties.markwon.sample.recycler.RecyclerActivity" /> | ||||||
|         <activity android:name="io.noties.markwon.sample.theme.ThemeActivity" /> |         <activity android:name="io.noties.markwon.sample.theme.ThemeActivity" /> | ||||||
|         <activity android:name=".html.HtmlActivity" /> | 
 | ||||||
|  |         <activity | ||||||
|  |             android:name=".html.HtmlActivity" | ||||||
|  |             android:exported="true" /> | ||||||
|  | 
 | ||||||
|         <activity android:name=".simpleext.SimpleExtActivity" /> |         <activity android:name=".simpleext.SimpleExtActivity" /> | ||||||
|         <activity android:name=".customextension2.CustomExtensionActivity2" /> |         <activity android:name=".customextension2.CustomExtensionActivity2" /> | ||||||
|         <activity android:name=".precomputed.PrecomputedActivity" /> |         <activity android:name=".precomputed.PrecomputedActivity" /> | ||||||
| @ -32,6 +36,7 @@ | |||||||
| 
 | 
 | ||||||
|         <activity |         <activity | ||||||
|             android:name=".editor.EditorActivity" |             android:name=".editor.EditorActivity" | ||||||
|  |             android:exported="true" | ||||||
|             android:windowSoftInputMode="adjustResize" /> |             android:windowSoftInputMode="adjustResize" /> | ||||||
| 
 | 
 | ||||||
|         <activity android:name=".inlineparser.InlineParserActivity" /> |         <activity android:name=".inlineparser.InlineParserActivity" /> | ||||||
|  | |||||||
| @ -5,6 +5,7 @@ import android.net.Uri; | |||||||
| import android.os.Bundle; | import android.os.Bundle; | ||||||
| import android.text.TextUtils; | import android.text.TextUtils; | ||||||
| import android.text.style.ForegroundColorSpan; | import android.text.style.ForegroundColorSpan; | ||||||
|  | import android.view.View; | ||||||
| import android.widget.ScrollView; | import android.widget.ScrollView; | ||||||
| import android.widget.TextView; | import android.widget.TextView; | ||||||
| 
 | 
 | ||||||
| @ -20,6 +21,7 @@ import java.util.Collections; | |||||||
| 
 | 
 | ||||||
| import io.noties.markwon.AbstractMarkwonPlugin; | import io.noties.markwon.AbstractMarkwonPlugin; | ||||||
| import io.noties.markwon.BlockHandlerDef; | import io.noties.markwon.BlockHandlerDef; | ||||||
|  | import io.noties.markwon.LinkResolverDef; | ||||||
| import io.noties.markwon.Markwon; | import io.noties.markwon.Markwon; | ||||||
| import io.noties.markwon.MarkwonConfiguration; | import io.noties.markwon.MarkwonConfiguration; | ||||||
| import io.noties.markwon.MarkwonSpansFactory; | import io.noties.markwon.MarkwonSpansFactory; | ||||||
| @ -153,7 +155,7 @@ public class BasicPluginsActivity extends ActivityWithMenuOptions { | |||||||
|      * <ul> |      * <ul> | ||||||
|      * <li>SyntaxHighlight</li> |      * <li>SyntaxHighlight</li> | ||||||
|      * <li>LinkSpan.Resolver</li> |      * <li>LinkSpan.Resolver</li> | ||||||
|      * <li>UrlProcessor</li> |      * <li>ImageDestinationProcessor</li> | ||||||
|      * <li>ImageSizeResolver</li> |      * <li>ImageSizeResolver</li> | ||||||
|      * </ul> |      * </ul> | ||||||
|      * <p> |      * <p> | ||||||
| @ -173,12 +175,18 @@ public class BasicPluginsActivity extends ActivityWithMenuOptions { | |||||||
|                     public void configureConfiguration(@NonNull MarkwonConfiguration.Builder builder) { |                     public void configureConfiguration(@NonNull MarkwonConfiguration.Builder builder) { | ||||||
|                         // for example if specified destination has no scheme info, we will |                         // for example if specified destination has no scheme info, we will | ||||||
|                         // _assume_ that it's network request and append HTTPS scheme |                         // _assume_ that it's network request and append HTTPS scheme | ||||||
|                         builder.urlProcessor(destination -> { |                         builder.linkResolver(new LinkResolverDef() { | ||||||
|                             final Uri uri = Uri.parse(destination); |                             @Override | ||||||
|                             if (TextUtils.isEmpty(uri.getScheme())) { |                             public void resolve(@NonNull View view, @NonNull String link) { | ||||||
|                                 return "https://" + destination; |                                 final String destination; | ||||||
|  |                                 final Uri uri = Uri.parse(link); | ||||||
|  |                                 if (TextUtils.isEmpty(uri.getScheme())) { | ||||||
|  |                                     destination = "https://" + link; | ||||||
|  |                                 } else { | ||||||
|  |                                     destination = link; | ||||||
|  |                                 } | ||||||
|  |                                 super.resolve(view, destination); | ||||||
|                             } |                             } | ||||||
|                             return destination; |  | ||||||
|                         }); |                         }); | ||||||
|                     } |                     } | ||||||
|                 }) |                 }) | ||||||
| @ -434,4 +442,25 @@ public class BasicPluginsActivity extends ActivityWithMenuOptions { | |||||||
| 
 | 
 | ||||||
|         markwon.setMarkdown(textView, md); |         markwon.setMarkdown(textView, md); | ||||||
|     } |     } | ||||||
|  | 
 | ||||||
|  | //    private void code() { | ||||||
|  | //        final String md = "" + | ||||||
|  | //                "hello `there`!\n\n" + | ||||||
|  | //                "so this, `is super duper long very very very long line that should be going further and further and further down` yep.\n\n" + | ||||||
|  | //                "`okay`"; | ||||||
|  | //        final Markwon markwon = Markwon.builder(this) | ||||||
|  | //                .usePlugin(new AbstractMarkwonPlugin() { | ||||||
|  | //                    @Override | ||||||
|  | //                    public void configureSpansFactory(@NonNull MarkwonSpansFactory.Builder builder) { | ||||||
|  | //                        builder.setFactory(Code.class, new SpanFactory() { | ||||||
|  | //                            @Override | ||||||
|  | //                            public Object getSpans(@NonNull MarkwonConfiguration configuration, @NonNull RenderProps props) { | ||||||
|  | //                                return new CodeTextView.CodeSpan(); | ||||||
|  | //                            } | ||||||
|  | //                        }); | ||||||
|  | //                    } | ||||||
|  | //                }) | ||||||
|  | //                .build(); | ||||||
|  | //        markwon.setMarkdown(textView, md); | ||||||
|  | //    } | ||||||
| } | } | ||||||
|  | |||||||
| @ -0,0 +1,192 @@ | |||||||
|  | package io.noties.markwon.sample.basicplugins; | ||||||
|  | 
 | ||||||
|  | import android.annotation.SuppressLint; | ||||||
|  | import android.content.Context; | ||||||
|  | import android.graphics.Canvas; | ||||||
|  | import android.graphics.Paint; | ||||||
|  | import android.graphics.RectF; | ||||||
|  | import android.os.Build; | ||||||
|  | import android.text.Layout; | ||||||
|  | import android.text.Spanned; | ||||||
|  | import android.util.AttributeSet; | ||||||
|  | import android.view.View; | ||||||
|  | import android.widget.TextView; | ||||||
|  | 
 | ||||||
|  | import androidx.annotation.NonNull; | ||||||
|  | import androidx.annotation.Nullable; | ||||||
|  | 
 | ||||||
|  | import io.noties.debug.Debug; | ||||||
|  | 
 | ||||||
|  | @SuppressLint("AppCompatCustomView") | ||||||
|  | public class CodeTextView extends TextView { | ||||||
|  | 
 | ||||||
|  |     static class CodeSpan { | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     private int paddingHorizontal; | ||||||
|  |     private int paddingVertical; | ||||||
|  | 
 | ||||||
|  |     private float cornerRadius; | ||||||
|  |     private float strokeWidth; | ||||||
|  |     private int strokeColor; | ||||||
|  |     private int backgroundColor; | ||||||
|  | 
 | ||||||
|  |     public CodeTextView(Context context) { | ||||||
|  |         super(context); | ||||||
|  |         init(context, null); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     public CodeTextView(Context context, AttributeSet attrs) { | ||||||
|  |         super(context, attrs); | ||||||
|  |         init(context, attrs); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     private void init(Context context, @Nullable AttributeSet attrs) { | ||||||
|  |         paint.setColor(0xFFff0000); | ||||||
|  |         paint.setStyle(Paint.Style.FILL); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     private final Paint paint = new Paint(Paint.ANTI_ALIAS_FLAG); | ||||||
|  | 
 | ||||||
|  |     @Override | ||||||
|  |     protected void onDraw(Canvas canvas) { | ||||||
|  |         final Layout layout = getLayout(); | ||||||
|  |         if (layout != null) { | ||||||
|  |             draw(this, canvas, layout); | ||||||
|  |         } | ||||||
|  |         super.onDraw(canvas); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     private void draw( | ||||||
|  |             @NonNull View view, | ||||||
|  |             @NonNull Canvas canvas, | ||||||
|  |             @NonNull Layout layout | ||||||
|  |     ) { | ||||||
|  | 
 | ||||||
|  |         final CharSequence cs = layout.getText(); | ||||||
|  |         if (!(cs instanceof Spanned)) { | ||||||
|  |             return; | ||||||
|  |         } | ||||||
|  |         final Spanned spanned = (Spanned) cs; | ||||||
|  | 
 | ||||||
|  |         final int save = canvas.save(); | ||||||
|  |         try { | ||||||
|  |             canvas.translate(view.getPaddingLeft(), view.getPaddingTop()); | ||||||
|  | 
 | ||||||
|  |             // TODO: block? | ||||||
|  |             // TODO: we must remove _original_ spans | ||||||
|  |             // TODO: cache (attach a listener?) | ||||||
|  |             // TODO: editor? | ||||||
|  | 
 | ||||||
|  |             final CodeSpan[] spans = spanned.getSpans(0, spanned.length(), CodeSpan.class); | ||||||
|  |             if (spans != null && spans.length > 0) { | ||||||
|  |                 for (CodeSpan span : spans) { | ||||||
|  | 
 | ||||||
|  |                     final int startOffset = spanned.getSpanStart(span); | ||||||
|  |                     final int endOffset = spanned.getSpanEnd(span); | ||||||
|  | 
 | ||||||
|  |                     final int startLine = layout.getLineForOffset(startOffset); | ||||||
|  |                     final int endLine = layout.getLineForOffset(endOffset); | ||||||
|  | 
 | ||||||
|  |                     // do we need to round them? | ||||||
|  |                     final float left = layout.getPrimaryHorizontal(startOffset) | ||||||
|  |                             + (-1 * layout.getParagraphDirection(startLine) * paddingHorizontal); | ||||||
|  | 
 | ||||||
|  |                     final float right = layout.getPrimaryHorizontal(endOffset) | ||||||
|  |                             + (layout.getParagraphDirection(endLine) * paddingHorizontal); | ||||||
|  | 
 | ||||||
|  |                     final float top = getLineTop(layout, startLine, paddingVertical); | ||||||
|  |                     final float bottom = getLineBottom(layout, endLine, paddingVertical); | ||||||
|  | 
 | ||||||
|  |                     Debug.i(new RectF(left, top, right, bottom).toShortString()); | ||||||
|  | 
 | ||||||
|  |                     if (startLine == endLine) { | ||||||
|  |                         canvas.drawRect(left, top, right, bottom, paint); | ||||||
|  |                     } else { | ||||||
|  |                         // draw first line (start until the lineEnd) | ||||||
|  |                         // draw everything in-between (startLine - endLine) | ||||||
|  |                         // draw last line (lineStart until the end | ||||||
|  | 
 | ||||||
|  |                         canvas.drawRect( | ||||||
|  |                                 left, | ||||||
|  |                                 top, | ||||||
|  |                                 layout.getLineRight(startLine), | ||||||
|  |                                 getLineBottom(layout, startLine, paddingVertical), | ||||||
|  |                                 paint | ||||||
|  |                         ); | ||||||
|  | 
 | ||||||
|  |                         for (int line = startLine + 1; line < endLine; line++) { | ||||||
|  |                             canvas.drawRect( | ||||||
|  |                                     layout.getLineLeft(line), | ||||||
|  |                                     getLineTop(layout, line, paddingVertical), | ||||||
|  |                                     layout.getLineRight(line), | ||||||
|  |                                     getLineBottom(layout, line, paddingVertical), | ||||||
|  |                                     paint | ||||||
|  |                             ); | ||||||
|  |                         } | ||||||
|  | 
 | ||||||
|  |                         canvas.drawRect( | ||||||
|  |                                 layout.getLineLeft(endLine), | ||||||
|  |                                 getLineTop(layout, endLine, paddingVertical), | ||||||
|  |                                 right, | ||||||
|  |                                 getLineBottom(layout, endLine, paddingVertical), | ||||||
|  |                                 paint | ||||||
|  |                         ); | ||||||
|  |                     } | ||||||
|  |                 } | ||||||
|  |             } | ||||||
|  |         } finally { | ||||||
|  |             canvas.restoreToCount(save); | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     private static float getLineTop(@NonNull Layout layout, int line, float padding) { | ||||||
|  |         float value = layout.getLineTop(line) - padding; | ||||||
|  |         if (line == 0) { | ||||||
|  |             value -= layout.getTopPadding(); | ||||||
|  |         } | ||||||
|  |         return value; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     private static float getLineBottom(@NonNull Layout layout, int line, float padding) { | ||||||
|  |         float value = getLineBottomWithoutSpacing(layout, line) - padding; | ||||||
|  |         if (line == (layout.getLineCount() - 1)) { | ||||||
|  |             value -= layout.getBottomPadding(); | ||||||
|  |         } | ||||||
|  |         return value; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     private static float getLineBottomWithoutSpacing(@NonNull Layout layout, int line) { | ||||||
|  |         final float value = layout.getLineBottom(line); | ||||||
|  | 
 | ||||||
|  |         final boolean isLastLineSpacingNotAdded = Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT; | ||||||
|  |         final boolean isLastLine = line == (layout.getLineCount() - 1); | ||||||
|  | 
 | ||||||
|  |         final float lineBottomWithoutSpacing; | ||||||
|  | 
 | ||||||
|  |         final float lineSpacingExtra = layout.getSpacingAdd(); | ||||||
|  |         final float lineSpacingMultiplier = layout.getSpacingMultiplier(); | ||||||
|  | 
 | ||||||
|  |         final boolean hasLineSpacing = Float.compare(lineSpacingExtra, .0F) != 0 | ||||||
|  |                 || Float.compare(lineSpacingMultiplier, 1F) != 0; | ||||||
|  | 
 | ||||||
|  |         if (!hasLineSpacing || isLastLine && isLastLineSpacingNotAdded) { | ||||||
|  |             lineBottomWithoutSpacing = value; | ||||||
|  |         } else { | ||||||
|  |             final float extra; | ||||||
|  |             if (Float.compare(lineSpacingMultiplier, 1F) != 0) { | ||||||
|  |                 final float lineHeight = getLineHeight(layout, line); | ||||||
|  |                 extra = lineHeight - (lineHeight - lineSpacingExtra) / lineSpacingMultiplier; | ||||||
|  |             } else { | ||||||
|  |                 extra = lineSpacingExtra; | ||||||
|  |             } | ||||||
|  |             lineBottomWithoutSpacing = value - extra; | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         return lineBottomWithoutSpacing; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     private static float getLineHeight(@NonNull Layout layout, int line) { | ||||||
|  |         return layout.getLineTop(line + 1) - layout.getLineTop(line); | ||||||
|  |     } | ||||||
|  | } | ||||||
| @ -8,8 +8,14 @@ import android.widget.Toast; | |||||||
| import androidx.annotation.NonNull; | import androidx.annotation.NonNull; | ||||||
| import androidx.annotation.Nullable; | import androidx.annotation.Nullable; | ||||||
| 
 | 
 | ||||||
|  | import org.commonmark.node.Block; | ||||||
|  | import org.commonmark.node.BlockQuote; | ||||||
| import org.commonmark.node.Node; | import org.commonmark.node.Node; | ||||||
|  | import org.commonmark.parser.Parser; | ||||||
| 
 | 
 | ||||||
|  | import java.util.Set; | ||||||
|  | 
 | ||||||
|  | import io.noties.markwon.AbstractMarkwonPlugin; | ||||||
| import io.noties.markwon.Markwon; | import io.noties.markwon.Markwon; | ||||||
| import io.noties.markwon.core.CorePlugin; | import io.noties.markwon.core.CorePlugin; | ||||||
| import io.noties.markwon.sample.ActivityWithMenuOptions; | import io.noties.markwon.sample.ActivityWithMenuOptions; | ||||||
| @ -26,7 +32,8 @@ public class CoreActivity extends ActivityWithMenuOptions { | |||||||
|         return MenuOptions.create() |         return MenuOptions.create() | ||||||
|                 .add("simple", this::simple) |                 .add("simple", this::simple) | ||||||
|                 .add("toast", this::toast) |                 .add("toast", this::toast) | ||||||
|                 .add("alreadyParsed", this::alreadyParsed); |                 .add("alreadyParsed", this::alreadyParsed) | ||||||
|  |                 .add("enabledBlockTypes", this::enabledBlockTypes); | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     @Override |     @Override | ||||||
| @ -132,4 +139,28 @@ public class CoreActivity extends ActivityWithMenuOptions { | |||||||
|         // apply parsed markdown |         // apply parsed markdown | ||||||
|         markwon.setParsedMarkdown(textView, spanned); |         markwon.setParsedMarkdown(textView, spanned); | ||||||
|     } |     } | ||||||
|  | 
 | ||||||
|  |     private void enabledBlockTypes() { | ||||||
|  | 
 | ||||||
|  |         final String md = "" + | ||||||
|  |                 "# Head\n\n" + | ||||||
|  |                 "> and disabled quote\n\n" + | ||||||
|  |                 "```\n" + | ||||||
|  |                 "yep\n" + | ||||||
|  |                 "```"; | ||||||
|  | 
 | ||||||
|  |         final Set<Class<? extends Block>> blocks = CorePlugin.enabledBlockTypes(); | ||||||
|  |         blocks.remove(BlockQuote.class); | ||||||
|  | 
 | ||||||
|  |         final Markwon markwon = Markwon.builder(this) | ||||||
|  |                 .usePlugin(new AbstractMarkwonPlugin() { | ||||||
|  |                     @Override | ||||||
|  |                     public void configureParser(@NonNull Parser.Builder builder) { | ||||||
|  |                         builder.enabledBlockTypes(blocks); | ||||||
|  |                     } | ||||||
|  |                 }) | ||||||
|  |                 .build(); | ||||||
|  | 
 | ||||||
|  |         markwon.setMarkdown(textView, md); | ||||||
|  |     } | ||||||
| } | } | ||||||
|  | |||||||
| @ -6,6 +6,7 @@ import android.text.SpannableStringBuilder; | |||||||
| import android.text.Spanned; | import android.text.Spanned; | ||||||
| import android.text.TextPaint; | import android.text.TextPaint; | ||||||
| import android.text.TextUtils; | import android.text.TextUtils; | ||||||
|  | import android.text.TextWatcher; | ||||||
| import android.text.method.LinkMovementMethod; | import android.text.method.LinkMovementMethod; | ||||||
| import android.text.style.ForegroundColorSpan; | import android.text.style.ForegroundColorSpan; | ||||||
| import android.text.style.MetricAffectingSpan; | import android.text.style.MetricAffectingSpan; | ||||||
| @ -25,8 +26,11 @@ import java.util.ArrayList; | |||||||
| import java.util.List; | import java.util.List; | ||||||
| import java.util.concurrent.Executors; | import java.util.concurrent.Executors; | ||||||
| 
 | 
 | ||||||
|  | import io.noties.debug.AndroidLogDebugOutput; | ||||||
|  | import io.noties.debug.Debug; | ||||||
| import io.noties.markwon.AbstractMarkwonPlugin; | import io.noties.markwon.AbstractMarkwonPlugin; | ||||||
| import io.noties.markwon.Markwon; | import io.noties.markwon.Markwon; | ||||||
|  | import io.noties.markwon.SoftBreakAddsNewLinePlugin; | ||||||
| import io.noties.markwon.core.spans.EmphasisSpan; | import io.noties.markwon.core.spans.EmphasisSpan; | ||||||
| import io.noties.markwon.core.spans.StrongEmphasisSpan; | import io.noties.markwon.core.spans.StrongEmphasisSpan; | ||||||
| import io.noties.markwon.editor.AbstractEditHandler; | import io.noties.markwon.editor.AbstractEditHandler; | ||||||
| @ -65,7 +69,8 @@ public class EditorActivity extends ActivityWithMenuOptions { | |||||||
|                 .add("multipleEditSpansPlugin", this::multiple_edit_spans_plugin) |                 .add("multipleEditSpansPlugin", this::multiple_edit_spans_plugin) | ||||||
|                 .add("pluginRequire", this::plugin_require) |                 .add("pluginRequire", this::plugin_require) | ||||||
|                 .add("pluginNoDefaults", this::plugin_no_defaults) |                 .add("pluginNoDefaults", this::plugin_no_defaults) | ||||||
|                 .add("heading", this::heading); |                 .add("heading", this::heading) | ||||||
|  |                 .add("newLine", this::newLine); | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     @Override |     @Override | ||||||
| @ -98,7 +103,10 @@ public class EditorActivity extends ActivityWithMenuOptions { | |||||||
|         super.onCreate(savedInstanceState); |         super.onCreate(savedInstanceState); | ||||||
|         createView(); |         createView(); | ||||||
| 
 | 
 | ||||||
|  |         Debug.init(new AndroidLogDebugOutput(true)); | ||||||
|  | 
 | ||||||
|         multiple_edit_spans(); |         multiple_edit_spans(); | ||||||
|  | //        newLine(); | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     private void simple_process() { |     private void simple_process() { | ||||||
| @ -230,6 +238,7 @@ public class EditorActivity extends ActivityWithMenuOptions { | |||||||
|                         builder.inlineParserFactory(inlineParserFactory); |                         builder.inlineParserFactory(inlineParserFactory); | ||||||
|                     } |                     } | ||||||
|                 }) |                 }) | ||||||
|  |                 .usePlugin(SoftBreakAddsNewLinePlugin.create()) | ||||||
|                 .build(); |                 .build(); | ||||||
| 
 | 
 | ||||||
|         final LinkEditHandler.OnClick onClick = (widget, link) -> markwon.configuration().linkResolver().resolve(widget, link); |         final LinkEditHandler.OnClick onClick = (widget, link) -> markwon.configuration().linkResolver().resolve(widget, link); | ||||||
| @ -280,6 +289,13 @@ public class EditorActivity extends ActivityWithMenuOptions { | |||||||
|                 editor, Executors.newSingleThreadExecutor(), editText)); |                 editor, Executors.newSingleThreadExecutor(), editText)); | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|  |     private void newLine() { | ||||||
|  |         final Markwon markwon = Markwon.create(this); | ||||||
|  |         final MarkwonEditor editor = MarkwonEditor.create(markwon); | ||||||
|  |         final TextWatcher textWatcher = MarkdownNewLine.wrap(MarkwonEditorTextWatcher.withProcess(editor)); | ||||||
|  |         editText.addTextChangedListener(textWatcher); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|     private void plugin_require() { |     private void plugin_require() { | ||||||
|         // usage of plugin from other plugins |         // usage of plugin from other plugins | ||||||
| 
 | 
 | ||||||
| @ -295,6 +311,8 @@ public class EditorActivity extends ActivityWithMenuOptions { | |||||||
|                 }) |                 }) | ||||||
|                 .build(); |                 .build(); | ||||||
| 
 | 
 | ||||||
|  |         editText.setMovementMethod(LinkMovementMethod.getInstance()); | ||||||
|  | 
 | ||||||
|         final MarkwonEditor editor = MarkwonEditor.create(markwon); |         final MarkwonEditor editor = MarkwonEditor.create(markwon); | ||||||
| 
 | 
 | ||||||
|         editText.addTextChangedListener(MarkwonEditorTextWatcher.withPreRender( |         editText.addTextChangedListener(MarkwonEditorTextWatcher.withPreRender( | ||||||
|  | |||||||
| @ -0,0 +1,129 @@ | |||||||
|  | package io.noties.markwon.sample.editor; | ||||||
|  | 
 | ||||||
|  | import android.text.Editable; | ||||||
|  | import android.text.TextUtils; | ||||||
|  | import android.text.TextWatcher; | ||||||
|  | 
 | ||||||
|  | import androidx.annotation.NonNull; | ||||||
|  | 
 | ||||||
|  | import java.util.regex.Matcher; | ||||||
|  | import java.util.regex.Pattern; | ||||||
|  | 
 | ||||||
|  | import io.noties.debug.Debug; | ||||||
|  | 
 | ||||||
|  | abstract class MarkdownNewLine { | ||||||
|  | 
 | ||||||
|  |     @NonNull | ||||||
|  |     static TextWatcher wrap(@NonNull TextWatcher textWatcher) { | ||||||
|  |         return new NewLineTextWatcher(textWatcher); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     private MarkdownNewLine() { | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     private static class NewLineTextWatcher implements TextWatcher { | ||||||
|  | 
 | ||||||
|  |         // NB! matches only bullet lists | ||||||
|  |         private final Pattern RE = Pattern.compile("^( {0,3}[\\-+* ]+)(.+)*$"); | ||||||
|  | 
 | ||||||
|  |         private final TextWatcher wrapped; | ||||||
|  | 
 | ||||||
|  |         private boolean selfChange; | ||||||
|  | 
 | ||||||
|  |         // this content is pending to be inserted at the beginning | ||||||
|  |         private String pendingNewLineContent; | ||||||
|  |         private int pendingNewLineIndex; | ||||||
|  | 
 | ||||||
|  |         // mark current edited line for removal (range start/end) | ||||||
|  |         private int clearLineStart; | ||||||
|  |         private int clearLineEnd; | ||||||
|  | 
 | ||||||
|  |         NewLineTextWatcher(@NonNull TextWatcher wrapped) { | ||||||
|  |             this.wrapped = wrapped; | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         @Override | ||||||
|  |         public void beforeTextChanged(CharSequence s, int start, int count, int after) { | ||||||
|  |             // no op | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         @Override | ||||||
|  |         public void onTextChanged(CharSequence s, int start, int before, int count) { | ||||||
|  |             if (selfChange) { | ||||||
|  |                 return; | ||||||
|  |             } | ||||||
|  | 
 | ||||||
|  |             // just one new character added | ||||||
|  |             if (before == 0 | ||||||
|  |                     && count == 1 | ||||||
|  |                     && '\n' == s.charAt(start)) { | ||||||
|  |                 int end = -1; | ||||||
|  |                 for (int i = start - 1; i >= 0; i--) { | ||||||
|  |                     if ('\n' == s.charAt(i)) { | ||||||
|  |                         end = i + 1; | ||||||
|  |                         break; | ||||||
|  |                     } | ||||||
|  |                 } | ||||||
|  | 
 | ||||||
|  |                 // start at the very beginning | ||||||
|  |                 if (end < 0) { | ||||||
|  |                     end = 0; | ||||||
|  |                 } | ||||||
|  | 
 | ||||||
|  |                 final String pendingNewLineContent; | ||||||
|  | 
 | ||||||
|  |                 final int clearLineStart; | ||||||
|  |                 final int clearLineEnd; | ||||||
|  | 
 | ||||||
|  |                 final Matcher matcher = RE.matcher(s.subSequence(end, start)); | ||||||
|  |                 if (matcher.matches()) { | ||||||
|  |                     // if second group is empty -> remove new line | ||||||
|  |                     final String content = matcher.group(2); | ||||||
|  |                     Debug.e("new line, content: '%s'", content); | ||||||
|  |                     if (TextUtils.isEmpty(content)) { | ||||||
|  |                         // another empty new line, remove this start | ||||||
|  |                         clearLineStart = end; | ||||||
|  |                         clearLineEnd = start; | ||||||
|  |                         pendingNewLineContent = null; | ||||||
|  |                     } else { | ||||||
|  |                         pendingNewLineContent = matcher.group(1); | ||||||
|  |                         clearLineStart = clearLineEnd = 0; | ||||||
|  |                     } | ||||||
|  |                 } else { | ||||||
|  |                     pendingNewLineContent = null; | ||||||
|  |                     clearLineStart = clearLineEnd = 0; | ||||||
|  |                 } | ||||||
|  |                 this.pendingNewLineContent = pendingNewLineContent; | ||||||
|  |                 this.pendingNewLineIndex = start + 1; | ||||||
|  |                 this.clearLineStart = clearLineStart; | ||||||
|  |                 this.clearLineEnd = clearLineEnd; | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         @Override | ||||||
|  |         public void afterTextChanged(Editable s) { | ||||||
|  |             if (selfChange) { | ||||||
|  |                 return; | ||||||
|  |             } | ||||||
|  | 
 | ||||||
|  |             if (pendingNewLineContent != null || clearLineStart < clearLineEnd) { | ||||||
|  |                 selfChange = true; | ||||||
|  |                 try { | ||||||
|  |                     if (pendingNewLineContent != null) { | ||||||
|  |                         s.insert(pendingNewLineIndex, pendingNewLineContent); | ||||||
|  |                         pendingNewLineContent = null; | ||||||
|  |                     } else { | ||||||
|  |                         s.replace(clearLineStart, clearLineEnd, ""); | ||||||
|  |                         clearLineStart = clearLineEnd = 0; | ||||||
|  |                     } | ||||||
|  |                 } finally { | ||||||
|  |                     selfChange = false; | ||||||
|  |                 } | ||||||
|  |             } | ||||||
|  | 
 | ||||||
|  |             // NB, we assume MarkdownEditor text watcher that only listens for this event, | ||||||
|  |             // other text-watchers must be interested in other events also | ||||||
|  |             wrapped.afterTextChanged(s); | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | } | ||||||
| @ -0,0 +1,242 @@ | |||||||
|  | package io.noties.markwon.sample.html; | ||||||
|  | 
 | ||||||
|  | import android.graphics.Canvas; | ||||||
|  | import android.graphics.Paint; | ||||||
|  | import android.graphics.Path; | ||||||
|  | import android.os.Build; | ||||||
|  | import android.text.Layout; | ||||||
|  | import android.text.Spanned; | ||||||
|  | import android.text.TextPaint; | ||||||
|  | import android.text.TextUtils; | ||||||
|  | import android.text.style.LineBackgroundSpan; | ||||||
|  | import android.text.style.MetricAffectingSpan; | ||||||
|  | import android.util.Log; | ||||||
|  | import android.widget.TextView; | ||||||
|  | 
 | ||||||
|  | import androidx.annotation.NonNull; | ||||||
|  | import androidx.annotation.Px; | ||||||
|  | import androidx.annotation.RequiresApi; | ||||||
|  | 
 | ||||||
|  | import io.noties.markwon.core.spans.TextLayoutSpan; | ||||||
|  | import io.noties.markwon.core.spans.TextViewSpan; | ||||||
|  | 
 | ||||||
|  | import static java.lang.Math.max; | ||||||
|  | import static java.lang.Math.min; | ||||||
|  | 
 | ||||||
|  | /** | ||||||
|  |  * Credit goes to [Romain Guy](https://github.com/romainguy/elegant-underline) | ||||||
|  |  * <p> | ||||||
|  |  * Failed attempt to create elegant underline as a span | ||||||
|  |  * <ul> | ||||||
|  |  * <li>in a `TextView` span is rendered, but `draw` method is invoked constantly which put pressure on CPU and memory | ||||||
|  |  * <li>in an `EditText` only the first line draws this underline span (seems to be a weird | ||||||
|  |  * issue between LineBackgroundSpan and EditText). Also, in `EditText` `draw` method is invoked | ||||||
|  |  * constantly (for each drawing of the blinking cursor) | ||||||
|  |  * <li>cannot reliably receive proper text, for example if underline is applied to a text range which has | ||||||
|  |  * different typefaces applied to different words (underline cannot know that, which applied to which) | ||||||
|  |  * </ul> | ||||||
|  |  */ | ||||||
|  | // will apply other spans that 100% contain this one, so for example if | ||||||
|  | // an underline that inside some other spans (different typeface), they won't be applied and thus | ||||||
|  | // underline would be incorrect | ||||||
|  | // do not use in editor, due to some obscure thing, LineBackgroundSpan would be applied to the first line only | ||||||
|  | // also, in editor this span would be redrawn with each blink of the cursor | ||||||
|  | @RequiresApi(Build.VERSION_CODES.KITKAT) | ||||||
|  | class ElegantUnderlineSpan implements LineBackgroundSpan { | ||||||
|  | 
 | ||||||
|  |     private static final float DEFAULT_UNDERLINE_HEIGHT_DIP = 0.8F; | ||||||
|  |     private static final float DEFAULT_UNDERLINE_CLEAR_GAP_DIP = 5.5F; | ||||||
|  | 
 | ||||||
|  |     @NonNull | ||||||
|  |     public static ElegantUnderlineSpan create() { | ||||||
|  |         return new ElegantUnderlineSpan(0, 0); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     @NonNull | ||||||
|  |     public static ElegantUnderlineSpan create(@Px int underlineHeight) { | ||||||
|  |         return new ElegantUnderlineSpan(underlineHeight, 0); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     @NonNull | ||||||
|  |     public static ElegantUnderlineSpan create(@Px int underlineHeight, @Px int underlineClearGap) { | ||||||
|  |         return new ElegantUnderlineSpan(underlineHeight, underlineClearGap); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     // TODO: underline color? | ||||||
|  |     private final int underlineHeight; | ||||||
|  |     private final int underlineClearGap; | ||||||
|  | 
 | ||||||
|  |     private final Path underline = new Path(); | ||||||
|  |     private final Path outline = new Path(); | ||||||
|  |     private final Paint stroke = new Paint(); | ||||||
|  |     private final Path strokedOutline = new Path(); | ||||||
|  | 
 | ||||||
|  |     private final CharCache charCache = new CharCache(); | ||||||
|  | 
 | ||||||
|  |     private final TextPaint tempTextPaint = new TextPaint(); | ||||||
|  | 
 | ||||||
|  |     protected ElegantUnderlineSpan(@Px int underlineHeight, @Px int underlineClearGap) { | ||||||
|  |         this.underlineHeight = underlineHeight; | ||||||
|  |         this.underlineClearGap = underlineClearGap; | ||||||
|  |         stroke.setStyle(Paint.Style.FILL_AND_STROKE); | ||||||
|  |         stroke.setStrokeCap(Paint.Cap.BUTT); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     // is it possible that LineBackgroundSpan is not receiving proper spans? like typeface? | ||||||
|  |     //  it complicates things (like the need to have own copy of paint) | ||||||
|  | 
 | ||||||
|  |     // is it possible that LineBackgroundSpan is called constantly even in a TextView? | ||||||
|  | 
 | ||||||
|  |     @Override | ||||||
|  |     public void drawBackground( | ||||||
|  |             Canvas c, | ||||||
|  |             Paint p, | ||||||
|  |             int left, | ||||||
|  |             int right, | ||||||
|  |             int top, | ||||||
|  |             int baseline, | ||||||
|  |             int bottom, | ||||||
|  |             CharSequence text, | ||||||
|  |             int start, | ||||||
|  |             int end, | ||||||
|  |             int lnum | ||||||
|  |     ) { | ||||||
|  | 
 | ||||||
|  | //        Debug.trace(); | ||||||
|  | 
 | ||||||
|  |         final Spanned spanned = (Spanned) text; | ||||||
|  |         final TextView textView = TextViewSpan.textViewOf(spanned); | ||||||
|  | 
 | ||||||
|  |         if (textView == null) { | ||||||
|  |             // TextView is required | ||||||
|  |             Log.e("EU", "no text view"); | ||||||
|  |             return; | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         final Layout layout; | ||||||
|  |         { | ||||||
|  |             // check if there is dedicated layout, if not, use from textView | ||||||
|  |             //  (think tableRowSpan that uses own Layout) | ||||||
|  |             final Layout layoutFromSpan = TextLayoutSpan.layoutOf(spanned); | ||||||
|  |             if (layoutFromSpan != null) { | ||||||
|  |                 layout = layoutFromSpan; | ||||||
|  |             } else { | ||||||
|  |                 layout = textView.getLayout(); | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         if (layout == null) { | ||||||
|  |             // we could call `p.setUnderlineText(true)` here a fallback, | ||||||
|  |             //  but this would make __all__ text in a TextView underlined, which is not | ||||||
|  |             //  what we want | ||||||
|  |             Log.e("EU", "no layout"); | ||||||
|  |             return; | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         tempTextPaint.set((TextPaint) p); | ||||||
|  | 
 | ||||||
|  |         // we must use _selfStart_ because underline can start **not** at the beginning of a line. | ||||||
|  |         // as we are using LineBackground `start` would indicate the start position of the line | ||||||
|  |         //  and not start of the span (self). The same goes for selfEnd (ended before line) | ||||||
|  |         final int selfStart = spanned.getSpanStart(this); | ||||||
|  |         final int selfEnd = spanned.getSpanEnd(this); | ||||||
|  | 
 | ||||||
|  |         final int s = max(selfStart, start); | ||||||
|  | 
 | ||||||
|  |         // all lines should use (end - 1) to receive proper line end coordinate X, | ||||||
|  |         //  unless it is last line in _layout_ | ||||||
|  |         final boolean isLastLine = lnum == (layout.getLineCount() - 1); | ||||||
|  |         final int e = min(selfEnd, end - (isLastLine ? 0 : 1)); | ||||||
|  | 
 | ||||||
|  |         if (true) { | ||||||
|  |             Log.e("EU", String.format("lnum: %s, hash: %s, text: '%s'", | ||||||
|  |                     lnum, text.subSequence(s, e).hashCode(), text.subSequence(s, e))); | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         final int leading; | ||||||
|  |         final int trailing; | ||||||
|  |         { | ||||||
|  |             final int l = (int) (layout.getPrimaryHorizontal(s) + .5F); | ||||||
|  |             final int r = (int) (layout.getPrimaryHorizontal(e) + .5F); | ||||||
|  |             leading = min(l, r); | ||||||
|  |             trailing = max(l, r); | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         underline.rewind(); | ||||||
|  | 
 | ||||||
|  |         // middle between baseline and descent | ||||||
|  |         final int diff = (int) (p.descent() / 2F + .5F); | ||||||
|  | 
 | ||||||
|  |         underline.addRect( | ||||||
|  |                 leading, baseline + diff, | ||||||
|  |                 trailing, baseline + diff + underlineHeight(textView), | ||||||
|  |                 Path.Direction.CW | ||||||
|  |         ); | ||||||
|  | 
 | ||||||
|  |         outline.rewind(); | ||||||
|  | 
 | ||||||
|  |         final int charsLength = e - s; | ||||||
|  |         final char[] chars = charCache.chars(charsLength); | ||||||
|  |         TextUtils.getChars(spanned, s, e, chars, 0); | ||||||
|  | 
 | ||||||
|  |         if (true) { | ||||||
|  |             final MetricAffectingSpan[] metricAffectingSpans = spanned.getSpans(s, e, MetricAffectingSpan.class); | ||||||
|  | //            Log.e("EU", Arrays.toString(metricAffectingSpans)); | ||||||
|  |             for (MetricAffectingSpan span : metricAffectingSpans) { | ||||||
|  |                 span.updateMeasureState(tempTextPaint); | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         // todo: styleSpan | ||||||
|  |         // todo all other spans (maybe UpdateMeasureSpans?) | ||||||
|  |         tempTextPaint.getTextPath( | ||||||
|  |                 chars, | ||||||
|  |                 0, charsLength, | ||||||
|  |                 leading, baseline, | ||||||
|  |                 outline | ||||||
|  |         ); | ||||||
|  | 
 | ||||||
|  |         outline.op(underline, Path.Op.INTERSECT); | ||||||
|  | 
 | ||||||
|  |         strokedOutline.rewind(); | ||||||
|  |         stroke.setStrokeWidth(underlineClearGap(textView)); | ||||||
|  |         stroke.getFillPath(outline, strokedOutline); | ||||||
|  | 
 | ||||||
|  |         underline.op(strokedOutline, Path.Op.DIFFERENCE); | ||||||
|  | 
 | ||||||
|  |         c.drawPath(underline, p); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     private int underlineHeight(@NonNull TextView textView) { | ||||||
|  |         if (underlineHeight > 0) { | ||||||
|  |             return underlineHeight; | ||||||
|  |         } | ||||||
|  |         return (int) (DEFAULT_UNDERLINE_HEIGHT_DIP * textView.getResources().getDisplayMetrics().density + 0.5F); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     private int underlineClearGap(@NonNull TextView textView) { | ||||||
|  |         if (underlineClearGap > 0) { | ||||||
|  |             return underlineClearGap; | ||||||
|  |         } | ||||||
|  |         return (int) (DEFAULT_UNDERLINE_CLEAR_GAP_DIP * textView.getResources().getDisplayMetrics().density + 0.5F); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     // primitive cache that grows internal array (never shrinks, nor clear buffer) | ||||||
|  |     // TODO: but... each span has own instance, so not much of the memory saving | ||||||
|  |     private static class CharCache { | ||||||
|  | 
 | ||||||
|  |         @NonNull | ||||||
|  |         char[] chars(int ofLength) { | ||||||
|  |             final char[] out; | ||||||
|  |             if (chars == null || chars.length < ofLength) { | ||||||
|  |                 out = chars = new char[ofLength]; | ||||||
|  |             } else { | ||||||
|  |                 out = chars; | ||||||
|  |             } | ||||||
|  |             return out; | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         private char[] chars; | ||||||
|  |     } | ||||||
|  | } | ||||||
|  | 
 | ||||||
| @ -1,18 +1,18 @@ | |||||||
| package io.noties.markwon.sample.html; | package io.noties.markwon.sample.html; | ||||||
| 
 | 
 | ||||||
|  | import android.os.Build; | ||||||
| import android.os.Bundle; | import android.os.Bundle; | ||||||
| import android.text.Layout; | import android.text.Layout; | ||||||
| import android.text.TextUtils; | import android.text.TextUtils; | ||||||
| import android.text.style.AbsoluteSizeSpan; | import android.text.style.AbsoluteSizeSpan; | ||||||
| import android.text.style.AlignmentSpan; | import android.text.style.AlignmentSpan; | ||||||
| import android.widget.TextView; | import android.widget.TextView; | ||||||
|  | import android.widget.Toast; | ||||||
| 
 | 
 | ||||||
| import androidx.annotation.NonNull; | import androidx.annotation.NonNull; | ||||||
| import androidx.annotation.Nullable; | import androidx.annotation.Nullable; | ||||||
| import androidx.annotation.Px; | import androidx.annotation.Px; | ||||||
| 
 | 
 | ||||||
| import org.commonmark.node.Paragraph; |  | ||||||
| 
 |  | ||||||
| import java.util.Collection; | import java.util.Collection; | ||||||
| import java.util.Collections; | import java.util.Collections; | ||||||
| import java.util.Random; | import java.util.Random; | ||||||
| @ -23,6 +23,7 @@ import io.noties.markwon.MarkwonConfiguration; | |||||||
| import io.noties.markwon.MarkwonVisitor; | import io.noties.markwon.MarkwonVisitor; | ||||||
| import io.noties.markwon.RenderProps; | import io.noties.markwon.RenderProps; | ||||||
| import io.noties.markwon.SpannableBuilder; | import io.noties.markwon.SpannableBuilder; | ||||||
|  | import io.noties.markwon.html.HtmlEmptyTagReplacement; | ||||||
| import io.noties.markwon.html.HtmlPlugin; | import io.noties.markwon.html.HtmlPlugin; | ||||||
| import io.noties.markwon.html.HtmlTag; | import io.noties.markwon.html.HtmlTag; | ||||||
| import io.noties.markwon.html.MarkwonHtmlRenderer; | import io.noties.markwon.html.MarkwonHtmlRenderer; | ||||||
| @ -42,7 +43,10 @@ public class HtmlActivity extends ActivityWithMenuOptions { | |||||||
|                 .add("align", this::align) |                 .add("align", this::align) | ||||||
|                 .add("randomCharSize", this::randomCharSize) |                 .add("randomCharSize", this::randomCharSize) | ||||||
|                 .add("enhance", this::enhance) |                 .add("enhance", this::enhance) | ||||||
|                 .add("image", this::image); |                 .add("image", this::image) | ||||||
|  | //                .add("elegantUnderline", this::elegantUnderline) | ||||||
|  |                 .add("iframe", this::iframe) | ||||||
|  |                 .add("emptyTagReplacement", this::emptyTagReplacement); | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     private TextView textView; |     private TextView textView; | ||||||
| @ -56,8 +60,8 @@ public class HtmlActivity extends ActivityWithMenuOptions { | |||||||
|         // let's define some custom tag-handlers |         // let's define some custom tag-handlers | ||||||
| 
 | 
 | ||||||
|         textView = findViewById(R.id.text_view); |         textView = findViewById(R.id.text_view); | ||||||
| 
 |          | ||||||
|         align(); |         emptyTagReplacement(); | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     // we can use `SimpleTagHandler` for _simple_ cases (when the whole tag content |     // we can use `SimpleTagHandler` for _simple_ cases (when the whole tag content | ||||||
| @ -268,4 +272,86 @@ public class HtmlActivity extends ActivityWithMenuOptions { | |||||||
| 
 | 
 | ||||||
|         markwon.setMarkdown(textView, md); |         markwon.setMarkdown(textView, md); | ||||||
|     } |     } | ||||||
|  | 
 | ||||||
|  |     private void elegantUnderline() { | ||||||
|  | 
 | ||||||
|  |         if (Build.VERSION.SDK_INT < Build.VERSION_CODES.KITKAT) { | ||||||
|  |             Toast.makeText( | ||||||
|  |                     this, | ||||||
|  |                     "Elegant underline is supported on KitKat and up", | ||||||
|  |                     Toast.LENGTH_LONG | ||||||
|  |             ).show(); | ||||||
|  |             return; | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         final String underline = "Well well wel, and now <u>Gogogo, quite **perfect** yeah</u> and nice and elegant"; | ||||||
|  | 
 | ||||||
|  |         final String md = "" + | ||||||
|  |                 underline + "\n\n" + | ||||||
|  |                 "<b>" + underline + "</b>\n\n" + | ||||||
|  |                 "<font name=serif>" + underline + "</font>\n\n" + | ||||||
|  |                 "<font name=sans-serif>" + underline + underline + underline + "</font>\n\n" + | ||||||
|  |                 "<font name=monospace>" + underline + "</font>\n\n" + | ||||||
|  |                 ""; | ||||||
|  | 
 | ||||||
|  |         final Markwon markwon = Markwon.builder(this) | ||||||
|  |                 .usePlugin(HtmlPlugin.create(plugin -> plugin | ||||||
|  |                         .addHandler(new HtmlFontTagHandler()) | ||||||
|  |                         .addHandler(new HtmlElegantUnderlineTagHandler()))) | ||||||
|  |                 .build(); | ||||||
|  | 
 | ||||||
|  |         markwon.setMarkdown(textView, md); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     private void iframe() { | ||||||
|  |         final String md = "" + | ||||||
|  |                 "# Hello iframe\n\n" + | ||||||
|  |                 "<p class=\"p1\"><img title=\"JUMP FORCE\" src=\"https://img1.ak.crunchyroll.com/i/spire1/f0c009039dd9f8dff5907fff148adfca1587067000_full.jpg\" alt=\"JUMP FORCE\" width=\"640\" height=\"362\" /></p>\n" + | ||||||
|  |                 "<p class=\"p2\"> </p>\n" + | ||||||
|  |                 "<p class=\"p1\">Switch owners will soon get to take part in the ultimate <em>Shonen Jump </em>rumble. Bandai Namco announced plans to bring <strong><em>Jump Force </em></strong>to <strong>Switch</strong> as <strong><em>Jump Force Deluxe Edition</em></strong>, with a release set for sometime this year. This version will include all of the original playable characters and those from Character Pass 1, and <strong>Character Pass 2 is also in the works </strong>for all versions, starting with <strong>Shoto Todoroki from </strong><span style=\"color: #ff9900;\"><a href=\"/my-hero-academia?utm_source=editorial_cr&utm_medium=news&utm_campaign=article_driven&referrer=editorial_cr_news_article_driven\"><span style=\"color: #ff9900;\"><strong><em>My Hero Academia</em></strong></span></a></span>.</p>\n" + | ||||||
|  |                 "<p class=\"p2\"> </p>\n" + | ||||||
|  |                 "<p class=\"p1\">Other than Todoroki, Bandai Namco hinted that the four other Character Pass 2 characters will hail from <span style=\"color: #ff9900;\"><a href=\"/hunter-x-hunter?utm_source=editorial_cr&utm_medium=news&utm_campaign=article_driven&referrer=editorial_cr_news_article_driven\"><span style=\"color: #ff9900;\"><em>Hunter x Hunter</em></span></a></span>, <em>Yu Yu Hakusho</em>, <span style=\"color: #ff9900;\"><a href=\"/bleach?utm_source=editorial_cr&utm_medium=news&utm_campaign=article_driven&referrer=editorial_cr_news_article_driven\"><span style=\"color: #ff9900;\"><em>Bleach</em></span></a></span>, and <span style=\"color: #ff9900;\"><a href=\"/jojos-bizarre-adventure?utm_source=editorial_cr&utm_medium=news&utm_campaign=article_driven&referrer=editorial_cr_news_article_driven\"><span style=\"color: #ff9900;\"><em>JoJo's Bizarre Adventure</em></span></a></span>. Character Pass 2 will be priced at $17.99, and Todoroki launches this spring.<span class=\"Apple-converted-space\"> </span></p>\n" + | ||||||
|  |                 "<p class=\"p2\"> </p>\n" + | ||||||
|  |                 "<p class=\"p1\"><iframe style=\"display: block; margin-left: auto; margin-right: auto;\" src=\"https://www.youtube.com/embed/At1qTj-LWCc\" frameborder=\"0\" width=\"640\" height=\"360\"></iframe></p>\n" + | ||||||
|  |                 "<p class=\"p2\"> </p>\n" + | ||||||
|  |                 "<p class=\"p1\">Character Pass 2 promo:</p>\n" + | ||||||
|  |                 "<p class=\"p2\"> </p>\n" + | ||||||
|  |                 "<p class=\"p1\"><iframe style=\"display: block; margin-left: auto; margin-right: auto;\" src=\"https://www.youtube.com/embed/CukwN6kV4R4\" frameborder=\"0\" width=\"640\" height=\"360\"></iframe></p>\n" + | ||||||
|  |                 "<p class=\"p2\"> </p>\n" + | ||||||
|  |                 "<p class=\"p1\"><a href=\"https://got.cr/PremiumTrial-NewsBanner4\"><img style=\"display: block; margin-left: auto; margin-right: auto;\" src=\"https://img1.ak.crunchyroll.com/i/spire4/78f5441d927cf160a93e037b567c2b1f1587067041_full.png\" alt=\"\" width=\"640\" height=\"43\" /></a></p>\n" + | ||||||
|  |                 "<p class=\"p2\"> </p>\n" + | ||||||
|  |                 "<p class=\"p1\">-------</p>\n" + | ||||||
|  |                 "<p class=\"p1\"><em>Joseph Luster is the Games and Web editor at </em><a href=\"http://www.otakuusamagazine.com/ME2/Default.asp\"><em>Otaku USA Magazine</em></a><em>. You can read his webcomic, </em><a href=\"http://subhumanzoids.com/comics/big-dumb-fighting-idiots/\">BIG DUMB FIGHTING IDIOTS</a><em> at </em><a href=\"http://subhumanzoids.com/\"><em>subhumanzoids</em></a><em>. Follow him on Twitter </em><a href=\"https://twitter.com/Moldilox\"><em>@Moldilox</em></a><em>.</em><span class=\"Apple-converted-space\"> </span></p>"; | ||||||
|  | 
 | ||||||
|  |         final Markwon markwon = Markwon.builder(this) | ||||||
|  |                 .usePlugin(ImagesPlugin.create()) | ||||||
|  |                 .usePlugin(HtmlPlugin.create()) | ||||||
|  |                 .usePlugin(new IFrameHtmlPlugin()) | ||||||
|  |                 .build(); | ||||||
|  | 
 | ||||||
|  |         markwon.setMarkdown(textView, md); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     private void emptyTagReplacement() { | ||||||
|  | 
 | ||||||
|  |         final String md = "" + | ||||||
|  |                 "<empty></empty> the `<empty></empty>` is replaced?"; | ||||||
|  | 
 | ||||||
|  |         final Markwon markwon = Markwon.builder(this) | ||||||
|  |                 .usePlugin(HtmlPlugin.create(plugin -> { | ||||||
|  |                     plugin.emptyTagReplacement(new HtmlEmptyTagReplacement() { | ||||||
|  |                         @Nullable | ||||||
|  |                         @Override | ||||||
|  |                         public String replace(@NonNull HtmlTag tag) { | ||||||
|  |                             if ("empty".equals(tag.name())) { | ||||||
|  |                                 return "REPLACED_EMPTY_WITH_IT"; | ||||||
|  |                             } | ||||||
|  |                             return super.replace(tag); | ||||||
|  |                         } | ||||||
|  |                     }); | ||||||
|  |                 })) | ||||||
|  |                 .build(); | ||||||
|  | 
 | ||||||
|  |         markwon.setMarkdown(textView, md); | ||||||
|  |     } | ||||||
| } | } | ||||||
|  | |||||||
| @ -0,0 +1,38 @@ | |||||||
|  | package io.noties.markwon.sample.html; | ||||||
|  | 
 | ||||||
|  | import android.os.Build; | ||||||
|  | 
 | ||||||
|  | import androidx.annotation.NonNull; | ||||||
|  | import androidx.annotation.RequiresApi; | ||||||
|  | 
 | ||||||
|  | import java.util.Collection; | ||||||
|  | import java.util.Collections; | ||||||
|  | 
 | ||||||
|  | import io.noties.markwon.MarkwonVisitor; | ||||||
|  | import io.noties.markwon.SpannableBuilder; | ||||||
|  | import io.noties.markwon.html.HtmlTag; | ||||||
|  | import io.noties.markwon.html.MarkwonHtmlRenderer; | ||||||
|  | import io.noties.markwon.html.TagHandler; | ||||||
|  | 
 | ||||||
|  | @RequiresApi(Build.VERSION_CODES.KITKAT) | ||||||
|  | public class HtmlElegantUnderlineTagHandler extends TagHandler { | ||||||
|  | 
 | ||||||
|  |     @Override | ||||||
|  |     public void handle(@NonNull MarkwonVisitor visitor, @NonNull MarkwonHtmlRenderer renderer, @NonNull HtmlTag tag) { | ||||||
|  |         if (tag.isBlock()) { | ||||||
|  |             visitChildren(visitor, renderer, tag.getAsBlock()); | ||||||
|  |         } | ||||||
|  |         SpannableBuilder.setSpans( | ||||||
|  |                 visitor.builder(), | ||||||
|  |                 ElegantUnderlineSpan.create(), | ||||||
|  |                 tag.start(), | ||||||
|  |                 tag.end() | ||||||
|  |         ); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     @NonNull | ||||||
|  |     @Override | ||||||
|  |     public Collection<String> supportedTags() { | ||||||
|  |         return Collections.singleton("u"); | ||||||
|  |     } | ||||||
|  | } | ||||||
| @ -0,0 +1,42 @@ | |||||||
|  | package io.noties.markwon.sample.html; | ||||||
|  | 
 | ||||||
|  | import android.text.TextUtils; | ||||||
|  | import android.text.style.TypefaceSpan; | ||||||
|  | 
 | ||||||
|  | import androidx.annotation.NonNull; | ||||||
|  | 
 | ||||||
|  | import java.util.Collection; | ||||||
|  | import java.util.Collections; | ||||||
|  | 
 | ||||||
|  | import io.noties.markwon.MarkwonVisitor; | ||||||
|  | import io.noties.markwon.SpannableBuilder; | ||||||
|  | import io.noties.markwon.html.HtmlTag; | ||||||
|  | import io.noties.markwon.html.MarkwonHtmlRenderer; | ||||||
|  | import io.noties.markwon.html.TagHandler; | ||||||
|  | 
 | ||||||
|  | public class HtmlFontTagHandler extends TagHandler { | ||||||
|  | 
 | ||||||
|  |     @Override | ||||||
|  |     public void handle(@NonNull MarkwonVisitor visitor, @NonNull MarkwonHtmlRenderer renderer, @NonNull HtmlTag tag) { | ||||||
|  | 
 | ||||||
|  |         if (tag.isBlock()) { | ||||||
|  |             visitChildren(visitor, renderer, tag.getAsBlock()); | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         final String font = tag.attributes().get("name"); | ||||||
|  |         if (!TextUtils.isEmpty(font)) { | ||||||
|  |             SpannableBuilder.setSpans( | ||||||
|  |                     visitor.builder(), | ||||||
|  |                     new TypefaceSpan(font), | ||||||
|  |                     tag.start(), | ||||||
|  |                     tag.end() | ||||||
|  |             ); | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     @NonNull | ||||||
|  |     @Override | ||||||
|  |     public Collection<String> supportedTags() { | ||||||
|  |         return Collections.singleton("font"); | ||||||
|  |     } | ||||||
|  | } | ||||||
| @ -0,0 +1,48 @@ | |||||||
|  | package io.noties.markwon.sample.html; | ||||||
|  | 
 | ||||||
|  | import androidx.annotation.NonNull; | ||||||
|  | import androidx.annotation.Nullable; | ||||||
|  | 
 | ||||||
|  | import org.commonmark.node.Image; | ||||||
|  | 
 | ||||||
|  | import java.util.Collection; | ||||||
|  | import java.util.Collections; | ||||||
|  | 
 | ||||||
|  | import io.noties.debug.Debug; | ||||||
|  | import io.noties.markwon.AbstractMarkwonPlugin; | ||||||
|  | import io.noties.markwon.MarkwonConfiguration; | ||||||
|  | import io.noties.markwon.RenderProps; | ||||||
|  | import io.noties.markwon.html.HtmlPlugin; | ||||||
|  | import io.noties.markwon.html.HtmlTag; | ||||||
|  | import io.noties.markwon.html.tag.SimpleTagHandler; | ||||||
|  | import io.noties.markwon.image.ImageProps; | ||||||
|  | import io.noties.markwon.image.ImageSize; | ||||||
|  | 
 | ||||||
|  | public class IFrameHtmlPlugin extends AbstractMarkwonPlugin { | ||||||
|  |     @Override | ||||||
|  |     public void configure(@NonNull Registry registry) { | ||||||
|  |         registry.require(HtmlPlugin.class, htmlPlugin -> { | ||||||
|  |             // TODO: empty tag replacement | ||||||
|  |             htmlPlugin.addHandler(new EmbedTagHandler()); | ||||||
|  |         }); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     private static class EmbedTagHandler extends SimpleTagHandler { | ||||||
|  | 
 | ||||||
|  |         @Nullable | ||||||
|  |         @Override | ||||||
|  |         public Object getSpans(@NonNull MarkwonConfiguration configuration, @NonNull RenderProps renderProps, @NonNull HtmlTag tag) { | ||||||
|  |             final ImageSize imageSize = new ImageSize(new ImageSize.Dimension(640, "px"), new ImageSize.Dimension(480, "px")); | ||||||
|  |             ImageProps.IMAGE_SIZE.set(renderProps, imageSize); | ||||||
|  |             ImageProps.DESTINATION.set(renderProps, "https://hey.com/1.png"); | ||||||
|  |             return configuration.spansFactory().require(Image.class) | ||||||
|  |                     .getSpans(configuration, renderProps); | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         @NonNull | ||||||
|  |         @Override | ||||||
|  |         public Collection<String> supportedTags() { | ||||||
|  |             return Collections.singleton("iframe"); | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | } | ||||||
| @ -6,10 +6,14 @@ import android.widget.TextView; | |||||||
| import androidx.annotation.NonNull; | import androidx.annotation.NonNull; | ||||||
| import androidx.annotation.Nullable; | import androidx.annotation.Nullable; | ||||||
| 
 | 
 | ||||||
|  | import org.commonmark.internal.inline.AsteriskDelimiterProcessor; | ||||||
|  | import org.commonmark.internal.inline.UnderscoreDelimiterProcessor; | ||||||
| import org.commonmark.node.Block; | import org.commonmark.node.Block; | ||||||
| import org.commonmark.node.BlockQuote; | import org.commonmark.node.BlockQuote; | ||||||
|  | import org.commonmark.node.FencedCodeBlock; | ||||||
| import org.commonmark.node.Heading; | import org.commonmark.node.Heading; | ||||||
| import org.commonmark.node.HtmlBlock; | import org.commonmark.node.HtmlBlock; | ||||||
|  | import org.commonmark.node.IndentedCodeBlock; | ||||||
| import org.commonmark.node.ListBlock; | import org.commonmark.node.ListBlock; | ||||||
| import org.commonmark.node.ThematicBreak; | import org.commonmark.node.ThematicBreak; | ||||||
| import org.commonmark.parser.InlineParserFactory; | import org.commonmark.parser.InlineParserFactory; | ||||||
| @ -22,7 +26,9 @@ import java.util.Set; | |||||||
| import io.noties.markwon.AbstractMarkwonPlugin; | import io.noties.markwon.AbstractMarkwonPlugin; | ||||||
| import io.noties.markwon.Markwon; | import io.noties.markwon.Markwon; | ||||||
| import io.noties.markwon.inlineparser.BackticksInlineProcessor; | import io.noties.markwon.inlineparser.BackticksInlineProcessor; | ||||||
|  | import io.noties.markwon.inlineparser.BangInlineProcessor; | ||||||
| import io.noties.markwon.inlineparser.CloseBracketInlineProcessor; | import io.noties.markwon.inlineparser.CloseBracketInlineProcessor; | ||||||
|  | import io.noties.markwon.inlineparser.HtmlInlineProcessor; | ||||||
| import io.noties.markwon.inlineparser.MarkwonInlineParser; | import io.noties.markwon.inlineparser.MarkwonInlineParser; | ||||||
| import io.noties.markwon.inlineparser.MarkwonInlineParserPlugin; | import io.noties.markwon.inlineparser.MarkwonInlineParserPlugin; | ||||||
| import io.noties.markwon.inlineparser.OpenBracketInlineProcessor; | import io.noties.markwon.inlineparser.OpenBracketInlineProcessor; | ||||||
| @ -41,7 +47,9 @@ public class InlineParserActivity extends ActivityWithMenuOptions { | |||||||
|                 .add("links_only", this::links_only) |                 .add("links_only", this::links_only) | ||||||
|                 .add("disable_code", this::disable_code) |                 .add("disable_code", this::disable_code) | ||||||
|                 .add("pluginWithDefaults", this::pluginWithDefaults) |                 .add("pluginWithDefaults", this::pluginWithDefaults) | ||||||
|                 .add("pluginNoDefaults", this::pluginNoDefaults); |                 .add("pluginNoDefaults", this::pluginNoDefaults) | ||||||
|  |                 .add("disableHtmlInlineParser", this::disableHtmlInlineParser) | ||||||
|  |                 .add("disableHtmlSanitize", this::disableHtmlSanitize); | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     @Override |     @Override | ||||||
| @ -173,4 +181,67 @@ public class InlineParserActivity extends ActivityWithMenuOptions { | |||||||
|         markwon.setMarkdown(textView, md); |         markwon.setMarkdown(textView, md); | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|  |     private void disableHtmlInlineParser() { | ||||||
|  |         final String md = "# Html <b>disabled</b>\n\n" + | ||||||
|  |                 "<em>emphasis <strong>strong</strong>\n\n" + | ||||||
|  |                 "<p>paragraph <img src='hey.jpg' /></p>\n\n" + | ||||||
|  |                 "<test></test>\n\n" + | ||||||
|  |                 "<test>"; | ||||||
|  | 
 | ||||||
|  |         final Markwon markwon = Markwon.builder(this) | ||||||
|  |                 .usePlugin(MarkwonInlineParserPlugin.create()) | ||||||
|  |                 .usePlugin(new AbstractMarkwonPlugin() { | ||||||
|  |                     @Override | ||||||
|  |                     public void configure(@NonNull Registry registry) { | ||||||
|  |                         // NB! `AsteriskDelimiterProcessor` and `UnderscoreDelimiterProcessor` | ||||||
|  |                         //  handles both emphasis and strong-emphasis nodes | ||||||
|  |                         registry.require(MarkwonInlineParserPlugin.class, plugin -> { | ||||||
|  |                             plugin.factoryBuilder() | ||||||
|  |                                     .excludeInlineProcessor(HtmlInlineProcessor.class) | ||||||
|  |                                     .excludeInlineProcessor(BangInlineProcessor.class) | ||||||
|  |                                     .excludeInlineProcessor(OpenBracketInlineProcessor.class) | ||||||
|  |                                     .excludeDelimiterProcessor(AsteriskDelimiterProcessor.class) | ||||||
|  |                                     .excludeDelimiterProcessor(UnderscoreDelimiterProcessor.class); | ||||||
|  |                         }); | ||||||
|  |                     } | ||||||
|  | 
 | ||||||
|  |                     @Override | ||||||
|  |                     public void configureParser(@NonNull Parser.Builder builder) { | ||||||
|  |                         builder.enabledBlockTypes(new HashSet<>(Arrays.asList( | ||||||
|  |                                 Heading.class, | ||||||
|  | //                        HtmlBlock.class, | ||||||
|  |                                 ThematicBreak.class, | ||||||
|  |                                 FencedCodeBlock.class, | ||||||
|  |                                 IndentedCodeBlock.class, | ||||||
|  |                                 BlockQuote.class, | ||||||
|  |                                 ListBlock.class | ||||||
|  |                         ))); | ||||||
|  |                     } | ||||||
|  |                 }) | ||||||
|  |                 .build(); | ||||||
|  | 
 | ||||||
|  |         markwon.setMarkdown(textView, md); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     private void disableHtmlSanitize() { | ||||||
|  |         final String md = "# Html <b>disabled</b>\n\n" + | ||||||
|  |                 "<em>emphasis <strong>strong</strong>\n\n" + | ||||||
|  |                 "<p>paragraph <img src='hey.jpg' /></p>\n\n" + | ||||||
|  |                 "<test></test>\n\n" + | ||||||
|  |                 "<test>"; | ||||||
|  | 
 | ||||||
|  |         final Markwon markwon = Markwon.builder(this) | ||||||
|  |                 .usePlugin(new AbstractMarkwonPlugin() { | ||||||
|  |                     @NonNull | ||||||
|  |                     @Override | ||||||
|  |                     public String processMarkdown(@NonNull String markdown) { | ||||||
|  |                         return markdown | ||||||
|  |                                 .replaceAll("<", "<") | ||||||
|  |                                 .replaceAll(">", ">"); | ||||||
|  |                     } | ||||||
|  |                 }) | ||||||
|  |                 .build(); | ||||||
|  | 
 | ||||||
|  |         markwon.setMarkdown(textView, md); | ||||||
|  |     } | ||||||
| } | } | ||||||
|  | |||||||
| @ -68,7 +68,8 @@ public class LatexActivity extends ActivityWithMenuOptions { | |||||||
|                 .add("textColor", this::textColor) |                 .add("textColor", this::textColor) | ||||||
|                 .add("defaultTextColor", this::defaultTextColor) |                 .add("defaultTextColor", this::defaultTextColor) | ||||||
|                 .add("inlineAndBlock", this::inlineAndBlock) |                 .add("inlineAndBlock", this::inlineAndBlock) | ||||||
|                 .add("dark", this::dark); |                 .add("dark", this::dark) | ||||||
|  |                 .add("omega", this::omega); | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     @Override |     @Override | ||||||
| @ -221,6 +222,18 @@ public class LatexActivity extends ActivityWithMenuOptions { | |||||||
|         renderWithBlocksAndInlines(md); |         renderWithBlocksAndInlines(md); | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|  |     private void omega() { | ||||||
|  |         final String md = "" + | ||||||
|  |                 "# Block\n\n" + | ||||||
|  |                 "$$\n" + | ||||||
|  |                 "\\Omega\n" + | ||||||
|  |                 "$$\n\n" + | ||||||
|  |                 "# Inline\n\n" + | ||||||
|  |                 "$$\\Omega$$"; | ||||||
|  | 
 | ||||||
|  |         renderWithBlocksAndInlines(md); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|     @NonNull |     @NonNull | ||||||
|     private static String wrapLatexInSampleMarkdown(@NonNull String latex) { |     private static String wrapLatexInSampleMarkdown(@NonNull String latex) { | ||||||
|         return "" + |         return "" + | ||||||
|  | |||||||
| @ -34,6 +34,8 @@ import io.noties.markwon.MarkwonVisitor; | |||||||
| import io.noties.markwon.core.CorePlugin; | import io.noties.markwon.core.CorePlugin; | ||||||
| import io.noties.markwon.html.HtmlPlugin; | import io.noties.markwon.html.HtmlPlugin; | ||||||
| import io.noties.markwon.image.ImagesPlugin; | import io.noties.markwon.image.ImagesPlugin; | ||||||
|  | import io.noties.markwon.image.destination.ImageDestinationProcessor; | ||||||
|  | import io.noties.markwon.image.destination.ImageDestinationProcessorRelativeToAbsolute; | ||||||
| import io.noties.markwon.image.file.FileSchemeHandler; | import io.noties.markwon.image.file.FileSchemeHandler; | ||||||
| import io.noties.markwon.image.network.OkHttpNetworkSchemeHandler; | import io.noties.markwon.image.network.OkHttpNetworkSchemeHandler; | ||||||
| import io.noties.markwon.image.svg.SvgMediaDecoder; | import io.noties.markwon.image.svg.SvgMediaDecoder; | ||||||
| @ -42,8 +44,6 @@ import io.noties.markwon.recycler.SimpleEntry; | |||||||
| import io.noties.markwon.recycler.table.TableEntry; | import io.noties.markwon.recycler.table.TableEntry; | ||||||
| import io.noties.markwon.recycler.table.TableEntryPlugin; | import io.noties.markwon.recycler.table.TableEntryPlugin; | ||||||
| import io.noties.markwon.sample.R; | import io.noties.markwon.sample.R; | ||||||
| import io.noties.markwon.urlprocessor.UrlProcessor; |  | ||||||
| import io.noties.markwon.urlprocessor.UrlProcessorRelativeToAbsolute; |  | ||||||
| 
 | 
 | ||||||
| public class RecyclerActivity extends Activity { | public class RecyclerActivity extends Activity { | ||||||
| 
 | 
 | ||||||
| @ -100,7 +100,7 @@ public class RecyclerActivity extends Activity { | |||||||
|                 .usePlugin(new AbstractMarkwonPlugin() { |                 .usePlugin(new AbstractMarkwonPlugin() { | ||||||
|                     @Override |                     @Override | ||||||
|                     public void configureConfiguration(@NonNull MarkwonConfiguration.Builder builder) { |                     public void configureConfiguration(@NonNull MarkwonConfiguration.Builder builder) { | ||||||
|                         builder.urlProcessor(new UrlProcessorInitialReadme()); |                         builder.imageDestinationProcessor(new ImageDestinationProcessorInitialReadme()); | ||||||
|                     } |                     } | ||||||
| 
 | 
 | ||||||
|                     @Override |                     @Override | ||||||
| @ -182,12 +182,12 @@ public class RecyclerActivity extends Activity { | |||||||
|         return out; |         return out; | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     private static class UrlProcessorInitialReadme implements UrlProcessor { |     private static class ImageDestinationProcessorInitialReadme extends ImageDestinationProcessor { | ||||||
| 
 | 
 | ||||||
|         private static final String GITHUB_BASE = "https://github.com/noties/Markwon/raw/master/"; |         private static final String GITHUB_BASE = "https://github.com/noties/Markwon/raw/master/"; | ||||||
| 
 | 
 | ||||||
|         private final UrlProcessorRelativeToAbsolute processor |         private final ImageDestinationProcessorRelativeToAbsolute processor | ||||||
|                 = new UrlProcessorRelativeToAbsolute(GITHUB_BASE); |                 = new ImageDestinationProcessorRelativeToAbsolute(GITHUB_BASE); | ||||||
| 
 | 
 | ||||||
|         @NonNull |         @NonNull | ||||||
|         @Override |         @Override | ||||||
|  | |||||||
| @ -7,10 +7,11 @@ import android.widget.TextView; | |||||||
| import androidx.annotation.NonNull; | import androidx.annotation.NonNull; | ||||||
| import androidx.annotation.Nullable; | import androidx.annotation.Nullable; | ||||||
| 
 | 
 | ||||||
| import io.noties.debug.Debug; |  | ||||||
| import io.noties.markwon.Markwon; | import io.noties.markwon.Markwon; | ||||||
|  | import io.noties.markwon.ext.latex.JLatexMathPlugin; | ||||||
| import io.noties.markwon.ext.tables.TablePlugin; | import io.noties.markwon.ext.tables.TablePlugin; | ||||||
| import io.noties.markwon.ext.tables.TableTheme; | import io.noties.markwon.image.ImagesPlugin; | ||||||
|  | import io.noties.markwon.inlineparser.MarkwonInlineParserPlugin; | ||||||
| import io.noties.markwon.linkify.LinkifyPlugin; | import io.noties.markwon.linkify.LinkifyPlugin; | ||||||
| import io.noties.markwon.sample.ActivityWithMenuOptions; | import io.noties.markwon.sample.ActivityWithMenuOptions; | ||||||
| import io.noties.markwon.sample.MenuOptions; | import io.noties.markwon.sample.MenuOptions; | ||||||
| @ -25,7 +26,9 @@ public class TableActivity extends ActivityWithMenuOptions { | |||||||
|     public MenuOptions menuOptions() { |     public MenuOptions menuOptions() { | ||||||
|         return MenuOptions.create() |         return MenuOptions.create() | ||||||
|                 .add("customize", this::customize) |                 .add("customize", this::customize) | ||||||
|                 .add("tableAndLinkify", this::tableAndLinkify); |                 .add("tableAndLinkify", this::tableAndLinkify) | ||||||
|  |                 .add("withImages", this::withImages) | ||||||
|  |                 .add("withLatex", this::withLatex); | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     private TextView textView; |     private TextView textView; | ||||||
| @ -86,4 +89,47 @@ public class TableActivity extends ActivityWithMenuOptions { | |||||||
| 
 | 
 | ||||||
|         markwon.setMarkdown(textView, md); |         markwon.setMarkdown(textView, md); | ||||||
|     } |     } | ||||||
|  | 
 | ||||||
|  |     private void withImages() { | ||||||
|  | 
 | ||||||
|  |         final String md = "" + | ||||||
|  |                 "| HEADER | HEADER |\n" + | ||||||
|  |                 "|:----:|:----:|\n" + | ||||||
|  |                 "|  | Build |\n" + | ||||||
|  |                 "| Stable |  |\n" + | ||||||
|  |                 "| BIG |  |\n" + | ||||||
|  |                 "\n"; | ||||||
|  | 
 | ||||||
|  |         final Markwon markwon = Markwon.builder(this) | ||||||
|  |                 .usePlugin(ImagesPlugin.create()) | ||||||
|  |                 .usePlugin(TablePlugin.create(this)) | ||||||
|  |                 .build(); | ||||||
|  | 
 | ||||||
|  |         markwon.setMarkdown(textView, md); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     private void withLatex() { | ||||||
|  | 
 | ||||||
|  |         String latex = "\\begin{array}{cc}"; | ||||||
|  |         latex += "\\fbox{\\text{A framed box with \\textdbend}}&\\shadowbox{\\text{A shadowed box}}\\cr"; | ||||||
|  |         latex += "\\doublebox{\\text{A double framed box}}&\\ovalbox{\\text{An oval framed box}}\\cr"; | ||||||
|  |         latex += "\\end{array}"; | ||||||
|  | 
 | ||||||
|  |         final String md = "" + | ||||||
|  |                 "| HEADER | HEADER |\n" + | ||||||
|  |                 "|:----:|:----:|\n" + | ||||||
|  |                 "|  | Build |\n" + | ||||||
|  |                 "| Stable |  |\n" + | ||||||
|  |                 "| BIG | $$" + latex + "$$ |\n" + | ||||||
|  |                 "\n"; | ||||||
|  | 
 | ||||||
|  |         final Markwon markwon = Markwon.builder(this) | ||||||
|  |                 .usePlugin(MarkwonInlineParserPlugin.create()) | ||||||
|  |                 .usePlugin(ImagesPlugin.create()) | ||||||
|  |                 .usePlugin(JLatexMathPlugin.create(textView.getTextSize(), builder -> builder.inlinesEnabled(true))) | ||||||
|  |                 .usePlugin(TablePlugin.create(this)) | ||||||
|  |                 .build(); | ||||||
|  | 
 | ||||||
|  |         markwon.setMarkdown(textView, md); | ||||||
|  |     } | ||||||
| } | } | ||||||
|  | |||||||
| @ -14,6 +14,7 @@ | |||||||
|         android:textAppearance="?android:attr/textAppearanceMedium" |         android:textAppearance="?android:attr/textAppearanceMedium" | ||||||
|         android:textColor="#000" |         android:textColor="#000" | ||||||
|         android:textSize="16sp" |         android:textSize="16sp" | ||||||
|  |         android:padding="8dip" | ||||||
|         tools:text="whatever" /> |         tools:text="whatever" /> | ||||||
| 
 | 
 | ||||||
| </ScrollView> | </ScrollView> | ||||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user
	 Dimitry
						Dimitry