上周看到 Josep Bigorra,也就是 jjba23,2026 年 9 月 24 日在 jointhefreeworld.org 发了篇介绍 OrgWebAlchemy 的文章,项目用 Guile Scheme 写,解析 Org-mode 文档,生成 AST,再渲染成 HTML。文章标着阅读时间大约 10 分钟,我实际花了快 30 分钟,因为到“嵌套列表”那一节,我来回翻了三遍。
这头牛我不钻到底不舒服:为什么 Org 的嵌套列表,解析器不直接生成一棵树,反而要先吐一堆扁平的列表项,再在 AST 阶段才按缩进叠起来?
作者一开始写着他用朴素正则去解析 Org。这一步看得我头皮发紧。Org 表面是个大纲工具,实际解析时要处理一大堆构造:标题、段落、无序/有序/描述列表、随便嵌多少层的层级、不同的缩进、行内斜体粗体代码、带或不带描述的链接、链接文本本身还能嵌套,更别说源码块、示例块、引用块、表格还有转义。正则在这种地方就是越修越泥,抓块级抓不稳缩进,抓行内又抓不准嵌套。作者说他发现需要更硬的手段,才转向 Guile 的 (ice-9 peg) 模块,靠 GNU 的文档和教程把 PEG 啃下来。他没写“正则真垃圾”这种话,已经很客气。
PEG 那段我读得舒服。作者特意提到 Guile 允许把 PEG 写成 S 表达式,也能用更传统的文法语法,他选了前者,因为自己好 Lisp 这口。element 模式那一块的选项列得跟 Org 目录一样:空行、标题、分隔线、表格、src 块、引用块、示例块、HTML 导出块、描述列表、无序列表、有序列表、段落。作者的原话我看了两三遍,大意是文法写出来开始像目标语言的文档,而不是解析器 bug 历史的记录。这句话戳中我了。正则写久了,第二周回去看就跟破译自己留下的符咒一样,PEG 至少可读性还站在活人这边。
然后他突然抛出整体流程:Org 文档进 PEG 解析,生成 AST,再转成 SXML,最后渲染 HTML。我起初没太当回事,SXML 这选择无非就是“既然在 Lisp 里,就顺手把 HTML 也写成 Lisp 数据”。但他在后面点了一句,说生成 SXML 和渲染 HTML 算是表现层的事,解析和渲染之间松耦合,将来可以再加 Markdown 导出。这一下我就知道他没把项目只当一次性玩具来做。解析器拆出来,渲染器就不被绑死在 Org 一种格式上,这种解耦在看简历项目时很常见,真自己写起来能守住的其实不多。
真正把我摁住的是嵌套列表的实现。作者写得非常短:解析器先产生扁平的列表项序列,AST 处理阶段再根据缩进把扁平序列变成嵌套结构,HTML 渲染器最后才生成嵌套的 ul/li。就这么两句话,我愣是在原文里来回滚。
我第一反应是:PEG 又不是不会递归,你在列表项的文法里再引用列表项,一层层缩进不就出来了吗?作者完全没解释为什么偏要先拍平,只把做法摆了出来。我琢磨了一阵,猜测卡点在于 Org 的列表比 HTML 那套 ul/ol 脏得多。你要在文法递归过程中同时处理行内标记、链接文本再嵌套、不同列表类型混在同一级,PEG 的上下文切来切去很容易把缩进这条线切断。先拍平,等于把最脏的“扫描每一行是什么类型”先单独解决掉,把层级关系延迟到 AST 里再一棵一棵焊上。这也正好能解释他在描述列表 AST 例子里的那些节点:description-list、unordered-item、desc-key、line-content。解析阶段产出的节点还是相对独立的,层级并没有全部缝死。
这种“先扫平再重建”我熟。不是 Lisp 熟,是前端里处理树状视图的数据也常这么干:先拿一个数组攒所有节点,再靠父 id 或者缩进字段把树拼回去。作者写的是 Guile,我读到那一段却像被拉回前端工具箱。你说气不气,我本来想借一篇 Lisp 文章换换脑子,结果又撞上了树重建。
他还不瞒着,说当前有个小问题:不同列表类型互相嵌套混合时处理不顺利,希望是个容易修的 bug。读到这儿我差点笑出声。无序列表里放一个有序列表,有序列表里再塞一个描述列表,描述列表的键底下还挂着几行无序项——这种组合我光在前端 ul/ol 里就绕哭过,更别说 Org 的 line-content 会突然冒出来。他说容易修,我先在心里打了个问号。不过 v1.0 就剩这个小口子,也算他心不黑。
我一边看一边把他列出来的 v1.0 已支持构造理了张表。不是浏览器兼容表,我知道。但这回让我头皮发麻的地方就是列表层级,它在这篇文章里的地位,跟 Safari 交互细节在我以前钻的 CSS 牛角里一个样。
| 构造 | v1.0 状态 | 备注 |
|---|---|---|
| 标题、段落、行内斜体、粗体、代码 | 支持 | 行内代码会绕进 code 元素 |
| 无序、有序、描述列表 | 支持,任意嵌套层级 | 混合嵌套仍有一处小 bug |
| 链接 | 带或不带描述,文本可嵌套解析 | 这个我是服的 |
| 水平分隔线 | 五个以上连字符 | 要求还挺具体 |
| 表格 | 支持 | 原文没展开讲 |
| src/example/quote 块 | 支持,可嵌套解析 | 块内还能各自解析 |
| HTML 导出块 | 支持,绕过行内解析 | 可信原始 HTML 字面输出 |
这里没有 Chrome 版本号,也没有 Safari 在哪儿装死。但这张表跟我以前的浏览器兼容表干的是同一件事:把“支持”拆成细的,别上来甩一句“现代平台都行”就完。
作者还提了一句 AI 在理解 PEG 和调试上帮了忙,但开发主体是自己写,靠单元测试、人工验证和一大堆 AST 打印把实现磨出来。这话我信。AI 写个 PEG 雏形不难,可解析器这种东西,缩进丢了、上下文错了,AST 会歪到他妈都不认识。不打印 AST,只看几个输入输出样例,根本发现不了某一层列表被吞了,或者描述列表键和正文互相反着挂。这跟我一直念叨的“AI 写完 CSS 不逐浏览器验证等于白写”是一路货。
仓库里已经有一组测试套件。作者说它既当安全网,又展示了解析器的能力。我喜欢这个做法。一个 v1.0 的解析器,没有能重复跑的测试,光靠 README 里一个漂亮样例,说“稳定”都是虚的。他敢在文章里提 v1.0,说明测试至少不是摆拍。
另外,HTML 渲染器没有把样式硬编码成某一种网站风格。他写可以通过 Guile 参数自定义输出,比如按标题层级传不同的 class 列表,级别 1、级别 2 和其他级别分开配。这个我要点一下。Org 转 HTML 的工具要是不留这个口子,标个“可 hack”就是嘴硬。能改 class 才谈得上别人拿过去做自己的站。
项目版本在文章写作时是 v1.0,作者说已经有一定稳定性,还计划尽快把它打包成 Guix 里的 guile-orgwebalchemy。代码许可他写的是 GNU LGPL v3 或更高。我特意在脑子里记了一笔:不是 Apache,是 LGPL v3。上次听人随口扯了句 Apache,我差点就跟着记错。文章页脚本身又标着 AGPL v3,一个管文章,一个管库,不冲突,只是容易把人绕晕。
文章末尾还提到,他本可以用 Emacs 完成 Org 导出,但更想做一个小型、可 hack、自由软件的 Lisp 解析器,让别人能接渲染器或者补新特性。还公开喊大家给他的文法、AST 设计、解析器架构和没覆盖到的构造提意见。这态度在 v1.0 的个人项目里不多见,很多人 v1.0 只想听夸奖,不想听“你这里会翻车”。
他列了几个延伸阅读:PEG 的 Wikipedia 页面、GNU Emacs Lisp 的 PEG 教程、Guile 的 PEG 教程。我原计划三个都点开,最后只点开了 Guile 那个,剩下两个到现在还躺在我浏览器标签页里。估计还会继续躺到下个月。哎。
最后,说个跟这篇八竿子打不着的。我读这篇文章的时候,电脑风扇一直响,我一开始以为 Guile 在后台编译什么,后来发现只是 Steam 在更新。连机器都在提醒我该干点正事了。扯远了,反正——嘿嘿,下次见。
