乐趣区

关于前端:Markdownit-原理解析

前言

在《一篇带你用 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办法中也能够看出,其渲染分为两个过程:

  1. Parse:将 Markdown 文件 Parse 为 Tokens
  2. 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 的区别:

  1. Tokens 只是一个简略的数组
  2. 起始标签和闭合标签是离开的

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,比方 &#123;`&quot;等:

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、集体服务器等平台。

  1. 一篇带你用 VuePress + GitHub Pages 搭建博客
  2. 一篇教你代码同步 GitHub 和 Gitee
  3. 还不会用 GitHub Actions?看看这篇
  4. Gitee 如何主动部署 Pages?还是用 GitHub Actions!
  5. 一份前端够用的 Linux 命令
  6. 一份简略够用的 Nginx Location 配置解说
  7. 一篇从购买服务器到部署博客代码的具体教程
  8. 一篇域名从购买到备案到解析的具体教程
  9. VuePress 博客优化之 last updated 最初更新工夫如何设置
  10. VuePress 博客优化之增加数据统计性能
  11. VuePress 博客优化之开启 HTTPS
  12. VuePress 博客优化之开启 Gzip 压缩
  13. 从零实现一个 VuePress 插件

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

如果有谬误或者不谨严的中央,请务必给予斧正,非常感激。如果喜爱或者有所启发,欢送 star,对作者也是一种激励。

退出移动版