Joshua's Docs - Notes on Markdown-it and Building a Plugin
Light

Markdown-It Plugin Development

Resources

Useful tools / Resources:

Sample Plugins (small, single-purpose, easier to read):

You can also search for plugins across NPM by using the markdown-it tag keyword search, or more specifically, searching by markdown-it-plugin.

Renderers vs Rules

Rules get applied before renderers get called, taking state and mutating tokens, and renderers are the final functions that transform tokens into the output string.

See docs:

For example, you might have a rule that takes a single text token and splits it into two new tokens - a text token and a html_block token. Then, at the final stage, the text token would be processed by the default text renderer, and the html_block would be turned into the output string by the default html_block renderer.

Writing a Rule

When writing a rule, there are multiple ways that you can inspect the state of Markdown-it's parsing and modify the output. However, this is also slightly dependent on which part of MDIT you are hooking into (core, block, or inline):

  • core: Rules are called by Core.process (in parser_core.js).

    • Called with just state
    • expected void return
    • position is not tracked
  • block: Rules are called by .tokenize() (in paser_block.js).

    • Called with state, line, endLine, silent
    • expected boolean return (true stops further rule processing)
  • inline: Similar to block. Rules are called by .tokenize() (in parser_inline.js).

    • Called with state, silent
    • expected boolean return (true stops further rule processing)
    • Can update state.pos to move pointer for next chunk of text to become pending

Regardless of which rule type you are writing, usually you are modifying output by either removing, adding, and/or replacing tokens in the chain, which can be directly accessed through state.tokens, as well as utility methods, like state.push or arrayReplaceAt used with tokens.

💡 Tip: If you are looking for examples of how to write rules, make sure to reference the source of the rules that are bundled with Markdown-it! These are contained in lib/rules_core, lib/rules_block, and lib/rules_inline

Overriding Default Renderers

💡 Tip: Given the option, modifying rendering via rules instead of overriding the default renderer is almost always preferred. Or, creating new token types with rules and applying them through new renderers. However, sometimes overriding the default renderer is unavoidable.

For understanding the difference and relationship between renderers and rules, see "Renderers vs Rules". In short, renderers are one of the very last steps before output is returned, so they can be thought of as lower level methods.

The source code actually calls these renderer rules, but I'll refer to them as just renderers to avoid confusion with higher level rules.

The default renderers are accessed via markdownItInstance.renderer.rules.___.

You can override the default renderers by just assigning your own custom render function. For example, if we wanted to replace a macro of {NAME} with your own value:

import type { RenderRule } from 'markdown-it/lib/renderer';

/**
 * We are overriding the default text renderer
 * @see https://github.com/markdown-it/markdown-it/blob/064d602c6890715277978af810a903ab014efc73/lib/renderer.js#L116-L118
 */
const TextRendererOverride: RenderRule = (tokens, index) => {
	let text = tokens[index].content;
	return text.replace(/{NAME}/g, 'Joshua');
};
md.renderer.rules.text = TextRendererOverride;

Since renderer rules live with the instance, it is safe to overwrite them without mutating the uninstantiated class object.

Getting Loaded Rules

The public method for retrieving rules is .getRules() defined here. This method lives on each Ruler instance, and can be accessed like so:

// md is instance of MarkdownIt()

const coreRules = md.core.ruler.getRules('');
const inlineRules = md.inline.ruler.getRules('');
const blockRules = md.block.ruler.getRules('');

However, this has some important limitations. A) it returns functions, without the original rule name, and B) omits rules that are loaded, but disabled.

I'm wary to recommend it, as it relies on methods that are explicitly meant to only be used internally (non-public), but this is the reliable way I have found to retrieve the full lits of rules that have been loaded into Markdown-it:

interface InternalRuleTracker {
	name: string;
	enabled: boolean;
	fn: Function;
	alt: string[];
}

let allRules: InternalRuleTracker[] = [];
const ruleGroups = ['core', 'block', 'inline'] as const;
ruleGroups.forEach((chain) => {
	allRules = allRules.concat(md[chain].ruler.__rules__ || []);
});

You can find this code, and some other helpful methods in a recent utility file I coded.

Misc. FAQ / How Do I...

  • How do I reset the settings for Markdown-it to default?

    • There are some times, especially if your code takes an existing MDIT instance outside of your control, when you might want make sure that all the settings are reset to a standard baseline. You can do this with the .configure() method, which can easily load a preset in one-shot.
  • How do I add extra spacing between elements?

    • You can use a text token, with content of \n (newline character). Example code.

TypeScript Support

Types are currently not bundled with the main markdown-it package, so you will need to install the DT provided types if you want full TS support:

npm i -D @types/markdown-it

Once that is installed, I find that TypeScript support with Markdown-it works well, although it also helps to refer to the source code to better understand the internals of the package.

Once quick tip I can provide is that the .use() method has a generic slot, which is important if you want strong type-checking of the options that belong to the plugin you are loading. For example:

import {MdPlugin, IMdPluginOptions} from 'my-mdit-plugin';


md.use<IMdPluginOptions>(MdPlugin, {
	// ...
	// without passing in the options interface type, these options
	// would not be strongly typed
});
Markdown Source Last Updated:
Mon Jun 28 2021 20:55:36 GMT+0000 (Coordinated Universal Time)
Markdown Source Created:
Tue Jun 15 2021 14:37:44 GMT+0000 (Coordinated Universal Time)
© 2021 Joshua Tzucker, Built with Gatsby
Feedback