博文

ST 代码格式化工具:设计文档与演进记录

ST / 工具 / IEC61131-3 / 格式化

线上工具:ST 代码格式化(纯前端,代码不上传)。

ST 代码格式化工具 · 设计文档与演进记录

单文件、纯前端、离线可用的 IEC 61131-3 结构化文本(Structured Text, ST) 格式化小工具。 交付物:code-formatter.html(HTML + CSS + JS 全内联,无任何外部依赖、无网络请求、无上传)。 本文件记录其设计核心、算法流水线、已落地特性与演进过程,便于后续维护或改造成网页小工具。


1. 工具定位与设计目标

维度说明
用途把”挤在一行 / 缩进混乱 / 风格不一”的 ST 代码,格式化为层级清晰、可读的版本
语言范围 IEC 61131-3 ST(不支持 JSON/JS/HTML 等其它语言)
运行形态单个 .html 文件,双击即用;也可直接托管到任意静态网站
数据安全所有格式化在浏览器本地完成,代码不上传、不发请求
视觉风格深色主题;行号栏 + 层级引导线 + 语法高亮,强调”层级递增区分明显”

用户核心诉求(迭代中逐步明确)

  1. 多层嵌套时缩进层级一目了然(行号 + 竖向引导线)。
  2. 连续赋值语句的 := 对齐到同一条竖线(PLC 列式赋值风格)。
  3. 带多个参数的功能块/函数调用自动展开为每行一个参数,参数内 := / => 也对齐。
  4. 块结束符 END_IF / END_FOR / END_CASE … 自动补 ;(语句级)。
  5. 右侧含函数调用的连续赋值(如 x := FUNC(a);)也要统一对齐

2. 支持的 ST 语法结构

  • POUPROGRAM / FUNCTION_BLOCK / FUNCTION 及对应 END_*
  • 配置CONFIGURATION / RESOURCEEND_*
  • 变量声明块VAR / VAR_INPUT / VAR_OUTPUT / VAR_IN_OUT / VAR_GLOBAL / VAR_TEMP / VAR_EXTERNAL / VAR_ACCESS / VAR_CONSTANTEND_VAR;声明用单冒号 name : TYPE
  • 类型TYPE / STRUCT / END_STRUCT / END_TYPEARRAY[lo..hi] OF T(不在 OF 处断行)
  • 控制流
    • IF / THEN / ELSIF / ELSE / END_IF
    • CASE / OF … END_CASE,标签支持 1:2,3:4..6:ELSE
    • FOR / TO / BY / DO / END_FOR
    • WHILE / DO / END_WHILE
    • REPEAT / UNTIL / END_REPEAT
  • 运算符:赋值 :=、输出参数 =>、比较 <= >= <>、声明冒号 :(与 := 区分)
  • 指针/成员a.bp^.carr[i]
  • 注释:块注释 (* *)、行注释 //(可独占一行或尾随)
  • 字符串'...' / "...",支持 ST 的 $ 转义(如 $'$N
  • 关键字大小写UPPER / lower / 保持原样 三档

3. 核心算法:formatST(code, opt)

入口 formatST(code, { indent, kwCase, addSemicolon }),返回 { text, depths }text 为格式化后纯文本,depths 为每行缩进层级(供 UI 统计”最深 N 层”、未来可做层级色带)。

整体是一个 “保护 → 折叠 → 重排 → 缩进 → 展开/对齐/补分号 → 还原” 的流水线:

步骤 1 · 保护字符串与注释(占位法)

先把字符串与注释从源码中抽取出来,用哨兵占位符替换,使其在后续空白折叠 / 正则重排中不被破坏:

  • 字符串 → \u0000n\u0000n 为序号)
  • 注释 → \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/REPEATELSEELSIF/UNTILEND_* 等关键字的换行

步骤 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).gutterwhite-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 已是单文件、零依赖、纯本地计算,可直接当作静态资源托管,无需后端。若要做成网站上的小工具,可考虑:

  1. 直接托管:把 code-formatter.html 放到任意静态托管(GitHub Pages / Vercel / Netlify / 对象存储 / 自己的服务器),即是一个可用页面。
  2. 嵌入现有站点:把 <style><script> 内联内容拷进站内某个页面的 <div> 容器即可,不影响其它逻辑。
  3. 产品化增强(可选)
    • 增加”分享/生成永久链接”(把输入 Base64 进 URL hash,打开即还原);
    • 加”示例库”下拉(PLC 常见片段);
    • 导出为 .st 文件已支持,可再加”复制为 Markdown 代码块”;
    • 暗/亮主题切换;
    • 把”缩进大小 / 关键字大小写 / 补全结束符”等偏好存到 localStorage 持久化。
  4. 注意事项
    • 依旧不上传代码,符合隐私预期;
    • 仍是”正则 + 深度栈”的轻量格式化,不是完整 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 局限”。

← 返回博文列表