经常用markdown写博客,会发现markdown的功能并不够用,比如原生markdown不支持表格合并功能:
列合并 | 列合并 | 行合并 | ||
- | - | - | ||
- | 行列都合并 | |||
- |
原生markdown不支持文字颜色。原生markown不支持数学公式:
原生markown还不支持显示pdf和video的语法,等等。
想要解决这些问题,必须从markdown解析着手,https://github.com/markdown-it/markdown-it是一款将markdown解析成html的库,支持自定义语法插件。有了它之后,再写加几个按钮,写点css,一个markdown编辑器就诞生了。
本文主要讲解markdown-it的工作流程,以及自定义语法插件。
function MarkdownIt(presetName, options) {
// 这3个用于将marken解析成token
this.inline = new ParserInline();
this.block = new ParserBlock();
this.core = new ParserCore();
// 用于将token转化为html输出
this.renderer = new Renderer();
...
}
重要的是use方法,因为我们就靠它添加插件:
MarkdownIt.prototype.use = function (plugin /*, params, ... */) {
var args = [ this ].concat(Array.prototype.slice.call(arguments, 1));
plugin.apply(plugin, args);
return this;
};
use方法会直接apply(plugin, args)
,就是调用了插件中的函数。
当我们调用render函数时,先会调用parse函数将markdown解析成tokens,然后再将tokens转换成html
MarkdownIt.prototype.render = function (src, env) {
env = env || {};
return this.renderer.render(this.parse(src, env), this.options, env);
};
显然,核心在于如何将markdown解析成token。
token是什么?其实就是js对象:
{
"type": "heading_open",
"tag": "h1",
"attrs": null,
"map": [
2,
3
],
"nesting": 1,
"level": 0,
"children": null,
"content": "",
"markup": "#",
"info": "",
"meta": null,
"block": true,
"hidden": false
}
在官方的例子中,右侧点击debug可以看到tokens输出。
在parse函数中,首先会调用this.core.process(state)
,在这个函数中会执行如下核心规则
var _rules = [
[ 'normalize', require('./rules_core/normalize') ],
[ 'block', require('./rules_core/block') ],
[ 'inline', require('./rules_core/inline') ],
[ 'linkify', require('./rules_core/linkify') ],
[ 'replacements', require('./rules_core/replacements') ],
[ 'smartquotes', require('./rules_core/smartquotes') ]
];
显然,重点在block和inline这两个核心规则。
核心block规则最终会执行MarkdownIt.block.tokenize()
函数,在这个函数中,会从MarkdownIt.block.ruler
取出所有block规则,然后对每一行都执行所有block规则:
ParserBlock.prototype.tokenize = function (state, startLine, endLine) {
...
while (line < endLine) {
for (i = 0; i < len; i++) {
ok = rules[i](state, line, endLine, false);
if (ok) { break; }
}
}
...
};
每个合格的block规则都会做这3件事
state.line
state.tokens
核心block规则结束后,会生产一系列token,对于需要inline规则继续处理的内容会包裹在类型为inline的token中。
核心inline规则最终会对所有类型为inline的token执行MarkdownIt.inline.tokenize()
函数,在这个函数中,会从MarkdownIt.inline.ruler
取出所有inline规则,对该token的每个字符都执行所有inline规则:
ParserInline.prototype.tokenize = function (state) {
...
while (state.pos < end) {
for (i = 0; i < len; i++) {
ok = rules[i](state, false);
if (ok) { break; }
}
}
...
};
每个合格的inline规则都会做这3件事
state.pos
state.tokens
显然MarkdownIt.block.ruler
和MarkdownIt.inline.ruler
中含有哪些规则是markdown解析的重点。
初始化时,MarkdownIt会将如下block规则放入MarkdownIt.block.ruler
:
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') ],
[ 'heading', require('./rules_block/heading'), [ 'paragraph', 'reference', 'blockquote' ] ],
[ 'lheading', require('./rules_block/lheading') ],
[ 'html_block', require('./rules_block/html_block'), [ 'paragraph', 'reference', 'blockquote' ] ],
[ 'paragraph', require('./rules_block/paragraph') ]
];
同样的,初始化时会将如下inline规则放入MarkdownIt.inline.ruler
:
var _rules = [
[ 'text', require('./rules_inline/text') ],
[ 'newline', require('./rules_inline/newline') ],
[ 'escape', require('./rules_inline/escape') ],
[ 'backticks', require('./rules_inline/backticks') ],
[ 'strikethrough', require('./rules_inline/strikethrough').tokenize ],
[ 'emphasis', require('./rules_inline/emphasis').tokenize ],
[ 'link', require('./rules_inline/link') ],
[ 'image', require('./rules_inline/image') ],
[ 'autolink', require('./rules_inline/autolink') ],
[ 'html_inline', require('./rules_inline/html_inline') ],
[ 'entity', require('./rules_inline/entity') ]
];
var _rules2 = [
[ 'balance_pairs', require('./rules_inline/balance_pairs') ],
[ 'strikethrough', require('./rules_inline/strikethrough').postProcess ],
[ 'emphasis', require('./rules_inline/emphasis').postProcess ],
[ 'text_collapse', require('./rules_inline/text_collapse') ]
];
在这些规则中,会解析markdown文本,将对应的内容转换为token,至此markdown解析部分结束。
渲染的时候更简单,只需根据token的type去MarkdownIt.render.rules
中找到对应的渲染函数,执行即可拿到html:
Renderer.prototype.render = function (tokens, options, env) {
...
for (i = 0, len = tokens.length; i < len; i++) {
result += rules[tokens[i].type](tokens, i, options, env, this);
}
...
return result;
};
前面提到过,当我们use一个插件时,MarkdownIt会自动调用插件中的函数,例如:
var md = require('markdown-it')()
.use(require('markdown-it-multimd-table'));
所以我们只需在插件中新增或修改MarkdownIt.block.ruler
、MarkdownIt.inline.ruler
、MarkdownIt.render.rules
中的规则,即可达到自定义的目的。
插件中能获得什么参数呢?
block规则传入的参数是StateBlock
类:
function StateBlock(src, md, env, tokens) {
this.src = src;
this.tokens = tokens;
this.bMarks = []; // 行的起始位置
this.eMarks = []; // 行的结束位置
this.line = 0; // 行号
...
}
inline规则传入的参数是StateInline
类:
function StateInline(src, md, env, outTokens) {
this.src = src;
this.tokens = outTokens;
this.pos = 0;
...
}
两者主要的区别是block规则中this.line
指当处理的行在src中的索引;inline规则中this.pos
指当前处理字符串在src中的索引。
需求:平时会翻译一些英文文章,自己写的翻译和官方翻译放在一起容易弄混,所以想给自己写的翻译加个颜色,以便与官方翻译区分开来。我的想法是以“&”开头的段落就自动变成蓝色。
首先设计token,翻译段落不需要继续解析inline规则,所以单个token足以:
{
"type": "colorblock",
"tag": "",
"attrs": null,
"map": null,
"nesting": 0,
"level": 0,
"children": null,
"content": "",
"markup": "&",
"info": "",
"meta": null,
"block": true,
"hidden": false
}
如果某行第一个字符是"&",第二个字符是空格,则加入token,并将line+1.
function tokenize(state, startLine, endLine, silent) {
if (silent) {
return true;
}
var pos = state.bMarks[startLine],
max = state.eMarks[startLine];
if (state.src.charCodeAt(pos) !== 0x26/* & */) {
return false;
}
if (state.src.charCodeAt(pos + 1) !== 0x20/* 空格 */) {
return false;
}
let token = state.push('colorblock', '', 0);
token.markup = "&";
token.content = state.src.slice(pos + 2, max);
state.line = startLine + 1;
return true
}
返回html时将内容加个style:
function colorblockRenderer(tokens, idx) {
return '<p><span style="color:blue">' + tokens[idx].content + '</span></p>';
}
最后,将block规则加入到md.block.ruler
中,将渲染规则加入到md.renderer.rules
中:
module.exports = function colorblock(md, options) {
md.block.ruler.after('blockquote', 'colorblock', tokenize);
md.renderer.rules.colorblock = colorblockRenderer;
}