From e1d553096240b74f37f9be91c2d74a893171932f Mon Sep 17 00:00:00 2001 From: Dimitry Ivanov Date: Sun, 24 Feb 2019 17:10:58 +0300 Subject: [PATCH] Working with documentation --- docs/.vuepress/config.js | 3 +- docs/docs/v3/README.md | 2 +- docs/docs/v3/core/configuration.md | 182 +++++++++++++++++- docs/docs/v3/core/core-plugin.md | 2 + docs/docs/v3/core/images.md | 34 ++++ docs/docs/v3/core/plugins.md | 2 +- docs/docs/v3/core/render-props.md | 75 ++++++++ docs/docs/v3/core/visitor.md | 74 ++++++- .../ru/noties/markwon/MarkwonVisitor.java | 10 +- .../ru/noties/markwon/utils/DumpNodes.java | 110 +++++++++++ .../sample/recycler/RecyclerActivity.java | 34 ++++ 11 files changed, 518 insertions(+), 10 deletions(-) create mode 100644 docs/docs/v3/core/render-props.md create mode 100644 markwon-core/src/main/java/ru/noties/markwon/utils/DumpNodes.java 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: `![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 + + + +``` + +`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