自从来了微信之后,好少写文章了,最新一篇去还停留在年 10 月 24 日。真鸡儿惭愧。这里开个坑,继续写写。
下面开始写点正文。
来了微信之后,接手的主要是偏 Node 这块,最近负责了 微信文档的更新,里面用到很多技术点,感觉很适合讲一讲。
微信文档的所有内容都是基于 markdown 来的,md2html 的工具使用了 markdown-it 这个 plugin 化的开源库。当然,还有比这个库,更多 star 的库比如 markup,但是,其提供的自定义功能太少,后面综合考虑就直接使用 markdown-it 来搞。
这里先简单介绍一下 markdown-it.
Markdown-it 里面的解析流 基本是从 core=> rule=>block => inline 这么走的,最终会由 renderer 方法渲染出最终结果。或者,你也可以通过 parse 方法得到最终生成的 token tree. parse
在构建 markdown-it 生态插件来说,其实不太重要,顶多是调试用,引用官方一段话就是:
You should not call this method directly, until you write custom renderer (for example, to produce AST).
根据上面的流程,MI (markdownIt) 的主要部分可以分为:
- Ruler: 用来将纯字符串编译为 token tree 的过程
- Renderer: 用来将 token tree 编译为 html 的模块
- Parse: 用来得到当前 tokens 的方法
当然,如果你看了官方文档,上面所述的 模块概念可能不止上面这一点。不过,我这里推崇的是,先能快速理解核心,然后其他周边概念等你熟悉了之后,再去理解消化,应该就事半功倍了。
另外,还想吐槽一下,markdown-it 的文档。真的!首页 README.md 写这么多,感觉都很重要,结果看一遍之后,想找的没找到。后面找了半天,才找到 markdown-it 最值得看的文档,markdown-it 框架原理。(这是我认为最值得看的,可能大家不这么认为,这里就当我瞎逼逼就行)
如果你要写一个 markdown-it plugin, 中间肯定会涉及到 token 的查找和命名。这里有个必备的 debug 网站 markdown-it demo ,直接打开 debug 对照看就行。
为啥写 plugin?
在动手写一个 plugin 之前,我们需要明白一件事,我们做这件事的需求是啥? 其实看 markdown-it 中的官方文档推荐内容和源码,就可以了解到,上面所有的这些 rule,inline ruleXX 以及 render 都是一个一个 plugin.
# 官方给的架构图
core
core.rule1 (normalize)
...
core.ruleX
block
block.rule1 (blockquote)
...
block.ruleX
core.ruleX1 (intermediate rule that applies on block tokens, nothing yet)
...
core.ruleXX
inline (applied to each block token with "inline" type)
inline.rule1 (text)
...
inline.ruleX
core.ruleYY (applies to all tokens)
... (abbreviation, footnote, typographer, linkifier)
只是有一些是常用的,markdown-it 就手动加进去了,比如下面代码。最终的结果就是搞出了初始化能用的 markdown-it。
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') ]
];
所以,假定你这边有一下需求,那么你可以考虑手写一个 plugin:
- 修改 anchor 的 target 属性
- 需要将已有的 link render 出来的 a 标签加上自定义化的结构
- 有自己独特的解析规则,比如
{% fun(xx) %}
- …
这里写一个 plugin 也是分难度的,从 覆盖更新 plugin 到 重写一个 plugin,难度是越来越大。
如果你只是想修改一个 anchor 的 target 属性,可以直接覆盖更新 link_open
的 rules 。
md.renderer.rules.link_open = (tokens, idx, options, env, self) => {
//... dosth
return defaultRender(tokens, idx, options, env, self)
}
如果你要写一个完整的 plugin,那么你需要分析改语法是 inline 还是 block 的 rule,以及,在 render 时,需要渲染成什么样的结构等等。
到了这一步,你就需要了解 token 长啥样,怎么去操作 token,操作 src 的字符串内容。如果真的要写的话,推荐先了解一下 状态机原理
,因为 markdown-it 里的 token rule 都是按照这个原则来写的。一段字符只对应一个状态,随着读取字符串的进度,其会从一个状态变为另外一个状态。
上面其实就是 编译原理
里面非常重要的一环,后面如果有时间,在开坑写吧。
具体例子,在 link 解析规则中,一个完整的解析过程,就是字符串内容的读取过程。
// 获得当前 rule 的上下文
var attrs,
code,
label,
labelEnd,
labelStart,
pos,
res,
ref,
title,
token,
href = '',
oldPos = state.pos,
max = state.posMax,
start = state.pos,
parseReference = true;
// 开始读取字符
if (state.src.charCodeAt(state.pos) !== 0x5B/* [ */) { return false; }
labelStart = state.pos + 1;
labelEnd = state.md.helpers.parseLinkLabel(state, state.pos, true);
// parser failed to find ']', so it's not a valid link
if (labelEnd < 0) { return false; }
pos = labelEnd + 1;
//... 其它读写规则
// 读写完毕后,将解析得到的 token 放到 state 中,并记录当前读取字符的位置
if (!silent) {
state.pos = labelStart;
state.posMax = labelEnd;
token = state.push('link_open', 'a', 1);
token.attrs = attrs = [ [ 'href', href ] ];
if (title) {
attrs.push([ 'title', title ]);
}
state.md.inline.tokenize(state);
token = state.push('link_close', 'a', -1);
}
state.pos = pos;
state.posMax = max;
return true;
一般而言,当你需要添加一个单一的 state 规则外,还需要配套有对应的解析规则,也就是 ruler。比如说,上面你定义的 state 的 token type 是 link_close,那么你应该对应还需要具备一个 link_close 的 ruler 解析规则,而这个解析规则就是对应你的 token 字段解析来做,最终返回的是一个符合 HTML 规则的字符串。比如,下面的 code_inline 解析 ruler.
default_rules.code_inline = function (tokens, idx, options, env, slf) {
var token = tokens[idx];
return '<code' + slf.renderAttrs(token) + '>' +
escapeHtml(tokens[idx].content) +
'</code>';
};
官方的顶一下
Ps: 欢迎关注我滴公众号 《前端小吉米》
后面会改名 🙂
混个脸熟
所以以前的文章在那写的😋,扔个链接?