Event system
Introduction
The Event System is the basis of the new Listener extensions, replacing the "old" extension system altogether.
In short, the Event System lifecycle looks as follows:
-
A sub-parser emits an event.
Each sub-parser can emit a batch of events (see the list below)
-
Extension A (which is a Listener Extension) registers and listens to a specific event.
An extension can only register for a specific event
-
Extension A receives an event object and modifies it.
Certain properties of the event object can be changed, which will change the behavior or output of the sub-parser
-
Extension A returns the event object to the converter.
-
The converter passes the received event object to the next extension in the chain.
Event Object
Properties
matches
matches is an object that holds the text captured by the sub-parser. The properties of this object are different for each sub-parser.
Some properties can be read-only: their names start with _ (underscore).
blockquote onCapture event
{
_wholeMatch: "> some awesome quote",
blockquote: "some awesome quote"
}
Event types
Events are emitted when a sub-parser runs (or is about to be run).
The makehtml (Markdown to HTML) sub-parsers emit up to four events, in this strict order: onStart -> onCapture -> onHash -> onEnd.
The makeMarkdown (HTML to Markdown) sub-parsers emit a subset of these — see makeMarkdown events below.
onStart
<converter>.<subparser>.onStart: always runs except if the sub-parser is disabled.
Emitted when the sub-parser has started, but no capturing or modifications to the text were done.
Always runs except if the sub-parser is disabled via options.
When to use onStart event
Use this event when you want to change the input passed to the sub-parser.
Please note that the input is the full text that was passed to the converter.
Properties
| property | type | access | description |
|---|---|---|---|
input |
string |
read |
Full text that was passed to the subparser |
output |
string |
write |
Full text with modification that will be passed along the chain |
regexp |
null |
||
matches |
null |
||
attributes |
null |
onCapture
<converter>.<subparser>.onCapture: might not be run.
Emitted when a regex match is found and capture was successful. Further normalization and modification of the regex captured groups might be performed.
Might not be run if no regex match found.
When to use onCapture event
Use this event if you want to:
- modify the sub-parser behavior, text;
- modify the HTML output of the sub-parser.
Properties
| property | type | access | description |
|---|---|---|---|
input |
string |
readonly |
The captured text |
output |
string |
write |
null or well-formed HTML (see the Important Note below) |
regexp |
RegExp |
readonly |
Regular Expression to capture groups |
matches |
object |
read/write |
Match groups. Changes to this object are reflected in the output. |
attributes |
object |
read/write |
Attributes to add to the HTML output |
IMPORTANT NOTE
Extensions listening to the onCapture event should avoid changing the output property.
Instead, they should modify the values of the matches and attribute objects.
The reason is that the output property takes precedence over the matches objects and prevents showdown from calling other sub-parsers inside the captured fragment.
The above means the following:
- If something is passed as the output property, any changes to the matches and attributes objects will be ignored.
- Any changes made by other extensions to the matches or attributes objects will be ignored.
- Showdown will not call other sub-parsers, such as encode code or span gamut in the text fragment, which may lead to unexpected results.
Example
// Showdown extension 1 that is listening to makehtml.blockquote.onCapture
function extension_1 (showdownEvent) {
// Let's imagine you're a bad person who writes to output
showdownEvent.output = '<blockquote>foo</blockquote>'; // must be a well-formed HTML
showdownEvent.matches.blockquote = 'some nice quote';
}
// Showdown extension 2 that is also listening to makehtml.blockquote.onCapture
function extension_2 (showdownEvent) {
// I make blockquotes bold
let quote = showdownEvent.matches.blockquote;
showdownEvent.matches.blockquote = '<strong>' + quote + '</strong>';
}
In the example above, the result will always be <blockquote>foo</blockquote>, regardless of the order of extension loading and call.
Infinite loop
Do not pass the input as output to the onCapture event, or you might trigger an infinite loop.
Open external links in a new tab
The link sub-parsers expose the anchor's attributes on their onCapture event, so a
listener can add target/rel to the generated <a>. This replaces the removed
openLinksInNewWindow option — and, unlike the old option, you control exactly which links
are affected.
The link sub-parsers emit one event per link type (inline, reference, angleBrackets,
autoLink), so register a listener for each type you want to cover:
const converter = new showdown.Converter();
['inline', 'reference', 'angleBrackets', 'autoLink'].forEach(function (type) {
converter.listen('makehtml.link.' + type + '.onCapture', function (evt) {
// leave in-page hash links (#section) opening in the same tab
if (!/^#/.test(evt.attributes.href)) {
evt.attributes.target = '_blank';
evt.attributes.rel = 'noopener noreferrer';
}
return evt;
});
});
converter.makeHtml('[showdown](https://github.com/showdownjs/showdown)');
// <p><a href="https://github.com/showdownjs/showdown" target="_blank"
// rel="noopener noreferrer">showdown</a></p>
Customizing GFM task-list checkboxes
GFM task-list items (- [ ] todo / - [x] done, enabled with the tasklists option) have
their checkbox rendered by a dedicated sub-parser that both list parsers — the default one
and the commonmark-flavor one — delegate to. Because of that, a single listener covers
task lists in every flavor.
It runs on the item's raw source line, before any inline/block parsing, and emits two
events under the makehtml.list.taskListItem namespace:
makehtml.list.taskListItem.checkbox.onCapture— fired on a matched task line. Theattributesobject holds the<input>checkbox attributes, so a listener can re-style or tag the checkbox; thematchesobject exposes the whole line. As with everyonCaptureevent, prefer mutatingmatches/attributesover writingoutput(see the note above).makehtml.list.taskListItem.checkbox.onHash— fired with the rendered line (<input ...> todo) just before it is handed back to the list parser.
onCapture properties
| property | type | access | description |
|---|---|---|---|
input |
string |
readonly |
The full task source line, e.g. [ ] buy milk |
output |
string |
write |
null, or well-formed HTML for the whole line (takes precedence) |
regexp |
RegExp |
readonly |
The marker-matching regular expression |
matches |
object |
read/write |
See below |
attributes |
object |
read/write |
Attributes of the <input> checkbox (type, disabled, checked, …) |
The matches object:
{
_wholeMatch: "[ ] buy milk", // the matched source line
_taskListButton: "[ ]", // the literal marker
_taskListButtonChecked: " ", // the marker char: " ", "x" or "X"
_taskListItemText: " buy milk" // everything after the marker (write to relabel)
}
const converter = new showdown.Converter({ tasklists: true });
// Give every checkbox a class and tag completed items.
converter.listen('makehtml.list.taskListItem.checkbox.onCapture', function (evt) {
evt.attributes.classes = ['task-checkbox'];
if (evt.attributes.checked) {
evt.attributes.classes.push('is-done');
}
return evt;
});
converter.makeHtml('- [x] ship it');
// <ul>
// <li><input checked disabled type="checkbox" class="task-checkbox is-done"> ship it</li>
// </ul>
The makehtml.list.taskListItem namespace
These checkbox events are nested under the broader makehtml.list event family.
Both list parsers — the default one and the commonmark-flavor one — emit the same set,
so every event below fires in every flavor:
makehtml.list.{onStart,onCapture,onHash,onEnd}— the whole list block.makehtml.list.listItem.{onCapture,onHash}— each non-task<li>.makehtml.list.taskListItem.{onCapture,onHash}— each task<li>(the surrounding item, carrying the full item — which for a loose item may span several blocks). This is the event to use to read or rewrite a task item (checkbox and label).makehtml.list.taskListItem.checkbox.{onCapture,onHash}— just the checkbox/label line (above), via the shared sub-parser.
onHash
<converter>.<subparser>.onHash: always runs.
Raised before the output is hashed.
Always runs (except if the sub-parser is disabled via options), even if no hashing is performed.
When to use onHash event
Use this event when you want to change the sub-parser's raw output before it is hashed.
Properties
| property | type | access | description |
|---|---|---|---|
input |
string |
read |
The captured text |
output |
string |
write |
The text that will be passed along the chain |
regexp |
null |
||
matches |
null |
||
attributes |
null |
onEnd
<converter>.<subparser>.onEnd: always runs;
Emitted when the sub-parser has finished its work and is about to exit.
Always runs (except if the sub-parser is disabled via options).
When to use onEnd event
Use this event when you want to run code or perform changes to the text after the subparser has run and its output was hashed.
Please note that the input is the full text and might contain hashed elements.
Properties
| property | type | access | description |
|---|---|---|---|
input |
string |
read |
Full text with the subparser modifications (contains hashes) |
output |
string |
write |
The text that will be passed to other subparsers |
regexp |
null |
||
matches |
null |
makeHtml document-level events
Besides the per-sub-parser events above, makeHtml() emits three document-level events that wrap the whole conversion. They are the place to transform the entire document before or after parsing, and are what lang/output extensions are built on:
makehtml.onStart— emitted once with the raw Markdown, before any escaping or line-ending normalization. Listeners here see the literal source (real$,¨,\r\n, …) and can rewrite it wholesale.makehtml.onPreParse— emitted once after escaping/normalization and immediately before the sub-parsers run. This is wherelangextensions run (as listeners). The input at this stage contains Showdown's internal placeholders — e.g. an escaped$appears as¨Dand an escaped¨as¨T— so preferonStartif you need the untouched source.makehtml.onEnd— emitted once with the final HTML, after every sub-parser and the optional complete-document wrapping. This is whereoutputextensions run (as listeners); use it to post-process the generated HTML.
Properties
| property | type | access | description |
|---|---|---|---|
input |
string |
read |
onStart: raw Markdown. onPreParse: escaped/normalized Markdown. onEnd: the final HTML. |
output |
string |
write |
The text that will be passed along (return a string from the listener, or set this and return the event). |
regexp |
null |
||
matches |
null |
||
attributes |
null |
// Add a class to every paragraph in the final HTML
converter.listen('makehtml.onEnd', function (evt) {
return evt.input.replace(/<p>/g, '<p class="md">');
});
makeMarkdown (HTML to Markdown) events
The reverse converter — <converter>.makeMarkdown(), which turns HTML back into Markdown — also emits namespaced events. Because its sub-parsers operate on DOM nodes (one construct at a time) rather than running regular expressions over text, they emit only two of the lifecycle events: onStart and onEnd. There is no onCapture/onHash phase, since there is no regex capture step.
Event names follow the same <converter>.<subparser>.<event> convention, with makeMarkdown as the converter prefix:
makeMarkdown.<subparser>.onStartmakeMarkdown.<subparser>.onEnd
emitted by each of these sub-parsers: blockquote, break, codeBlock, codeSpan, emphasis, header, hr, image, input, links, list, listItem, paragraph, pre, strikethrough, strong, table, tableCell, txt, underline.
The recursive node dispatcher (the analogue of makehtml's blockGamut/spanGamut) also emits makeMarkdown.node.onStart / makeMarkdown.node.onEnd for every node it processes. Because every node passes through it, this is the one place to observe (or override) content that has no dedicated sub-parser — HTML comments and unknown/raw elements.
In addition, two document-level events wrap the whole conversion:
makeMarkdown.onStart— emitted once with the raw HTML source, before it is parsed into a DOM. Listeners can rewrite the source.makeMarkdown.onEnd— emitted once with the final generated Markdown. Listeners can post-process it.
Properties
Unlike the makehtml events, the input of a makeMarkdown event is a single node/fragment, not the full document text:
| property | type | access | description |
|---|---|---|---|
input |
string |
read |
onStart: the serialized HTML (or text value) of the node being processed. onEnd: the generated Markdown. |
output |
string |
write |
The Markdown string that will be used / passed along (see the override note below). |
matches |
object |
read |
Holds { node } — the source DOM node currently being converted. |
regexp |
null |
||
attributes |
null |
Overriding a node's rendering with onStart
The onStart event's output starts as null. If a listener sets it to a non-empty string, that string is used as the sub-parser's result and the default rendering of the node is skipped (the matching onEnd event still runs). This is the makeMarkdown equivalent of the onCapture output-precedence behavior.
// Render every <a> as bare text instead of a Markdown link
converter.listen('makeMarkdown.links.onStart', function (evt) {
evt.output = evt.matches.node.textContent;
return evt;
});