markdown-it 插件如何写(二)

前言

《一篇带你用 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 类:

1var md = require('markdown-it')(); 2 3md.block.ruler.before('paragraph', 'my_rule', function replace(state) { 4 //... 5});

这句话的意思是指在 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为例,可以看到源码里写了这些规则:

1var _rules = [ 2 // First 2 params - rule name & source. Secondary array - list of rules, 3 // which can be terminated by this one. 4 [ 'table', require('./rules_block/table'), [ 'paragraph', 'reference' ] ], 5 [ 'code', require('./rules_block/code') ], 6 [ 'fence', require('./rules_block/fence'), [ 'paragraph', 'reference', 'blockquote', 'list' ] ], 7 [ 'blockquote', require('./rules_block/blockquote'), [ 'paragraph', 'reference', 'blockquote', 'list' ] ], 8 [ 'hr', require('./rules_block/hr'), [ 'paragraph', 'reference', 'blockquote', 'list' ] ], 9 [ 'list', require('./rules_block/list'), [ 'paragraph', 'reference', 'blockquote' ] ], 10 [ 'reference', require('./rules_block/reference') ], 11 [ 'html_block', require('./rules_block/html_block'), [ 'paragraph', 'reference', 'blockquote' ] ], 12 [ 'heading', require('./rules_block/heading'), [ 'paragraph', 'reference', 'blockquote' ] ], 13 [ 'lheading', require('./rules_block/lheading') ], 14 [ 'paragraph', require('./rules_block/paragraph') ] 15];

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

1module.exports = function heading(state, startLine, endLine, silent) { 2 var ch, level, tmp, token, 3 pos = state.bMarks[startLine] + state.tShift[startLine], 4 max = state.eMarks[startLine]; 5 6 // ... 7};

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

实例讲解

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

1var md = window.markdownit(); 2// md.block.ruler.before(...) 3 4var result = md.render(`@ header 5contentTwo 6`); 7 8console.log(result);

正常它的渲染结果是:

1<p>@ header 2contentTwo</p>

现在期望的渲染结果是:

1<h1>header</h1> 2<p>contentTwo</p>

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

1md.block.ruler.before('paragraph','@header',function(state, startLine, endLine, silent){ 2 var ch, level, tmp, token, 3 pos = state.bMarks[startLine] + state.tShift[startLine], 4 max = state.eMarks[startLine]; 5 6 //... 7})

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

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

1md.block.ruler.before('paragraph','@header',function(state, startLine, endLine, silent){ 2 var ch, level, tmp, token, 3 pos = state.bMarks[startLine] + state.tShift[startLine], 4 max = state.eMarks[startLine]; 5 6 console.log(JSON.parse(JSON.stringify(state)), startLine, endLine); 7})

这是打印的结果:

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

1{ 2 "src": "@ header\ncontentTwo\n", 3 "md": {...}, 4 "env": {...}, 5 "tokens": [...], 6 "bMarks": [0, 9, 20], 7 "eMarks": [8, 19, 20], 8 "tShift": [0, 0, 0], 9 "line": 0 10}

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,我们可以截取出这行字符串:

1md.block.ruler.before('paragraph','@header',function(state, startLine, endLine, silent){ 2 var ch, level, tmp, token, 3 pos = state.bMarks[startLine] + state.tShift[startLine], 4 max = state.eMarks[startLine]; 5 6 console.log(JSON.parse(JSON.stringify(state)), startLine, endLine); 7 let text = state.src.substring(pos, max); 8 console.log(text); 9 10 state.line = startLine + 1; 11 return true 12})

打印结果为:

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

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

1md.block.ruler.before('paragraph', 'myplugin', function (state,startLine,endLine) { 2 var ch, level, tmp, token, 3 pos = state.bMarks[startLine] + state.tShift[startLine], 4 max = state.eMarks[startLine]; 5 ch = state.src.charCodeAt(pos); 6 7 if (ch !== 0x40/*@*/ || pos >= max) { return false; } 8 9 let text = state.src.substring(pos, max); 10 let rg = /^@\s(.*)/; 11 let match = text.match(rg); 12 13 if (match && match.length) { 14 let result = match[1]; 15 token = state.push('heading_open', 'h1', 1); 16 token.markup = '@'; 17 token.map = [ startLine, state.line ]; 18 19 token = state.push('inline', '', 0); 20 token.content = result; 21 token.map = [ startLine, state.line ]; 22 token.children = []; 23 24 token = state.push('heading_close', 'h1', -1); 25 token.markup = '@'; 26 27 state.line = startLine + 1; 28 return true; 29 } 30})

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

系列文章

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

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

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

点赞
收藏

评论区

加载中...

相关推荐

有的时候我觉得我不会 Markdown

前言在中,我们使用VuePress搭建了一个博客,最终的效果查看:。在优化博客的过程中,因为需要写markdownit插件,翻了下markdown的,突然发现对Markdown还远不够了解:软换行(Softlinebreaks)换行符不在行内代码或HTML标签内,前面没有两个或以上的空格,将解析为软换行(Softlinebr

从零实现一个 VuePress 插件

前言在中,我们使用VuePress搭建了一个博客,最终的效果查看:。但在搭建VuePress博客的过程中,也并不是所有的插件都能满足需求,所以本篇我们以实现一个代码复制插件为例,教大家如何从零实现一个VuePress插件。本地开发开发插件第一个要解决的问题就是如何本地开发,我们查看VuePress1.0官方文档的「」章节,并没有找到解决

VuePress 博客优化之 last updated 最后更新时间如何设置

前言在中,我们使用VuePress搭建了一个博客,但是浏览最终搭建的站点:,我们会发现,在每篇文章的底部,并没有像VuePress官方文档那样,出现最后更新的时间:这篇我们来探究下如何实现最后更新时间。官方自带查阅VuePress的,我们可以知道,VuePress自带显示最后更新时间的插件,在默认主题下,无需安装本插件,因为VuePre

VuePress 博客优化之增加 Valine 评论功能

前言在中,我们使用VuePress搭建了一个博客,最终的效果查看:。本篇讲讲如何使用Valine快速的实现评论功能。主题内置因为我用的是vuepressthemereco主题,主题内置评论插件@vuepressreco/vuepressplugincomments,可以根据自己的喜好选择Valine或者Vssue。本篇讲讲使用Val

VuePress 博客优化之增加 Vssue 评论功能

前言在中,我们使用VuePress搭建了一个博客,最终的效果查看:。本篇讲讲如何使用Vssue快速的实现评论功能。主题内置因为我用的是vuepressthemereco主题,主题内置评论插件@vuepressreco/vuepressplugincomments,可以根据自己的喜好选择Valine或者Vssue。那我们来介绍下Vss

markdown-it 插件如何写(三)

前言在中,我们使用VuePress搭建了一个博客,最终的效果查看:。在搭建博客的过程中,我们出于实际的需求,在中讲解了如何写一个markdownit插件,又在中讲解了markdownit的执行原理,本篇我们将讲解具体的实战代码,帮助大家更好的写插件。markdownitinlinemarkdownit的作者提供了markdownitinin