ST 代码格式化工具:设计文档与演进记录
线上工具:ST 代码格式化(纯前端,代码不上传)。
ST 代码格式化工具 · 设计文档与演进记录
单文件、纯前端、离线可用的 IEC 61131-3 结构化文本(Structured Text, ST) 格式化小工具。 交付物:
code-formatter.html(HTML + CSS + JS 全内联,无任何外部依赖、无网络请求、无上传)。 本文件记录其设计核心、算法流水线、已落地特性与演进过程,便于后续维护或改造成网页小工具。
1. 工具定位与设计目标
| 维度 | 说明 |
|---|---|
| 用途 | 把”挤在一行 / 缩进混乱 / 风格不一”的 ST 代码,格式化为层级清晰、可读的版本 |
| 语言范围 | 仅 IEC 61131-3 ST(不支持 JSON/JS/HTML 等其它语言) |
| 运行形态 | 单个 .html 文件,双击即用;也可直接托管到任意静态网站 |
| 数据安全 | 所有格式化在浏览器本地完成,代码不上传、不发请求 |
| 视觉风格 | 深色主题;行号栏 + 层级引导线 + 语法高亮,强调”层级递增区分明显” |
用户核心诉求(迭代中逐步明确)
- 多层嵌套时缩进层级一目了然(行号 + 竖向引导线)。
- 连续赋值语句的
:=对齐到同一条竖线(PLC 列式赋值风格)。 - 带多个参数的功能块/函数调用自动展开为每行一个参数,参数内
:=/=>也对齐。 - 块结束符
END_IF / END_FOR / END_CASE …自动补;(语句级)。 - 右侧含函数调用的连续赋值(如
x := FUNC(a);)也要统一对齐。
2. 支持的 ST 语法结构
- POU:
PROGRAM/FUNCTION_BLOCK/FUNCTION及对应END_* - 配置:
CONFIGURATION/RESOURCE及END_* - 变量声明块:
VAR/VAR_INPUT/VAR_OUTPUT/VAR_IN_OUT/VAR_GLOBAL/VAR_TEMP/VAR_EXTERNAL/VAR_ACCESS/VAR_CONSTANT及END_VAR;声明用单冒号name : TYPE - 类型:
TYPE/STRUCT/END_STRUCT/END_TYPE;ARRAY[lo..hi] OF T(不在OF处断行) - 控制流:
IF / THEN / ELSIF / ELSE / END_IFCASE / OF … END_CASE,标签支持1:、2,3:、4..6:、ELSEFOR / TO / BY / DO / END_FORWHILE / DO / END_WHILEREPEAT / UNTIL / END_REPEAT
- 运算符:赋值
:=、输出参数=>、比较<=>=<>、声明冒号:(与:=区分) - 指针/成员:
a.b、p^.c、arr[i] - 注释:块注释
(* *)、行注释//(可独占一行或尾随) - 字符串:
'...'/"...",支持 ST 的$转义(如$'、$N) - 关键字大小写:
UPPER/lower/保持原样三档
3. 核心算法:formatST(code, opt)
入口 formatST(code, { indent, kwCase, addSemicolon }),返回 { text, depths }。
text 为格式化后纯文本,depths 为每行缩进层级(供 UI 统计”最深 N 层”、未来可做层级色带)。
整体是一个 “保护 → 折叠 → 重排 → 缩进 → 展开/对齐/补分号 → 还原” 的流水线:
步骤 1 · 保护字符串与注释(占位法)
先把字符串与注释从源码中抽取出来,用哨兵占位符替换,使其在后续空白折叠 / 正则重排中不被破坏:
- 字符串 →
\u0000n\u0000(n为序号) - 注释 →
\u0001n\u0001 - 字符串扫描尊重
$转义;块注释识别(* *),行注释识别//到行尾。
步骤 2 · 折叠空白
所有连续空白(空格/Tab/换行)压成单个空格并 trim,得到”一行流”文本,便于后续统一重排。
步骤 3 · 运算符 / 冒号间距
为 := => <= >= <> 两侧补空格;声明冒号 name : TYPE 用正则 ([A-Za-z_]\w*)[ ]*:(?!=)(负向断言排除 :=)补空格。
步骤 4 · 语句换行重排(正则驱动)
通过一系列正则把”一行流”拆成逻辑行:
;→;\n(每条语句换行)- 去除
END_*关键字后的;(避免拆出孤立;行,见 §5 bug 修复) - 注释占位符独占一行
CASE..OF断行(刻意不误伤ARRAY[..] OF,靠[^;\n]*?约束)VAR*/TYPE/STRUCT、POU 头、THEN/DO/REPEAT、ELSE、ELSIF/UNTIL、END_*等关键字的换行
步骤 5 · 拆行 + 关键字大小写
按 \n 切分、去空行;逐行套用 applyKwCase(按 KW_RE 对关键字/类型词做大小写归一)。
步骤 6 · 深度栈缩进(核心层级算法)
逐行用计数器 + 栈维护缩进深度:
- 遇到”开始关键字”(
IF/FOR/WHILE/REPEAT/CASE/PROGRAM/VAR*/TYPE/STRUCT等)→ 先输出当前层级,再depth++、入栈。 - 遇到”结束关键字”(
END_*)→ 先depth--、出栈,再输出(实现”闭合回到上一级”)。 CASE标签(1:/2,3:/ELSE)特殊处理:标签行下沉一层(CASELABEL状态),标签后若紧跟语句则”下沉”到下一行独立输出。ELSIF/UNTIL/ELSE输出到depth-1层级(与所属IF/REPEAT同级)。- 栈保证
END_*只减不”只增不减”,避免越往后越往右。
步骤 7 · 后处理三连
- 7.5 调用展开
expandFunctionCalls(out, depths, unit)扫描顶层Name(...),按顶层逗号拆成”一行一参数”,参数缩进加深一层、闭合)挂最后一行(前留一空格,贴合Jerk := MPara^.LrJerk );风格);递归展开嵌套调用;单参数调用不展开。同时同步depths数组(之前曾因不同步导致层级统计错位,见 §5)。 - 7.6
:=/=>列对齐alignAssignments(out, unit)对”连续、同层级、左侧为简单引用(/^[\w^.\[\]]+$/,不含括号/逗号)“的赋值成组,取组内:=/=>最大列用空格补齐。分组关键:仅缩进层级(前导空白宽度)相同的连续行成组——这样右侧带函数调用的同级赋值也能统一对齐,且更深一层的调用参数不会与后续同级语句误并。 - 7.65 声明冒号
:与默认值:=双重列对齐alignDeclarations(out, unit)对”连续、同层级、形如Name : Type ;”的声明(VAR 块成员 / STRUCT 成员)成组。两遍对齐:第一遍对齐变量名后的:;第二遍对带默认值的行(Type后有:=)再对齐:=。辅助函数findTopAssign()在**顶层(非括号内、非字符串内)**查找第一个:=,避免被默认值里的数组字面量[..]或嵌套调用里的:=干扰。无默认值的声明(如Name : TYPE ;)只参与:对齐。 - 7.66 行尾
//注释列对齐alignComments(out, unit)对”连续、同层级、行尾带//注释”的行,把//起始列对齐到同一列(至少留 1 空格)。结构关键字行(IF/THEN/END_* 等)不拉伸,避免大段空白。 - 7.8 补全结束符分号
若开启
addSemicolon,给语句级块结束符END_IF/END_FOR/END_WHILE/END_CASE/END_REPEAT(行尾无;时)补;。不包含声明级结束符(END_VAR/END_PROGRAM/END_FUNCTION_BLOCK …,按 ST 惯例不加;)。
步骤 8 · 还原占位符
把 \u0000n\u0000 / \u0001n\u0001 替换回原始字符串与注释,得到最终 text。
4. UI 与可视化
- 输入:左侧
<textarea>(无拼写检查)。 - 输出:右侧”行号栏
.gutter(sticky、竖排)+ 代码区.code-out(<pre>)”。 - 层级引导线:代码区背景用
repeating-linear-gradient按--u(缩进单位 CSS 变量)画竖向引导线,嵌套越深线越多;缩进/用 Tab 时同步setUnit()。 - 语法高亮
highlight():同样先保护字符串/注释,再对**关键字(蓝)/ 类型(绿)/ 注释(灰斜体)/ 字符串(橙)**着色输出 HTML。复制/下载始终用纯文本lastText,不受高亮影响。 - 工具栏:缩进(空格数 / Tab)、关键字大小写、格式化 / 复制 / 下载
.st/ 载入示例 / 清空;开关:层级引导线、语法高亮、补全结束符、自动格式化。 - 状态栏:显示”已格式化 · N 行 · 最深 M 层”或错误。
5. 演进过程中修复的关键 Bug(踩坑记录)
| 现象 | 根因 | 修复 |
|---|---|---|
| 输出区代码整体被推到右侧(~378px 才开始显示) | 输出容器设了 white-space:nowrap,行号栏 .gutter 未设自身换行规则、继承了 nowrap,所有行号被横向排成一条 → gutter 实测宽 597px(正常 ~39px) | 给 .gutter 加 white-space:pre,行号正确竖排(回填到 39px,代码首字符回到正常位置) |
END_IF; / END_FOR; 等被拆出孤立 ; 行 | 重排时所有 ; 后换行,把结束关键字后的 ; 变成独立空行 | 换行前用 \b(END_[A-Z_]+)\s*;/gi → $1 去掉结束符后的分号 |
| 展开调用后”最深 N 层”统计异常 / 尾部层级错位 | expandFunctionCalls 增加行数但 depths 数组未同步 | expandFunctionCalls / expandOne 改为同步返回 {lines, depths},参数行 depth = 调用层 depth + 1 |
连续赋值 := 未统一对齐(右侧含函数调用值) | 对齐组边界用”行含 ) 即断开”,把右侧 LREAL_TO_REAL(...) 误判为调用闭合 | 改为按缩进层级相同分组:仅前导空白宽度相等的连续赋值成组 |
验证方式:每次改动都用 Node 把
formatST核心单独抽出跑真实片段(含你的示例Reset.Execute/Cmd^.bReset/…、Jog(...)等)做断言;关键渲染用本机 Chrome 无头模式--dump-dom/--screenshot实测并截图核对。
6. 后续:改造为个人网站小工具
当前 code-formatter.html 已是单文件、零依赖、纯本地计算,可直接当作静态资源托管,无需后端。若要做成网站上的小工具,可考虑:
- 直接托管:把
code-formatter.html放到任意静态托管(GitHub Pages / Vercel / Netlify / 对象存储 / 自己的服务器),即是一个可用页面。 - 嵌入现有站点:把
<style>与<script>内联内容拷进站内某个页面的<div>容器即可,不影响其它逻辑。 - 产品化增强(可选):
- 增加”分享/生成永久链接”(把输入 Base64 进 URL hash,打开即还原);
- 加”示例库”下拉(PLC 常见片段);
- 导出为
.st文件已支持,可再加”复制为 Markdown 代码块”; - 暗/亮主题切换;
- 把”缩进大小 / 关键字大小写 / 补全结束符”等偏好存到
localStorage持久化。
- 注意事项:
- 依旧不上传代码,符合隐私预期;
- 仍是”正则 + 深度栈”的轻量格式化,不是完整 ST 编译器级 parser,极复杂的宏/预处理或厂商私有语法可能需补充规则。
7. 已知局限
- 属美化级(beautifier),非 Prettier/编译器级 parser;对语法合法性不做严格校验。
- 仅识别标准 IEC 61131-3 ST 关键字;厂商扩展(如 TwinCAT/
{attribute}、CODESYS 特定指令)未专门处理。 - 对齐仅作用于”左侧简单引用”的
:=/=>;含表达式左侧(如arr[i] := …已支持,a+b := …不纳入对齐)。 - 声明级结束符(
END_VAR等)默认不加;(符合多数 ST 风格)。
8. 文件清单
| 文件 | 作用 |
|---|---|
code-formatter.html | 成品:ST 格式化工具(单文件) |
st-formatter-doc.md | 本文档(设计 + 演进记录) |
协作约定:本工具持续在本地迭代;迭代后请同步更新本文档的”§5 演进”与”§7 局限”。