Skip to content

Commit

Permalink
Added examples on how to add and modify rules (#619)
Browse files Browse the repository at this point in the history
  • Loading branch information
saenglert committed Apr 18, 2022
1 parent e6d1bfd commit bab0baf
Showing 1 changed file with 262 additions and 0 deletions.
262 changes: 262 additions & 0 deletions docs/examples/renderer_rules.md
@@ -0,0 +1,262 @@
# Adding or modifying rules
## Default renderer rules
Rules on how to translate markdown content to HTML elements are stored in `renderer.rules`:

```
const MarkdownIt = require('markdown-it');
const md = new MarkdownIt();
console.log(Object.keys(md.renderer.rules))
```
Output:
```
[
'code_inline',
'code_block',
'fence',
'image',
'hardbreak',
'softbreak',
'text',
'html_block',
'html_inline'
]
```
These are the default renderer rules. For any element that is not explicitly listed in this array its default rule applies. For example the rule `bullet_list_open` is not defined, so when markdown-it tries to parse a list to HTML it defaults to ua generic renderer called `Renderer.prototype.renderToken`.

## The demo tool

You can use the [demo tool](https://markdown-it.github.io/) to see which specific rule name corresponds to which HTML tag (switch to the debug tab in the output).

Let's use a Hello World example:
[Link to Demo](https://markdown-it.github.io/#md3=%7B%22source%22%3A%22-%20Hello%20World%22%2C%22defaults%22%3A%7B%22html%22%3Afalse%2C%22xhtmlOut%22%3Afalse%2C%22breaks%22%3Afalse%2C%22langPrefix%22%3A%22language-%22%2C%22linkify%22%3Afalse%2C%22typographer%22%3Afalse%2C%22_highlight%22%3Afalse%2C%22_strict%22%3Afalse%2C%22_view%22%3A%22debug%22%7D%7D)

Now take a closer look at the first element in the resulting list:
```
{
"type": "bullet_list_open",
"tag": "ul",
"attrs": null,
"map": [
0,
1
],
"nesting": 1,
"level": 0,
"children": null,
"content": "",
"markup": "-",
"info": "",
"meta": null,
"block": true,
"hidden": false
}
```
This is a [Token](https://markdown-it.github.io/markdown-it/#Token). Its corresponding HTML `tag` is `ul` and its nesting is `1`. This means this specific token represents the opening tag of the HTML list we want to generate from markdown.

* `{ nesting: 1}` is an opening tag: `<ul>`
* `{ nesting: -1}` is a closing tag: `</ul>`
* `{ nesting: 0}` is a self-closing tag: `<br />`

## Adding new rules
### To add a default CSS class to an element

Let's set ourself a goal:
```
Create a rule to add the CSS class "lorem_ipsum" to every <ul>
```

Rules are functions that accept a number of parameters:
```
const MarkdownIt = require('markdown-it');
const md = new MarkdownIt();
md.renderer.rules.bullet_list_open = function(tokens, idx, options, env, self) {
// tokes: List of all tokens being parsed
// idx: Number that corresponds to the key of the current token in tokens
// options: The options defined when creating the new markdown-it object ({} in our case)
// env ???
// self: A reference to the renderer itself
};
```
We assign the new rule to the key that corresponds to the html tag we want to modify.

#### Reusing existing rules

It is good practice however to save the default renderer for your element and only make minimal chances to the rules in place, instead of reinventing the wheel:

```
const MarkdownIt = require('markdown-it');
const md = new MarkdownIt();
const proxy = (tokens, idx, options, env, self) => self.renderToken(tokens, idx, options);
const defaultBulletListOpenRenderer = md.renderer.rules.bullet_list_open || proxy;
md.renderer.rules.bullet_list_open = function(tokens, idx, options, env, self) {
// Make your changes here ...
// ... then render it using the existing logic
return defaultBulletListOpenRenderer(tokens, idx, options, env, self)
};
```
Earlier we noticed that `renderer.rules.bullet_list_open` is undefined by default. So `proxy` is the most basic rule to render a token and is used if the specific rule is undefined.

CSS classes are attributes on HTML elements. If we think back to the object representation of the `ul` element we looked at, we might remember that it contained an `attrs` key with the value `null`. This means this token had no attributes. `attrs` can be an array of `[key, value]` pairs which describe attributes to be added to the token.

Looking at [the API documention for Token objects](https://markdown-it.github.io/markdown-it/#Token.attrJoin) we find the `attrJoin` method. This method allows us to join an existing attributes value with a new value or create the attribute if it doens't exist yet. Simply pushing the value (for example with `token.attr.push(["key", "value"]`) would overwrite any previous change:

```
const MarkdownIt = require('markdown-it');
const md = new MarkdownIt();
const proxy = (tokens, idx, options, env, self) => self.renderToken(tokens, idx, options);
const defaultBulletListOpenRenderer = md.renderer.rules.bullet_list_open || proxy;
md.renderer.rules.bullet_list_open = function(tokens, idx, options, env, self) {
// Make your changes here ...
tokens[idx].attrJoin("class", "lorem_ipsum")
// ... then render it using the existing logic
return defaultBulletListOpenRenderer(tokens, idx, options, env, self)
};
```
Let's test the finished rule:
```
const MarkdownIt = require('markdown-it');
const md = new MarkdownIt();
const proxy = (tokens, idx, options, env, self) => self.renderToken(tokens, idx, options);
const defaultBulletListOpenRenderer = md.renderer.rules.bullet_list_open || proxy;
md.renderer.rules.bullet_list_open = function(tokens, idx, options, env, self) {
// Make your changes here ...
tokens[idx].attrJoin("class", "lorem_ipsum");
// ... then render it using the existing logic
return defaultBulletListOpenRenderer(tokens, idx, options, env, self)
};
console.log(md.render("- Hello World"));
```
Output:
```
<ul class="lorem_ipsum">
<li>Hello World</li>
</ul>
```
### To add a wrapper element
Let's imagine we are using CSS pseudo classes such as `:before` and `:after` to style our list because using `list-style-type` doesn't provide the bullet types we want and `list-style-image` isn't flexible enough to position itself properly across all major browsers.

To keep a proper line wrapping in our list we have set all elements in our `li` to display as a block (`li * {display: block;}`). This works for our pseudo classes and other `HTMLElements`. However, it does not work for `TextNodes`. So having this output will produce weird line indents:
```
<ul>
<li>Hello World</li>
<ul>
```

To fix this we can use a wrapper element which can be properly displayed as a block:

```
<ul>
<li>
<span>Hello World</span>
</li>
<ul>
```

So our next goal is:
```
Add a rule that wraps the content of every <li> in a <span>
```

Keen observers might have already noticed that rules return their HTML tags as strings. So this modification is rather straight forward.

Let's use the [demo tool](https://markdown-it.github.io/#md3=%7B%22source%22%3A%22-%20Hello%20World%22%2C%22defaults%22%3A%7B%22html%22%3Afalse%2C%22xhtmlOut%22%3Afalse%2C%22breaks%22%3Afalse%2C%22langPrefix%22%3A%22language-%22%2C%22linkify%22%3Afalse%2C%22typographer%22%3Afalse%2C%22_highlight%22%3Afalse%2C%22_strict%22%3Afalse%2C%22_view%22%3A%22debug%22%7D%7D) again and check which keys we need to add in the `renderer.rules` object to access the opening and closing tags of an `li` element:

```
list_item_open
list_item_close
```

Now use this information to add the new rules:

```
const MarkdownIt = require('markdown-it');
const md = new MarkdownIt();
const proxy = (tokens, idx, options, env, self) => self.renderToken(tokens, idx, options);
const defaultListItemOpenRenderer = md.renderer.rules.list_item_open || proxy;
md.renderer.rules.list_item_open = function(tokens, idx, options, env, self) {
return `${defaultListItemOpenRenderer(tokens, idx, options, env, self)}<span>`;
};
const defaultListItemCloseRenderer = md.renderer.rules.list_item_close || proxy;
md.renderer.rules.list_item_close = function(tokens, idx, options, env, self) {
return `</span>${defaultListItemCloseRenderer(tokens, idx, options, env, self)}`;
};
```
Testing our modification:

```
const MarkdownIt = require('markdown-it');
const md = new MarkdownIt();
const proxy = (tokens, idx, options, env, self) => self.renderToken(tokens, idx, options);
const defaultListItemOpenRenderer = md.renderer.rules.list_item_open || proxy;
md.renderer.rules.list_item_open = function(tokens, idx, options, env, self) {
return `${defaultListItemOpenRenderer(tokens, idx, options, env, self)}<span>`;
};
const defaultListItemCloseRenderer = md.renderer.rules.list_item_close || proxy;
md.renderer.rules.list_item_close = function(tokens, idx, options, env, self) {
return `</span>${defaultListItemCloseRenderer(tokens, idx, options, env, self)}`;
};
console.log(md.render("- Hello World"));
```
Output:
```
<ul>
<li>
<span>Hello World</span>
</li>
</ul>
```

Of course using string manipulation might get really messy for bigger changes. So consider using `markdown-it`s Token class instead:
```
const MarkdownIt = require('markdown-it');
const Token = require('markdown-it/lib/token');
const md = new MarkdownIt();
const proxy = (tokens, idx, options, env, self) => self.renderToken(tokens, idx, options);
const defaultListItemOpenRenderer = md.renderer.rules.list_item_open || proxy;
const defaultSpanOpenRenderer = md.renderer.rules.span_open || proxy;
md.renderer.rules.list_item_open = function(tokens, idx, options, env, self) {
const span = new Token("span_open", "span", 1);
return `${defaultListItemOpenRenderer(tokens, idx, options, env, self)}${defaultSpanOpenRenderer([span], 0, options, env, self)}`;
};
const defaultListItemCloseRenderer = md.renderer.rules.list_item_close || proxy;
const defaultSpanCloseRenderer = md.renderer.rules.span_close|| proxy;
md.renderer.rules.list_item_close = function(tokens, idx, options, env, self) {
const span = new Token("span_close", "span", -1);
return `${defaultSpanCloseRenderer([span], 0, options, env, self)}${defaultListItemCloseRenderer(tokens, idx, options, env, self)}`;
};
console.log(md.render("- Hello World"));
```

Output:

```
<ul>
<li>
<span>Hello World<span>
</li>
</ul>
```

0 comments on commit bab0baf

Please sign in to comment.