编写markdown-it的插件和规则

前言 最近看vuePress源码时发现在使用markdownLoader之余使用了大量的 markdown-it 插件,除了社区插件(如高亮代码、代码块包裹、emoji等),同时也自行编写了很多自定义插件(如内外链区分渲染等)。
文章结合源码和自己之前写过的插件来详细解读如何编写一个 markdown-it 插件规则。
简介 【编写markdown-it的插件和规则】markdown-it 是一个辅助解析markdown的库,可以完成从 # testtest 的转换,渲染过程和babel类似为Parse -> Transform -> Generate。
Parse source通过3个嵌套的规则链core、block、inline进行解析:

core core.rule1 (normalize) ... core.ruleXblock block.rule1 (blockquote) ... block.ruleXinline (applied to each block token with "inline" type) inline.rule1 (text) ... inline.ruleX

解析的结果是一个token列表,将传递给renderer以生成html内容。
如果要实现新的markdown语法,可以从Parse过程入手:
可以在 md.core.rulermd.block.rulermd.inline.ruler 中自定义规则,规则的定义方法有 beforeafteratdisableenable 等。
// @vuepress/markdown代码片段 md.block.ruler.before('fence', 'snippet', function replace(state, startLine, endLine, silent) { //... });

上述代码在 md.block.ruler.fence 之前加入snippet规则,用作解析 <<< @/filepath 这样的代码,它会把其中的文件路径拿出来和 root 路径拼起来,然后读取其中文件内容。
具体代码就不详细分析了,一般parse阶段用到的情况比较少,感兴趣的可以自行查看vuePress源码。
Transform Token
通过官方在线示例拿 # test 举例,会得到如下结果:
[ { "type": "heading_open", "tag": "h1", "attrs": null, "map": [ 0, 1 ], "nesting": 1, "level": 0, "children": null, "content": "", "markup": "#", "info": "", "meta": null, "block": true, "hidden": false }, { "type": "inline", "tag": "", "attrs": null, "map": [ 0, 1 ], "nesting": 0, "level": 1, "children": [ { "type": "text", "tag": "", "attrs": null, "map": null, "nesting": 0, "level": 0, "children": null, "content": "test", "markup": "", "info": "", "meta": null, "block": false, "hidden": false } ], "content": "test", "markup": "", "info": "", "meta": null, "block": true, "hidden": false }, { "type": "heading_close", "tag": "h1", "attrs": null, "map": null, "nesting": -1, "level": 0, "children": null, "content": "", "markup": "#", "info": "", "meta": null, "block": true, "hidden": false } ]

使用更底层的数据表示Token,代替传统的AST。区别很简单:
  • 是一个简单的数组
  • 开始和结束标签是分开的
  • 会有一些特殊token (type: "inline") 嵌套token,根据标记顺序(bold, italic, text, ...)排序
更详细的数据模型可以通过 Token类定义 查看。
Renderer
token生成后被传递给renderer,renderer会将所有token传递给每个与token类型相同的rule规则。
renderer的rule规则都定义在 md.renderer.rules[name],是参数相同的函数。
Rules
代表对token的渲染规则,可以被更新或扩展。
用法 基础用法
const MarkdownIt = require('markdown-it'); const md = new MarkdownIt(); const result = md.render('# test');

预设
预设(preset)定义了激活的规则以及选项的组合。可以是 commonmarkzerodefault
  • commonmark 严格的 CommonMark 模式
  • default 默认的 GFM 模式, 没有 html、 typographer、autolinker 选项
  • zero 无任何规则
// commonmark 模式 const md = require('markdown-it')('commonmark'); // default 模式 const md = require('markdown-it')(); // 启用所有 const md = require('markdown-it')({ html: true, linkify: true, typographer: true });

选项文档:
参数 类型 默认值 说明
html Boolean false 在源码中启用 HTML 标签
xhtmlOut Boolean false 使用 / 来闭合单标签 (比如

这个选项只对完全的 CommonMark 模式兼容
breaks Boolean false 转换段落里的 \n
langPrefix String language- 给围栏代码块的 CSS 语言前缀
对于额外的高亮代码非常有用
linkify Boolean false 将类似 URL 的文本自动转换为链接
typographer Boolean false 启用语言无关的替换
美化引号
quotes String \ Array “”‘’ 双引号或单引号或智能引号替换对,当 typographer 启用时
highlight Function function (str, lang) { return ''; } 高亮函数,会返回转义的HTML或''
如果源字符串未更改,则应在外部进行转义
如果结果以
开头,内部包装器则会跳过
实例 transform阶段一般有两种写法
  • 重写 md.renderer.rules[name]
  • require('markdown-it')().use(plugin1).use(plugin2, opts, ...)
在搭建组件库文档过程中,需要判断是否为http开头的外部链接,内链直接通过a标签跳转相对路由,外链则新开窗口打开。
代码地址
const MarkdownIt = require('markdown-it'); const md = new MarkdownIt({ html: true, highlight, ...options }); const defaultRender = md.renderer.rules.link_open || function(tokens, idx, options, env, self) { return self.renderToken(tokens, idx, options); }; md.renderer.rules.link_open = function(tokens, idx, options, env, self) { const hrefAttr = tokens[idx].attrGet('href'); if (/^https?/.test(hrefAttr)) { tokens[idx].attrPush(['target', '_blank']); // add new attribute }return defaultRender(tokens, idx, options, env, self); };

plugin有 markdown-it-for-inline、markdown-it-anchor 等,以上例为例,如果你需要添加属性,可以在没有覆盖规则的情况下做一些事情。
接下来用markdown-it-for-inline插件来完成上例一样的功能。
const MarkdownIt = require('markdown-it'); const iterator = require('markdown-it-for-inline'); const md = new MarkdownIt({ html: true, highlight, ...options }); md.use(iterator, 'url_new_win', 'link_open', function (tokens, idx) { const hrefAttr = tokens[idx].attrGet('href'); if (/^https?/.test(hrefAttr)) { tokens[idx].attrPush(['target', '_blank']); // add new attribute } });

这比直接渲染器覆盖规则要慢,但写法更简单。
参考文档 markdown-it design principles
markdown-it

    推荐阅读