前言

最近使用 markdown-it 比较多,也开发了一些插件,在这个过程中对源码进行了研读,最终写了这篇文章。需要了解细节的读者可以自行阅读文档。

此文分为两个部分:原理剖析和原理应用(编写插件)。

markdown-it 原理

输入一串 markdown 代码,最后得到一串 html 代码,整体流程如下:

我们以一个简单的例子来解释整个流程:​# 我是一个例子​ -> ​<h1>我是一个例子</h1>​

首先,它会被解析器拿到,经过各个解析规则处理后得到一个 token 流,接着这个 token 流被渲染器拿到,经过各个渲染规则处理后逐步拼接成一个 html 字符串。

解析器

markdown-it 内置了七个核心规则,在上图我对解析规则使用了虚线,因为它们是可以被启用/禁用的。我们这篇文章只来聊聊最核心的两个规则:block 和 inline。

规范指出:

我们可以将一篇 Markdown 文档视为一系列块,块是一种结构化的元素,如段落,块引用,列表,标题,规则和代码块。一些块(如块引号和列表项)可以包含其他块; 其他(如标题和段落)包含内联内容,如文本,链接,强调文本,图像,行内代码等。
块结构的解析优先级始终高于内联结构。这意味着解析可以分两步进行:
1.识别 markdown 文档的块结构;
2.将段落,标题和其他块结构中的文本行,作为内联结构解析。
注意,第一步需要按顺序处理行,但第二步可以并行化,因为一个块元素的内联解析不会影响任何其他块的内联解析。
块分为两种类型:容器块和叶子块,容器块可以包含其他块,但叶子块不能包含其他块。

具体解析时,会围绕着 line 和 character 两个维度来解析。

对于每一行来说,解释的结果有以下三种:

  1. 用来关闭一个或多个块结构。
  2. 用来创建一个或多个新块结构,作为最后打开的块结构的子节点。
  3. 可以将文本添加到树上剩余的最后(最深的)打开的块结构上。

对于我们这个例子,会先创建一个 heading 块,然后将文本内容添加到这个块上。下一行没有内容,于是块关闭。

字符包括非空白字符和空格(​U+0020​),制表符 (​U+0009​),换行符(​U+000A​),行列表(​U+000B​),换页(​U+000C​)或回车(​U+000D​)这些空白字符。这里我们不做展开。

这期间会接触到的规则有 block、inline、heading、text。

  1. block 规则,会用来解析 ​# 我是一个例子​
  • 先进入 tokenize 函数,内含十一个 block 规则。
  • heading 规则
  • 得到 heading_open 、inline、 heading_close 三个 token
  1. inline 规则,会用来解析 ​我是一个例子​
  • 先进入 parse 函数,内含四个 inline 规则
  • text 规则
  • 得到 text 的 token

解析完毕,我们得到了 3 + 1 个 token:

token 流

这里我们得到的结果不是一颗 AST 树,而是一个数组,markdown-it 称之为 token 流。为什么呢?

官方解释是:

  • Tokens 是一个简单的数组。(AST 是一个对象)
  • 打开的标签和关闭的标签可以隔离。
  • 将“内联容器(inline container)”作为一种特殊的 block token 对象。它有嵌套的 tokens,如粗体,斜体,文本等等。

这样做有什么好处呢?这样就可以并行处理 block 和 inline 类型的 token 了。

生成 token 流后,它们就被会传递给 renderer。

渲染器

它会遍历所有 token,将每个 token 传递给与 token 的 type 属性同名的规则。markdown-it 内置了九种规则:围栏、行内代码、代码块、html 块、行内 html、图片、硬换行、软换行、文本。

type 属性不在内置规则的 token 将会被被传入 renderToken 中当一个普通 token 处理,这里不作展开。

回到我们的例子中来:

heading_open 会被渲染成 ​<h1>​

inline 中的 text 会被渲染成 ​我是一个例子​

heading_close 会被渲染成 ​</h1>​

markdown-it 插件

一些 markdown-it 插件就利用了上述的原理。

markdown-it-container

这个插件可以让你支持内容块:比如 vuepress 的内容块:

这是如何实现的呢?我们可以根据之前的介绍推测一个内容块的 token 流:

第一行和第三行有 block 型的 token,一个代表 open,一个代表 close。第二行是 inline 型的 token,其中的内容是 inline 型的。

由于内容块中是 inline 类型,所以围栏、行内代码、代码块、html 块、行内 html、图片、硬换行、软换行、文本都是支持的。

实际上,我们会逐行扫描,找到匹配 ​::: tip​ 这样的内容块语法,将它作为一个块结构开始进行解析,直到有 ​:::​ 的行结束。其中的每一行,都将解析为 paragraph_open、inline、paragraph_close。

解析后的 token 流最后分别渲染 ​<div>​ 、若干 p 标签、 ​</div>​。

markdown-it-anchor

这个插件可以对标题进行锚点抽取,以便阅读文档时能快速定位位置。

这里也可以推测一下,是不是往原本是 heading_open type 的 token 之前插入了一个 token 呢?这个 token 渲染出来就是锚点。

实际上,的确是插入了 token,但不止一个,因为锚点是可点击的,所以实际上是一个 a 链接,也就是 link_open、inline、link_close 三个 token。而且也不是插入在 heading_open 之前,而是 heading_open 和 heading_close 之间的 inline 子元素里了,因为 ​#​ 是和 ​Markdown 语法​平级的。

注意事项:
1.因为标题可能是@#$等特殊字符,会造成 url 哈希无效,所以需要对锚点的哈希值转义。
2.可能会出现重名的标题,所以需要对哈希进行标记

给链接添加属性

官方有一个写插件的例子:添加 target="_blank" 属性到所有链接。

有两种方式:

  1. 修改渲染器规则
// 如果覆盖,或者是对默认渲染器的代理,则记住老的渲染器。
var defaultRender = md.renderer.rules.link_open || function(tokens, idx, options, env, self) {return self.renderToken(tokens, idx, options);
};md.renderer.rules.link_open = function (tokens, idx, options, env, self) {// 如果你确认其他的插件不能添加 `target` - 放弃以下检查:var aIndex = tokens[idx].attrIndex('target');if (aIndex < 0) {tokens[idx].attrPush(['target', '_blank']); // 添加新属性} else {tokens[idx].attrs[aIndex][1] = '_blank';    // 替换已经存在的属性值}// 传递 token 到默认的渲染器。return defaultRender(tokens, idx, options, env, self);
};

  1. 修改 token
var iterator = require('markdown-it-for-inline');var md = require('markdown-it')().use(iterator, 'url_new_win', 'link_open', function (tokens, idx) {var aIndex = tokens[idx].attrIndex('target');if (aIndex < 0) {tokens[idx].attrPush(['target', '_blank']);} else {tokens[idx].attrs[aIndex][1] = '_blank';}});

结语

markdown-it 作为一款经典的 js 解析 markdown 的库,其中思想和设计都可以细细揣摩,回味久久。

解析markdown_markdown-it 原理浅析相关推荐

  1. Python标准库threading模块Condition原理浅析

    Python标准库threading模块Condition原理浅析 本文环境python3.5.2 threading模块Condition的实现思路 在Python的多线程实现过程中,在Linux平 ...

  2. .NET1.1中预编译ASP.NET页面实现原理浅析[1]自动预编译机制浅析

    .NET1.1中预编译ASP.NET页面实现原理浅析[1]自动预编译机制浅析 .NET1.1中预编译ASP.NET页面实现原理浅析[1]自动预编译机制浅析 作者:&;nbsp来自:网络 htt ...

  3. 沉淀,再出发:docker的原理浅析

    沉淀,再出发:docker的原理浅析 一.前言 在我们使用docker的时候,很多情况下我们对于一些概念的理解是停留在名称和用法的地步,如果更进一步理解了docker的本质,我们的技术一定会有质的进步 ...

  4. GPU加速原理浅析及代码实现

    GPU加速原理浅析及代码实现 一.CUDA简介 二.GPU架构特点 三.CUDA线程模型 四.CUDA内存模型 五.CUDA编程规范 **第一个要掌握的编程要点**:**我们怎么写一个能在GPU跑的程 ...

  5. Spring Security入门到实践(一)HTTP Basic在Spring Security中的应用原理浅析

    一.Spring Security简介 打开Spring Security的官网,从其首页的预览上就可以看见如下文字: Spring Security is a powerful and highly ...

  6. 【vue双向绑定原理浅析】

    vue双向绑定原理浅析 1.什么是双向绑定? ​ 所谓双向绑定,指的是vue实例中的data与其渲染的DOM元素的内容保持一致,无论谁被改变,另一方会相应的更新为相同的数据.(数据变化更新视图,视图变 ...

  7. Springboot整合redis实现缓存及其缓存运行原理浅析

    声明:小白,学习阶段,主要目的是为了记录学习过程,本文仅供参考,如有不足的地方欢迎指出讨论交流 本文基于Springboot2.1.3版本开发: 准备阶段 首先是pom.xml文件所需的依赖: < ...

  8. Seata 分布式事务的使用和原理浅析

    Seata 分布式事务的精简使用教程和原理浅析 一.说明 二.Seata 简介 2.1.Seata 是什么? 2.2.Seata 的整体架构 2.2.1.主要角色 2.2.2.整体架构和工作流程图 2 ...

  9. Python标准库queue模块原理浅析

    Python标准库queue模块原理浅析 本文环境python3.5.2 queue模块的实现思路 作为一个线程安全的队列模块,该模块提供了线程安全的一个队列,该队列底层的实现基于Python线程th ...

  10. 第一篇: 词向量之Word2vector原理浅析

    第一篇: 词向量之Word2vector原理浅析 作者 Aroundtheworld 2016.11.05 18:50 字数 1353 阅读 5361评论 1喜欢 9 一.概述 本文主要是从deep ...

最新文章

  1. Python之路【第十二篇】:函数
  2. Ganglia 应用实践
  3. Qt Creator可视化Chrome跟踪事件
  4. Redis中的zset 存储结构(实现)原理
  5. html原生音频播放器倍速,HTML5倍数功能视频播放器(加速2倍,1.5倍播放)
  6. 夺命雷公狗---微信开发39----微信语言识别接口1
  7. 用python怎么读_python怎么读sql数据?
  8. AC双链路冷备份详解及配置原理
  9. java activit怎么画图_Activiti 入门示例
  10. 一文7个步骤教你搭建测试web测试项目实战环境,
  11. scratch优秀案例-中国风-西游记故事系列之孙悟空为何大战白骨精
  12. 深入理解 JVM 第三版
  13. 2019年Python数据挖掘就业前景前瞻
  14. openbsd mysql_在OpenBSD上运行MyDNSNameservers(MySQL / PHP + MyDNS + MyDNSConfig)
  15. 2018年38种自由职业大盘点
  16. 陈 好 性 感 写 真 ,难得一见哦[贴图]
  17. 小米air2se耳机只有一边有声音怎么办_小米耳机只有一边有声音,这问题怎么解决...
  18. for循环语句执行顺序
  19. .Net IIS 内存溢出(System.OutOfMemoryException)
  20. 数据字典项设计实现方案

热门文章

  1. html 添加窗口小部件,如何:为自定义窗口小部件定义主题(样式)项
  2. USB应用开发笔记之一:STM32上实现USB主机读写U盘
  3. React Native新手引导
  4. c++ 中文乱码_Visual Studio Code 中 CodeRunner 插件的输出窗口中文乱码
  5. Spring框架概述(快速入门)
  6. Android studio的Activity详解
  7. 静态成员 java_JAVA中的静态成员
  8. 电脑打字学习_寒假就这样做!即不让孩子烦,还能让他有进步,开学就能迅速进入学习状态!...
  9. int转unsigned int_谢劲课题组在基于锰催化的转金属化基元反应取得系列进展
  10. python tkinter画笑脸_Python3 tkinter基础 Canvas create_polygon 画三角形