diff --git a/docs/.vuepress/config.js b/docs/.vuepress/config.js
index 50b76e0d..29ab8971 100644
--- a/docs/.vuepress/config.js
+++ b/docs/.vuepress/config.js
@@ -40,7 +40,8 @@ module.exports = {
'/docs/v3/core/spans-factory.md',
'/docs/v3/core/html-renderer.md',
'/docs/v3/core/core-plugin.md',
- '/docs/v3/core/movement-method-plugin.md'
+ '/docs/v3/core/movement-method-plugin.md',
+ '/docs/v3/core/render-props.md'
]
},
'/docs/v3/ext-latex/',
diff --git a/docs/docs/v3/README.md b/docs/docs/v3/README.md
index 936d3aed..ef82ddba 100644
--- a/docs/docs/v3/README.md
+++ b/docs/docs/v3/README.md
@@ -86,7 +86,7 @@ and 2 themes included: Light & Dark. It can be downloaded from [releases](ht
---
-[Help to improve][awesome_link] this section by submitting your application/library/anything
+[Help to improve][awesome_link] this section by submitting your application or library
that is using `Markwon`
diff --git a/docs/docs/v3/core/configuration.md b/docs/docs/v3/core/configuration.md
index af4abbfe..245b3310 100644
--- a/docs/docs/v3/core/configuration.md
+++ b/docs/docs/v3/core/configuration.md
@@ -1 +1,181 @@
-# Configuration
\ No newline at end of file
+# Configuration
+
+`MarkwonConfiguration` class holds common Markwon functionality.
+These are _configurable_ properties:
+* `SyntaxHighlight`
+* `LinkSpan.Resolver`
+* `UrlProcessor`
+* `ImageSizeResolver`
+* `MarkwonHtmlParser`
+
+:::tip
+Additionally `MarkwonConfiguration` holds:
+* `MarkwonTheme`
+* `AsyncDrawableLoader`
+* `MarkwonHtmlRenderer`
+* `MarkwonSpansFactory`
+
+Please note that these values can be retrieved from `MarkwonConfiguration`
+instance, but their _configuration_ must be done by a `Plugin` by overriding
+one of the methods:
+* `Plugin#configureTheme`
+* `Plugin#configureImages`
+* `Plugin#configureHtmlRenderer`
+* `Plugin#configureSpansFactory`
+:::
+
+## SyntaxHighlight
+
+```java
+final Markwon markwon = Markwon.builder(this)
+ .usePlugin(new AbstractMarkwonPlugin() {
+ @Override
+ public void configureConfiguration(@NonNull MarkwonConfiguration.Builder builder) {
+ builder.syntaxHighlight(new SyntaxHighlightNoOp());
+ }
+ })
+ .build();
+```
+
+:::tip
+Use [syntax-highlight](/docs/v3/syntax-highlight/) to add syntax highlighting
+to your application
+:::
+
+## LinkSpan.Resolver
+
+React to a link click event. By default `LinkResolverDef` is used,
+which tries to start an Activity given the `link` argument. If no
+Activity can handle `link` `LinkResolverDef` silently ignores click event
+
+```java
+final Markwon markwon = Markwon.builder(this)
+ .usePlugin(new AbstractMarkwonPlugin() {
+ @Override
+ public void configureConfiguration(@NonNull MarkwonConfiguration.Builder builder) {
+ builder.linkResolver(new LinkSpan.Resolver() {
+ @Override
+ public void resolve(View view, @NonNull String link) {
+ // react to link click here
+ }
+ });
+ }
+ })
+ .build();
+```
+
+:::tip
+Please note that `Markwon` will apply `LinkMovementMethod` to a resulting TextView
+if there is none registered. if you wish to register own instance of a `MovementMethod`
+apply it directly to a TextView or use [MovementMethodPlugin](/docs/v3/core/movement-method-plugin.md)
+:::
+
+## UrlProcessor
+
+Process URLs in your markdown (for links and images). If not provided explicitly,
+default **no-op** implementation will be used, which does not modify URLs (keeping them as-is).
+
+`Markwon` provides 2 implementations of `UrlProcessor`:
+* `UrlProcessorRelativeToAbsolute`
+* `UrlProcessorAndroidAssets`
+
+### UrlProcessorRelativeToAbsolute
+
+`UrlProcessorRelativeToAbsolute` can be used to make relative URL absolute. For example if an image is
+defined like this: `` and `UrlProcessorRelativeToAbsolute`
+is created with `https://github.com/noties/Markwon/raw/master/` as the base:
+`new UrlProcessorRelativeToAbsolute("https://github.com/noties/Markwon/raw/master/")`,
+then final image will have `https://github.com/noties/Markwon/raw/master/art/image.JPG`
+as the destination.
+
+### UrlProcessorAndroidAssets
+
+`UrlProcessorAndroidAssets` can be used to make processed links to point to Android assets folder.
+So an image: `` will have `file:///android_asset/art/image.JPG` as the
+destination.
+
+:::tip
+Please note that `UrlProcessorAndroidAssets` will process only URLs that have no `scheme` information,
+so a `./art/image.png` will become `file:///android_asset/art/image.JPG` whilst `https://so.me/where.png`
+will be kept as-is.
+:::
+
+:::warning
+In order to display an image from assets you still need to register `ImagesPlugin#createWithAssets(Context)`
+plugin in resulting `Markwon` instance. As `UrlProcessorAndroidAssets` only
+_processes_ URLs and doesn't take any part in displaying an image.
+:::
+
+
+## ImageSizeResolver
+
+`ImageSizeResolver` controls the size of an image to be displayed. Currently it
+handles only HTML images (specified via `img` tag).
+
+```java
+final Markwon markwon = Markwon.builder(this)
+ .usePlugin(new AbstractMarkwonPlugin() {
+ @Override
+ public void configureConfiguration(@NonNull MarkwonConfiguration.Builder builder) {
+ builder.imageSizeResolver(new ImageSizeResolver() {
+ @NonNull
+ @Override
+ public Rect resolveImageSize(
+ @Nullable ImageSize imageSize,
+ @NonNull Rect imageBounds,
+ int canvasWidth,
+ float textSize) {
+ return null;
+ }
+ });
+ }
+ })
+ .build();
+```
+
+If not provided explicitly, default `ImageSizeResolverDef` implementation will be used.
+It handles 3 dimension units:
+* `%` (percent, relative to Canvas width)
+* `em` (relative to text size)
+* `px` (absolute size, every dimension that is not `%` or `em` is considered to be _absolute_)
+
+```html
+
+
+
+```
+
+`ImageSizeResolverDef` keeps the ratio of original image if one of the dimensions is missing.
+
+:::warning Height%
+There is no support for `%` units for `height` dimension. This is due to the fact that
+height of an TextView in which markdown is displayed is non-stable and changes with time
+(for example when image is loaded and applied to a TextView it will _increase_ TextView's height),
+so we will have no point-of-reference from which to _calculate_ image height.
+:::
+
+:::tip
+`ImageSizeResolverDef` also takes care for an image to **not** exceed
+canvas width. If an image has greater width than a TextView Canvas, then
+image will be _scaled-down_ to fit the canvas. Please note that this rule
+applies only if image has no absolute sizes (for example width is specified
+in pixels).
+:::
+
+## MarkwonHtmlParser
+
+Specify which HTML parser to use. Default implementation is **no-op**.
+
+:::warning
+One must explicitly use [HtmlPlugin](/docs/v3/html/) in order to display
+HTML content in markdown. Without specified HTML parser **no HTML content
+will be rendered**.
+
+```java
+Markwon.builder(context)
+ .usePlugin(HtmlPlugin.create())
+```
+
+Please note that adding `HtmlPlugin` will take care of initializing parser,
+so after `HtmlPlugin` is used, no additional configuration steps are required.
+:::
\ No newline at end of file
diff --git a/docs/docs/v3/core/core-plugin.md b/docs/docs/v3/core/core-plugin.md
index e6da8eab..3a63c2c5 100644
--- a/docs/docs/v3/core/core-plugin.md
+++ b/docs/docs/v3/core/core-plugin.md
@@ -75,6 +75,8 @@ Beware of this if you would like to override only one of the list types. This is
done to correspond to `commonmark-java` implementation.
:::
+More information about props can be found [here](/docs/v3/core/render-props.md)
+
---
:::tip Soft line break
diff --git a/docs/docs/v3/core/images.md b/docs/docs/v3/core/images.md
index b2f93c05..01ca81ce 100644
--- a/docs/docs/v3/core/images.md
+++ b/docs/docs/v3/core/images.md
@@ -113,6 +113,40 @@ be used.
## MediaDecoder
+By default `core` artifact comes with _default image decoder_ only. It's called
+`ImageMediaDecoder` and it can decode all the formats that `BitmapFactory#decodeStream(InputStream)`
+can.
+
+```java
+final Markwon markwon = Markwon.builder(this)
+ .usePlugin(ImagesPlugin.create(this))
+ .usePlugin(new AbstractMarkwonPlugin() {
+ @Override
+ public void configureImages(@NonNull AsyncDrawableLoader.Builder builder) {
+ builder.addMediaDecoder("text/plain", new TextPlainMediaDecoder());
+ }
+ })
+ .build();
+
+```
+
+`MediaDecoder` is a class to turn `InputStream` into a `Drawable`:
+
+```java
+public abstract class MediaDecoder {
+
+ @Nullable
+ public abstract Drawable decode(@NonNull InputStream inputStream);
+}
+```
+
+:::tip
+If you want to display GIF or SVG images also, you can use [image-gif](/docs/v3/image/gif.md)
+and [image-svg](/docs/v3/image/svg.md) modules.
+:::
+
+##
+
:::tip
If you are using [html](/docs/v3/html/) you do not have to additionally setup
images displayed via `
` tag, as `HtmlPlugin` automatically uses configured
diff --git a/docs/docs/v3/core/plugins.md b/docs/docs/v3/core/plugins.md
index 32e4c51b..dd83ab76 100644
--- a/docs/docs/v3/core/plugins.md
+++ b/docs/docs/v3/core/plugins.md
@@ -425,7 +425,7 @@ queried directly from a TextView.
## What happens underneath
-Here is an approximation of how a `Markwon` instance will handle plugins:
+Here is what happens inside `Markwon` when `setMarkdown` method is called:
```java
// `Markwon#create` implicitly uses CorePlugin
diff --git a/docs/docs/v3/core/render-props.md b/docs/docs/v3/core/render-props.md
new file mode 100644
index 00000000..9dd18004
--- /dev/null
+++ b/docs/docs/v3/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/v3/core/visitor.md b/docs/docs/v3/core/visitor.md
index 3e8ba057..f0cce0f2 100644
--- a/docs/docs/v3/core/visitor.md
+++ b/docs/docs/v3/core/visitor.md
@@ -1 +1,73 @@
-# Visitor
\ No newline at end of file
+# 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/v3/core/configuration.md)
+* `MarkwonVisitor#renderProps` - getter for current [RenderProps](/docs/v3/core/render-props.md)
+* `MarkwonVisitor#builder` - getter for current `SpannableBuilder`
+
+It contains also a number of utility functions:
+* `visitChildren(Node)` - will visit all children of supplied Node
+* `hasNext(Node)` - utility function to check if supplied Node has a Node after it (useful for white-space management, so there should be no blank new line after last BlockNode)
+* `ensureNewLine` - will insert a new line at current `SpannableBuilder` position only if current (last) character is not a new-line
+* `forceNewLine` - will insert a new line character without any condition checking
+* `length` - helper function to call `visitor.builder().length()`, returns current length of `SpannableBuilder`
+* `clear` - will clear state for `RenderProps` and `SpannableBuilder`, this is done by `Markwon` automatically after each render call
+
+And some utility functions to control the spans:
+* `setSpans(int start, Object spans)` - will apply supplied `spans` on `SpannableBuilder` starting at `start` position and ending at `SpannableBuilder#length`. `spans` can be `null` (no spans will be applied) or an array of spans (each span of this array will be applied)
+* `setSpansForNodeOptional(N node, int start)` - helper method to set spans for specified `node` (internally obtains `SpanFactory` for that node and uses it to apply spans)
+* `setSpansForNode(N node, int start)` - almost the same as `setSpansForNodeOptional` but instead of silently ignoring call if none `SpanFactory` is registered, this method will throw an exception.
+
+```java
+@Override
+public void configureVisitor(@NonNull MarkwonVisitor.Builder builder) {
+ builder.on(Heading.class, new MarkwonVisitor.NodeVisitor() {
+ @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/markwon-core/src/main/java/ru/noties/markwon/MarkwonVisitor.java b/markwon-core/src/main/java/ru/noties/markwon/MarkwonVisitor.java
index 52cc6cb6..e7a83db0 100644
--- a/markwon-core/src/main/java/ru/noties/markwon/MarkwonVisitor.java
+++ b/markwon-core/src/main/java/ru/noties/markwon/MarkwonVisitor.java
@@ -79,6 +79,11 @@ public interface MarkwonVisitor extends Visitor {
*/
int length();
+ /**
+ * Clears state of visitor (both {@link RenderProps} and {@link SpannableBuilder} will be cleared
+ */
+ void clear();
+
/**
* Sets spans
to underlying {@link SpannableBuilder} from start
* to {@link SpannableBuilder#length()}.
@@ -88,11 +93,6 @@ public interface MarkwonVisitor extends Visitor {
*/
void setSpans(int start, @Nullable Object spans);
- /**
- * Clears state of visitor (both {@link RenderProps} and {@link SpannableBuilder} will be cleared
- */
- void clear();
-
/**
* Helper method to obtain and apply spans for supplied Node. Internally queries {@link SpanFactory}
* for the node (via {@link MarkwonSpansFactory#require(Class)} thus throwing an exception
diff --git a/markwon-core/src/main/java/ru/noties/markwon/utils/DumpNodes.java b/markwon-core/src/main/java/ru/noties/markwon/utils/DumpNodes.java
new file mode 100644
index 00000000..3b45d238
--- /dev/null
+++ b/markwon-core/src/main/java/ru/noties/markwon/utils/DumpNodes.java
@@ -0,0 +1,110 @@
+package ru.noties.markwon.utils;
+
+import android.support.annotation.NonNull;
+import android.support.annotation.Nullable;
+
+import org.commonmark.node.Block;
+import org.commonmark.node.Node;
+import org.commonmark.node.Visitor;
+
+import java.lang.reflect.InvocationHandler;
+import java.lang.reflect.Method;
+import java.lang.reflect.Proxy;
+
+// utility class to print parsed Nodes hierarchy
+public abstract class DumpNodes {
+
+ public interface NodeProcessor {
+ @NonNull
+ String process(@NonNull Node node);
+ }
+
+ @NonNull
+ public static String dump(@NonNull Node node) {
+ return dump(node, null);
+ }
+
+ @NonNull
+ public static String dump(@NonNull Node node, @Nullable NodeProcessor nodeProcessor) {
+
+ final NodeProcessor processor = nodeProcessor != null
+ ? nodeProcessor
+ : new NodeProcessorToString();
+
+ final Indent indent = new Indent();
+ final StringBuilder builder = new StringBuilder();
+ final Visitor visitor = (Visitor) Proxy.newProxyInstance(
+ Visitor.class.getClassLoader(),
+ new Class[]{Visitor.class},
+ new InvocationHandler() {
+ @Override
+ public Object invoke(Object proxy, Method method, Object[] args) {
+
+ final Node argument = (Node) args[0];
+
+ // initial indent
+ indent.appendTo(builder);
+
+ // node info
+ builder.append(processor.process(argument));
+
+ if (argument instanceof Block) {
+ builder.append(" [\n");
+ indent.increment();
+ visitChildren((Visitor) proxy, argument);
+ indent.decrement();
+ indent.appendTo(builder);
+ builder.append("]\n");
+ } else {
+ builder.append('\n');
+ }
+ return null;
+ }
+ });
+ node.accept(visitor);
+ return builder.toString();
+ }
+
+ private DumpNodes() {
+ }
+
+ private static class Indent {
+
+ private int count;
+
+ void increment() {
+ count += 1;
+ }
+
+ void decrement() {
+ count -= 1;
+ }
+
+ void appendTo(@NonNull StringBuilder builder) {
+ for (int i = 0; i < count; i++) {
+ builder
+ .append(' ')
+ .append(' ');
+ }
+ }
+ }
+
+ private static void visitChildren(@NonNull Visitor visitor, @NonNull Node parent) {
+ Node node = parent.getFirstChild();
+ while (node != null) {
+ // A subclass of this visitor might modify the node, resulting in getNext returning a different node or no
+ // node after visiting it. So get the next node before visiting.
+ Node next = node.getNext();
+ node.accept(visitor);
+ node = next;
+ }
+ }
+
+ private static class NodeProcessorToString implements NodeProcessor {
+ @NonNull
+ @Override
+ public String process(@NonNull Node node) {
+ return node.toString();
+ }
+ }
+}
diff --git a/sample/src/main/java/ru/noties/markwon/sample/recycler/RecyclerActivity.java b/sample/src/main/java/ru/noties/markwon/sample/recycler/RecyclerActivity.java
index c230aa6c..d366a64e 100644
--- a/sample/src/main/java/ru/noties/markwon/sample/recycler/RecyclerActivity.java
+++ b/sample/src/main/java/ru/noties/markwon/sample/recycler/RecyclerActivity.java
@@ -13,6 +13,8 @@ import android.text.TextUtils;
import org.commonmark.ext.gfm.tables.TableBlock;
import org.commonmark.ext.gfm.tables.TablesExtension;
import org.commonmark.node.FencedCodeBlock;
+import org.commonmark.node.Heading;
+import org.commonmark.node.SoftLineBreak;
import org.commonmark.parser.Parser;
import java.io.BufferedReader;
@@ -27,7 +29,9 @@ import ru.noties.markwon.AbstractMarkwonPlugin;
import ru.noties.markwon.Markwon;
import ru.noties.markwon.MarkwonConfiguration;
import ru.noties.markwon.MarkwonVisitor;
+import ru.noties.markwon.SpanFactory;
import ru.noties.markwon.core.CorePlugin;
+import ru.noties.markwon.core.CoreProps;
import ru.noties.markwon.html.HtmlPlugin;
import ru.noties.markwon.image.ImagesPlugin;
import ru.noties.markwon.image.svg.SvgPlugin;
@@ -48,6 +52,36 @@ public class RecyclerActivity extends Activity {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_recycler);
+ {
+final Markwon markwon = Markwon.builder(contex)
+ .usePlugin(new AbstractMarkwonPlugin() {
+@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();
+ }
+ }
+ });
+}
+ });
+ }
+
// create MarkwonAdapter and register two blocks that will be rendered differently
// * fenced code block (can also specify the same Entry for indended code block)
// * table block