前情提要

想写一本书,尝试了 LeanPub,支持 markdown,太好了!但是没有所见即所得功能,预览起来的步骤非常复杂而且慢。又试了一下 Overleaf,非常好,可惜 LaTex 学习曲线陡峭,仅支持基本的 markdown,如果我在 markdown 里嵌入 mermaidjs,就不能正常预览了。语雀可以预览 mermaidjs,编辑功能也非常棒,但是整个目录组织调整起来比较麻烦。又试了 Confluence、notion 等等,都感觉不满意。

最终还是决定使用静态站点生成工具来做,试了 nextjs、gatsbyjs 等,都略重。发现 vitepress 非常轻量,大喜!它不仅支持我想要的几乎所有功能,而且非常灵活可以自定义,并且支持在线编辑和实时预览!

不过,一番操作下来,就发现有一丢丢小小的瑕疵,那就是不支持层级组织文件结构,有点失望。但是,这已经是过去时了,现在已经支持,这都得感谢 ChatGPT!

最终效果:完美的 markdown 写作体验

https://identity.jefftian.dev/

我很喜欢 mermaidjs,希望可以随时在写作中插入 mermaidjs 图形以辅助表达,虽然上面提到语雀也可以,但是不能尝鲜使用 mermaidjs 还未正式发布的 beta 特性!而使用 vitepress,只需要在项目中引用它的 beta 包即可:

json dependencies: { @mermaid-js/mermaid-mindmap: ^9.3.0, mermaid: ^9.3.0, }

另外,对于写书,我希望有这样的层级结构,每一部分、每一章节,都有各自的目录,最终通过一个主入口文件包含它们。即主文件依次包含进第一层文件,而每一个第一层文件依次包含进各个章文件,然后每个章文件包含进每一节文件,如此嵌套下去。

1687949791154 28fab743 4070 48e0 8b97 effd2704bc48

比如入口文件是这样的:

markdown [[toc]]

而第 1 部分的内容是这样的:

markdown ...

第一章里又有其他的节,如此嵌套包含。

最终只需要通过主入口文件地址访问,就能渲染所有内容:https://identity.jefftian.dev/main

实现过程

我这样组织好 markdown 文件后,打开 main 后发现只能看到第一级被包含的文件,更深层的文件,根本没有在 main 中被渲染出来。于是,先去 vitepress 许了个愿。因为之前在别的开源项目里,有许愿成功过,即想要一个功能,在 Issues 里描述了一下,然后就有人帮忙实现了: https://github.com/NervJS/taro-ui/issues/1582

1688038147721 c969bddd 57f0 42c4 a448 c72e693ca5e5

许愿

于是这次也希望有人帮忙实现这个功能,就去 vuejs/vitepress 里提了这个需求: https://github.com/vuejs/vitepress/issues/2544

1688038333223 f8941fbc d03f 45e0 9424 1073ff863316

分析

但是这次,等了几天没有人响应,又急着要用这个功能,于是准备自己动手,就开始分析了一下,找到了相关的源代码:

https://github.com/vuejs/vitepress/blob/b2a129f49b8c83e528f594af977b1e901a57313e/src/node/markdownToVue.ts#L99C62-L99C62

1688038489919 2d7d4513 9275 4cb7 9fc6 ca98845f0a7a

很快找到了文件包含的相关代码,明显看出只处理一层包含,不支持嵌套。我的直觉告诉我,修改起来应该很简单,递归处理就行了。

先写测试

但是得抑制住直接修改实现代码的冲动,先写个测试,来重现不能嵌套的问题。这个我比较擅长,因为我有 TDD 的习惯,现在还轮不到找 ChatGPT 帮忙。

测试准备

我先添加了一个 markdown 文件,并在文件内容里加上了包含另一个文件的指令:

1688038788136 af1387d7 68e9 46d3 aedf 59c0d728ecdb

然后,在一级文件中添加包含这个新加的文件的指令:

1688038815116 b8ae5a2c be26 40d0 a6b6 9f1d19e54c8e

实现测试用例

比较简单,定位到新增加内容的标题的锚点(vitepress 会自动给标题添加锚点,规则是大致将字母全改成小写,并使用短划线替换空格),再基于这个锚点往后寻找最近的一级标题,确认它的 id 是被嵌套包含进来的那个 foo-1 (因为使用了同一个 foo.md文件,其同一个标题在页面上出现了两次,第二个标题自动生成的 id,为了和第一个区别开,vitepress 自动给它添加了一个编号后缀,就和多次下载同一个文件时,文件名会增加后缀一样)即可。

1688039140694 05ec16ac c030 4b46 90fd ee13f5efc3c7

在实现测试用例时,也顺手改了一下文档,以终为始嘛。

1688039229955 8982e275 a421 4dda bd25 56c81dd8c8e9

运行测试,显然失败了,因为被深层嵌套包含的文件,最终没能渲染出现,自然也找不到对应的锚点。

修改实现代码

在这里,我意识到自己的基本功太差,反复修改了几版,就是改不好。我明明还专门反复阅读了《<font style=color:rgb(133, 133, 133);>计算机程序的构造与解释》(简称 SICP)一书,还做了不少练习,并放到了网上:https://sicp.jiwai.win/zh_cn/。这些练习里我印象最深的就是 lisp 语言不支持循环等控制语句,很多时候,都需要刻意使用递归来解决问题,但我不知道为什么,当遇到实际问题时,就又不会了。总之,我的直觉告诉我,只要把当前实现中的 replacer 函数,修改成递归运行的,就能通过测试用例,但花了很久也没能成功改好。

ChatGPT 来帮忙

恼羞成怒的我,打开 ChatGPT,将现有代码扔给它,问怎么让它支持嵌套包含这个功能?

1688039606150 886f1b9c ff4e 4c2b aadc 51b36281c297

果不其然,ChatGPT 给到的解决方案就是用递归!但它不仅给思路,还直接给了一段代码!

1688039771225 118c4891 bc36 485e 86cb bdd6227aca65

我半信半疑,将它拷贝粘贴到我克隆的 vuejs/vitepress 项目里,重新运行测试,居然通过了!!!

显然,论写代码,ChatGPT 比我强多了!我想基于它给的答案再优化一下,无奈水平有限,最终只是将它的 processedContent 变量内联,直接 return 了。这一个优化项显而易见,我也就只能做这种显而易见的事情了……

提交 Pull request

既然通过了测试,我就直接提交了代码,并给 vuejs/vitepress 发了 PR: 1688040079797 bada98fc c653 4400 8c4e ccc544a07839

1688040252283 6510c00c 5cd9 43a6 bc89 a0ab729570fe

PR 被采纳

没想到,PR 很快得到了 vitepress 维护者的采纳!就这样轻松混了个 vuejs 组织下的贡献者身份。

1688040328177 a734f2c9 f12d 491d 90ae 622c044adabf

彩蛋

当然,维护者毕竟是高手,对代码又做了进一步的优化,让我们一起来围观大佬做了哪些改进吧! 1688040675142 f9c65f54 4df2 4f74 8c17 604f80b9a742

结语

这就是我利用 ChatGPT 给知名开源项目做贡献的全部实录,希望也给你勇气:**基本功太差并不要紧,善于利用工具,还是可以改变世界的!**现在,我有了一个完美的 markdown 写作神器(改变了自己)!

当然,我的 PR 虽然被采纳,但目前官方并未发布新的版本。但是我已经在利用这个新功能了,怎么做到的呢?其实是发布了一个自己的版本:https://www.npmjs.com/package/@jeff-tian/vitepress

1688041051734 908d781b 388a 43d1 a259 65291cef1886

但是,这只是临时做法,我相信官方很快会发布新的版本,到时候所有人都可以使用嵌套包含的功能了。耶!又改变了世界!!(一丢丢……)

希望本文给你启发,并激励你也多做开源贡献。同时,对于写书、写文档的同学们,我在此墙裂向你们安利 vuejs/vitepress!