前言

在 《一篇带你用 VuePress + Github Pages 搭建博客》中,咱们应用 VuePress 搭建了一个博客,最终的成果查看:TypeScript 中文文档。

在搭建博客的过程中,咱们出于理论的需要,在《VuePress 博客优化之拓展 Markdown 语法》中解说了如何写一个 markdown-it插件,又在 《markdown-it 原理解析》中解说了 markdown-it的执行原理,本篇咱们将解说具体的实战代码,帮忙大家更好的写插件。

renderer

markdown-it的渲染过程分为两局部,ParseRender,如果咱们要更改渲染的成果,就比方在外层包裹一层 div,或者批改 HTML 元素的属性、增加 class 等,就能够从 Render 过程动手。

在 markdown-it 的官网文档里就能够找到自定义 Render 渲染规定的形式:

Instance of Renderer. Use it to modify output look. Or to add rendering rules for new token types, generated by plugins.
var md = require('markdown-it')();function myToken(tokens, idx, options, env, self) {  //...  return result;};md.renderer.rules['my_token'] = myToken

markdown-it 内置了一些默认的 rules,你能够间接批改它们,具体有哪些以及渲染形式能够查看 renderer.js 的源码,这里间接列出来:

  • code_inline
  • code_block
  • fence
  • image
  • hardbreak
  • softbreak
  • text
  • html_block
  • html_inline

实例一

如果咱们查看 VuePress 中代码块的渲染后果,咱们会发现每一个代码块外层都包了一层带 extra-class类名的 div:

其实,这就是 VuePress 批改了渲染规定产生的,查看 VuePress 源码:

module.exports = md => {  const fence = md.renderer.rules.fence  md.renderer.rules.fence = (...args) => {    const [tokens, idx] = args    const token = tokens[idx]    const rawCode = fence(...args)    return `<!--beforebegin--><div class="language-${token.info.trim()} extra-class">` +    `<!--afterbegin-->${rawCode}<!--beforeend--></div><!--afterend-->`  }}

咱们能够看到这里十分奇妙的避开了解决 token,间接应用渲染后的后果,在外层包了一层 div。

实例二

相似于 VuePress 的这种形式,咱们也能够在获取默认渲染内容后,再应用 replace 替换掉一些内容,比方在《VuePress 博客优化之拓展 Markdown 语法》这篇中,咱们自定义了一个代码块语法,就是在 rules.fence中批改了渲染的内容:

md.use(function(md) {  const fence = md.renderer.rules.fence  md.renderer.rules.fence = (...args) => {    let rawCode = fence(...args);    rawCode = rawCode.replace(/<span class="token comment">\/\/ try-link https:\/\/(.*)<\/span>\n/ig, '<a href="$1" class="try-button" target="_blank">Try</a>');    return `${rawCode}`  }})

实例三

但不可能总是能够这么取巧,有的时候就是须要解决 token,这里咱们参考 markdown-it 官网提供的设计准则中的例子,当渲染一个图片的时候,如果链接匹配 /^https?:\/\/(www\.)?vimeo.com\/(\d+)($|\/)/,咱们就将其渲染为一个 iframe ,其余的则放弃默认渲染形式:

var md = require('markdown-it')();var defaultRender = md.renderer.rules.image,    vimeoRE       = /^https?:\/\/(www\.)?vimeo.com\/(\d+)($|\/)/;md.renderer.rules.image = function (tokens, idx, options, env, self) {  var token = tokens[idx],      aIndex = token.attrIndex('src');  if (vimeoRE.test(token.attrs[aIndex][1])) {    var id = token.attrs[aIndex][1].match(vimeoRE)[2];    return '<div class="embed-responsive embed-responsive-16by9">\n' +           '  <iframe class="embed-responsive-item" src="//player.vimeo.com/video/' + id + '"></iframe>\n' +           '</div>\n';  }  // pass token to default renderer.  return defaultRender(tokens, idx, options, env, self);};

rules.image 传入的函数参数能够查看 renderer.js 的源码:

Renderer.prototype.render = function (tokens, options, env) {  var i, len, type,      result = '',      rules = this.rules;  for (i = 0, len = tokens.length; i < len; i++) {    type = tokens[i].type;    if (type === 'inline') {      result += this.renderInline(tokens[i].children, options, env);    } else if (typeof rules[type] !== 'undefined') {      result += rules[tokens[i].type](tokens, i, options, env, this);    } else {      result += this.renderToken(tokens, i, options, env);    }  }  return result;};

咱们能够看到 rules 传入的参数,其中 tokens 就是指 tokens 列表,idx 则是指要渲染的 token 索引,所以在代码里才能够通过 tokens[index]获取指标 token。

而后咱们又应用了 tokens.attrIndex,tokens 提供了哪些办法能够查看官网 API,或者间接查看 Token 源码。

咱们解释一下这个示例里用到的一些办法,先从 token 开始说起,咱们举个例子,看下![video link]([https://www.vimeo.com/123)](https://www.vimeo.com/123))这句 markdown 语法产生的 token(这里进行了简化):

{    "type": "image",    "tag": "img",    "attrs": [        [            "src",            "https://www.vimeo.com/123"        ],        [            "alt",            ""        ]    ],    "children": [        {            "type": "text",            "tag": "",            "attrs": null,            "children": null,            "content": "video link",        }    ],    "content": "video link"}

能够看到 token 是有一个 attr 属性的,表明了要渲染的 img 标签的属性有哪些,token.attrIndex 就是通过名字获取属性索引,而后再通过 token.attrs[aIndex][1]获取具体的属性值。

实例四

同样是来自 markdown-it 官网提供的设计准则中的例子,给所有的链接增加 target="_blank"

// Remember old renderer, if overridden, or proxy to default renderervar 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) {  // If you are sure other plugins can't add `target` - drop check below  var aIndex = tokens[idx].attrIndex('target');  if (aIndex < 0) {    tokens[idx].attrPush(['target', '_blank']); // add new attribute  } else {    tokens[idx].attrs[aIndex][1] = '_blank';    // replace value of existing attr  }  // pass token to default renderer.  return defaultRender(tokens, idx, options, env, self);};

兴许你会奇怪,为什么会有 rules.link_open?这个并不在默认规定里呀,能够间接应用吗?

还真是能够的,其实这里的 link_open 和之前的 imagefence 等都是 token 的 type,所以只有是 token 的 type 就能够,那 token 有哪些 type 呢?有具体的阐明文档吗?

对于这个问题, markdown-it 也有 issues 里提出:

作者的意思就是,没有,如果你想写插件,你就去看源码……

那成吧,其实在咱们的理论开发中,如果你想要晓得某一个 token type,其实齐全能够打印出 token 看一下,官网的 Live Demo 提供了 debug 模式用于查看 token:

当然就这个例子里的需要,作者还提供了 markdown-it-for-inline 插件用于简化代码书写:

var iterator = require('markdown-it-for-inline');var md = require('markdown-it')()            .use(iterator, 'url_new_win', 'link_open', function (tokens, idx) {              var aIndex = tokens[idx].attrIndex('target');              if (aIndex < 0) {                tokens[idx].attrPush(['target', '_blank']);              } else {                tokens[idx].attrs[aIndex][1] = '_blank';              }            });

对于 markdown-it-for-inline就在当前的文章里介绍了。

系列文章

博客搭建系列是我至今写的惟一一个偏实战的系列教程,预计 20 篇左右,解说如何应用 VuePress 搭建、优化博客,并部署到 GitHub、Gitee、公有服务器等平台。全系列文章地址:https://github.com/mqyqingfeng/Blog

微信:「mqyqingfeng」,加我进冴羽惟一的读者群。

如果有谬误或者不谨严的中央,请务必给予斧正,非常感激。如果喜爱或者有所启发,欢送 star,对作者也是一种激励。