调试 VitePress localSearchPlugin 的 Bug
Lee Dogeon @moreal@hackers.pub
原文链接:https://moreal.hashnode.dev/vitepress-localsearch-debugging 。内容相同,但因为使用 Hashnode 托管图片,出于礼貌(?)在此注明 🙏
昨天我正在计划并尝试实现一个机器人,用于通知日本技术博客服务 Zenn 上的热门文章。由于这个机器人是针对 ActivityPub 协议的,我查看了 BotKit,为了了解 BotKit 提供的方法的功能,我甚至查阅了 Fedify 文档。在这个过程中,我发现 Fedify 文档的搜索功能出现了问题,于是我提交了一个 issue。他们请求我也在 VitePress 上提交一个 issue,为了准备 issue 内容我进行了调查,最终甚至提交了一个 PR。这篇文章就是讲述我如何进行了一次牦牛剃毛的经历 🤣
复现 Bug(确认原因变量)
在向 VitePress 仓库提交 issue 时,我需要在正文中说明如何复现问题、具体在什么情况下会复现,以及为什么会发生这个问题,所以我尝试了各种方法来复现这个问题。
最初,就像我在 Fedify issue 中提到的那样,从代码块开始的部分在搜索结果中没有显示,因此我推测包含代码块的内容不会出现在搜索结果中。
由于 Fedify 文档使用了很多插件,获取反馈需要一些时间。我在临时目录(/tmp
)中初始化了一个项目,并安装了 Fedify 使用的 vitepress@1.6.3
包。然后我运行 yarn vitepress init
命令,使用默认模板生成文档,并在 .vitepress/config.mts
中将 search.provider
值设置为 "local"
。我从 Fedify 文档中复制了一些标题添加进来,但搜索功能正常工作。
接下来,我决定进一步模拟环境。我首先应用了 Markdown 相关设置,从 @shikijs/vitepress-twoslash
开始,但 bug 没有出现。然后我添加了 Markdown 语法相关的插件,这时 bug 复现了。接着,我一个一个地移除 Markdown 插件,以确定哪个插件造成了影响,最终确认使用 markdown-it-jsr-ref 插件时会复现 bug。
markdown-it-jsr-ref 插件的功能是当你在 Markdown 中写类似 `Type`
这样指代特定类型或函数的内容时,它会将其链接到 JSR 中的文档。考虑 HTML 输出的变化,原本只用 <code>
标签包裹的部分现在会额外被 <a>
标签包裹。基于这一点,我创建了两个简单的测试用例:## With <a>a tag</a> heading
和 ## With <code>code tag</code> heading
,发现前者在搜索结果中只显示到 "With "。
定义了可复现 bug 的测试用例后,我确信这是 VitePress 的 bug,因为我没有在 CommonMark 规范中找到标题中不能包含 <a>
标签的规定。
修复 Bug
由于我不熟悉 VitePress 的代码库,首先需要找出哪些代码受到影响。我知道 search.provider
设置的值是 "local"
,所以我用 "local" 这个关键词进行搜索。这时我注意到了 localSearchPlugin.ts
这个文件。查看内容后,我发现它使用了 MiniSearch 这个搜索库,这似乎就是我要找的东西。
首先,我注意到 clearHtmlTags
函数似乎是用来清除 HTML 标签以便在搜索结果中显示文本的,所以我在这里设置了断点,通过调试器跟踪并理解了上层的 splitPageIntoSections
函数。
在使用 headingContentRegex
正则表达式的那一行,两个测试用例的处理结果不同。理解 headingContentRegex
正则表达式后,我发现它接收标题作为输入并尝试捕获两部分:第一部分是 <a>
标签前的内容,变量名为 title
,这是在搜索结果中显示的部分;第二部分是捕获 <a>
标签的 href
属性内容,且该值必须以 #
开头。
const headingContentRegex = /(.*?)<a.*? href="#(.*?)".*?>.*?<\/a>/i
这里的目的是提取以 #
开头的锚点,VitePress 会在标题末尾自动添加锚点。以下是 Fedify 文档中标题的 HTML 值示例:
Implement the <a href="https://jsr.io/@fedify/fedify@1.6.2/doc/federation/~/KvStore"><code>KvStore</code></a> interface <a class="header-anchor" href="#implement-the-kvstore-interface" aria-label="Permalink to "Implement the `KvStore` interface""></a>
将测试用例代入上述正则表达式,结果如下。从返回数组的第二个值可以看出,它只返回了 With
,而第三个值正确地获取了锚点:
>>> /(.*?)<a.*? href="#(.*?)".*?>.*?<\/a>/i.exec('With <a href="https://example.com">a tag</a> heading <a href="#anchor"></a>')
Array(3) [ 'With <a href="https://example.com/">a tag</a> heading <a href="#anchor"></a>', "With ", "anchor" ]
总结一下情况:原本的意图是获取标题末尾的 <a>
标签的 href
值以及该标签前面的所有内容,但实际上没有正常工作。
问题出在第一个捕获组 (.*?)
中的 ?
符号。根据 MDN 的正则表达式文档,当 ?
符号紧跟在 *
、+
、?
或 {}
等量词后面时,会使量词变为"非贪婪模式"(匹配最少次数),而默认是"贪婪模式"(匹配最多次数)。
如果紧跟在任何量词
*
、+
、?
或{}
之后,会使量词变为非贪婪模式(匹配最少次数),而不是默认的贪婪模式(匹配最多次数)。
虽然我对正则表达式的理解不够全面,但结合上述引用,我认为问题在于非贪婪模式导致第一个捕获组没有获取可能的最大内容 "With <a href="https://example.com">a tag</a> heading "
,而只获取了最小内容 "With "
。剩余部分被 <a.*?
部分捕获。如果将这部分也用括号包围成捕获组进行测试,结果如下:
>>> /(.*?)<a(.*?) href="#(.*?)".*?>.*?<\/a>/i.exec('With <a href="https://example.com">a tag</a> heading <a href="#anchor"></a>')
Array(4) [ 'With <a href="https://example.com">a tag</a> heading <a href="#anchor"></a>', "With ", ' href="https://example.com">a tag</a> heading <a', "anchor" ]
因此,需要将第一个捕获组改回贪婪模式,只需删除一个 ?
符号。测试后发现工作正常。太好了,bug 修复完成!
>>> /(.*)<a.*? href="#(.*?)".*?>.*?<\/a>/i.exec('With <a href="https://example.com">a tag</a> heading <a href="#anchor"></a>')
Array(3) [ 'With <a href="https://example.com">a tag</a> heading <a href="#anchor"></a>', 'With <a href="https://example.com">a tag</a> heading ', "anchor" ]
创建 PR
是时候提交 PR 了。在开启 PR 时,需要向维护者充分解释这个 bug。为此,我创建了两个单独的仓库并部署到 GitHub Pages,制作了两个文档。一个用于在现有的 1.6.3 版本中展示 bug,另一个则应用了删除 ?
符号的修改,以证明这个 PR 的更改可以修复该 bug。
我附上了截图,但由于难以截取完整内容,请在PR页面查看完整内容 🙏
我花了大约一个小时准备并提交了 PR,但它被合并得非常快,我既感谢快速审核,同时又感到有点尴尬 😅
回顾
做得好的方面
- 虽然是牦牛剃毛,但过程很愉快。
- 使用了最近推出的 Zed Debugger 进行调试,这是一次很好的体验。
不足之处(遗憾的地方)
- 很难知道是否已经有解决相关 bug 的 issue 或 PR 存在。
- 准备 PR 说明花费了相当多的时间。
- 构建复现环境也花费了不少时间。
- 写这篇回顾文章也花了大约两个小时。
- 由于对正则表达式理解不够全面,无法准确判断
<a>
标签的具体问题。
改进点
- 写下来看,这个过程相当程序化,是否可以通过 Agent 自动化这个过程?
- 是否可以更轻松地构建和部署复现环境以及修改后的环境?
- 是否有更简单的方法来确认是否有人已经在处理这项工作?
- 应该更全面地理解正则表达式。