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,对作者也是一种鼓励。



目录
相关文章
|
11月前
|
运维 持续交付 云计算
深入解析云计算中的微服务架构:原理、优势与实践
深入解析云计算中的微服务架构:原理、优势与实践
600 86
|
8月前
|
安全 算法 网络协议
解析:HTTPS通过SSL/TLS证书加密的原理与逻辑
HTTPS通过SSL/TLS证书加密,结合对称与非对称加密及数字证书验证实现安全通信。首先,服务器发送含公钥的数字证书,客户端验证其合法性后生成随机数并用公钥加密发送给服务器,双方据此生成相同的对称密钥。后续通信使用对称加密确保高效性和安全性。同时,数字证书验证服务器身份,防止中间人攻击;哈希算法和数字签名确保数据完整性,防止篡改。整个流程保障了身份认证、数据加密和完整性保护。
|
10月前
|
存储 缓存 算法
HashMap深度解析:从原理到实战
HashMap,作为Java集合框架中的一个核心组件,以其高效的键值对存储和检索机制,在软件开发中扮演着举足轻重的角色。作为一名资深的AI工程师,深入理解HashMap的原理、历史、业务场景以及实战应用,对于提升数据处理和算法实现的效率至关重要。本文将通过手绘结构图、流程图,结合Java代码示例,全方位解析HashMap,帮助读者从理论到实践全面掌握这一关键技术。
322 14
|
7月前
|
机器学习/深度学习 数据可视化 PyTorch
深入解析图神经网络注意力机制:数学原理与可视化实现
本文深入解析了图神经网络(GNNs)中自注意力机制的内部运作原理,通过可视化和数学推导揭示其工作机制。文章采用“位置-转移图”概念框架,并使用NumPy实现代码示例,逐步拆解自注意力层的计算过程。文中详细展示了从节点特征矩阵、邻接矩阵到生成注意力权重的具体步骤,并通过四个类(GAL1至GAL4)模拟了整个计算流程。最终,结合实际PyTorch Geometric库中的代码,对比分析了核心逻辑,为理解GNN自注意力机制提供了清晰的学习路径。
533 7
深入解析图神经网络注意力机制:数学原理与可视化实现
|
7月前
|
机器学习/深度学习 缓存 自然语言处理
深入解析Tiktokenizer:大语言模型中核心分词技术的原理与架构
Tiktokenizer 是一款现代分词工具,旨在高效、智能地将文本转换为机器可处理的离散单元(token)。它不仅超越了传统的空格分割和正则表达式匹配方法,还结合了上下文感知能力,适应复杂语言结构。Tiktokenizer 的核心特性包括自适应 token 分割、高效编码能力和出色的可扩展性,使其适用于从聊天机器人到大规模文本分析等多种应用场景。通过模块化设计,Tiktokenizer 确保了代码的可重用性和维护性,并在分词精度、处理效率和灵活性方面表现出色。此外,它支持多语言处理、表情符号识别和领域特定文本处理,能够应对各种复杂的文本输入需求。
944 6
深入解析Tiktokenizer:大语言模型中核心分词技术的原理与架构
|
8月前
|
机器学习/深度学习 算法 数据挖掘
解析静态代理IP改善游戏体验的原理
静态代理IP通过提高网络稳定性和降低延迟,优化游戏体验。具体表现在加快游戏网络速度、实时玩家数据分析、优化游戏设计、简化更新流程、维护网络稳定性、提高连接可靠性、支持地区特性及提升访问速度等方面,确保更流畅、高效的游戏体验。
211 22
解析静态代理IP改善游戏体验的原理
|
8月前
|
编解码 缓存 Prometheus
「ximagine」业余爱好者的非专业显示器测试流程规范,同时也是本账号输出内容的数据来源!如何测试显示器?荒岛整理总结出多种测试方法和注意事项,以及粗浅的原理解析!
本期内容为「ximagine」频道《显示器测试流程》的规范及标准,我们主要使用Calman、DisplayCAL、i1Profiler等软件及CA410、Spyder X、i1Pro 2等设备,是我们目前制作内容数据的重要来源,我们深知所做的仍是比较表面的活儿,和工程师、科研人员相比有着不小的差距,测试并不复杂,但是相当繁琐,收集整理测试无不花费大量时间精力,内容不完善或者有错误的地方,希望大佬指出我们好改进!
551 16
「ximagine」业余爱好者的非专业显示器测试流程规范,同时也是本账号输出内容的数据来源!如何测试显示器?荒岛整理总结出多种测试方法和注意事项,以及粗浅的原理解析!
|
7月前
|
传感器 人工智能 监控
反向寻车系统怎么做?基本原理与系统组成解析
本文通过反向寻车系统的核心组成部分与技术分析,阐述反向寻车系统的工作原理,适用于适用于商场停车场、医院停车场及火车站停车场等。如需获取智慧停车场反向寻车技术方案前往文章最下方获取,如有项目合作及技术交流欢迎私信作者。
505 2
|
9月前
|
机器学习/深度学习 自然语言处理 搜索推荐
自注意力机制全解析:从原理到计算细节,一文尽览!
自注意力机制(Self-Attention)最早可追溯至20世纪70年代的神经网络研究,但直到2017年Google Brain团队提出Transformer架构后才广泛应用于深度学习。它通过计算序列内部元素间的相关性,捕捉复杂依赖关系,并支持并行化训练,显著提升了处理长文本和序列数据的能力。相比传统的RNN、LSTM和GRU,自注意力机制在自然语言处理(NLP)、计算机视觉、语音识别及推荐系统等领域展现出卓越性能。其核心步骤包括生成查询(Q)、键(K)和值(V)向量,计算缩放点积注意力得分,应用Softmax归一化,以及加权求和生成输出。自注意力机制提高了模型的表达能力,带来了更精准的服务。
11085 46
|
8月前
|
Java 数据库 开发者
详细介绍SpringBoot启动流程及配置类解析原理
通过对 Spring Boot 启动流程及配置类解析原理的深入分析,我们可以看到 Spring Boot 在启动时的灵活性和可扩展性。理解这些机制不仅有助于开发者更好地使用 Spring Boot 进行应用开发,还能够在面对问题时,迅速定位和解决问题。希望本文能为您在 Spring Boot 开发过程中提供有效的指导和帮助。
973 12

推荐镜像

更多
  • DNS