跳到主要内容
稻草人
プロフィール

EP.06

🚀Build & Ship

TypeScript 写了一个 CFML Formatter

介绍我开发 CFML Auto Formatter 的原因、TypeScript 与 VS Code Extension API 实现、CFTag 和 CFScript 格式化、SQL 处理,以及发布到 Visual Studio Marketplace 和 Open VSX 的过程。

TypeScript

我平时会接触一些 CFML 项目。CFML 本身并不是只有一种语法,一个 .cfm 文件里经常同时出现 CFTag、CFScript、SQL、HTML、CSS 和 JavaScript。代码可以正常运行,但文件写久以后,缩进和标签结构很容易变乱。

市面上当然有各种代码格式化工具,不过直接拿通用 Formatter 处理 CFML,结果经常不太理想。它可能认识 HTML,却不认识 cfquery、cfloop 和 cfoutput;也可能能整理 JavaScript,却会把 CFML 中的表达式或混合内容改坏。

我需要的其实不是一个会把所有代码重写成固定风格的工具,而是一个更保守的 Formatter:把缩进和标签结构整理好,同时尽量不修改原来的代码风格。

所以我用 TypeScript 开发了 CFML Auto Formatter,并把它作为 VS Code 扩展发布了出来。

项目使用 MIT License,目前插件可以直接从 Visual Studio Marketplace 或 Open VSX 安装。

目次

这个插件解决什么问题

CFML 文件比较特殊。下面这种代码在实际项目里很常见:

<cfquery name="userList" datasource="mainDb">
SELECT id,name,email FROM users WHERE status = <cfqueryparam value="#status#">
</cfquery>

这里既有 CFML 标签,也有 SQL 和 CFML 表达式。再复杂一点的页面还会继续混入 HTML、CSS、JavaScript 和 cfscript。

如果只按照 HTML 的规则处理,SQL 不会得到正确缩进;如果把整个文件当成 JavaScript,CFML 标签又会被破坏。格式化 CFML 的难点不只是增加几个空格,而是先判断当前处理的是哪一种内容,再把对应范围交给合适的逻辑。

因此这个插件把格式化入口拆成了几种类型:

格式化类型处理内容
CFTag SyntaxCFML 标签、嵌套结构和缩进
Script Syntaxcfscript 和 JavaScript 风格代码
SQL Syntaxcfquery 或选中范围中的 SQL
HTML Syntax.cfm 中的 HTML 内容
CSS Syntax.cfm 中的 CSS 内容

我可以只选择某个代码块进行格式化,不必每次都处理整个文件。对于混合内容很多的 .cfm 文件,这种方式更容易控制结果。

主要功能

目前插件实现了这些功能:

  • .cfm 和 .cfc 文件的 CFML 缩进整理;
  • CFML 标签自动换行;
  • 按块级标签进行局部格式化;
  • 单独格式化 CFTag、CFScript、SQL、HTML 和 CSS;
  • 支持 VS Code 手动格式化和保存时格式化;
  • 尽量保留已有换行和原本的代码风格;
  • 支持 Tab、空格数、换行保留、括号风格等配置;
  • SQL 可以选择 MySQL、PostgreSQL、SQLite、Oracle、SQL Server 等方言;
  • 提供中文、英文和日文提示信息。

插件不依赖本机的 ColdFusion 或 Lucee 运行环境。安装扩展以后,格式化逻辑直接在 VS Code 的 Node.js Extension Host 中运行。

技术栈

部分使用的技术
主要语言TypeScript
编辑器接口VS Code Extension API
CFML 标签处理自定义解析与正则预处理
Script、HTML、CSS 美化js-beautify
SQL 格式化sql-formatter + 自定义兼容逻辑
构建Webpack、ts-loader
生产压缩TerserPlugin
扩展打包VSCE
发布平台Visual Studio Marketplace、Open VSX

我选择 TypeScript 的原因很直接:VS Code Extension API 本身有完整的类型定义,TextDocument、Range、TextEdit、FormattingOptions 这些对象如果只用 JavaScript 开发,很容易在格式化范围和返回值上写错。

格式化工具又属于对代码原文进行修改的程序。一个行号算错、一个 Range 边界不对,就可能替换掉不该替换的内容。TypeScript 不能解决全部问题,但至少能提前挡住一部分参数和返回类型错误。

VS Code 格式化接口怎么接入

插件激活后,会给 cfml、cfm 和 cfc 注册两类 Provider:

  • DocumentFormattingEditProvider:处理整个文件;
  • DocumentRangeFormattingEditProvider:只处理选中的范围。

核心思路不是直接修改编辑器文本,而是计算一组 TextEdit,再交给 VS Code 应用。这样可以继续使用编辑器原生的撤销、保存时格式化和 Format Document 流程。

简化以后,结构大致是这样:

const provider: vscode.DocumentFormattingEditProvider = {
    async provideDocumentFormattingEdits(document, options, token) {
        const manager = new FormatterManager();
        return manager.formatDocument(document, options, token);
    },
};

vscode.languages.registerDocumentFormattingEditProvider(
    "cfml",
    provider
);

局部格式化也使用相同思路,只是多接收一个 Range。插件会先确认选区是不是完整的 CFML 块级标签范围,再决定是否继续处理。这样可以避免用户只选中了半个标签时直接修改代码。

CFML 标签自动换行

这个插件里有一层 CFML 标签预处理。它会先找出一行中的 CFML 标签,把标签、标签前的普通文本和标签后的内容拆开,再生成对应的 TextEdit。

例如一行里同时写了多个标签:

<cfif isLogin><cfoutput>#userName#</cfoutput></cfif>

预处理会先把标签结构拆清楚,然后再进入缩进阶段。

这里不能简单使用 <.*?>。CFML 标签属性的字符串中也可能包含 >,如果正则在字符串内部提前结束,就会把一个完整标签切坏。因此我的匹配逻辑会分别处理单引号、双引号、转义字符、开始标签、结束标签和自闭合标签。

插件还会判断 cfif、cfloop、cfoutput 等块级标签是否前后对应。做局部格式化时,只有选区能够构成完整代码块才会继续执行。

这套实现目前没有假装自己是完整的 CFML AST。它更接近一套为实际格式化需求设计的轻量解析和预处理逻辑。这样部署简单,也不需要用户额外安装 Lucee 或 ColdFusion。

为什么不是所有内容都用同一个 Formatter

CFML 的混合语法决定了一个 Formatter 很难把全部内容都处理好。

对于 cfscript、JavaScript、HTML 和 CSS,我使用 js-beautify 作为基础能力,再把 VS Code 中的设置转换成对应选项。用户可以控制:

  • 使用 Tab 还是空格;
  • 空格缩进宽度;
  • 是否保留原有换行;
  • 最多保留多少连续空行;
  • 大括号使用 collapse、expand 还是其他风格;
  • 是否拆分链式调用;
  • 文件末尾是否保留换行。

SQL 部分使用 sql-formatter,并允许选择 SQL 方言。但是 cfquery 中可能存在 cfloop、cfif 或 #expression#,这些内容不是普通 SQL。遇到这种情况,单纯调用第三方库并不够,所以插件还加入了自己的兼容处理。

这也是我开发这个插件过程中感受最明显的一点:引入一个 Formatter 库并不等于功能已经完成。真正花时间的是确定边界、保护 CFML 内容,以及处理各种混合语法。

处理 Auto Close Tag 冲突

开发过程中还遇到过一个比较隐蔽的问题:格式化标签时,Auto Close Tag 或 CFML 自动闭合功能可能认为我正在输入新的开始标签,于是自动插入结束标签。

最后结果就会变成,Formatter 正常整理了一次代码,另一个插件又在中间追加了标签。

现在执行手动格式化时,插件会:

  1. 读取 Auto Close Tag 和 CFML Auto Close 的原始设置;
  2. 临时关闭相关自动闭合功能;
  3. 执行本次格式化;
  4. 无论成功还是失败,都在 finally 中恢复用户原来的设置。

这里的重点是恢复“原值”,而不是格式化完成后一律打开。因为用户原本可能就关闭了这些选项,插件不应该替用户修改长期设置。

命令和快捷键

安装以后,可以在编辑器右键菜单的 Format Code (satt) 中选择不同格式化方式,也可以使用命令面板。

当前提供的快捷键如下:

功能快捷键
CFTag SyntaxShift + Alt + T
Script SyntaxShift + Alt + S
HTML SyntaxShift + Alt + H
CSS SyntaxShift + Alt + C
SQL SyntaxShift + Alt + Q

如果选择了代码范围,插件只处理选中内容;没有选择内容时,CFTag 格式化可以处理整个文件。

构建和打包

源码使用 TypeScript 编写,入口文件是 src/extension.ts。构建时通过 ts-loader 交给 Webpack,最终输出 VS Code 加载的 dist/extension.js。

生产构建还使用 TerserPlugin 删除 console 和 debugger,并生成不包含源码正文的 Source Map。扩展本身运行在 Node.js 环境中,vscode 模块由 VS Code 提供,所以不会被打进 Bundle。

主要命令比较简单:

npm run build
npm run package

build 负责生成生产代码,package 通过 VSCE 生成可以安装和发布的 .vsix 文件。

发布到两个插件市场

目前插件已经发布到两个平台:

Visual Studio Marketplace

在 Visual Studio Marketplace 安装 CFML Auto Formatter

这是 VS Code 默认使用的扩展市场。普通 VS Code 用户直接在 Extensions 中搜索 CFML Auto Formatter 就可以安装。

Open VSX Registry

在 Open VSX 安装 CFML Auto Formatter

Open VSX 主要服务于使用开放扩展注册表的编辑器和发行版。发布到这里以后,不只标准版 VS Code 用户可以获取插件,使用 Open VSX 生态的用户也能安装。

我选择同时发布两个市场,是因为插件本身没有必要被某一个编辑器分发渠道限制。代码仓库也是公开的,安装页面、源码和 Issue 都可以互相对应。

目前的限制

这个项目现在已经能解决我日常遇到的大部分 .cfc 格式化问题,但我不想把它描述成已经支持所有 CFML 代码。

目前 .cfc 的自动缩进相对稳定;.cfm 因为经常混合 HTML、XML、CSS、JavaScript 和 SQL,整份文件一次性格式化仍有可能出现不理想的结果。因此插件在对 .cfm 执行全文格式化前会显示警告。

对于复杂页面,我更建议:

  • 先选中完整的 CFML 块再格式化;
  • SQL、HTML、CSS 和 Script 分别处理;
  • 格式化后通过 Git Diff 确认改动;
  • 特别复杂的 cfscript 字符串、XML 或动态 HTML 不要直接批量处理。

自动标签换行对于属性很多的标签比较有用,但很短的内联标签也可能被拆行。这部分之后还可以继续增加更细的配置。

我从这个项目里学到的东西

这个插件看起来只是“整理一下代码”,真正实现以后才发现,它同时涉及编辑器 API、文本范围计算、混合语言解析、第三方 Formatter 的边界、插件冲突和扩展发布。

我没有重新实现所有格式化算法。能交给 js-beautify 和 sql-formatter 的部分就继续使用成熟库,CFML 特有的标签、块范围和混合语法再由自己的逻辑处理。

对我来说,这种拆分比从零写一个完整 Parser 更符合当前项目的实际需求:用户不需要准备 CFML 运行环境,插件安装后就能使用,我也可以根据真实项目中的问题逐步改进规则。

后面我会继续改善 .cfm 混合内容的处理、复杂 CFML 模板的兼容性,以及格式化结果的稳定性。如果你也在维护 ColdFusion 或 Lucee 项目,可以直接安装试用;遇到有代表性的代码结构,也可以在 GitHub 提交 Issue。

项目链接