结构化文本处理利器 unified 生态介绍

Posted by imyzf on 2022-01-24

概述

Content as structured data.

—— unified 官网题词

unified 是一套文本处理相关的生态体系,结合其生态上的相关插件,能够处理 Markdown、HTML、自然语言等。而 unified 库本身又作为一个统一的执行接口,担任执行器的角色,调用其生态上相关的插件完成处理任务。

unified 官网 上可以看到,目前 unified 的使用非常广泛,包括 Prettier、Node.js 官网、Gatsby 都运用了 unified 的能力完成了一些功能。

图:unified 官网的使用举例

鉴于目前国内对 unified 体系的介绍文章非常少,本文将对 unified 的相关插件生态、工作原理作介绍,并对一些使用例子作解析,帮助读者了解 unified 体系的能力、原理和用途。

插件生态

图:unified 生态相关插件

remark

remark 是 Markdown 相关的插件集合,提供了 Markdown 的解析、修改、转换为 HTML 等能力。

目前提供的一些常用插件:

完整的插件列表可以参考此处,大约有 150 多个插件可供选择。

我们可以在项目中使用这种便捷的方式调用 remark:

remark()
.processSync('# Hello, world!')

等价于如下方式:

unified()
.use(remarkParse)
.use(remarkStringify)
.processSync('# Hello, world!')

图:remark 使用和转换示例

另外需要注意的是,GitHub 上目前有一个同名项目 gnab/remark,其官网为 remarkjs.com,虽然也是与 Markdown 相关的工具,但其与 unified 生态下的 remark 没有任何关系,本文提到的 remark 的官网为 remark.js.org,通过搜索引擎搜索相关资料时需要避免混淆。

rehype

与 remark 类似,rehype 是 HTML 相关的插件集合,提供了 HTML 的格式化、压缩、文档生成等能力。

相比之下,rehype 的插件相对较少,只有 40 多个,详细的插件列表可以参考插件列表文档

同时,我们也可以使用rehype-remarkremark-rehype实现两种语言的插件体系之间的互相转换。例如下面的例子,可以实现将 stdin 输入的 HTML 内容转换为 Markdown:

import {unified} from 'unified'
import {stream} from 'unified-stream'
import rehypeParse from 'rehype-parse'
import rehypeRemark from 'rehype-remark'
import remarkStringify from 'remark-stringify'

const processor = unified()
.use(rehypeParse) // 解析 HTML
.use(rehypeRemark) // 转换到 remark 体系
.use(remarkStringify) // 将语法树转换为 Markdown 字符串

process.stdin.pipe(stream(processor)).pipe(process.stdout)

其他

retextredot 是两个比较小众的体系,使用量较少,开发也不如前述两个体系活跃,其用途如下:

  • retext: 提供自然语言的处理能力,包括拼写检查、错误修正、可读性检查等
  • redot: 提供 graphviz 的解析能力

另外在 Markdown 领域,有两个非 re 开头的体系,mdxmicromark,分别对应特定的 markdown 使用场景:

  • mdx: 提供在 Markdown 文档中编写 JSX 的能力,实现在文档中引入各类组件,编写可交互的文档
  • micromark: 一个极简的 Markdown 转换库,支持少量扩展插件,适合简单的 Markdown 转 HTML 场景,同时 remark 也复用了 micromark 的解析能力

具体的信息可以查看项目文档了解,这里不再赘述。

工作原理

unified 的核心机制是基于 AST(abstract syntax trees,抽象语法树),在执行插件时 AST 会被传递给插件,可以对其进行各种处理。同时,也可以基于 AST 进行各种语言的转换,例如将 Markdown 文档解析后,转换为 HTML 进行处理,之后再转回 Markdown。

例如我们可以在插件中遍历 AST,将所有 heading 节点打印出来:

module.exports = () => tree => {
visit(tree, 'heading', node => {
console.log(node)
})
}

上面例子中的 visit 方法来自 unist-util-visit 工具,提供了遍历节点的功能。unified 使用了一种称为 unist 或者 UST 的 AST 标准,使得相同的工具能够在不同的语言上使用。例如针对 Markdown 和 HTML 语言的 AST,由于他们基于相同的标准,我们可以使用同样的 visit API 实现同样的功能:

visit(markdownAST, 'images', transformImages)
visit(htmlAST, 'img', transformImgs)

场景举例

接下来将列举一些基于 unified 生态的使用场景,帮助大家进一步了解其用途。

Node.js 官网

Node.js 官网主要在两个方面使用了 unified:

  • 使用 remark-cli 检查 Markdown 文档,参考其 package.json 中的脚本配置
  • 使用 unified 进行文档构建,参考 generate.mjs 中的代码

dumi

dumi 是一款为组件开发场景定制的文档工具,其核心功能就是将 Markdown 文档转换为 HTML 页面。查看其源码,我们会发现其使用了 unified 作为转换器,在 remark/index.ts 中引入了 unified,并调用了一些列自定义的或者官方提供的插件进行处理。

由于使用了非常多的自定义插件,dumi 源码可以作为 unified 插件开发极佳的参考例子。例如参考 link.ts,可以了解如何将 Markdown 中的外部链接,通过修改 AST,在生成的页面中增加一个链接小图标,提示用户这是一个指向外部站点的链接。

文档源码:

[云音乐官网](https://music.163.com/)

转换为:

<a target="_blank" rel="noopener noreferrer" href="https://github.com/umijs/umi-hd">
云音乐官网
<svg class="__dumi-default-external-link-icon">……</svg>
</a>

react-markdown

react-markdown 作为 remark 体系的一部分,是基于 unified 生态的上层封装,提供了一个能够渲染 Markdown 的 React 组件。在 React 框架中,比起直接使用 remark 将 Markdown 转换为 HTML 再使用 dangerouslySetInnerHTML 渲染,使用 react-markdown 更加安全可靠,使用方式也更加简单便捷。

图:react-markdown 工作原理

上图展示了 react-markdown 的工作原理,流程如下:

  1. 通过 remark 将 Markdown 转换为对应的 AST —— mdast
  2. 使用 remark 插件对 mdast 进行处理
  3. 通过 remark-rehype 将 mdast 转换为 HTML 的 AST —— hast
  4. 使用 rehype 插件对 hast 进行处理
  5. 使用 React 组件渲染 hast 为 React 元素

以上整个流程其实是 Markdown 渲染为 HTML 的通用处理流程 ,我们在实现类似的库时,也可以作为参考。

关于作者

目前 unified 生态总共有 502 个包和 331 个开源项目(截至 2021.12.23),其核心开发者为 Titus Wormer。从 Wormer 的个人网站可以了解到,他来自荷兰,毕业于阿姆斯特丹应用科学大学,并且曾经担任过该大学的讲师。作为一名全职开源贡献者,总共维护了 535 多个项目,其中有 50% 的时间精力投入到 unified 项目上。能够凭一己之力,对开源社区作出如此大的贡献,非常值得敬佩。关于他是如何管理 unified 的组织的,可以参考 unified collective 文档作进一步了解。

知识共享许可协议 本文发表于www.imyzf.com,采用知识共享署名-相同方式共享 4.0 国际许可协议进行许可。