Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

markdown-it 插件如何写(二) #254

Open
mqyqingfeng opened this issue Jan 21, 2022 · 0 comments
Open

markdown-it 插件如何写(二) #254

mqyqingfeng opened this issue Jan 21, 2022 · 0 comments

Comments

@mqyqingfeng
Copy link
Owner

前言

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

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

Parse

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

markdown-it 的官方文档里可以找到自定义 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 相关的 API,还有 afteratdisableenable等方法,这是因为规则是按照顺序执行的,某一规则的改变可能会影响其他规则。

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

如果是md.block,查看 parse_block.js,如果是md.inline,查看 parse_inline.js,如果是 md.core,查看 parse_core.js,我们以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,而具体这其中的代码怎么写,其实最好的方式就是参考这些已经实现的代码。

实例讲解

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

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 的代码依葫芦画瓢:

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 文件,这其中:

  • bMarks 表示每一行的起始位置
  • eMarks 表示每一行的终止位置
  • tShift 表示每一行第一个非空格字符的位置

我们看下 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;
      }
})

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

系列文章

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

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

如果有错误或者不严谨的地方,请务必给予指正,十分感谢。如果喜欢或者有所启发,欢迎 star,对作者也是一种鼓励。

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

1 participant