diff --git a/README.md b/README.md
index 075186b0..ddfd65dd 100644
--- a/README.md
+++ b/README.md
@@ -97,15 +97,6 @@ Please visit [documentation] web-site for reference
[documentation]: https://noties.github.io/Markwon
----
-
-## Applications using Markwon
-
-* [Partiko](https://partiko.app)
-* [FairNote Notepad](https://play.google.com/store/apps/details?id=com.rgiskard.fairnote)
-* [Boxcryptor](https://www.boxcryptor.com)
-
-
---
# Demo
diff --git a/_CHANGES.md b/_CHANGES.md
index 0c8ea7de..e20c7985 100644
--- a/_CHANGES.md
+++ b/_CHANGES.md
@@ -9,4 +9,5 @@
* images-plugin moved to standalone again
* removed MarkwonPlugin#configureHtmlRenderer -> now part of HtmlPlugin
* TagHandler now has `supportedTags()` method
-* html is moved completely to html-plugin
\ No newline at end of file
+* html is moved completely to html-plugin
+* OnTextAddedListener
\ No newline at end of file
diff --git a/docs/.vuepress/.artifacts.js b/docs/.vuepress/.artifacts.js
index 2a1c3f43..2f41fc13 100644
--- a/docs/.vuepress/.artifacts.js
+++ b/docs/.vuepress/.artifacts.js
@@ -1,4 +1,4 @@
// this is a generated file, do not modify. To update it run 'collectArtifacts.js' script
-const artifacts = [{"id":"core","name":"Core","group":"ru.noties.markwon","description":"Core Markwon artifact that includes basic markdown parsing and rendering"},{"id":"ext-latex","name":"LaTeX","group":"ru.noties.markwon","description":"Extension to add LaTeX formulas to Markwon markdown"},{"id":"ext-strikethrough","name":"Strikethrough","group":"ru.noties.markwon","description":"Extension to add strikethrough markup to Markwon markdown"},{"id":"ext-tables","name":"Tables","group":"ru.noties.markwon","description":"Extension to add tables markup (GFM) to Markwon markdown"},{"id":"ext-tasklist","name":"Task List","group":"ru.noties.markwon","description":"Extension to add task lists (GFM) to Markwon markdown"},{"id":"html","name":"HTML","group":"ru.noties.markwon","description":"Provides HTML parsing functionality"},{"id":"image-gif","name":"Image GIF","group":"ru.noties.markwon","description":"Adds GIF media support to Markwon markdown"},{"id":"image-okhttp","name":"Image OkHttp","group":"ru.noties.markwon","description":"Adds OkHttp client to retrieve images data from network"},{"id":"image-svg","name":"Image SVG","group":"ru.noties.markwon","description":"Adds SVG media support to Markwon markdown"},{"id":"recycler","name":"Recycler","group":"ru.noties.markwon","description":"Provides RecyclerView.Adapter to display Markwon markdown"},{"id":"recycler-table","name":"Recycler Table","group":"ru.noties.markwon","description":"Provides MarkwonAdapter.Entry to render TableBlocks inside Android-native TableLayout widget"},{"id":"syntax-highlight","name":"Syntax Highlight","group":"ru.noties.markwon","description":"Add syntax highlight to Markwon markdown via Prism4j library"}];
+const artifacts = [{"id":"core","name":"Core","group":"io.noties.markwon","description":"Core Markwon artifact that includes basic markdown parsing and rendering"},{"id":"ext-latex","name":"LaTeX","group":"io.noties.markwon","description":"Extension to add LaTeX formulas to Markwon markdown"},{"id":"ext-strikethrough","name":"Strikethrough","group":"io.noties.markwon","description":"Extension to add strikethrough markup to Markwon markdown"},{"id":"ext-tables","name":"Tables","group":"io.noties.markwon","description":"Extension to add tables markup (GFM) to Markwon markdown"},{"id":"ext-tasklist","name":"Task List","group":"io.noties.markwon","description":"Extension to add task lists (GFM) to Markwon markdown"},{"id":"html","name":"HTML","group":"io.noties.markwon","description":"Provides HTML parsing functionality"},{"id":"image","name":"Image","group":"io.noties.markwon","description":"Markwon image loading module (with optional GIF and SVG support)"},{"id":"image-gif","name":"Image GIF","group":"io.noties.markwon","description":"Adds GIF media support to Markwon markdown"},{"id":"image-glide","name":"Image Glide","group":"io.noties.markwon","description":"Markwon image loading module (based on Glide library)"},{"id":"image-okhttp","name":"Image OkHttp","group":"io.noties.markwon","description":"Adds OkHttp client to retrieve images data from network"},{"id":"image-picasso","name":"Image Picasso","group":"io.noties.markwon","description":"Markwon image loading module (based on Picasso library)"},{"id":"image-svg","name":"Image SVG","group":"io.noties.markwon","description":"Adds SVG media support to Markwon markdown"},{"id":"linkify","name":"Linkify","group":"io.noties.markwon","description":"Markwon plugin to linkify text (based on Android Linkify)"},{"id":"recycler","name":"Recycler","group":"io.noties.markwon","description":"Provides RecyclerView.Adapter to display Markwon markdown"},{"id":"recycler-table","name":"Recycler Table","group":"io.noties.markwon","description":"Provides MarkwonAdapter.Entry to render TableBlocks inside Android-native TableLayout widget"},{"id":"syntax-highlight","name":"Syntax Highlight","group":"io.noties.markwon","description":"Add syntax highlight to Markwon markdown via Prism4j library"}];
export { artifacts };
diff --git a/docs/.vuepress/.artifacts.v3.js b/docs/.vuepress/.artifacts.v3.js
new file mode 100644
index 00000000..2a1c3f43
--- /dev/null
+++ b/docs/.vuepress/.artifacts.v3.js
@@ -0,0 +1,4 @@
+
+// this is a generated file, do not modify. To update it run 'collectArtifacts.js' script
+const artifacts = [{"id":"core","name":"Core","group":"ru.noties.markwon","description":"Core Markwon artifact that includes basic markdown parsing and rendering"},{"id":"ext-latex","name":"LaTeX","group":"ru.noties.markwon","description":"Extension to add LaTeX formulas to Markwon markdown"},{"id":"ext-strikethrough","name":"Strikethrough","group":"ru.noties.markwon","description":"Extension to add strikethrough markup to Markwon markdown"},{"id":"ext-tables","name":"Tables","group":"ru.noties.markwon","description":"Extension to add tables markup (GFM) to Markwon markdown"},{"id":"ext-tasklist","name":"Task List","group":"ru.noties.markwon","description":"Extension to add task lists (GFM) to Markwon markdown"},{"id":"html","name":"HTML","group":"ru.noties.markwon","description":"Provides HTML parsing functionality"},{"id":"image-gif","name":"Image GIF","group":"ru.noties.markwon","description":"Adds GIF media support to Markwon markdown"},{"id":"image-okhttp","name":"Image OkHttp","group":"ru.noties.markwon","description":"Adds OkHttp client to retrieve images data from network"},{"id":"image-svg","name":"Image SVG","group":"ru.noties.markwon","description":"Adds SVG media support to Markwon markdown"},{"id":"recycler","name":"Recycler","group":"ru.noties.markwon","description":"Provides RecyclerView.Adapter to display Markwon markdown"},{"id":"recycler-table","name":"Recycler Table","group":"ru.noties.markwon","description":"Provides MarkwonAdapter.Entry to render TableBlocks inside Android-native TableLayout widget"},{"id":"syntax-highlight","name":"Syntax Highlight","group":"ru.noties.markwon","description":"Add syntax highlight to Markwon markdown via Prism4j library"}];
+export { artifacts };
diff --git a/docs/.vuepress/components/ArtifactPicker.vue b/docs/.vuepress/components/ArtifactPicker.vue
index 453b1cd2..1c5a9ca8 100644
--- a/docs/.vuepress/components/ArtifactPicker.vue
+++ b/docs/.vuepress/components/ArtifactPicker.vue
@@ -29,7 +29,7 @@
+
+
\ No newline at end of file
diff --git a/docs/.vuepress/config.js b/docs/.vuepress/config.js
index 8de47eef..91f11d14 100644
--- a/docs/.vuepress/config.js
+++ b/docs/.vuepress/config.js
@@ -18,10 +18,10 @@ module.exports = {
text: 'API Version',
items: [
{ text: 'Current (3.x.x)', link: '/' },
+ { text: 'Beta (4.x.x)', link: '/docs/v4/install.md' },
{ text: 'Legacy (2.x.x)', link: '/docs/v2/' }
]
},
- { text: 'Sandbox', link: '/sandbox.md' },
{ text: 'Github', link: 'https://github.com/noties/Markwon' }
],
sidebar: {
@@ -36,6 +36,27 @@ module.exports = {
'/docs/v2/html.md',
'/docs/v2/view.md'
],
+ '/docs/v4': [
+ '/docs/v4/install.md',
+ {
+ title: 'Core',
+ collapsable: false,
+ children: [
+ '/docs/v4/core/getting-started.md',
+ '/docs/v4/core/plugins.md',
+ '/docs/v4/core/registry.md',
+ '/docs/v4/core/theme.md',
+ '/docs/v4/core/configuration.md',
+ '/docs/v4/core/visitor.md',
+ '/docs/v4/core/spans-factory.md',
+ '/docs/v4/core/core-plugin.md',
+ '/docs/v4/core/movement-method-plugin.md',
+ '/docs/v4/core/render-props.md'
+ ]
+ },
+ '/docs/v4/recipes.md',
+ '/docs/v4/migration-3-4.md'
+ ],
'/': [
'',
{
diff --git a/docs/docs/v4/core/configuration.md b/docs/docs/v4/core/configuration.md
new file mode 100644
index 00000000..4c4ad645
--- /dev/null
+++ b/docs/docs/v4/core/configuration.md
@@ -0,0 +1,173 @@
+# Configuration
+
+`MarkwonConfiguration` class holds common Markwon functionality.
+These are _configurable_ properties:
+* `AsyncDrawableLoader` (back here since )
+* `SyntaxHighlight`
+* `LinkSpan.Resolver`
+* `UrlProcessor`
+* `ImageSizeResolver`
+
+:::tip
+Additionally `MarkwonConfiguration` holds:
+* `MarkwonTheme`
+* `MarkwonSpansFactory`
+
+Please note that these values can be retrieved from `MarkwonConfiguration`
+instance, but their _configuration_ must be done by a `Plugin` by overriding
+one of the methods:
+* `Plugin#configureTheme`
+* `Plugin#configureSpansFactory`
+:::
+
+## AsyncDrawableLoader
+
+Allows loading and displaying of images in markdown. Please note that if one is not specified
+directly (or via plugin) no images will be displayed.
+
+```java
+final Markwon markwon = Markwon.builder(context)
+ .usePlugin(new AbstractMarkwonPlugin() {
+ @Override
+ public void configureConfiguration(@NonNull MarkwonConfiguration.Builder builder) {
+ builder.asyncDrawableLoader(AsyncDrawableLoader.noOp());
+ }
+ })
+ .build();
+```
+
+Currently `Markwon` provides 3 implementations for loading images:
+* [own implementation](/docs/v4/image.md) with SVG, GIF, data uri and android_assets support
+* [based on Picasso](/docs/v4/image-picasso.md)
+* [based on Glide](/docs/v4/image-glide.md)
+
+## SyntaxHighlight
+
+```java
+final Markwon markwon = Markwon.builder(this)
+ .usePlugin(new AbstractMarkwonPlugin() {
+ @Override
+ public void configureConfiguration(@NonNull MarkwonConfiguration.Builder builder) {
+ builder.syntaxHighlight(new SyntaxHighlightNoOp());
+ }
+ })
+ .build();
+```
+
+:::tip
+Use [syntax-highlight](/docs/v4/syntax-highlight/) to add syntax highlighting
+to your application
+:::
+
+## LinkSpan.Resolver
+
+React to a link click event. By default `LinkResolverDef` is used,
+which tries to start an Activity given the `link` argument. If no
+Activity can handle `link` `LinkResolverDef` silently ignores click event
+
+```java
+final Markwon markwon = Markwon.builder(this)
+ .usePlugin(new AbstractMarkwonPlugin() {
+ @Override
+ public void configureConfiguration(@NonNull MarkwonConfiguration.Builder builder) {
+ builder.linkResolver(new LinkSpan.Resolver() {
+ @Override
+ public void resolve(View view, @NonNull String link) {
+ // react to link click here
+ }
+ });
+ }
+ })
+ .build();
+```
+
+:::tip
+Please note that `Markwon` will apply `LinkMovementMethod` to a resulting TextView
+if there is none registered. if you wish to register own instance of a `MovementMethod`
+apply it directly to a TextView or use [MovementMethodPlugin](/docs/v4/core/movement-method-plugin.md)
+:::
+
+## UrlProcessor
+
+Process URLs in your markdown (for links and images). If not provided explicitly,
+default **no-op** implementation will be used, which does not modify URLs (keeping them as-is).
+
+`Markwon` provides 2 implementations of `UrlProcessor`:
+* `UrlProcessorRelativeToAbsolute`
+* `UrlProcessorAndroidAssets`
+
+### UrlProcessorRelativeToAbsolute
+
+`UrlProcessorRelativeToAbsolute` can be used to make relative URL absolute. For example if an image is
+defined like this: `` and `UrlProcessorRelativeToAbsolute`
+is created with `https://github.com/noties/Markwon/raw/master/` as the base:
+`new UrlProcessorRelativeToAbsolute("https://github.com/noties/Markwon/raw/master/")`,
+then final image will have `https://github.com/noties/Markwon/raw/master/art/image.JPG`
+as the destination.
+
+### UrlProcessorAndroidAssets
+
+`UrlProcessorAndroidAssets` can be used to make processed links to point to Android assets folder.
+So an image: `` will have `file:///android_asset/art/image.JPG` as the
+destination.
+
+:::tip
+Please note that `UrlProcessorAndroidAssets` will process only URLs that have no `scheme` information,
+so a `./art/image.png` will become `file:///android_asset/art/image.JPG` whilst `https://so.me/where.png`
+will be kept as-is.
+:::
+
+## ImageSizeResolver
+
+`ImageSizeResolver` controls the size of an image to be displayed. Currently it
+handles only HTML images (specified via `img` tag).
+
+```java
+final Markwon markwon = Markwon.builder(this)
+ .usePlugin(new AbstractMarkwonPlugin() {
+ @Override
+ public void configureConfiguration(@NonNull MarkwonConfiguration.Builder builder) {
+ builder.imageSizeResolver(new ImageSizeResolver() {
+ @NonNull
+ @Override
+ public Rect resolveImageSize(
+ @Nullable ImageSize imageSize,
+ @NonNull Rect imageBounds,
+ int canvasWidth,
+ float textSize) {
+ return null;
+ }
+ });
+ }
+ })
+ .build();
+```
+
+If not provided explicitly, default `ImageSizeResolverDef` implementation will be used.
+It handles 3 dimension units:
+* `%` (percent, relative to Canvas width)
+* `em` (relative to text size)
+* `px` (absolute size, every dimension that is not `%` or `em` is considered to be _absolute_)
+
+```html
+
+
+
+```
+
+`ImageSizeResolverDef` keeps the ratio of original image if one of the dimensions is missing.
+
+:::warning Height%
+There is no support for `%` units for `height` dimension. This is due to the fact that
+height of an TextView in which markdown is displayed is non-stable and changes with time
+(for example when image is loaded and applied to a TextView it will _increase_ TextView's height),
+so we will have no point-of-reference from which to _calculate_ image height.
+:::
+
+:::tip
+`ImageSizeResolverDef` also takes care for an image to **not** exceed
+canvas width. If an image has greater width than a TextView Canvas, then
+image will be _scaled-down_ to fit the canvas. Please note that this rule
+applies only if image has no absolute sizes (for example width is specified
+in pixels).
+:::
\ No newline at end of file
diff --git a/docs/docs/v4/core/core-plugin.md b/docs/docs/v4/core/core-plugin.md
new file mode 100644
index 00000000..750e3c59
--- /dev/null
+++ b/docs/docs/v4/core/core-plugin.md
@@ -0,0 +1,141 @@
+# Core plugin
+
+Since with introduction of _plugins_, Markwon
+**core** functionality was moved to a dedicated plugin.
+
+```java
+CorePlugin.create();
+```
+
+## Node visitors
+
+`CorePlugin` registers these `commonmark-java` node visitors:
+* `Text`
+* `StrongEmphasis`
+* `Emphasis`
+* `BlockQuote`
+* `Code`
+* `Image`
+* `FencedCodeBlock`
+* `IndentedCodeBlock`
+* `BulletList`
+* `OrderedList`
+* `ListItem`
+* `ThematicBreak`
+* `Heading`
+* `SoftLineBreak`
+* `HardLineBreak`
+* `Paragraph`
+* `Link`
+
+## Span factories
+
+`CorePlugin` adds these `SpanFactory`s:
+* `StrongEmphasis`
+* `Emphasis`
+* `BlockQuote`
+* `Code`
+* `FencedCodeBlock`
+* `IndentedCodeBlock`
+* `ListItem`
+* `Heading`
+* `Link`
+* `ThematicBreak`
+
+
+:::tip
+By default `CorePlugin` does not register a `Paragraph` `SpanFactory` but
+this can be done in your custom plugin:
+
+```java
+Markwon.builder(context)
+ .usePlugin(new AbstractMarkwonPlugin() {
+ @Override
+ public void configureSpansFactory(@NonNull MarkwonSpansFactory.Builder builder) {
+ builder.setFactory(Paragraph.class, (configuration, props) ->
+ new ForegroundColorSpan(Color.RED));
+ }
+ })
+```
+:::
+
+## Props
+These props are exported by `CorePlugin` and can be found in `CoreProps`:
+* `Prop LIST_ITEM_TYPE` (BULLET | ORDERED)
+* `Prop BULLET_LIST_ITEM_LEVEL`
+* `Prop ORDERED_LIST_ITEM_NUMBER`
+* `Prop HEADING_LEVEL`
+* `Prop LINK_DESTINATION`
+* `Prop PARAGRAPH_IS_IN_TIGHT_LIST`
+
+:::warning List item type
+Before `Markwon` had 2 distinct lists (bullet and ordered).
+Since a single `SpanFactory` is used, which internally checks
+for `Prop LIST_ITEM_TYPE`.
+Beware of this if you would like to override only one of the list types. This is
+done to correspond to `commonmark-java` implementation.
+:::
+
+More information about props can be found [here](/docs/v4/core/render-props.md)
+
+---
+
+:::tip Soft line break
+Since Markwon core does not give an option to
+insert a new line when there is a soft line break in markdown. Instead a
+custom plugin can be used:
+
+```java
+final Markwon markwon = Markwon.builder(this)
+ .usePlugin(new AbstractMarkwonPlugin() {
+ @Override
+ public void configureVisitor(@NonNull MarkwonVisitor.Builder builder) {
+ builder.on(SoftLineBreak.class, (visitor, softLineBreak) ->
+ visitor.forceNewLine());
+ }
+ })
+ .build();
+```
+:::
+
+:::warning
+Please note that `CorePlugin` will implicitly set a `LinkMovementMethod` on a TextView
+if one is not present. If you wish to customize a MovementMethod that is used, apply
+one manually to a TextView (before applying markdown) or use the [MovementMethodPlugin](/docs/v4/core/movement-method-plugin.md)
+which accepts a MovementMethod as an argument.
+:::
+
+## OnTextAddedListener
+
+Since `4.0.0` `CorePlugin` provides ability to receive text-added event. This can
+be useful in order to process raw text (for example to [linkify](/docs/v4/linkify.md) it):
+
+```java
+final Markwon markwon = Markwon.builder(context)
+ .usePlugin(new AbstractMarkwonPlugin() {
+ @Override
+ public void configure(@NonNull Registry registry) {
+ registry.require(CorePlugin.class, new Action() {
+ @Override
+ public void apply(@NonNull CorePlugin corePlugin) {
+ corePlugin.addOnTextAddedListener(new CorePlugin.OnTextAddedListener() {
+ @Override
+ public void onTextAdded(@NonNull MarkwonVisitor visitor, @NonNull String text, int start) {
+
+ // NB text is already added and you are __strongly__ adviced not to
+ // modify visitor here, but only add spans
+ //
+ // this will make all text BLUE
+ visitor.builder().setSpan(
+ new ForegroundColorSpan(Color.BLUE),
+ start,
+ visitor.length()
+ );
+ }
+ });
+ }
+ });
+ }
+ })
+ .build();
+```
\ No newline at end of file
diff --git a/docs/docs/v4/core/getting-started.md b/docs/docs/v4/core/getting-started.md
new file mode 100644
index 00000000..91848f71
--- /dev/null
+++ b/docs/docs/v4/core/getting-started.md
@@ -0,0 +1,52 @@
+# Getting started
+
+:::tip Installation
+Please follow [installation](/docs/v4/install.md) instructions
+to learn how to add `Markwon` to your project
+:::
+
+## Quick one
+
+This is the most simple way to set markdown to a `TextView` or any of its siblings:
+
+```java
+// obtain an instance of Markwon
+final Markwon markwon = Markwon.create(context);
+
+// set markdown
+markwon.setMarkdown(textView, "**Hello there!**");
+```
+
+The most simple way to obtain markdown to be applied _somewhere_ else:
+
+```java
+// obtain an instance of Markwon
+final Markwon markwon = Markwon.create(context);
+
+// parse markdown and create styled text
+final Spanned markdown = markwon.toMarkdown("**Hello there!**");
+
+// use it
+Toast.makeText(context, markdown, Toast.LENGTH_LONG).show();
+```
+
+## Longer one
+
+With explicit `parse` and `render` methods:
+
+```java
+// obtain an instance of Markwon
+final Markwon markwon = Markwon.create(context);
+
+// parse markdown to commonmark-java Node
+final Node node = markwon.parse("Are **you** still there?");
+
+// create styled text from parsed Node
+final Spanned markdown = markwon.render(node);
+
+// use it on a TextView
+markwon.setParsedMarkdown(textView, markdown);
+
+// or a Toast
+Toast.makeText(context, markdown, Toast.LENGTH_LONG).show();
+```
diff --git a/docs/docs/v4/core/movement-method-plugin.md b/docs/docs/v4/core/movement-method-plugin.md
new file mode 100644
index 00000000..6bb50c87
--- /dev/null
+++ b/docs/docs/v4/core/movement-method-plugin.md
@@ -0,0 +1,17 @@
+# Movement method plugin
+
+`MovementMethodPlugin` can be used to apply a `MovementMethod` to a TextView
+(important if you have links inside your markdown). By default `CorePlugin`
+will set a `LinkMovementMethod` on a TextView if one is missing. If you have
+specific needs for a `MovementMethod` and `LinkMovementMethod` doesn't answer
+your needs use `MovementMethodPlugin`:
+
+```java
+Markwon.builder(context)
+ .usePlugin(MovementMethodPlugin.create(ScrollingMovementMethod.getInstance()))
+```
+
+:::tip
+If you are having trouble with system `LinkMovementMethod` as an alternative
+[BetterLinkMovementMethod](https://github.com/saket/Better-Link-Movement-Method) library can be used.
+:::
diff --git a/docs/docs/v4/core/plugins.md b/docs/docs/v4/core/plugins.md
new file mode 100644
index 00000000..c44954b3
--- /dev/null
+++ b/docs/docs/v4/core/plugins.md
@@ -0,0 +1,361 @@
+# Plugins
+
+Since `MarkwonPlugin` takes the key role in
+processing and rendering markdown. Even **core** functionaly is abstracted
+into a `CorePlugin`. So it's still possible to use `Markwon` with a completely
+own set of plugins.
+
+To register a plugin `Markwon.Builder` must be used:
+
+```java
+Markwon.builder(context)
+ // @since 4.0.0 there is no need to register CorePlugin, as it's registered automatically
+// .usePlugin(CorePlugin.create())
+ .usePlugin(MyPlugin.create())
+ .build();
+```
+
+All the process of transforming _raw_ markdown into a styled text (Spanned)
+will go through plugins. A plugin can:
+
+* [configure plugin registry](#registry)
+* [configure commonmark-java `Parser`](#parser)
+* [configure `MarkwonTheme`](#markwontheme)
+* [configure `AsyncDrawableLoader` (used to display images in markdown)](#images)
+* [configure `MarkwonConfiguration`](#configuration)
+* [configure `MarkwonVisitor` (extensible commonmark-java Node visitor)](#visitor)
+* [configure `MarkwonSpansFactory` (factory to hold spans information for each Node)](#spans-factory)
+
+---
+
+* [process raw input markdown before parsing it](#process-markdown)
+* [inspect/modify commonmark-java Node after it's been parsed, but before rendering](#inspect-modify-node)
+* [inspect commonmark-java Node after it's been rendered](#inspect-node-after-render)
+* [prepare TextView to display markdown _before_ markdown is applied to a TextView](#prepare-textview)
+* [post-process TextView _after_ markdown was applied](#textview-after-markdown-applied)
+
+:::tip
+if you need to override only few methods of `MarkwonPlugin` (since it is an interface),
+`AbstractMarkwonPlugin` can be used.
+:::
+
+## Registry
+
+Registry is a special step to pre-configure all registered plugins. It is also
+used to determine the order of plugins inside `Markwon` instance.
+
+```java
+final Markwon markwon = Markwon.builder(context)
+ .usePlugin(new AbstractMarkwonPlugin() {
+ @Override
+ public void configure(@NonNull Registry registry) {
+
+ final CorePlugin corePlugin = registry.require(CorePlugin.class);
+
+ // or
+ registry.require(CorePlugin.class, new Action() {
+ @Override
+ public void apply(@NonNull CorePlugin corePlugin) {
+
+ }
+ });
+ }
+ })
+ .build();
+```
+
+More information about registry can be found [here](/docs/v4/core/registry.md)
+
+## Parser
+
+For example, let's register a new commonmark-java Parser extension:
+
+```java
+final Markwon markwon = Markwon.builder(context)
+ .usePlugin(new AbstractMarkwonPlugin() {
+ @Override
+ public void configureParser(@NonNull Parser.Builder builder) {
+ // no need to call `super.configureParser(builder)`
+ builder.extensions(Collections.singleton(StrikethroughExtension.create()));
+ }
+ })
+ .build();
+```
+
+There are no limitations on what to do with commonmark-java Parser. For more info
+_what_ can be done please refer to .
+
+## MarkwonTheme
+
+Starting `MarkwonTheme` represents _core_ theme. Aka theme for
+things core module knows of. For example it doesn't know anything about `strikethrough`
+or `tables` (as they belong to different modules).
+
+```java
+final Markwon markwon = Markwon.builder(context)
+ .usePlugin(new AbstractMarkwonPlugin() {
+ @Override
+ public void configureTheme(@NonNull MarkwonTheme.Builder builder) {
+ builder
+ .codeTextColor(Color.BLACK)
+ .codeBackgroundColor(Color.GREEN);
+ }
+ })
+ .build();
+```
+
+:::tip
+`CorePlugin` has special handling - it will be added automatically
+when `Markwon.builder(Context)` method is used. If you wish to create
+Markwon instance _without_ CorePlugin registered -
+use `Markwon.builderNoCore(Context)` method instead
+:::
+
+More information about `MarkwonTheme` can be found [here](/docs/v4/core/theme.md).
+
+
+## Configuration
+
+`MarkwonConfiguration` is a set of common tools that are used by different parts
+of `Markwon`. It allows configurations of these:
+
+* `AsyncDrawableLoader` (image loading)
+* `SyntaxHighlight` (highlighting code blocks)
+* `LinkResolver` (opens links in markdown)
+* `UrlProcessor` (process URLs in markdown for both links and images)
+* `ImageSizeResolver` (resolve image sizes, like `fit-to-canvas`, etc)
+
+```java
+final Markwon markwon = Markwon.builder(context)
+ .usePlugin(new AbstractMarkwonPlugin() {
+ @Override
+ public void configureConfiguration(@NonNull MarkwonConfiguration.Builder builder) {
+ builder.linkResolver(new LinkResolverDef());
+ }
+ })
+ .build();
+```
+
+More information about `MarkwonConfiguration` can be found [here](/docs/v4/core/configuration.md)
+
+
+## Visitor
+
+`MarkwonVisitor` is commonmark-java Visitor that allows
+configuration of how each Node is visited. There is no longer need to create
+own subclass of Visitor and override required methods (like in `2.x.x` versions).
+`MarkwonVisitor` also allows registration of Nodes, that `core` module knows
+nothing about (instead of relying on `visit(CustomNode)` method)).
+
+For example, let's add `strikethrough` Node visitor:
+
+```java
+final Markwon markwon = Markwon.builder(context)
+ .usePlugin(new AbstractMarkwonPlugin() {
+ @Override
+ public void configureVisitor(@NonNull MarkwonVisitor.Builder builder) {
+ // please note that strike-through parser extension must be registered
+ // in order to receive such callback
+ builder
+ .on(Strikethrough.class, new MarkwonVisitor.NodeVisitor() {
+ @Override
+ public void visit(@NonNull MarkwonVisitor visitor, @NonNull Strikethrough strikethrough) {
+ final int length = visitor.length();
+ visitor.visitChildren(strikethrough);
+ visitor.setSpansForNodeOptional(strikethrough, length);
+ }
+ });
+ }
+ })
+ .build();
+```
+
+:::tip
+`MarkwonVisitor` also allows _overriding_ already registered nodes. For example,
+you can disable `Heading` Node rendering:
+
+```java
+builder.on(Heading.class, null);
+```
+:::
+
+More information about `MarkwonVisitor` can be found [here](/docs/v4/core/visitor.md)
+
+
+## Spans Factory
+
+`MarkwonSpansFactory` is an abstract factory (factory that produces other factories)
+for spans that `Markwon` uses. It controls what spans to use for certain Nodes.
+
+```java
+final Markwon markwon = Markwon.builder(context)
+ .usePlugin(new AbstractMarkwonPlugin() {
+ @Override
+ public void configureSpansFactory(@NonNull MarkwonSpansFactory.Builder builder) {
+ // override emphasis factory to make all emphasis nodes underlined
+ builder.setFactory(Emphasis.class, new SpanFactory() {
+ @Override
+ public Object getSpans(@NonNull MarkwonConfiguration configuration, @NonNull RenderProps props) {
+ return new UnderlineSpan();
+ }
+ });
+ }
+ })
+ .build();
+```
+
+:::tip
+`SpanFactory` allows to return an _array_ of spans to apply multiple spans
+for a Node:
+
+```java
+@Override
+public Object getSpans(@NonNull MarkwonConfiguration configuration, @NonNull RenderProps props) {
+ // make underlined and set text color to red
+ return new Object[]{
+ new UnderlineSpan(),
+ new ForegroundColorSpan(Color.RED)
+ };
+}
+```
+:::
+
+More information about spans factory can be found [here](/docs/v4/core/spans-factory.md)
+
+
+## Process markdown
+
+A plugin can be used to _pre-process_ input markdown (this will be called before _parsing_):
+
+```java
+final Markwon markwon = Markwon.builder(context)
+ .usePlugin(new AbstractMarkwonPlugin() {
+ @NonNull
+ @Override
+ public String processMarkdown(@NonNull String markdown) {
+ return markdown.replaceAll("foo", "bar");
+ }
+ })
+ .build();
+```
+
+## Inspect/modify Node
+
+A plugin can inspect/modify commonmark-java Node _before_ it's being rendered.
+
+```java
+final Markwon markwon = Markwon.builder(context)
+ .usePlugin(new AbstractMarkwonPlugin() {
+ @Override
+ public void beforeRender(@NonNull Node node) {
+
+ // for example inspect it with custom visitor
+ node.accept(new MyVisitor());
+
+ // or modify (you know what you are doing, right?)
+ node.appendChild(new Text("Appended"));
+ }
+ })
+ .build();
+```
+
+## Inspect Node after render
+
+A plugin can inspect commonmark-java Node after it's been rendered.
+Modifying Node at this point makes not much sense (it's already been
+rendered and all modifications won't change anything). But this method can be used,
+for example, to clean-up some internal state (after rendering). Generally
+speaking, a plugin must be stateless, but if it cannot, then this method is
+the best place to clean-up.
+
+```java
+final Markwon markwon = Markwon.builder(context)
+ .usePlugin(new AbstractMarkwonPlugin() {
+ @Override
+ public void afterRender(@NonNull Node node, @NonNull MarkwonVisitor visitor) {
+ cleanUp();
+ }
+ })
+ .build();
+```
+
+## Prepare TextView
+
+A plugin can _prepare_ a TextView before markdown is applied. For example `images`
+unschedules all previously scheduled `AsyncDrawableSpans` (if any) here. This way
+when new markdown (and set of Spannables) arrives, previous set won't be kept in
+memory and could be garbage-collected.
+
+```java
+final Markwon markwon = Markwon.builder(context)
+ .usePlugin(new AbstractMarkwonPlugin() {
+ @Override
+ public void beforeSetText(@NonNull TextView textView, @NonNull Spanned markdown) {
+ // clean-up previous
+ AsyncDrawableScheduler.unschedule(textView);
+ }
+ })
+ .build();
+```
+
+## TextView after markdown applied
+
+A plugin will receive a callback _after_ markdown is applied to a TextView.
+For example `images` uses this callback to schedule new set of Spannables.
+
+```java
+final Markwon markwon = Markwon.builder(context)
+ .usePlugin(new AbstractMarkwonPlugin() {
+ @Override
+ public void afterSetText(@NonNull TextView textView) {
+ AsyncDrawableScheduler.schedule(textView);
+ }
+ })
+ .build();
+```
+
+:::tip
+Please note that unlike `#beforeSetText`, `#afterSetText` won't receive
+`Spanned` markdown. This happens because at this point spans must be
+queried directly from a TextView.
+:::
+
+## What happens underneath
+
+Here is what happens inside `Markwon` when `setMarkdown` method is called:
+
+```java
+final Markwon markwon = Markwon.create(context);
+
+// warning: pseudo-code
+
+// 0. each plugin will be called to _pre-process_ raw input markdown
+rawInput = plugins.reduce(rawInput, (input, plugin) -> plugin.processMarkdown(input));
+
+// 1. after input is processed it's being parsed to a Node
+node = parser.parse(rawInput);
+
+// 2. each plugin will be able to inspect or manipulate resulting Node
+// before rendering
+plugins.forEach(plugin -> plugin.beforeRender(node));
+
+// 3. node is being visited by a visitor
+node.accept(visitor);
+
+// 4. each plugin will be called after node is being visited (aka rendered)
+plugins.forEach(plugin -> plugin.afterRender(node, visitor));
+
+// 5. styled markdown ready at this point
+final Spanned markdown = visitor.markdown();
+
+// NB, points 6-8 are applied **only** if markdown is set to a TextView
+
+// 6. each plugin will be called before styled markdown is applied to a TextView
+plugins.forEach(plugin -> plugin.beforeSetText(textView, markdown));
+
+// 7. markdown is applied to a TextView
+textView.setText(markdown);
+
+// 8. each plugin will be called after markdown is applied to a TextView
+plugins.forEach(plugin -> plugin.afterSetText(textView));
+```
\ No newline at end of file
diff --git a/docs/docs/v4/core/registry.md b/docs/docs/v4/core/registry.md
new file mode 100644
index 00000000..1161600c
--- /dev/null
+++ b/docs/docs/v4/core/registry.md
@@ -0,0 +1,97 @@
+# Registry
+
+`Registry` allows to pre-configure other plugins and/or declare a dependency on a plugin,
+which also will modify internal order of plugins inside a `Markwon` instance.
+
+For example, you have a configurable plugin:
+
+```java
+public class MyPlugin extends AbstractMarkwonPlugin {
+
+ private boolean enabled;
+
+ public boolean enabled() {
+ return enabled;
+ }
+
+ @NonNull
+ public MyPlugin enabled(boolean enabled) {
+ this.enabled = enabled;
+ return this;
+ }
+
+ {...}
+}
+```
+
+and other plugin that needs to access `MyPlugin` or modify/configure it:
+
+```java
+public class MyOtherPlugin extends AbstractMarkwonPlugin {
+ @Override
+ public void configure(@NonNull Registry registry) {
+ registry.require(MyPlugin.class, new Action() {
+ @Override
+ public void apply(@NonNull MyPlugin myPlugin) {
+ myPlugin.enabled(false);
+ }
+ });
+ }
+}
+```
+
+```java
+final Markwon markwon = Markwon.builder(context)
+ .usePlugin(new MyOtherPlugin())
+ .usePlugin(new MyPlugin())
+ .build();
+```
+
+_Internal_ plugins order (in this case) will be:
+* `CorePlugin` (added automatically and always the first one)
+* `MyPlugin` (was required by `MyOtherPlugin`)
+* `MyOtherPlugin`
+
+:::tip
+There is no need to _require_ `CorePlugin` as it will be the first one inside
+`Markwon` instance.
+:::
+
+The order matters if you want to _override_ some plugin. For example, `CoolPlugin`
+adds a `SpanFactory` for a `Cool` markdown node. Other `NotCoolPlugin` wants to
+use a different `SpanFactory`, then:
+
+```java
+final Markwon markwon = Markwon.builder(context)
+ .usePlugin(CoolPlugin.create())
+ .usePlugin(new NotCoolPlugin() {
+
+ @Override
+ public void configure(@NonNull MarkwonPlugin.Registry registry) {
+ registry.require(CoolPlugin.class);
+ }
+
+ @Override
+ public void configureSpansFactory(@NonNull MarkwonSpansFactory.Builder builder) {
+ builder.setFactory(Cool.class, new NotCoolSpanFactory());
+ }
+ })
+ .build();
+```
+
+---
+
+All `require` calls to the `Registry` will also validate at runtime that
+_required_ plugins are registered.
+
+```java
+final Markwon markwon = Markwon.builder(context)
+ .usePlugin(new AbstractMarkwonPlugin() {
+ @Override
+ public void configure(@NonNull Registry registry) {
+ // will throw an exception if `NotPresentPlugin` is not present
+ registry.require(NotPresentPlugin.class);
+ }
+ })
+ .build();
+```
\ No newline at end of file
diff --git a/docs/docs/v4/core/render-props.md b/docs/docs/v4/core/render-props.md
new file mode 100644
index 00000000..9dd18004
--- /dev/null
+++ b/docs/docs/v4/core/render-props.md
@@ -0,0 +1,75 @@
+# RenderProps
+
+`RenderProps` encapsulates passing arguments from a node visitor to a node renderer.
+Without hardcoding arguments into an API method calls.
+
+`RenderProps` is the state collection for `Props` that are set by a node visitor and
+retrieved by a node renderer.
+
+```java
+public class Prop {
+
+ @NonNull
+ public static Prop of(@NonNull String name) {
+ return new Prop<>(name);
+ }
+
+ /* ... */
+}
+```
+
+For example `CorePlugin` defines a _Heading level_ prop (inside `CoreProps` class):
+
+```java
+public static final Prop HEADING_LEVEL = Prop.of("heading-level");
+```
+
+Then CorePlugin registers a `Heading` node visitor and applies heading value:
+
+```java
+@Override
+public void configureVisitor(@NonNull MarkwonVisitor.Builder builder) {
+ builder.on(Heading.class, new MarkwonVisitor.NodeVisitor() {
+ @Override
+ public void visit(@NonNull MarkwonVisitor visitor, @NonNull Heading heading) {
+
+ /* Heading node handling logic */
+
+ // set heading level
+ CoreProps.HEADING_LEVEL.set(visitor.renderProps(), heading.getLevel());
+
+ // a helper method to apply span(s) for a node
+ // (internally obtains a SpanFactory for Heading or silently ignores
+ // this call if no factory for a Heading is registered)
+ visitor.setSpansForNodeOptional(heading, start);
+
+ /* Heading node handling logic */
+ }
+ });
+}
+```
+
+And finally `HeadingSpanFactory` (which is also registered by `CorePlugin`):
+
+```java
+public class HeadingSpanFactory implements SpanFactory {
+ @Nullable
+ @Override
+ public Object getSpans(@NonNull MarkwonConfiguration configuration, @NonNull RenderProps props) {
+ return new HeadingSpan(
+ configuration.theme(),
+ CoreProps.HEADING_LEVEL.require(props)
+ );
+ }
+}
+```
+
+---
+
+`Prop` has these methods:
+
+* `@Nullable T get(RenderProps)` - returns value stored in RenderProps or `null` if none is present
+* `@NonNull T get(RenderProps, @NonNull T defValue)` - returns value stored in RenderProps or default value (this method always return non-null value)
+* `@NonNull T require(RenderProps)` - returns value stored in RenderProps or _throws an exception_ if none is present
+* `void set(RenderProps, @Nullable T value)` - updates value stored in RenderProps, passing `null` as value is the same as calling `clear`
+* `void clear(RenderProps)` - clears value stored in RenderProps
diff --git a/docs/docs/v4/core/spans-factory.md b/docs/docs/v4/core/spans-factory.md
new file mode 100644
index 00000000..9567a788
--- /dev/null
+++ b/docs/docs/v4/core/spans-factory.md
@@ -0,0 +1,103 @@
+# Spans Factory
+
+Starting with `MarkwonSpansFactory` controls what spans are displayed
+for markdown nodes.
+
+```java
+Markwon.builder(context)
+ .usePlugin(new AbstractMarkwonPlugin() {
+ @Override
+ public void configureSpansFactory(@NonNull MarkwonSpansFactory.Builder builder) {
+ // passing null as second argument will remove previously added
+ // factory for the Link node
+ builder.setFactory(Link.class, null);
+ }
+ });
+```
+
+## SpanFactory
+
+In order to create a _generic_ interface for all possible Nodes, a `SpanFactory`
+was added:
+
+```java
+builder.setFactory(Link.class, new SpanFactory() {
+ @Nullable
+ @Override
+ public Object getSpans(@NonNull MarkwonConfiguration configuration, @NonNull RenderProps props) {
+ return null;
+ }
+});
+```
+
+All possible arguments are passed via [RenderProps](/docs/v4/core/render-props.md):
+
+```java
+builder.setFactory(Link.class, new SpanFactory() {
+ @Override
+ public Object getSpans(@NonNull MarkwonConfiguration configuration, @NonNull RenderProps props) {
+ final String href = CoreProps.LINK_DESTINATION.require(props);
+ return new LinkSpan(configuration.theme(), href, configuration.linkResolver());
+ }
+});
+```
+
+`SpanFactory` allows returning `null` for a certain span (no span will be applied).
+Or an array of spans (you _can_ go deeper):
+
+```java
+builder.setFactory(Link.class, new SpanFactory() {
+ @Override
+ public Object getSpans(@NonNull MarkwonConfiguration configuration, @NonNull RenderProps props) {
+ return new Object[]{
+ new LinkSpan(
+ configuration.theme(),
+ CoreProps.LINK_DESTINATION.require(props),
+ configuration.linkResolver()),
+ new ForegroundColorSpan(Color.RED)
+ };
+ }
+});
+```
+
+---
+
+Since you can _add_ multiple `SpanFactory` for a single node:
+
+```java
+final Markwon markwon = Markwon.builder(context)
+ .usePlugin(new AbstractMarkwonPlugin() {
+ @Override
+ public void configureSpansFactory(@NonNull MarkwonSpansFactory.Builder builder) {
+ // this factory will be used _along_ with all other factories for specified node
+ builder.addFactory(Code.class, new SpanFactory() {
+ @Override
+ public Object getSpans(@NonNull MarkwonConfiguration configuration, @NonNull RenderProps props) {
+ return new ForegroundColorSpan(Color.GREEN);
+ }
+ });
+ }
+ })
+ .build();
+```
+
+---
+
+If you wish to inspect existing factory you can use:
+* `builder#getFactory()` -> returns registered factory or `null`
+* `builder#requireFactory()` -> returns registered factory or throws
+
+```java
+final Markwon markwon = Markwon.builder(context)
+ .usePlugin(new AbstractMarkwonPlugin() {
+ @Override
+ public void configureSpansFactory(@NonNull MarkwonSpansFactory.Builder builder) {
+ final SpanFactory codeFactory = builder.requireFactory(Code.class);
+ final SpanFactory linkFactory = builder.getFactory(Link.class);
+ if (linkFactory != null) {
+ {...}
+ }
+ }
+ })
+ .build();
+```
\ No newline at end of file
diff --git a/docs/docs/v4/core/theme.md b/docs/docs/v4/core/theme.md
new file mode 100644
index 00000000..672babc6
--- /dev/null
+++ b/docs/docs/v4/core/theme.md
@@ -0,0 +1,187 @@
+# Theme
+
+Here is the list of properties that can be configured via `MarkwonTheme.Builder` class.
+
+:::tip
+Starting with there is no need to manually construct a `MarkwonTheme`.
+Instead a `Plugin` should be used:
+```java
+final Markwon markwon = Markwon.builder(context)
+ .usePlugin(new AbstractMarkwonPlugin() {
+ @Override
+ public void configureTheme(@NonNull MarkwonTheme.Builder builder) {
+ builder
+ .codeTextColor(Color.BLACK)
+ .codeBackgroundColor(Color.GREEN);
+ }
+ })
+ .build();
+```
+:::
+
+## Link color
+
+Controls the color of a [link](#)
+
+
+
+* `TextPaint#linkColor` will be used to determine linkColor of a context
+
+## Block margin
+
+Starting margin before text content for the:
+* lists
+* blockquotes
+* task lists
+
+
+
+## Block quote
+
+Customizations for the `blockquote` stripe
+
+> Quote
+
+### Stripe width
+
+Width of a blockquote stripe
+
+
+
+### Stripe color
+
+Color of a blockquote stripe
+
+
+
+## List
+
+### List item color
+
+Controls the color of a list item. For ordered list: leading number,
+for unordered list: bullet.
+
+* UL
+1. OL
+
+
+
+### Bullet item stroke width
+
+Border width of a bullet list item (level 2)
+
+* First
+* * Second
+* * * Third
+
+
+
+### Bullet width
+
+The width of the bullet item
+
+* First
+ * Second
+ * Third
+
+
+
+## Code
+
+### Inline code text color
+
+The color of the `code` content
+
+
+
+### Inline code background color
+
+The color of `background` of a code content
+
+
+
+### Block code text color
+
+```
+The color of code block text
+```
+
+
+
+### Block code background color
+
+```
+The color of background of code block text
+```
+
+
+
+### Block code leading margin
+
+Leading margin for the block code content
+
+
+
+### Code typeface
+
+Typeface of code content
+
+
+
+### Block code typeface
+
+Typeface of block code content
+
+
+
+### Code text size
+
+Text size of code content
+
+
+
+### Block code text size
+
+Text size of block code content
+
+
+
+## Heading
+
+### Break height
+
+The height of a brake under H1 & H2
+
+
+
+### Break color
+
+The color of a brake under H1 & H2
+
+
+
+### Typeface
+
+The typeface of heading elements
+
+
+
+### Text size
+
+Array of heading text sizes _ratio_ that is applied to text size
+
+
+
+## Thematic break
+
+### Color
+
+Color of a thematic break
+
+
+
+### Height
+
+Height of a thematic break
+
+
diff --git a/docs/docs/v4/core/visitor.md b/docs/docs/v4/core/visitor.md
new file mode 100644
index 00000000..d03ff848
--- /dev/null
+++ b/docs/docs/v4/core/visitor.md
@@ -0,0 +1,73 @@
+# Visitor
+
+Starting with _visiting_ of parsed markdown
+nodes does not require creating own instance of commonmark-java `Visitor`,
+instead a composable/configurable `MarkwonVisitor` is used.
+
+## Visitor.Builder
+There is no need to create own instance of `MarkwonVisitor.Builder` as
+it is done by `Markwon` itself. One still can configure it as one wishes:
+
+```java
+final Markwon markwon = Markwon.builder(contex)
+ .usePlugin(new AbstractMarkwonPlugin() {
+ @Override
+ public void configureVisitor(@NonNull MarkwonVisitor.Builder builder) {
+ builder.on(SoftLineBreak.class, new MarkwonVisitor.NodeVisitor() {
+ @Override
+ public void visit(@NonNull MarkwonVisitor visitor, @NonNull SoftLineBreak softLineBreak) {
+ visitor.forceNewLine();
+ }
+ });
+ }
+ });
+```
+
+---
+
+`MarkwonVisitor` encapsulates most of the functionality of rendering parsed markdown.
+
+It holds rendering configuration:
+* `MarkwonVisitor#configuration` - getter for current [MarkwonConfiguration](/docs/v4/core/configuration.md)
+* `MarkwonVisitor#renderProps` - getter for current [RenderProps](/docs/v4/core/render-props.md)
+* `MarkwonVisitor#builder` - getter for current `SpannableBuilder`
+
+It contains also a number of utility functions:
+* `visitChildren(Node)` - will visit all children of supplied Node
+* `hasNext(Node)` - utility function to check if supplied Node has a Node after it (useful for white-space management, so there should be no blank new line after last BlockNode)
+* `ensureNewLine` - will insert a new line at current `SpannableBuilder` position only if current (last) character is not a new-line
+* `forceNewLine` - will insert a new line character without any condition checking
+* `length` - helper function to call `visitor.builder().length()`, returns current length of `SpannableBuilder`
+* `clear` - will clear state for `RenderProps` and `SpannableBuilder`, this is done by `Markwon` automatically after each render call
+
+And some utility functions to control the spans:
+* `setSpans(int start, Object spans)` - will apply supplied `spans` on `SpannableBuilder` starting at `start` position and ending at `SpannableBuilder#length`. `spans` can be `null` (no spans will be applied) or an array of spans (each span of this array will be applied)
+* `setSpansForNodeOptional(N node, int start)` - helper method to set spans for specified `node` (internally obtains `SpanFactory` for that node and uses it to apply spans)
+* `setSpansForNode(N node, int start)` - almost the same as `setSpansForNodeOptional` but instead of silently ignoring call if none `SpanFactory` is registered, this method will throw an exception.
+
+```java
+@Override
+public void configureVisitor(@NonNull MarkwonVisitor.Builder builder) {
+ builder.on(Heading.class, new MarkwonVisitor.NodeVisitor() {
+ @Override
+ public void visit(@NonNull MarkwonVisitor visitor, @NonNull Heading heading) {
+
+ // or just `visitor.length()`
+ final int start = visitor.builder().length();
+
+ visitor.visitChildren(heading);
+
+ // or just `visitor.setSpansForNodeOptional(heading, start)`
+ final SpanFactory factory = visitor.configuration().spansFactory().get(heading.getClass());
+ if (factory != null) {
+ visitor.setSpans(start, factory.getSpans(visitor.configuration(), visitor.renderProps()));
+ }
+
+ if (visitor.hasNext(heading)) {
+ visitor.ensureNewLine();
+ visitor.forceNewLine();
+ }
+ }
+ });
+}
+```
\ No newline at end of file
diff --git a/docs/docs/v4/install.md b/docs/docs/v4/install.md
new file mode 100644
index 00000000..c6de0e5a
--- /dev/null
+++ b/docs/docs/v4/install.md
@@ -0,0 +1,34 @@
+---
+prev: false
+next: /docs/v4/core/getting-started.md
+---
+
+# Installation
+
+
+
+
+
+
+## Snapshot
+
+In order to use latest `SNAPSHOT` version add snapshot repository
+to your root project's `build.gradle` file:
+
+```groovy
+allprojects {
+ repositories {
+ jcenter()
+ google()
+ // this one 👇
+ maven { url 'https://oss.sonatype.org/content/repositories/snapshots/' } // 👈 this one
+ // this one 👆
+ }
+}
+```
+
+:::tip Info
+All official artifacts share the same version number and all
+are uploaded to **release** and **snapshot** repositories
+:::
+
diff --git a/docs/docs/v4/migration-3-4.md b/docs/docs/v4/migration-3-4.md
new file mode 100644
index 00000000..5537e1e2
--- /dev/null
+++ b/docs/docs/v4/migration-3-4.md
@@ -0,0 +1,3 @@
+# Migration 3.x.x -> 4.x.x
+
+todo
\ No newline at end of file
diff --git a/docs/docs/v4/recipes.md b/docs/docs/v4/recipes.md
new file mode 100644
index 00000000..7fc70175
--- /dev/null
+++ b/docs/docs/v4/recipes.md
@@ -0,0 +1,3 @@
+# Recipes
+
+todo
\ No newline at end of file
diff --git a/markwon-image-gif/build.gradle b/markwon-image-gif/build.gradle
deleted file mode 100644
index bcb3fa77..00000000
--- a/markwon-image-gif/build.gradle
+++ /dev/null
@@ -1,25 +0,0 @@
-apply plugin: 'com.android.library'
-
-android {
-
- compileSdkVersion config['compile-sdk']
- buildToolsVersion config['build-tools']
-
- defaultConfig {
- minSdkVersion config['min-sdk']
- targetSdkVersion config['target-sdk']
- versionCode 1
- versionName version
- }
-}
-
-dependencies {
-
- api project(':markwon-core')
-
- deps.with {
- api it['android-gif']
- }
-}
-
-registerArtifact(this)
\ No newline at end of file
diff --git a/markwon-image-gif/gradle.properties b/markwon-image-gif/gradle.properties
deleted file mode 100644
index 2630a2f3..00000000
--- a/markwon-image-gif/gradle.properties
+++ /dev/null
@@ -1,4 +0,0 @@
-POM_NAME=Image GIF
-POM_ARTIFACT_ID=image-gif
-POM_DESCRIPTION=Adds GIF media support to Markwon markdown
-POM_PACKAGING=aar
\ No newline at end of file
diff --git a/markwon-image-gif/src/main/AndroidManifest.xml b/markwon-image-gif/src/main/AndroidManifest.xml
deleted file mode 100644
index 649a9a70..00000000
--- a/markwon-image-gif/src/main/AndroidManifest.xml
+++ /dev/null
@@ -1 +0,0 @@
-
diff --git a/markwon-image-gif/src/main/java/ru/noties/markwon/image/gif/GifMediaDecoder.java b/markwon-image-gif/src/main/java/ru/noties/markwon/image/gif/GifMediaDecoder.java
deleted file mode 100644
index 7c06e13a..00000000
--- a/markwon-image-gif/src/main/java/ru/noties/markwon/image/gif/GifMediaDecoder.java
+++ /dev/null
@@ -1,81 +0,0 @@
-package ru.noties.markwon.image.gif;
-
-import android.graphics.drawable.Drawable;
-import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
-
-import java.io.ByteArrayOutputStream;
-import java.io.IOException;
-import java.io.InputStream;
-
-import pl.droidsonroids.gif.GifDrawable;
-import ru.noties.markwon.image.DrawableUtils;
-
-/**
- * @since 1.1.0
- */
-@SuppressWarnings("WeakerAccess")
-public class GifMediaDecoder extends MediaDecoder {
-
- public static final String CONTENT_TYPE = "image/gif";
-
- @NonNull
- public static GifMediaDecoder create(boolean autoPlayGif) {
- return new GifMediaDecoder(autoPlayGif);
- }
-
- private final boolean autoPlayGif;
-
- protected GifMediaDecoder(boolean autoPlayGif) {
- this.autoPlayGif = autoPlayGif;
- }
-
- @Nullable
- @Override
- public Drawable decode(@NonNull InputStream inputStream) {
-
- Drawable out = null;
-
- final byte[] bytes = readBytes(inputStream);
- if (bytes != null) {
- try {
- out = newGifDrawable(bytes);
- DrawableUtils.applyIntrinsicBounds(out);
-
- if (!autoPlayGif) {
- ((GifDrawable) out).pause();
- }
- } catch (IOException e) {
- e.printStackTrace();
- }
- }
-
- return out;
- }
-
- @NonNull
- protected Drawable newGifDrawable(@NonNull byte[] bytes) throws IOException {
- return new GifDrawable(bytes);
- }
-
- @Nullable
- protected static byte[] readBytes(@NonNull InputStream stream) {
-
- byte[] out = null;
-
- try {
- final ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
- final int length = 1024 * 8;
- final byte[] buffer = new byte[length];
- int read;
- while ((read = stream.read(buffer, 0, length)) != -1) {
- outputStream.write(buffer, 0, read);
- }
- out = outputStream.toByteArray();
- } catch (IOException e) {
- e.printStackTrace();
- }
-
- return out;
- }
-}
diff --git a/markwon-image-gif/src/main/java/ru/noties/markwon/image/gif/GifPlugin.java b/markwon-image-gif/src/main/java/ru/noties/markwon/image/gif/GifPlugin.java
deleted file mode 100644
index 754f91ae..00000000
--- a/markwon-image-gif/src/main/java/ru/noties/markwon/image/gif/GifPlugin.java
+++ /dev/null
@@ -1,37 +0,0 @@
-package ru.noties.markwon.image.gif;
-
-import androidx.annotation.NonNull;
-
-import ru.noties.markwon.AbstractMarkwonPlugin;
-import ru.noties.markwon.image.AsyncDrawableLoader;
-import ru.noties.markwon.priority.Priority;
-
-public class GifPlugin extends AbstractMarkwonPlugin {
-
- @NonNull
- public static GifPlugin create() {
- return create(true);
- }
-
- @NonNull
- public static GifPlugin create(boolean autoPlay) {
- return new GifPlugin(autoPlay);
- }
-
- private final boolean autoPlay;
-
- public GifPlugin(boolean autoPlay) {
- this.autoPlay = autoPlay;
- }
-
- @Override
- public void configureImages(@NonNull AsyncDrawableLoader.Builder builder) {
- builder.addMediaDecoder(GifMediaDecoder.CONTENT_TYPE, GifMediaDecoder.create(autoPlay));
- }
-
- @NonNull
- @Override
- public Priority priority() {
- return Priority.after(ImagesPlugin.class);
- }
-}
diff --git a/markwon-image-okhttp/build.gradle b/markwon-image-okhttp/build.gradle
deleted file mode 100644
index 3f570553..00000000
--- a/markwon-image-okhttp/build.gradle
+++ /dev/null
@@ -1,25 +0,0 @@
-apply plugin: 'com.android.library'
-
-android {
-
- compileSdkVersion config['compile-sdk']
- buildToolsVersion config['build-tools']
-
- defaultConfig {
- minSdkVersion config['min-sdk']
- targetSdkVersion config['target-sdk']
- versionCode 1
- versionName version
- }
-}
-
-dependencies {
-
- api project(':markwon-core')
-
- deps.with {
- api it['okhttp']
- }
-}
-
-registerArtifact(this)
\ No newline at end of file
diff --git a/markwon-image-okhttp/gradle.properties b/markwon-image-okhttp/gradle.properties
deleted file mode 100644
index 8722d5cf..00000000
--- a/markwon-image-okhttp/gradle.properties
+++ /dev/null
@@ -1,4 +0,0 @@
-POM_NAME=Image OkHttp
-POM_ARTIFACT_ID=image-okhttp
-POM_DESCRIPTION=Adds OkHttp client to retrieve images data from network
-POM_PACKAGING=aar
\ No newline at end of file
diff --git a/markwon-image-okhttp/src/main/AndroidManifest.xml b/markwon-image-okhttp/src/main/AndroidManifest.xml
deleted file mode 100644
index 32240579..00000000
--- a/markwon-image-okhttp/src/main/AndroidManifest.xml
+++ /dev/null
@@ -1 +0,0 @@
-
\ No newline at end of file
diff --git a/markwon-image-okhttp/src/main/java/ru/noties/markwon/image/okhttp/OkHttpImagesPlugin.java b/markwon-image-okhttp/src/main/java/ru/noties/markwon/image/okhttp/OkHttpImagesPlugin.java
deleted file mode 100644
index 6a72136a..00000000
--- a/markwon-image-okhttp/src/main/java/ru/noties/markwon/image/okhttp/OkHttpImagesPlugin.java
+++ /dev/null
@@ -1,51 +0,0 @@
-package ru.noties.markwon.image.okhttp;
-
-import androidx.annotation.NonNull;
-
-import java.util.Arrays;
-
-import okhttp3.OkHttpClient;
-import ru.noties.markwon.AbstractMarkwonPlugin;
-import ru.noties.markwon.image.AsyncDrawableLoader;
-import ru.noties.markwon.priority.Priority;
-
-/**
- * Plugin to use OkHttpClient to obtain images from network (http and https schemes)
- *
- * @see #create()
- * @see #create(OkHttpClient)
- * @since 3.0.0
- */
-@SuppressWarnings("WeakerAccess")
-public class OkHttpImagesPlugin extends AbstractMarkwonPlugin {
-
- @NonNull
- public static OkHttpImagesPlugin create() {
- return new OkHttpImagesPlugin(new OkHttpClient());
- }
-
- @NonNull
- public static OkHttpImagesPlugin create(@NonNull OkHttpClient okHttpClient) {
- return new OkHttpImagesPlugin(okHttpClient);
- }
-
- private final OkHttpClient client;
-
- OkHttpImagesPlugin(@NonNull OkHttpClient client) {
- this.client = client;
- }
-
- @Override
- public void configureImages(@NonNull AsyncDrawableLoader.Builder builder) {
- builder.addSchemeHandler(
- Arrays.asList(NetworkSchemeHandler.SCHEME_HTTP, NetworkSchemeHandler.SCHEME_HTTPS),
- new OkHttpSchemeHandler(client)
- );
- }
-
- @NonNull
- @Override
- public Priority priority() {
- return Priority.after(ImagesPlugin.class);
- }
-}
diff --git a/markwon-image-okhttp/src/main/java/ru/noties/markwon/image/okhttp/OkHttpSchemeHandler.java b/markwon-image-okhttp/src/main/java/ru/noties/markwon/image/okhttp/OkHttpSchemeHandler.java
deleted file mode 100644
index e1ce930b..00000000
--- a/markwon-image-okhttp/src/main/java/ru/noties/markwon/image/okhttp/OkHttpSchemeHandler.java
+++ /dev/null
@@ -1,55 +0,0 @@
-package ru.noties.markwon.image.okhttp;
-
-import android.net.Uri;
-import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
-
-import java.io.IOException;
-import java.io.InputStream;
-
-import okhttp3.OkHttpClient;
-import okhttp3.Request;
-import okhttp3.Response;
-import okhttp3.ResponseBody;
-
-class OkHttpSchemeHandler extends SchemeHandler {
-
- private static final String HEADER_CONTENT_TYPE = "Content-Type";
-
- private final OkHttpClient client;
-
- OkHttpSchemeHandler(@NonNull OkHttpClient client) {
- this.client = client;
- }
-
- @Nullable
- @Override
- public ImageItem handle(@NonNull String raw, @NonNull Uri uri) {
- ImageItem out = null;
-
- final Request request = new Request.Builder()
- .url(raw)
- .tag(raw)
- .build();
-
- Response response = null;
- try {
- response = client.newCall(request).execute();
- } catch (IOException e) {
- e.printStackTrace();
- }
-
- if (response != null) {
- final ResponseBody body = response.body();
- if (body != null) {
- final InputStream inputStream = body.byteStream();
- if (inputStream != null) {
- final String contentType = response.header(HEADER_CONTENT_TYPE);
- out = new ImageItem(contentType, inputStream);
- }
- }
- }
-
- return out;
- }
-}
diff --git a/markwon-image-svg/build.gradle b/markwon-image-svg/build.gradle
deleted file mode 100644
index cfe7bcd1..00000000
--- a/markwon-image-svg/build.gradle
+++ /dev/null
@@ -1,25 +0,0 @@
-apply plugin: 'com.android.library'
-
-android {
-
- compileSdkVersion config['compile-sdk']
- buildToolsVersion config['build-tools']
-
- defaultConfig {
- minSdkVersion config['min-sdk']
- targetSdkVersion config['target-sdk']
- versionCode 1
- versionName version
- }
-}
-
-dependencies {
-
- api project(':markwon-core')
-
- deps.with {
- api it['android-svg']
- }
-}
-
-registerArtifact(this)
\ No newline at end of file
diff --git a/markwon-image-svg/gradle.properties b/markwon-image-svg/gradle.properties
deleted file mode 100644
index 26dce9a4..00000000
--- a/markwon-image-svg/gradle.properties
+++ /dev/null
@@ -1,4 +0,0 @@
-POM_NAME=Image SVG
-POM_ARTIFACT_ID=image-svg
-POM_DESCRIPTION=Adds SVG media support to Markwon markdown
-POM_PACKAGING=aar
\ No newline at end of file
diff --git a/markwon-image-svg/src/main/AndroidManifest.xml b/markwon-image-svg/src/main/AndroidManifest.xml
deleted file mode 100644
index 10432a1d..00000000
--- a/markwon-image-svg/src/main/AndroidManifest.xml
+++ /dev/null
@@ -1 +0,0 @@
-
diff --git a/markwon-image-svg/src/main/java/ru/noties/markwon/image/svg/SvgMediaDecoder.java b/markwon-image-svg/src/main/java/ru/noties/markwon/image/svg/SvgMediaDecoder.java
deleted file mode 100644
index 8b2a9527..00000000
--- a/markwon-image-svg/src/main/java/ru/noties/markwon/image/svg/SvgMediaDecoder.java
+++ /dev/null
@@ -1,72 +0,0 @@
-package ru.noties.markwon.image.svg;
-
-import android.content.res.Resources;
-import android.graphics.Bitmap;
-import android.graphics.Canvas;
-import android.graphics.drawable.BitmapDrawable;
-import android.graphics.drawable.Drawable;
-import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
-
-import com.caverock.androidsvg.SVG;
-import com.caverock.androidsvg.SVGParseException;
-
-import java.io.InputStream;
-
-import ru.noties.markwon.image.DrawableUtils;
-
-/**
- * @since 1.1.0
- */
-public class SvgMediaDecoder extends MediaDecoder {
-
- public static final String CONTENT_TYPE = "image/svg+xml";
-
- @NonNull
- public static SvgMediaDecoder create(@NonNull Resources resources) {
- return new SvgMediaDecoder(resources);
- }
-
- private final Resources resources;
-
- @SuppressWarnings("WeakerAccess")
- SvgMediaDecoder(Resources resources) {
- this.resources = resources;
- }
-
- @Nullable
- @Override
- public Drawable decode(@NonNull InputStream inputStream) {
-
- final Drawable out;
-
- SVG svg = null;
- try {
- svg = SVG.getFromInputStream(inputStream);
- } catch (SVGParseException e) {
- e.printStackTrace();
- }
-
- if (svg == null) {
- out = null;
- } else {
-
- final float w = svg.getDocumentWidth();
- final float h = svg.getDocumentHeight();
- final float density = resources.getDisplayMetrics().density;
-
- final int width = (int) (w * density + .5F);
- final int height = (int) (h * density + .5F);
-
- final Bitmap bitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_4444);
- final Canvas canvas = new Canvas(bitmap);
- canvas.scale(density, density);
- svg.renderToCanvas(canvas);
-
- out = new BitmapDrawable(resources, bitmap);
- DrawableUtils.applyIntrinsicBounds(out);
- }
-
- return out;
- }
-}
diff --git a/markwon-image-svg/src/main/java/ru/noties/markwon/image/svg/SvgPlugin.java b/markwon-image-svg/src/main/java/ru/noties/markwon/image/svg/SvgPlugin.java
deleted file mode 100644
index 34573c9f..00000000
--- a/markwon-image-svg/src/main/java/ru/noties/markwon/image/svg/SvgPlugin.java
+++ /dev/null
@@ -1,33 +0,0 @@
-package ru.noties.markwon.image.svg;
-
-import android.content.res.Resources;
-import androidx.annotation.NonNull;
-
-import ru.noties.markwon.AbstractMarkwonPlugin;
-import ru.noties.markwon.image.AsyncDrawableLoader;
-import ru.noties.markwon.priority.Priority;
-
-public class SvgPlugin extends AbstractMarkwonPlugin {
-
- @NonNull
- public static SvgPlugin create(@NonNull Resources resources) {
- return new SvgPlugin(resources);
- }
-
- private final Resources resources;
-
- public SvgPlugin(@NonNull Resources resources) {
- this.resources = resources;
- }
-
- @Override
- public void configureImages(@NonNull AsyncDrawableLoader.Builder builder) {
- builder.addMediaDecoder(SvgMediaDecoder.CONTENT_TYPE, SvgMediaDecoder.create(resources));
- }
-
- @NonNull
- @Override
- public Priority priority() {
- return Priority.after(ImagesPlugin.class);
- }
-}