markdown-it 插件如何写(二)

367次阅读  |  发布于2年以前

前言

在 《一篇带你用 VuePress + Github Pages 搭建博客》[1]中,我们使用 VuePress 搭建了一个博客,最终的效果查看:TypeScript 中文文档[2]。

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

Parse

markdown-it的渲染过程分为两部分,ParseRender,如果我们要实现新的 markdown 语法,举个例子,比如我们希望解析 @ headerheader,就可以从 Parse 过程入手。

在 markdown-it 的官方文档[5]里可以找到自定义 parse 规则的方式,那就是通过 Ruler 类:

var md = require('markdown-it')();

md.block.ruler.before('paragraph', 'my_rule', function replace(state) {
  //...
});

这句话的意思是指在 markdown-it 的解析 block 的一组规则中,在 paragraph 规则前插入一个名为 my_rule 的自定义规则,我们慢慢来解释。

首先是 md.block.ruler,除此之外,还有 md.inline.rulermd.core.ruler可以自定义其中的规则。

然后是 .before,查看 Ruler[6] 相关的 API,还有 afteratdisableenable等方法,这是因为规则是按照顺序执行的,某一规则的改变可能会影响其他规则。

接着是 paragraph,我怎么知道插入在哪个规则前面或者后面呢?这就需要你看源码了,并没有文档给你讲这个……

如果是md.block,查看 parse_block.js[7],如果是md.inline,查看 parse_inline.js[8],如果是 md.core,查看 parse_core.js[9],我们以md.block为例,可以看到源码里写了这些规则:

var _rules = [
  // First 2 params - rule name & source. Secondary array - list of rules,
  // which can be terminated by this one.
  [ 'table',      require('./rules_block/table'),      [ 'paragraph', 'reference' ] ],
  [ 'code',       require('./rules_block/code') ],
  [ 'fence',      require('./rules_block/fence'),      [ 'paragraph', 'reference', 'blockquote', 'list' ] ],
  [ 'blockquote', require('./rules_block/blockquote'), [ 'paragraph', 'reference', 'blockquote', 'list' ] ],
  [ 'hr',         require('./rules_block/hr'),         [ 'paragraph', 'reference', 'blockquote', 'list' ] ],
  [ 'list',       require('./rules_block/list'),       [ 'paragraph', 'reference', 'blockquote' ] ],
  [ 'reference',  require('./rules_block/reference') ],
  [ 'html_block', require('./rules_block/html_block'), [ 'paragraph', 'reference', 'blockquote' ] ],
  [ 'heading',    require('./rules_block/heading'),    [ 'paragraph', 'reference', 'blockquote' ] ],
  [ 'lheading',   require('./rules_block/lheading') ],
  [ 'paragraph',  require('./rules_block/paragraph') ]
];

最后是function replace(state),这里函数的参数其实不止有 state,我们查看任何一个具体规则的 parse 代码,就比如 heading.js

module.exports = function heading(state, startLine, endLine, silent) {
  var ch, level, tmp, token,
      pos = state.bMarks[startLine] + state.tShift[startLine],
      max = state.eMarks[startLine];

  // ...
};

可以看出除了 state,还有 startLineendLinesilent,而具体这其中的代码怎么写,其实最好的方式就是参考这些已经实现的代码。

实例讲解

接下来我们以解析 @ headerheader为例,讲解其中涉及的代码,这是要渲染的内容:

var md = window.markdownit();
// md.block.ruler.before(...)

var result = md.render(`@ header
contentTwo
`);

console.log(result);

正常它的渲染结果是:

<p>@ header
contentTwo</p>

现在期望的渲染结果是:

<h1>header</h1>
<p>contentTwo</p>

我们来看看如何实现,先参照 header.js[10] 的代码依葫芦画瓢:

md.block.ruler.before('paragraph','@header',function(state, startLine, endLine, silent){
  var ch, level, tmp, token,
      pos = state.bMarks[startLine] + state.tShift[startLine],
      max = state.eMarks[startLine];

  //...
})

parse 的过程是根据换行符逐行扫描的,所以每一行的内容都会执行我们这个自定义函数进行匹配,函数支持传入四个参数,其中,state 记录了各种状态数据,startLine 表示本次的起始行数,而 endLine 表示总的结束行数。

我们打印下 state``startLineendLine 等数据:

md.block.ruler.before('paragraph','@header',function(state, startLine, endLine, silent){
  var ch, level, tmp, token,
      pos = state.bMarks[startLine] + state.tShift[startLine],
      max = state.eMarks[startLine];

  console.log(JSON.parse(JSON.stringify(state)), startLine, endLine);
})

这是打印的结果:

其中 state 的内容我们简化下展示出来:

{
    "src": "@ header\ncontentTwo\n",
    "md": {...},
    "env": {...},
    "tokens": [...],
    "bMarks": [0, 9, 20],
    "eMarks": [8, 19, 20],
    "tShift": [0, 0, 0],
    "line": 0
}

state 中这些字段的具体含义可以查看 state_block.js[11] 文件,这其中:

我们看下 pos 的计算逻辑为 state.bMarks[startLine] + state.tShift[startLine],其中 startLine 是 0,所以 pos = 0 + 0 = 0

再看下 max 的计算逻辑为 state.eMarks[startLine],所以max = 8

从这也可以看出,其实 pos 就是这行字符的初始位置,max 这行字符的结束位置,通过 posmax,我们可以截取出这行字符串:

md.block.ruler.before('paragraph','@header',function(state, startLine, endLine, silent){
  var ch, level, tmp, token,
      pos = state.bMarks[startLine] + state.tShift[startLine],
      max = state.eMarks[startLine];

    console.log(JSON.parse(JSON.stringify(state)), startLine, endLine);
    let text = state.src.substring(pos, max);
    console.log(text);

    state.line = startLine + 1;
   return true
})

打印结果为:

在代码里我们加入了state.line = startLine + 1;return true,这是为了进入到下一行的遍历之中。

如果我们能取出每次用于判断的字符串,那我们就可以进行正则匹配,如果匹配,就自定义 tokens,剩下的逻辑很简单,我们直接给出最后的代码:

md.block.ruler.before('paragraph', 'myplugin', function (state,startLine,endLine) {
  var ch, level, tmp, token,
      pos = state.bMarks[startLine] + state.tShift[startLine],
      max = state.eMarks[startLine];
      ch  = state.src.charCodeAt(pos);

      if (ch !== 0x40/*@*/ || pos >= max) { return false; }

      let text = state.src.substring(pos, max);
      let rg = /^@\s(.*)/;
      let match = text.match(rg);

      if (match && match.length) {
        let result = match[1];
        token = state.push('heading_open', 'h1', 1);
        token.markup = '@';
        token.map = [ startLine, state.line ];

        token = state.push('inline', '', 0);
        token.content = result;
        token.map = [ startLine, state.line ];
        token.children = [];

        token = state.push('heading_close', 'h1', -1);
        token.markup = '@';

        state.line = startLine + 1;
        return true;
      }
})

至此,就实现了预期的效果:

参考资料

[1]《一篇带你用 VuePress + Github Pages 搭建博客》: https://github.com/mqyqingfeng/Blog/issues/235

[2]TypeScript 中文文档: http://ts.yayujs.com/

[3]《VuePress 博客优化之拓展 Markdown 语法》: https://github.com/mqyqingfeng/Blog/issues/251

[4]《markdown-it 原理解析》: https://github.com/mqyqingfeng/Blog/issues/252

[5]markdown-it 的官方文档: https://markdown-it.github.io/markdown-it/#Ruler

[6]Ruler: https://markdown-it.github.io/markdown-it/#Ruler

[7]parse_block.js: https://github.com/markdown-it/markdown-it/blob/master/lib/parser_block.js

[8]parse_inline.js: https://github.com/markdown-it/markdown-it/blob/master/lib/parser_inline.js

[9]parse_core.js: https://github.com/markdown-it/markdown-it/blob/master/lib/parser_core.js

[10]header.js: https://github.com/markdown-it/markdown-it/blob/master/lib/rules_block/heading.js

[11]state_block.js: https://github.com/markdown-it/markdown-it/blob/master/lib/rules_block/state_block.js

Copyright© 2013-2020

All Rights Reserved 京ICP备2023019179号-8