Plugins 3.0.0

Since 3.0.0 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:

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:


TIP

if you need to override only few methods of MarkwonPlugin (since it is an interface), AbstractMarkwonPlugin can be used.

Registry 4.0.0

Registry is a special step to pre-configure all registered plugins. It is also used to determine the order of plugins inside Markwon instance.

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<CorePlugin>() {
                    @Override
                    public void apply(@NonNull CorePlugin corePlugin) {

                    }
                });
            }
        })
        .build();

More information about registry can be found here

Parser

For example, let's register a new commonmark-java Parser extension:

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 commonmark-java documentation .

MarkwonTheme

Starting 3.0.0 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).

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.

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

Visitor

MarkwonVisitor 3.0.0 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:

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<Strikethrough>() {
                            @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:

builder.on(Heading.class, null);

More information about MarkwonVisitor can be found here

Spans Factory

MarkwonSpansFactory 3.0.0 is an abstract factory (factory that produces other factories) for spans that Markwon uses. It controls what spans to use for certain Nodes.

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:

@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

Process markdown

A plugin can be used to pre-process input markdown (this will be called before parsing):

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.

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.

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.

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.

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:

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));
Last Updated: 6/12/2019, 6:45:28 PM