Markdown-it 原理解析

简介: 「这是我参与2022首次更文挑战的第2天,活动详情查看:2022首次更文挑战」。

0.png


「这是我参与2022首次更文挑战的第2天,活动详情查看:2022首次更文挑战」。


前言


《一篇带你用 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 解析器,并且易于拓展。


其演示地址为: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 长什么样呢?我们不妨在演示页面中尝试一下:


1.png


可以看出 # 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表示替换字符:


2.png


之所以进行这个替换,其实也是遵循规范,我们查阅 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'
复制代码


效果如下,你会发现原本不可见的空字符被替换成替换字符后,展示了出来:


3.png


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 的规则:


4.png


关于这些规则我挑几个不常见的说明一下:


code 规则用于识别 Indented code blocks (4 spaces padded),在 markdown 中:


5.png


fence 规则用于识别 Fenced code blocks,在markdown 中:


6.png


hr 规则用于识别换行,在 markdown 中:


7.png


reference 规则用于识别 reference links,在 markdown 中:


8.png


html_block 用于识别 markdown 中的 HTML block 元素标签,就比如div


lheading 用于识别 Setext headings,在 markdown 中:


9.png


3. inline


inline 规则的作用则是解析 markdown 中的 inline,然后生成 tokens,之所以 block 先执行,是因为 block 可以包含 inline ,解析的规则可以查看 parser_inline.js


10.png


关于这些规则我挑几个不常见的说明一下:


newline规则用于识别 \n,将 \n 替换为一个 hardbreak 类型的 token

backticks 规则用于识别反引号:


11.png


entity 规则用于处理 HTML entity,比如 {``¯``"等:


12.png


4. linkify


自动识别链接


13.png


5. replacements


(c)`` (C) 替换成 ©,将 ???????? 替换成 ???,将 !!!!! 替换成 !!!,诸如此类:


14.png


6. smartquotes


为了方便印刷,对直引号做了处理:


15.png


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 搭建博客


  1. 一篇教你代码同步 GitHub 和 Gitee


  1. 还不会用 GitHub Actions ?看看这篇


  1. Gitee 如何自动部署 Pages?还是用 GitHub Actions!


  1. 一份前端够用的 Linux 命令


  1. 一份简单够用的 Nginx Location 配置讲解


  1. 一篇从购买服务器到部署博客代码的详细教程


  1. 一篇域名从购买到备案到解析的详细教程


  1. VuePress 博客优化之 last updated 最后更新时间如何设置


  1. VuePress 博客优化之添加数据统计功能


  1. VuePress 博客优化之开启 HTTPS


  1. VuePress 博客优化之开启 Gzip 压缩


  1. 从零实现一个 VuePress 插件


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


如果有错误或者不严谨的地方,请务必给予指正,十分感谢。如果喜欢或者有所启发,欢迎 star,对作者也是一种鼓励。



目录
相关文章
|
1天前
|
机器学习/深度学习 存储 算法
卷积神经网络(CNN)的数学原理解析
卷积神经网络(CNN)的数学原理解析
33 1
卷积神经网络(CNN)的数学原理解析
|
1天前
|
传感器 数据采集 存储
岩土工程监测仪器之一:振弦采集仪的工作原理解析
岩土工程监测仪器之一:振弦采集仪的工作原理解析
岩土工程监测仪器之一:振弦采集仪的工作原理解析
|
1天前
|
XML JavaScript 数据格式
Beautiful Soup 库的工作原理基于解析器和 DOM(文档对象模型)树的概念
【5月更文挑战第10天】Beautiful Soup 使用解析器(如 html.parser, lxml, html5lib)解析HTML/XML文档,构建DOM树。它提供方法查询和操作DOM,如find(), find_all()查找元素,get_text(), get()提取信息。还能修改DOM,添加、修改或删除元素,并通过prettify()输出格式化字符串。它是处理网页数据的利器,尤其在处理不规则结构时。
37 2
|
1天前
|
机器学习/深度学习 人工智能 数据可视化
号称能打败MLP的KAN到底行不行?数学核心原理全面解析
Kolmogorov-Arnold Networks (KANs) 是一种新型神经网络架构,挑战了多层感知器(mlp)的基础,通过在权重而非节点上使用可学习的激活函数(如b样条),提高了准确性和可解释性。KANs利用Kolmogorov-Arnold表示定理,将复杂函数分解为简单函数的组合,简化了神经网络的近似过程。与mlp相比,KAN在参数量较少的情况下能达到类似或更好的性能,并能直观地可视化,增强了模型的可解释性。尽管仍需更多研究验证其优势,KAN为深度学习领域带来了新的思路。
109 5
|
1天前
|
敏捷开发 测试技术 持续交付
极限编程(XP)原理与技巧:深入解析与实践
【5月更文挑战第8天】极限编程(XP)是一种敏捷开发方法,注重快速反馈、迭代开发和简单设计,以提高软件质量和项目灵活性。关键原则包括客户合作、集体代码所有权、持续集成等。实践中,使用故事卡片描述需求,遵循编程约定,实行TDD,持续重构,结对编程,并定期举行迭代会议。通过理解和应用XP,团队能提升效率,应对变化。
|
1天前
|
缓存 自然语言处理 JavaScript
万字长文深度解析JDK序列化原理及Fury高度兼容的极致性能实现
Fury是一个基于JIT动态编译的高性能多语言原生序列化框架,支持Java/Python/Golang/C++/JavaScript等语言,提供全自动的对象多语言/跨语言序列化能力,以及相比于别的框架最高20~200倍的性能。
168478 0
|
1天前
|
存储 芯片
【期末不挂科-单片机考前速过系列P11】(第十一章:15题速过串行口的工作原理和应用)经典例题盘点(带图解析)
【期末不挂科-单片机考前速过系列P11】(第十一章:15题速过串行口的工作原理和应用)经典例题盘点(带图解析)
【期末不挂科-单片机考前速过系列P10】(第十章:11题中断系统的工作原理及应用)经典例题盘点(带图解析)
【期末不挂科-单片机考前速过系列P10】(第十章:11题中断系统的工作原理及应用)经典例题盘点(带图解析)
|
1天前
|
C语言 C++
【期末不挂科-单片机考前速过系列P1】(第一章:27题搞定单片机&其工作原理)经典例题盘点【选择题&判断题&填空题】(带图解析)
【期末不挂科-单片机考前速过系列P1】(第一章:27题搞定单片机&其工作原理)经典例题盘点【选择题&判断题&填空题】(带图解析)
|
1天前
|
JavaScript 前端开发 算法
vue生命周期函数原理解析,vue阻止事件冒泡方法实现
vue生命周期函数原理解析,vue阻止事件冒泡方法实现

热门文章

最新文章

推荐镜像

更多