Plugins 3.0.0

WARNING

This is documentation for legacy versions. For the most current version click here.

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)
    .usePlugin(CorePlugin.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.

Parser

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

final Markwon markwon = Markwon.builder(context)
        .usePlugin(CorePlugin.create())
        .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();

WARNING

CorePlugin has special handling - it will be implicitly added if a plugin declares dependency on it. This is why in previous example we haven't added CorePlugin explicitly as AbstractMarkwonPlugin declares a dependency on it. If it's not desireable override AbstractMarkwonPlugin#priority method to specify own rules.

More information about MarkwonTheme can be found here.

Images

Since 3.0.0 core images functionality moved to the core module. Now Markwon comes bundled with support for regular images (no SVG or GIF, they defined in standalone modules now). And 3(4) schemes supported by default:

  • http (+https; using system built-in HttpURLConnection)
  • file (including Android assets)
  • data (image inline, data:image/svg+xml;base64,!@#$%^&*()
final Markwon markwon = Markwon.builder(context)
        .usePlugin(ImagesPlugin.create())
        .usePlugin(new AbstractMarkwonPlugin() {
            @Override
            public void configureImages(@NonNull AsyncDrawableLoader.Builder builder) {
                // sorry, these are not bundled with the library
                builder
                        .addSchemeHandler("ftp", new FtpSchemeHandler("root", ""))
                        .addMediaDecoder("text/plain", new AnsiiMediaDecoder());
            }
        })
        .build();

WARNING

Although ImagesPlugin is bundled with the core artifact, it is not used by default and one must explicitly add it:

Markwon.builder(context)
        .usePlugin(ImagesPlugin.create(context));

Without explicit usage of ImagesPlugin all image configuration will be ignored (no-op'ed)

More information about dealing with images 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:

  • SyntaxHighlight (highlighting code blocks)
  • LinkResolver (opens links in markdown)
  • UrlProcessor (process URLs in markdown for both links and images)
  • MarkwonHtmlParser (HTML parser)
  • 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) {
                // MarkwonHtmlParserImpl is defined in `markwon-html` artifact
                builder.htmlParser(MarkwonHtmlParserImpl.create());
            }
        })
        .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) {
                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, we can disable Heading Node rendering:

builder.on(Heading.class, null);

Please note that Priority plays nicely here to ensure that your custom Node override/disable happens after some plugin defines it.

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

HTML Renderer

MarkwonHtmlRenderer controls how HTML is rendered in markdown.

final Markwon markwon = Markwon.builder(context)
        .usePlugin(HtmlPlugin.create())
        .usePlugin(new AbstractMarkwonPlugin() {
            @Override
            public void configureHtmlRenderer(@NonNull MarkwonHtmlRenderer.Builder builder) {
                // <center> tag handling (deprecated but valid in our case)
                // can be any tag name, there is no connection with _real_ HTML tags,
                // <just-try-to-not-go-crazy-and-remember-about-portability>
                builder.addHandler("center", new SimpleTagHandler() {
                    @Override
                    public Object getSpans(@NonNull MarkwonConfiguration configuration, @NonNull RenderProps renderProps, @NonNull HtmlTag tag) {
                        return new AlignmentSpan() {
                            @Override
                            public Layout.Alignment getAlignment() {
                                return Layout.Alignment.ALIGN_CENTER;
                            }
                        };
                    }
                });
            }
        })
        .build();

WARNING

Although MarkwonHtmlRenderer is bundled with core artifact, actual HTML parser is placed in a standalone artifact and must be added to your project explicitly and then registered via Markwon.Builder#usePlugin(HtmlPlugin.create()). If not done so, no HTML will be parsed nor rendered.

More information about HTML rendering can be found here

Priority

Priority is an abstraction to state dependency connection between plugins. It is also used as a runtime graph validator. If a plugin defines a dependency on other, but other is not in resulting Markwon instance, then a runtime exception will be thrown. Priority is also defines the order in which plugins will be placed. So, if a plugin A states a plugin B as a dependency, then plugin A will come after plugin B.

final Markwon markwon = Markwon.builder(context)
        .usePlugin(new AbstractMarkwonPlugin() {
            @NonNull
            @Override
            public Priority priority() {
                return Priority.after(CorePlugin.class);
            }
        })
        .build();

WARNING

Please note that AbstractMarkwonPlugin implicitly defines CorePlugin as a dependency (return Priority.after(CorePlugin.class);). This will also add CorePlugin to a Markwon instance, because it will be added implicitly if a plugin defines it as a dependency.

Use one of the factory methods to create a Priority instance:

// none
Priority.none();

// single dependency
Priority.after(CorePlugin.class);

// 2 dependencies
Priority.after(CorePlugin.class, ImagesPlugin.class);

// for a number >2, use #builder
Priority.builder()
        .after(CorePlugin.class)
        .after(ImagesPlugin.class)
        .after(StrikethroughPlugin.class)
        .build();

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:

// `Markwon#create` implicitly uses CorePlugin
final Markwon markwon = Markwon.builder(context)
        .usePlugin(CorePlugin.create())
        .build();

// 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: 8/6/2019, 7:27:20 PM