Sascha
3 years ago
committed by
GitHub
1 changed files with 262 additions and 0 deletions
@ -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> |
|||
``` |
Loading…
Reference in new issue