markdown-it 插件如何写(二)
markdown-it 插件如何写(二)
前面我们讲了如何去修改渲染规则,这一篇讲的是如何修改解析规则
Parse
markdown-it
的渲染过程分为两部分,Parse
和 Render
,如果我们要实现新的 markdown 语法,举个例子,比如我们希望解析 @ header
为 <h1>header</h1>
,就可以从 Parse
过程入手。
自定义 parse 规则的流程
在 markdown-it 的官方文档 里可以找到自定义 parse 规则的方式,那就是通过 Ruler
类:
var md = require('markdown-it')();
/**
* 增加自定义规则来处理用 `@` 声明的标题
*
* @detail
* block 表示插入块规则
* before 表示在某规则前插入
* @param string beforName 表示要在哪条规则前插入
* @param string rulename 表示你的新规则名
*/
md.block.ruler.before('paragraph', 'my_rule', function replace(state) {
//...
});
(1) 指定块规则: block
这句话的意思是指在 markdown-it
的解析 block 的一组规则中,在 paragraph
(段落) 规则前插入一个名为 my_rule
的自定义规则,我们慢慢来解释:
- 首先是
md.block.ruler
,可以自定义块的规则。这里不仅是block规则,有:md.block.ruler
,查看 parse_block.jsmd.inline.ruler
,查看 parse_inline.jsmd.core.ruler
,查看 parse_core.js
(2) 指定新规则位置: paragraph 的 before
然后是 .before
,查看 Ruler 相关的 API,有下面的几种种方法。这是因为规则是按照顺序执行的,某一规则的改变可能会影响其他规则
before
after
at
disable
enable
- 等方法
接着是 paragraph
,我怎么知道插入在哪个规则前面或者后面呢?这就需要你看源码了,并没有文档给你讲这个……
我们以 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') ] // 这里我们要将新的解析规则插入到这里的前面
];
(3) 指定新规则内容
最后是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
,还有 startLine
、endLine
、silent
,而具体这其中的代码怎么写,其实最好的方式就是参考这些已经实现的代码。
demo1,用@符号声明标题
需求
接下来我们以解析 @ 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>
定义新规则的函数原型
parse 的过程是根据换行符逐行扫描的,所以每一行的内容都会执行我们这个自定义函数进行匹配。
函数支持传入四个参数。参数含义和打印如下:
/**
* 增加自定义规则来处理用 `@` 声明的标题
*
* @detail
* block 表示插入块规则
* before 表示在某规则前插入
* @param string beforName 表示要在哪条规则前插入
* @param string rulename 表示你的新规则名
* @param fn: T
* - @param state 记录了各种状态数据 打印: {...},其中包含了每行的起始和结束的字符序
* - @param startLine 表示本次的起始行数 打印: 0
* - @param endLine 表示总的结束行数 打印: 2
* - @return boolean isMatch
*/
md.block.ruler.before('paragraph','@header',function(state, startLine, endLine, silent){
// 我们打印下 `state`、`startLine`,`endLine` 等数据:
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](https://github.com/markdown-it/markdown-it/blob/master/lib/rules_block/state_block.js) 文件
如何获取每一行的内容
我们来看看如何实现,先参照前面写过的 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];
//...
})
通过 pos
和 max
,我们可以截取出这行字符串:
/**
* 增加自定义规则来处理用 `@` 声明的标题
*
* @detail
* block 表示插入块规则
* before 表示在某规则前插入
* @param string beforName 表示要在哪条规则前插入
* @param string rulename 表示你的新规则名
* @param fn: T
* - @param state 记录了各种状态数据 打印: {...},其中包含了每行的起始和结束的字符序
* - @param startLine 表示本次的起始行数 打印: 0
* - @param endLine 表示总的结束行数 打印: 2
*/
md.block.ruler.before('paragraph','@header',function(state, startLine, endLine, silent){
console.log(JSON.parse(JSON.stringify(state)), startLine, endLine);
var ch, level, tmp, token
// 这行字符的非空格初始位置。值:pos = 0 + 0 = 0
var pos = state.bMarks[startLine] + state.tShift[startLine];
// 这行字符的结束位置。值:8,@ header是8个字符
var max = state.eMarks[startLine];
// 获取到这一行的内容
let text = state.src.substring(pos, max);
console.log(text);
// 这两行是为了进入到下一行的遍历之中
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) { return false }
// 自定义插入tokens
{
token = state.push('heading_open', 'h1', 1); // 插入h1起始token
token.markup = '@';
token.map = [ startLine, state.line ];
token = state.push('inline', '', 0); // 插入h1块里的内联token
let result = match[1];
token.content = result;
token.map = [ startLine, state.line ];
token.children = [];
token = state.push('heading_close', 'h1', -1); // 插入h1结束token
token.markup = '@';
// 这两行是为了进入到下一行的遍历之中
state.line = startLine + 1;
return true;
}
})
至此,就实现了预期的效果:
Q
话说 state.line 和 startLine 这两个属性是不是重复了?