共计 6531 个字符,预计需要花费 17 分钟才能阅读完成。
前言
在《一篇带你用 VuePress + Github Pages 搭建博客》中,咱们应用 VuePress 搭建了一个博客,最终的成果查看:TypeScript 中文文档。
在搭建博客的过程中,咱们出于理论的需要,在《VuePress 博客优化之拓展 Markdown 语法》中解说了如何写一个 markdown-it
插件,本篇咱们将深刻 markdown-it
的源码,解说 markdown-it
的执行原理,旨在让大家对 markdown-it
有更加深刻的了解。
介绍
援用 markdown-it Github 仓库的介绍:
Markdown parser done right. Fast and easy to extend.
能够看出 markdown-it
是一个 markdown 解析器,并且易于拓展。
其演示地址为:https://markdown-it.github.io/
markdown-it
具备以下几个劣势:
- 遵循 CommonMark spec 并且增加了语法拓展和语法糖(如 URL 自动识别,针对印刷做了非凡解决)
- 可配置语法,你能够增加新的规定或者替换掉现有的规定
- 快
- 默认平安
- 社区有很多的插件或者其余包
应用
// 装置
npm install markdown-it --save
// node.js, "classic" way:
var MarkdownIt = require('markdown-it'),
md = new MarkdownIt();
var result = md.render('# markdown-it rulezz!');
// browser without AMD, added to "window" on script load
// Note, there is no dash in "markdownit".
var md = window.markdownit();
var result = md.render('# markdown-it rulezz!');
源码解析
咱们查看 markdown-it
的入口代码,能够发现其代码逻辑清晰明了:
// ...
var Renderer = require('./renderer');
var ParserCore = require('./parser_core');
var ParserBlock = require('./parser_block');
var ParserInline = require('./parser_inline');
function MarkdownIt(presetName, options) {
// ...
this.inline = new ParserInline();
this.block = new ParserBlock();
this.core = new ParserCore();
this.renderer = new Renderer();
// ...
}
MarkdownIt.prototype.parse = function (src, env) {
// ...
var state = new this.core.State(src, this, env);
this.core.process(state);
return state.tokens;
};
MarkdownIt.prototype.render = function (src, env) {env = env || {};
return this.renderer.render(this.parse(src, env), this.options, env);
};
从 render
办法中也能够看出,其渲染分为两个过程:
- Parse:将 Markdown 文件 Parse 为 Tokens
- Render:遍历 Tokens 生成 HTML
跟 Babel 很像,不过 Babel 是转换为形象语法树(AST),而 markdown-it
没有抉择应用 AST,次要是为了遵循 KISS(Keep It Simple, Stupid) 准则。
Tokens
那 Tokens 长什么样呢?咱们无妨在演示页面中尝试一下:
能够看出 # header
生成的 Token 格局为(注:这里为了展现不便,简化了):
[
{
"type": "heading_open",
"tag": "h1"
},
{
"type": "inline",
"tag": "","children": [
{
"type": "text",
"tag": "","content":"header"
}
]
},
{
"type": "heading_close",
"tag": "h1"
}
]
具体 Token 里的字段含意能够查看 Token Class。
通过这个简略的 Tokens 示例也能够看出 Tokens 和 AST 的区别:
- Tokens 只是一个简略的数组
- 起始标签和闭合标签是离开的
Parse
查看 parse 办法相干的代码:
// ...
var ParserCore = require('./parser_core');
function MarkdownIt(presetName, options) {
// ...
this.core = new ParserCore();
// ...
}
MarkdownIt.prototype.parse = function (src, env) {
// ...
var state = new this.core.State(src, this, env);
this.core.process(state);
return state.tokens;
};
能够看到其具体执行的代码,应该是写在了./parse_core
里,查看下 parse_core.js
的代码:
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') ]
];
function Core() {// ...}
Core.prototype.process = function (state) {
// ...
for (i = 0, l = rules.length; i < l; i++) {rules[i](state);
}
};
能够看出,Parse 过程默认有 6 条规定,其次要作用别离是:
1. normalize
在 CSS 中,咱们应用normalize.css
抹平各端差别,这里也是一样的逻辑,咱们查看 normalize 的代码,其实很简略:
// https://spec.commonmark.org/0.29/#line-ending
var NEWLINES_RE = /\r\n?|\n/g;
var NULL_RE = /\0/g;
module.exports = function normalize(state) {
var str;
// Normalize newlines
str = state.src.replace(NEWLINES_RE, '\n');
// Replace NULL characters
str = str.replace(NULL_RE, '\uFFFD');
state.src = str;
};
咱们晓得 \n
是匹配一个换行符,\r
是匹配一个回车符,那这里为什么要将 \r\n
替换成 \n
呢?
咱们能够在阮一峰老师的这篇《回车与换行》中找到 \r\n
呈现的历史:
在计算机还没有呈现之前,有一种叫做电传打字机(Teletype Model 33)的玩意,每秒钟能够打 10 个字符。然而它有一个问题,就是打完一行换行的时候,要用去 0.2 秒,正好能够打两个字符。要是在这 0.2 秒外面,又有新的字符传过来,那么这个字符将失落。
于是,研制人员想了个方法解决这个问题,就是在每行前面加两个示意完结的字符。一个叫做 ” 回车 ”,通知打字机把打印头定位在左边界;另一个叫做 ” 换行 ”,通知打字机把纸向下移一行。
这就是 ” 换行 ” 和 ” 回车 ” 的来历,从它们的英语名字上也能够看出一二。
起初,计算机创造了,这两个概念也就被般到了计算机上。那时,存储器很贵,一些科学家认为在每行结尾加两个字符太节约了,加一个就能够。于是,就呈现了一致。
Unix 零碎里,每行结尾只有 ”< 换行 >”,即 ”\n”;Windows 零碎外面,每行结尾是 ”< 回车 >< 换行 >”,即 ”\r\n”;Mac 零碎里,每行结尾是 ”< 回车 >”。一个间接结果是,Unix/Mac 零碎下的文件在 Windows 里关上的话,所有文字会变成一行;而 Windows 里的文件在 Unix/Mac 下关上的话,在每行的结尾可能会多出一个 ^M 符号。
之所以将 \r\n
替换成 \n
其实是遵循标准:
A line ending is a newline (U+000A), a carriage return (U+000D) not followed by a newline, or a carriage return and a following newline.
其中 U+000A 示意换行(LF),U+000D 示意回车(CR)。
除了替换回车符外,源码里还替换了空字符,在正则中,\0
示意匹配 NULL(U+0000)字符,依据 WIKI 的解释:
空字符(Null character)又称结束符,缩写 NUL,是一个数值为 0 的控制字符。
在许多字符编码中都包含空字符,包含 ISO/IEC 646(ASCII)、C0 管制码、通用字符集、Unicode 和 EBCDIC 等,简直所有支流的编程语言都包含有空字符
这个字符原来的意思相似 NOP 指令,当送到列表机或终端时,设施不需作任何的动作(不过有些设施会谬误的打印或显示一个空白)。
而咱们将空字符替换为 \uFFFD
,在 Unicode 中,\uFFFD
示意替换字符:
之所以进行这个替换,其实也是遵循标准,咱们查阅 CommonMark spec 2.3 章节:
For security reasons, the Unicode character U+0000 must be replaced with the REPLACEMENT CHARACTER (U+FFFD).
咱们测试下这个成果:
md.render('foo\u0000bar'), '<p>foo\uFFFDbar</p>\n'
成果如下,你会发现本来不可见的 空字符 被替换成 替换字符 后,展现了进去:
2. block
block 这个规定的作用就是找出 block,生成 tokens,那什么是 block?什么是 inline 呢?咱们也能够在 CommonMark spec 中的 Blocks and inlines 章节 找到答案:
We can think of a document as a sequence of blocks—structural elements like paragraphs, block quotations, lists, headings, rules, and code blocks. Some blocks (like block quotes and list items) contain other blocks; others (like headings and paragraphs) contain inline content—text, links, emphasized text, images, code spans, and so on.
翻译一下就是:
咱们认为文档是由一组 blocks 组成,结构化的元素相似于段落、援用、列表、题目、代码区块等。一些 blocks(像援用和列表)能够蕴含其余 blocks,其余的一些 blocks(像题目和段落)则能够蕴含 inline 内容,比方文字、链接、强调文字、图片、代码片段等等。
当然在 markdown-it
中,哪些会辨认成 blocks,能够查看 parser_block.js,这里同样定义了一些辨认和 parse 的规定:
对于这些规定我挑几个不常见的阐明一下:
code
规定用于辨认 Indented code blocks
(4 spaces padded),在 markdown 中:
fence
规定用于辨认 Fenced code blocks,在 markdown 中:
hr
规定用于辨认换行,在 markdown 中:
reference
规定用于辨认 reference links
,在 markdown 中:
html_block
用于辨认 markdown 中的 HTML block 元素标签,就比方div
。
lheading
用于辨认 Setext headings
,在 markdown 中:
3. inline
inline 规定的作用则是解析 markdown 中的 inline,而后生成 tokens,之所以 block 先执行,是因为 block 能够蕴含 inline,解析的规定能够查看 parser_inline.js:
对于这些规定我挑几个不常见的阐明一下:
newline
规定用于辨认 \n
,将 \n
替换为一个 hardbreak 类型的 token
backticks
规定用于辨认反引号:
entity
规定用于解决 HTML entity,比方 {
`¯`"
等:
4. linkify
自动识别链接
5. replacements
将 (c)
` (C) 替换成
©,将
???????? 替换成
???,将
!!!!! 替换成
!!!`,诸如此类:
6. smartquotes
为了不便印刷,对直引号做了解决:
Render
Render 过程其实就比较简单了,查看 renderer.js 文件,能够看到内置了一些默认的渲染 rules:
default_rules.code_inline
default_rules.code_block
default_rules.fence
default_rules.image
default_rules.hardbreak
default_rules.softbreak
default_rules.text
default_rules.html_block
default_rules.html_inline
其实这些名字也是 token 的 type,在遍历 token 的时候依据 token 的 type 对应这里的 rules 进行执行,咱们看下 code_inline 规定的内容,其实非常简单:
default_rules.code_inline = function (tokens, idx, options, env, slf) {var token = tokens[idx];
return '<code' + slf.renderAttrs(token) + '>' +
escapeHtml(tokens[idx].content) +
'</code>';
};
自定义 Rules
至此,咱们对 markdown-it 的渲染原理进行了简略的理解,无论是 Parse 还是 Render 过程中的 Rules,markdown-it 都提供了办法能够自定义这些 Rules,这些也是写 markdown-it 插件的要害,这些后续咱们会讲到。
系列文章
博客搭建系列是我至今写的惟一一个偏实战的系列教程,解说如何应用 VuePress 搭建博客,并部署到 GitHub、Gitee、集体服务器等平台。
- 一篇带你用 VuePress + GitHub Pages 搭建博客
- 一篇教你代码同步 GitHub 和 Gitee
- 还不会用 GitHub Actions?看看这篇
- Gitee 如何主动部署 Pages?还是用 GitHub Actions!
- 一份前端够用的 Linux 命令
- 一份简略够用的 Nginx Location 配置解说
- 一篇从购买服务器到部署博客代码的具体教程
- 一篇域名从购买到备案到解析的具体教程
- VuePress 博客优化之 last updated 最初更新工夫如何设置
- VuePress 博客优化之增加数据统计性能
- VuePress 博客优化之开启 HTTPS
- VuePress 博客优化之开启 Gzip 压缩
- 从零实现一个 VuePress 插件
微信:「mqyqingfeng」,加我进冴羽惟一的读者群。
如果有谬误或者不谨严的中央,请务必给予斧正,非常感激。如果喜爱或者有所启发,欢送 star,对作者也是一种激励。