惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

人人都是产品经理
人人都是产品经理
Stack Overflow Blog
Stack Overflow Blog
L
LINUX DO - 最新话题
Google Online Security Blog
Google Online Security Blog
Schneier on Security
Schneier on Security
Spread Privacy
Spread Privacy
www.infosecurity-magazine.com
www.infosecurity-magazine.com
雷峰网
雷峰网
Google DeepMind News
Google DeepMind News
Microsoft Azure Blog
Microsoft Azure Blog
IT之家
IT之家
V
Vulnerabilities – Threatpost
K
Kaspersky official blog
S
Schneier on Security
B
Blog
The Register - Security
The Register - Security
SecWiki News
SecWiki News
Hacker News: Ask HN
Hacker News: Ask HN
cs.AI updates on arXiv.org
cs.AI updates on arXiv.org
S
Security Affairs
T
The Blog of Author Tim Ferriss
G
Google Developers Blog
T
Tenable Blog
P
Proofpoint News Feed
Apple Machine Learning Research
Apple Machine Learning Research
D
DataBreaches.Net
S
Secure Thoughts
Security Latest
Security Latest
H
Heimdal Security Blog
The Hacker News
The Hacker News
O
OpenAI News
AWS News Blog
AWS News Blog
量子位
Exploit-DB.com RSS Feed
Exploit-DB.com RSS Feed
腾讯CDC
U
Unit 42
L
Lohrmann on Cybersecurity
cs.CL updates on arXiv.org
cs.CL updates on arXiv.org
L
LangChain Blog
阮一峰的网络日志
阮一峰的网络日志
T
The Exploit Database - CXSecurity.com
NISL@THU
NISL@THU
cs.CV updates on arXiv.org
cs.CV updates on arXiv.org
Application and Cybersecurity Blog
Application and Cybersecurity Blog
Hugging Face - Blog
Hugging Face - Blog
The Last Watchdog
The Last Watchdog
Recorded Future
Recorded Future
V2EX - 技术
V2EX - 技术
爱范儿
爱范儿
F
Full Disclosure

程沛权 - 养了三只猫

声画共生:黄贯中的摇滚美学宇宙 适用于 Oxlint 与 Oxfmt 的 Oxc 优先工作流 Vue Picture Cropper 发布 1.x 版本 讲一讲背后的设计理念 年终总结:2025 年的一些回顾和 2026 年的一些小规划 Obsidian x 飞牛 NAS:打造免费的跨平台笔记同步与备份方案 适用于 ESLint V9 的现代化扁平化配置 解决 better-sqlite3 连接 SQLite 时报错 Could not locate the bindings file 本色十年 记录一次 ERR_INCOMPLETE_CHUNKED_ENCODING 的问题排查 干净的 TypeScript 项目在编译时报错 Cannot find module 'undici-types' 的原因和解决 macOS 基于 Android Studio 修改模拟器 Hosts 给猫儿子的新年礼物 它很喜欢! 年终总结:2023 年的一些回顾和 2024 年的一些小规划 千元预算组装入门 NAS 设备 分享 NAS 的硬件基础知识 我写了一本书《前端工程化:基于 Vue.js 3.0 的设计与实践》 想分享一下它背后的故事 Git的选择性合并操作笔记:合并某个版本或某个提交 年终总结:2022 年的一些回顾和 2023 年的一些小规划 知乎收藏夹助手:自动化将专栏的文章添加到收藏夹 用Vite更简单的解决Vue3项目的预渲染问题 Pinia怎么用?Vue3全局状态的管理工具Pinia教程 每天吃什么?上班带什么饭?程序员教你做菜啦!
使用 remark-directive 为 Unifiedjs 提供 Markdown 视频语法的解析
chengpeiquan · 2024-11-09 · via 程沛权 - 养了三只猫

最近对博客进行了一次技术栈迁移,其中对 Markdown 的解析渲染支持也从 Markdown-it 系列迁移至 Unifiedjs 系列,在 Unified 的工作流程里,又包含了处理 Markdown 的 Remarkjs 系列以及处理 HTML 的 Rehypejs 系列。

在博客里, Markdown Parser 的整个工作流程都是自己管理的,包括不同结果的输出,例如:提供给 RSS 订阅用的 HTML ,提供给列表和搜索用的 Metadata ,以及提供给详情页作为 React 组件渲染内容用的 JSX ,这些过程并不算复杂,事实上进展确实是很顺利,但是在我以为即将大功告成之际,突然发现渲染出来的内容少了一个东西:我的视频呢?

改版前的表现

改版之前是使用 Markdown-it 作为技术栈, Markdown 代码与 HTML 代码的相处非常和谐,对于没有 Markdown 原生语法支持的 HTML 标签,都可以直接编写 HTML 代码进行渲染,内嵌视频最初就是这样子实现的。

像这样,在 Markdown 里直接编写 HTML 代码,即可直接输出 HTML 。

但改版后,原本应该渲染为视频的地方,都只剩下一个 <p></p> 标签,很明显是在 Markdown 代码转换过程中被过滤了。

理解 Unified 的工作流程

像我这种 Parser 流程比较长,中间处理环节还是动态变化的情况,很怕这些奇怪的问题,但还好 Unified 的设计非常清晰,先了解一下实现原理,更方便找到问题的原因。上面提到了 Unified 包含了 Remark 和 Rehype 两个系列的工作流,因为在 Unified 生态的工作过程中,都是基于 AST 语法树工作,可以简单地理解为:

  1. 先由 Remark 负责把 Markdown 文件的内容转为 MDAST ,在这个过程中所有代码都是围绕 Markdown 工作
  2. 再通过中间插件 remark-rehype ,把 MDAST 转为 HAST
  3. 最终由 Rehype 处理 HAST ,自此阶段开始,所有工作都是围绕 HTML 展开,最终输出什么样的结果也是在这个阶段处理

以上流程可以反过来,也就是先处理 HTML 再还原为 Markdown ,如果是这种流程,中间插件需要更换为 rehype-remark

名词解释:

MDAST —— Markdown Abstract Syntax Tree , Markdown 抽象语法树

HAST —— Hypertext Abstract Syntax Tree ,超文本抽象语法树

问题的排查

了解了工作流程,就可以分三个阶段排查问题了,要么就是在 Remark 环节把 HTML 代码屏蔽了,要么就是 Rehype 环节有问题,要么就是中间的 AST 转换抛弃了这部分代码。

此时 Parser 里的处理器插件是这么启用的:

这里的每一个 Plugins 变量都是一个数组,会根据我的构建场景动态启用插件(相关源码见:core/parser ),例如:

因此先仅启用 remarkPlugins ,发现一切正常,继续启用 remarkRehypePlugins ,就出问题了, Markdown 里的 <video /> 标签被过滤了。

所以我在 remark-rehype 的文档里找到了关于 HTML 标签过滤的说明:

因为在 markdown 中支持 HTML 是一项繁重的任务(性能和包大小) ,而且并不总是需要的,要同时使用两者,您还必须配置 allowDangerousHtml: true 选项。 —— 详见 When should I use this?

解决方案

原因被定位到了就很好解决,目前是找到了这些解决方案,可以根据需要处理。

开启 allowDangerousHtml

根据 remark-rehype 的文档,仅需开启该选项即可支持将 Markdown 里的 HTML 代码作为半标准节点嵌入 HAST 中 raw

注意:除了该插件需要开启该选项之外,像我的博客还使用了 rehype-stringify 插件,它也需要一起开启该选项。

由于我还使用了 rehype-sanitize 用于对 HTML 内容的清理,因此仅开启该选项在我的博客里并不能直接达到目的,还要在 Sanitize 进行放行,并且平时写 React 组件的习惯上,我对 dangerouslySetInnerHTML 的使用非常克制,有一些代码洁癖让我不喜欢这个方案,因此我放弃了它。

将 Raw HTML 转为 HAST

remark-rehype 的文档里,描述 allowDangerousHtml 部分提及到了另外一个插件: rehype-raw

这个插件很适合希望支持渲染嵌入在 Markdown 里的 HTML(需要传递 allowDangerousHtml: true 给 remark-rehype ),它可以获取 Markdown 里的 HTML 字符串并将它们作为实际节点包含到 HAST 中。

在开启 allowDangerousHtml 选项时, Markdown 里的 HTML 代码仅作为半标准节点嵌入 HAST 中 raw 属性,但配合这个插件,可以将原始的 HTML 字符串解析为标准的 HAST 节点。

处理过程需要依赖一个完整的 HTML 解析器(详见 parse5 ),它将完全按照浏览器解析的方式重新创建抽象语法树,同时保持原始数据和位置信息完好无损。

注意在使用过程中的插件顺序:

这个方案处理过程比较繁重,但这是支持不受信任内容的唯一方法,除非类似那种内容完全由用户提交的场景,否则在内容可控的场景下,都不推荐使用这个方案。

使用 Markdown 图片语法

这是一个最轻巧的解决方案,几乎没有多余的处理成本。

因为我的博客文章详情页最终是通过 JSX 进行渲染(可参考 markup/renderer ),因此完整的处理过程是:Markdown > MDAST > HAST > HTML > JSX ,在最后一个环节使用 rehype-react 的时候,可以将 HTML 代码转换为 React 组件需要的 JSX 代码。

所以我想到了一个方案,使用 Markdown 内置的图片语法,将视频链接放在原本需要放图片链接的位置,然后在转 JSX 的过程中,判断 URL 结尾的扩展名将视频 URL 分配给 Video 组件。

事实上在文章详情里实现很完美,但我考虑到了 RSS 订阅源里的 HTML 代码并没有得到解决,并且这种方式无法配置视频的 poster 属性,所以这个方案也被我放弃了。

使用 Markdown 自定义指令

这个方案是在 GitHub Remarkjs Discussions 里搜索时找到了几个讨论,提供了很棒的灵感!

Remark 提供了这方面的插件支持,仅需安装 remark-directive 插件,这是对 Markdown 指令语法提案 的实现(这个提案很有意思,值得阅读!),可以使用和 Markdown 十分接近的语法实现一些自定义功能。

看到这里的读者应该不会陌生,很多知名的静态生成器项目都支持自定义指令,例如:

最终实现方案

简单说一下实现方案,最终是通过编写一个 Remark 插件实现自定义指令,以 :::video 的语法,在 Markdown 内容里配置视频的 srcpostertitle 属性。

源码现在维护在 @blackwork/machine/remark-video ,这里贴的代码在未来可能会有变化。

所需的依赖

先安装依赖,由于不需要在运行时使用,所以统一安装到 devDependencies 里。

这些插件的作用:

插件作用写本文时使用的版本号
remark-directive添加对通用指令的支持^3.0.0
unist-util-visit遍历 AST 语法树节点,导出了一个 visit 方法^5.0.0
@types/mdast为 TypeScript 提供插件主要参数的类型^4.0.4

设计时的想法

考虑到需要配置的参数如 srcposter 的 URL 都比较长,用 leafDirective 语法会比较难维护,因此选择了 containerDirective 语法,按照约定,从上往下分为三行内容,分别是 srcposter 以及 title

其他的视频播放器属性,由指令插件统一管理,因此不需要在 Markdown 里自定义配置。

编写指令插件

按照 README 的例子,很快就能编写一个自定义插件了,这里就不赘述具体的过程,看代码和注释即可。

启用插件

在我的博客项目里,是在 core/parser 里启用插件(也就是最终提供给 unified().use() 使用 ),在使用的过程中,如果启用了另外一个 rehype-sanitize 插件,还需要在该插件的选项里配置 tagNamesattributes 的白名单列表。

最终结果

这就是这段 Markdown 指令渲染出来的效果(当然,不包括下面的标题展示,那是我另外包裹了一层 figure 标签,详见 parser/components ,在转换为 JSX 的时候处理的)。

山景房里的三只猫