Working with documentation

This commit is contained in:
Dimitry Ivanov 2019-02-24 17:10:58 +03:00
parent c390cb0502
commit e1d5530962
11 changed files with 518 additions and 10 deletions

View File

@ -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/',

View File

@ -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`

View File

@ -1 +1,181 @@
# Configuration
# 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: `![img](./art/image.JPG)` 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: `![img](./art/image.JPG)` will have `file:///android_asset/art/image.JPG` as the
destination.
:::tip
Please note that `UrlProcessorAndroidAssets` will process only URLs that have no `scheme` information,
so a `./art/image.png` will become `file:///android_asset/art/image.JPG` whilst `https://so.me/where.png`
will be kept as-is.
:::
:::warning
In order to display an image from assets you still need to register `ImagesPlugin#createWithAssets(Context)`
plugin in resulting `Markwon` instance. As `UrlProcessorAndroidAssets` only
_processes_ URLs and doesn't take any part in displaying an image.
:::
## ImageSizeResolver
`ImageSizeResolver` controls the size of an image to be displayed. Currently it
handles only HTML images (specified via `img` tag).
```java
final Markwon markwon = Markwon.builder(this)
.usePlugin(new AbstractMarkwonPlugin() {
@Override
public void configureConfiguration(@NonNull MarkwonConfiguration.Builder builder) {
builder.imageSizeResolver(new ImageSizeResolver() {
@NonNull
@Override
public Rect resolveImageSize(
@Nullable ImageSize imageSize,
@NonNull Rect imageBounds,
int canvasWidth,
float textSize) {
return null;
}
});
}
})
.build();
```
If not provided explicitly, default `ImageSizeResolverDef` implementation will be used.
It handles 3 dimension units:
* `%` (percent, relative to Canvas width)
* `em` (relative to text size)
* `px` (absolute size, every dimension that is not `%` or `em` is considered to be _absolute_)
```html
<img width="100%">
<img width="2em" height="10px">
<img style="{width: 100%; height: 8em;}">
```
`ImageSizeResolverDef` keeps the ratio of original image if one of the dimensions is missing.
:::warning Height%
There is no support for `%` units for `height` dimension. This is due to the fact that
height of an TextView in which markdown is displayed is non-stable and changes with time
(for example when image is loaded and applied to a TextView it will _increase_ TextView's height),
so we will have no point-of-reference from which to _calculate_ image height.
:::
:::tip
`ImageSizeResolverDef` also takes care for an image to **not** exceed
canvas width. If an image has greater width than a TextView Canvas, then
image will be _scaled-down_ to fit the canvas. Please note that this rule
applies only if image has no absolute sizes (for example width is specified
in pixels).
:::
## MarkwonHtmlParser
Specify which HTML parser to use. Default implementation is **no-op**.
:::warning
One must explicitly use [HtmlPlugin](/docs/v3/html/) in order to display
HTML content in markdown. Without specified HTML parser **no HTML content
will be rendered**.
```java
Markwon.builder(context)
.usePlugin(HtmlPlugin.create())
```
Please note that adding `HtmlPlugin` will take care of initializing parser,
so after `HtmlPlugin` is used, no additional configuration steps are required.
:::

View File

@ -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

View File

@ -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 `<img>` tag, as `HtmlPlugin` automatically uses configured

View File

@ -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

View File

@ -0,0 +1,75 @@
# RenderProps <Badge text="3.0.0" />
`RenderProps` encapsulates passing arguments from a node visitor to a node renderer.
Without hardcoding arguments into an API method calls.
`RenderProps` is the state collection for `Props` that are set by a node visitor and
retrieved by a node renderer.
```java
public class Prop<T> {
@NonNull
public static <T> Prop<T> of(@NonNull String name) {
return new Prop<>(name);
}
/* ... */
}
```
For example `CorePlugin` defines a _Heading level_ prop (inside `CoreProps` class):
```java
public static final Prop<Integer> HEADING_LEVEL = Prop.of("heading-level");
```
Then CorePlugin registers a `Heading` node visitor and applies heading value:
```java
@Override
public void configureVisitor(@NonNull MarkwonVisitor.Builder builder) {
builder.on(Heading.class, new MarkwonVisitor.NodeVisitor<Heading>() {
@Override
public void visit(@NonNull MarkwonVisitor visitor, @NonNull Heading heading) {
/* Heading node handling logic */
// set heading level
CoreProps.HEADING_LEVEL.set(visitor.renderProps(), heading.getLevel());
// a helper method to apply span(s) for a node
// (internally obtains a SpanFactory for Heading or silently ignores
// this call if no factory for a Heading is registered)
visitor.setSpansForNodeOptional(heading, start);
/* Heading node handling logic */
}
});
}
```
And finally `HeadingSpanFactory` (which is also registered by `CorePlugin`):
```java
public class HeadingSpanFactory implements SpanFactory {
@Nullable
@Override
public Object getSpans(@NonNull MarkwonConfiguration configuration, @NonNull RenderProps props) {
return new HeadingSpan(
configuration.theme(),
CoreProps.HEADING_LEVEL.require(props)
);
}
}
```
---
`Prop<T>` has these methods:
* `@Nullable T get(RenderProps)` - returns value stored in RenderProps or `null` if none is present
* `@NonNull T get(RenderProps, @NonNull T defValue)` - returns value stored in RenderProps or default value (this method always return non-null value)
* `@NonNull T require(RenderProps)` - returns value stored in RenderProps or _throws an exception_ if none is present
* `void set(RenderProps, @Nullable T value)` - updates value stored in RenderProps, passing `null` as value is the same as calling `clear`
* `void clear(RenderProps)` - clears value stored in RenderProps

View File

@ -1 +1,73 @@
# Visitor
# Visitor
Starting with <Badge text="3.0.0" /> _visiting_ of parsed markdown
nodes does not require creating own instance of commonmark-java `Visitor`,
instead a composable/configurable `MarkwonVisitor` is used.
## Visitor.Builder
There is no need to create own instance of `MarkwonVisitor.Builder` as
it is done by `Markwon` itself. One still can configure it as one wishes:
```java
final Markwon markwon = Markwon.builder(contex)
.usePlugin(new AbstractMarkwonPlugin() {
@Override
public void configureVisitor(@NonNull MarkwonVisitor.Builder builder) {
builder.on(SoftLineBreak.class, new MarkwonVisitor.NodeVisitor<SoftLineBreak>() {
@Override
public void visit(@NonNull MarkwonVisitor visitor, @NonNull SoftLineBreak softLineBreak) {
visitor.forceNewLine();
}
});
}
});
```
---
`MarkwonVisitor` encapsulates most of the functionality of rendering parsed markdown.
It holds rendering configuration:
* `MarkwonVisitor#configuration` - getter for current [MarkwonConfiguration](/docs/v3/core/configuration.md)
* `MarkwonVisitor#renderProps` - getter for current [RenderProps](/docs/v3/core/render-props.md)
* `MarkwonVisitor#builder` - getter for current `SpannableBuilder`
It contains also a number of utility functions:
* `visitChildren(Node)` - will visit all children of supplied Node
* `hasNext(Node)` - utility function to check if supplied Node has a Node after it (useful for white-space management, so there should be no blank new line after last BlockNode)
* `ensureNewLine` - will insert a new line at current `SpannableBuilder` position only if current (last) character is not a new-line
* `forceNewLine` - will insert a new line character without any condition checking
* `length` - helper function to call `visitor.builder().length()`, returns current length of `SpannableBuilder`
* `clear` - will clear state for `RenderProps` and `SpannableBuilder`, this is done by `Markwon` automatically after each render call
And some utility functions to control the spans:
* `setSpans(int start, Object spans)` - will apply supplied `spans` on `SpannableBuilder` starting at `start` position and ending at `SpannableBuilder#length`. `spans` can be `null` (no spans will be applied) or an array of spans (each span of this array will be applied)
* `setSpansForNodeOptional(N node, int start)` - helper method to set spans for specified `node` (internally obtains `SpanFactory` for that node and uses it to apply spans)
* `setSpansForNode(N node, int start)` - almost the same as `setSpansForNodeOptional` but instead of silently ignoring call if none `SpanFactory` is registered, this method will throw an exception.
```java
@Override
public void configureVisitor(@NonNull MarkwonVisitor.Builder builder) {
builder.on(Heading.class, new MarkwonVisitor.NodeVisitor<Heading>() {
@Override
public void visit(@NonNull MarkwonVisitor visitor, @NonNull Heading heading) {
// or just `visitor.length()`
final int start = visitor.builder().length();
visitor.visitChildren(heading);
// or just `visitor.setSpansForNodeOptional(heading, start)`
final SpanFactory factory = visitor.configuration().spansFactory().get(heading.getClass());
if (factory != null) {
visitor.setSpans(start, factory.getSpans(visitor.configuration(), visitor.renderProps()));
}
if (visitor.hasNext(heading)) {
visitor.ensureNewLine();
visitor.forceNewLine();
}
}
});
}
```

View File

@ -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 <code>spans</code> to underlying {@link SpannableBuilder} from <em>start</em>
* to <em>{@link SpannableBuilder#length()}</em>.
@ -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

View File

@ -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();
}
}
}

View File

@ -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<Heading>() {
@Override
public void visit(@NonNull MarkwonVisitor visitor, @NonNull Heading heading) {
// or just `visitor.length()`
final int start = visitor.builder().length();
visitor.visitChildren(heading);
// or just `visitor.setSpansForNodeOptional(heading, start)`
final SpanFactory factory = visitor.configuration().spansFactory().get(heading.getClass());
if (factory != null) {
visitor.setSpans(start, factory.getSpans(visitor.configuration(), visitor.renderProps()));
}
if (visitor.hasNext(heading)) {
visitor.ensureNewLine();
visitor.forceNewLine();
}
}
});
}
});
}
// 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