AST转换成另一种AST

林一二2022年04月24日 23:26

AST转换的铁三角

参考 remark-slate-transformer

  1. Builder:直接将一种 AST 节点的大部分属性转换为另一种节点的属性(退出条件);里面会调用 BuildNodes 处理子节点
  2. BuildNodes:用于转换子节点数组(递归步骤),因为子节点可以是任意类型,所以需要调用 Switcher
  3. Switcher:传入任意类型的 AST 节点,判断一下其类型,调用相应的 Builder

这必然导致三者循环引用,需要处于同一文件里,但 Builder 很多,不可能全放同一文件里,所以我们参考 mdast-util-to-hast 的做法重新抽象:

将转换函数作为依赖注入

  1. Handler:对各类型节点的处理,写在各自的文件里。然后在 handler/index.js 里集中引入集中导出,变成一个 handlers 字典;里面会调用 All 处理子节点
  2. All:和 BuildNodes 一样,对 children 字段做 while 循环处理,循环里对节点调用 One
  3. One:和 Switcher 一样,只不过从一个 handlers 字典里去取相应的 handler;这个字典通过参数传入(依赖注入),而不是直接从 handler 文件夹 import,避免循环引用
  4. Factory:集中引入 Handler、All 和 One,将 Handler 作为参数传给 One 或者 All

可以看出结构基本一致,只不过通过 import 成字典的形式来分文件,然后通过依赖注入的方式将三角△循环引用变成了Y集中引用。

如果尝试先创建字典,然后再用一个 register 函数注册各 Builder,则会遇到逆变协变问题,各 Builder 所要求的节点类型是子类,而 register 函数的参数必然是 AST 基类,无法逆变传入参数。通过 import 来直接创建字典就可以避免逆变问题。

文件结构

简单的 AST 转换器模式基本类似,可以用 yoman 这样的代码生成器生成样板代码:

  1. OneAll 放在 traverse.ts 里,这里不导入其它代码,只导入 IHandler 等等类型
  2. index.ts 导入 AllHandlers,将库调用者传入的 Root AST 节点的 children 传给 All, handlers 也作为参数传给 All
  3. handlers 文件夹,内有各种 handler 函数,集中导出为 handlers 对象

handlers 里每一个函数都只需要考虑对自己负责的类型做转换,实现关注点分离。而 traverse 里的递归逻辑则保证我们新加的 handler 即使是在树的中间被调用,它产出的结果也能被正确地安放在结果的中间。