Markdown-It Plugin Development
Resources
Useful tools / Resources:
- Source code
- Official docs
- Markdown-It web demo
- Use the
debug
tab to inspect the AST / children
- Use the
- utils.js
- PRs / issues around documentation: #619, #10, #711
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 byCore.process
(inparser_core.js
).- Called with just
state
- expected
void
return - position is not tracked
- Called with just
block
: Rules are called by.tokenize()
(in paser_block.js).- Called with
state
,line
,endLine
,silent
- expected
boolean
return (true
stops further rule processing)
- Called with
inline
: Similar toblock
. 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 becomepending
- Called with
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
, andlib/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.
- 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
- How do I add extra spacing between elements?
- You can use a
text
token, with content of\n
(newline character). Example code.
- You can use a
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
});