2026-08-01 合并后权威状态
PR-D #415 已于 2026-08-01T03:43:41Z 合入
dev/python-diagram-umbrella,实现 head 3e19c1d2,merge commit 1174eec3b9e1。合并时 47 项检查 success、0 failure、1 skipping。
至此伞 PR #383 下四个 sub PR 全部完成并合入 umbrella,计划中没有 PR-E: PR-A #384 、PR-B #386 (merge aa745161)、PR-C #389 (merge 46660c4c)、PR-D #415 (merge 1174eec3)。
本 issue 要的「嵌入 jsfcstm 可视化(SVG/PNG)」能力现已可用:
from pyfcstm .model import load_state_machine_from_text
view = load_state_machine_from_text (
"state Root { state Idle; state Busy; [*] -> Idle; "
"Idle -> Busy :: Start; Busy -> Idle :: Done; }"
).diagram ()
svg = view .to_svg () # str, 展开形式,自足不依赖字体
png = view .to_png (scale = 2.0 ) # bytes, 光栅
pdf = view .to_pdf () # bytes, 单页矢量
真实运行:
to_svg() 18598 字节 <text>=0 <marker>=0 font-family=0 <script>=0 <path>=24
to_png() scale=1 -> 367x396 / 11706B scale=2 -> 734x792 / 27916B scale=3 -> 1101x1188 / 46392B
to_pdf() 14559 字节 页数=1 位图对象=0 MediaBox=0 0 367. 396.
CLI json 2290B/745ms html 29406293B/1330ms svg 18598B/3090ms png 11706B/3124ms pdf 14559B/3167ms
SVG 的 <text>=0 是合同而非巧合:不展开的 SVG 里文字仍是 <text> + font-family,收件人机器缺字体时排版就崩;展开后字形是路径,文件自足。PDF 的 位图对象=0 同理,是真矢量而非把图片塞进 PDF 壳。
超出尺寸时是拒绝而不是崩溃或悄悄裁切,且限额在 WASM 之前检查——让渲染器 OOM 不是"拒绝",用户拿到的会是段错误而不是一句能读懂的话:
产品限额: 单边 <= 16384 px, 像素 <= 16777216
200 状态 scale=1 -> 296x15800 通过;scale=2 -> 拒绝 DiagramRenderLimitError limit_name=edge
新增的 DiagramRenderLimitError 刻意不继承 DiagramRenderError:它的含义是"把 scale 调小就行",而后者意味着"重试没用"。
本 issue 继续保持 open。 伞 PR 尚未合入 main,剩余门禁:同步最新 main、full unit/jsfcstm/VSCode/geometry/visual/package/docs/installed-wheel/PyInstaller gates 全绿、记录同步、issue #418 关闭、VSCode webview 展开宿主能力决策(实测 resvg 2,478,606 字节 + 字体 17,700,092 单语言 / 59,403,012 全部 13 种)、最终独立 review C=0 / I=0、维护者人工批准。
本段取代下方把 PR-D 描述为待实施、以及把同步三格式导出/Notebook/完整 CLI 描述为未交付的旧状态;旧 POC、hash 与调研材料继续保留作历史考据。
2026-07-30 合并后权威状态
PR-C #389 已于 2026-07-30T15:20:37Z 合入
dev/python-diagram-umbrella,实现 head 8a4f465611a832bd1422213a041523df4fb1e79d,merge commit 46660c4c9226c211c661349bf2e81f18fdeea50a。合并前 48 项检查通过、0 跳过。
本次交付的是公共 Python 可视化接口与浏览器侧对比视图:
StateMachine.diagram() 返回不可变的 Diagram 快照,to_dict / to_json / to_html /
save / show / with_options / with_view_state 构成公共面;
self-contained、zero-network 的 HTML viewer,含 fit/zoom/pan/reset 与浏览器侧
SVG/PNG/单页矢量 PDF 下载(实测 PDF images: 0、pages: 1);
pyfcstm diagram -i ... -o ....json|....html 与 --open;
content-addressed viewer cache,按每用户 0700 目录隔离,用前以 lstat 复核;
双语 Tutorial / How-to / Reference / Explanation 四类文档。
本 issue 继续保持 open。 本 issue 标题所指的同步 SVG/PNG 栅格化能力仍未交付:
Diagram.to_svg() / to_png() / to_pdf() / save() 这条同步导出 facade、CLI 的
-o ....svg|....png|....pdf、Notebook static SVG、installed-wheel 与 PyInstaller onefile
闭环,全部属于 PR-D。伞 PR #383 也仍不可合入
main:除 PR-D 之外还差同步最新 main、installed-wheel/PyInstaller 门禁、记录同步、
最终独立 review 与维护者批准。
本段取代下方把 PR-C 描述为待实施或待合入的旧状态;旧 POC、hash 与调研材料继续保留作历史考据。
需要特别注意:下方历史 POC 区里的 pyfcstm visualize CLI 与
pyfcstm.visualize.render_svg/render_png 接口已被伞 PR 合同取代 ,现行接口是
pyfcstm diagram 与 StateMachine.diagram()。
2026-07-20 合并后权威状态
PR-B #386 已于 2026-07-20T12:45:18Z 合入 dev/python-diagram-umbrella,实现 head b78ae178b7649aea48b8a7f313642e1caf863f08,merge commit aa745161980799a4ba9e56e3654040339f7faa98。merge 前 60 项检查通过,1 项设计内跳过,template-suite-gate 通过,最终 review 为 APPROVE / READY / CLEAR。
这只关闭 official resvg asset/runtime foundation:公共 StateMachine.diagram()/show()、Diagram facade、HTML viewer/cache、Notebook、完整 CLI、PDF 与最终 headless delivery 仍由 PR-C/PR-D 交付。因此本 issue 继续保持 open;当前不能因 PR-B 合并而关闭。证据:https://gist.github.com/HansBug/b43d202ccc9436cada8bc57fa3763450。
本段取代下方把 PR-B 描述为 pending 或 ready-to-merge 的旧状态;旧 POC、hash 与调研材料继续保留作历史考据。
PR-B pre-merge state (historical) (b78ae178, 2026-07-20)
This section supersedes conflicting current-state text below that refers to old PR-B
heads, request-only/raw-SVG rejection, fixed output limits, production strict validators,
or DiagramRenderLimitError. Older sections remain historical research material.
The current runtime uses official @resvg/resvg-wasm@2.6.2, default timeout=None,
finite-positive scale without production output caps, minimal production SVG/PNG checks,
and strict maintenance/CI visual validators. DiagramData remains the normal input;
canonical SVG compatibility input is allowed but is not a general-purpose sanitizer.
PR-B now also handles real DTD/entity declarations without rejecting legal comment/CDATA,
rejects non-mapping/non-finite JSON requests at the Python boundary, redacts evaluated
source from MiniRacer errors, and contains the Python 3.7 legacy timeout+malformed-JS
native abort as a loud asset error.
The issue remains open. PR-B only completes the asset/runtime foundation;
StateMachine.diagram/show, public export APIs, HTML viewer/cache, Notebook, CLI, PDF,
and final delivery remain PR-C/PR-D work.
PR-B final current state (09ce4b9 , 2026-07-20)
本段是 issue 当前权威状态,旧 POC 和历史 hash 保留为考据。
当前状态 (historical, 2026-07-20)
PR-A 已通过并合入伞 PR 分支,见 PR #384 。PR-B #386 已将 official @resvg/resvg-wasm@2.6.2、closed SVG、PNG 结构与非空 ink、scale/尺寸限制、CJK、reference parity、token-bound memory、provenance 和 package 门禁实现并推送到 umbrella,当前 head 为 98fd2158,正在等待最终 CI/review。当前 issue 继续保持 open :公共 StateMachine.diagram/show、导出 facade、HTML viewer/cache 与整体 P0 仍由 PR-C/D 交付。
当前已确定的实现边界
资源路径为 pyfcstm/diagram/assets/,由 make build_assets 唯一生成并随 wheel/sdist 发布;旧 pyfcstm/assets、pyfcstm/diagram_runtime 路径不再有效。
Python 运行时兼容 modern mini-racer 与 Python 3.7 legacy py-mini-racer,CJK 资源覆盖 SC/TC/HK/JP/KR 五地区 Regular/Bold OTF。
资源缺失、非法 JS/WASM/OpenType、初始化失败、非法 PNG 或 expanded SVG 会 loud fail;开发 checkout 提示 make build_assets,已安装包提示向 pyfcstm issues 报告。
PR fix(visualization): make nested A -> B arrow terminals normal to target border #377 的箭头目标边框法向接入、orient=auto、refX=10 和 18px 终段已由共享 renderer 继承。
PR-A 的历史远端 required checks 为 57 SUCCESS、1 SKIPPED、0 failed;PR-A 的 490565a5 保留为历史资产基线;PR-B 当前 base 为 bcadb92a、head 为 98fd2158,本地 test_assets.py 为 60 passed,完整慢测子集为 46830 passed。当前实现还增加了完整 PNG 结构校验(包括非法 scanline filter 拒绝)、固定 13-face 字体集合门禁、受控资源 ASCII/English 门禁(包括 .gitignore),以及物理缺失 packaged resource 的明确错误分类。完整证据见 CJK/runtime verification Gist 、asset fail-loud report 和 independent visual audit 。
下方原始 POC 方案保留作历史考据。它包含旧的 pyfcstm/jsruntime 路径、旧 visualize API、直接进 git 的资源方案以及“PDF 留单独 issue”等当时假设;这些内容已被 PR-A/伞 PR 当前合同取代,不应视为现行接口或资源纪律。
背景
pyfcstm 现在能产出 PlantUML / Mermaid 源码,但最终成图全靠下游渲染器,质量受限。VSCode 扩展那边已经证明可以做得好得多——ELK 层次化布局、palette 主题、可点击回跳源码——但这套实现完全在 editors/jsfcstm(TypeScript)和 editors/vscode(Vue webview)里,Python 包用不上。
目标:让 pyfcstm 用户不离开 Python 就能拿到同等质量的图 。pip install pyfcstm[viz] 之后用一个 CLI 子命令或一行 Python API 把 state_machine.fcstm 渲染成 SVG / PNG。
为什么必须套一个 JS 沙箱
jsfcstm 真正不可替代的部分是 elkjs 对复合状态图的层次化布局 ——Python 生态没有任何近似实现。把 ELK 在 Python 里重写一遍要面对几万行 Java 算法(GWT 自动转译成的 JS),不现实。务实做法:直接嵌一个 JS 沙箱,把 jsfcstm + elkjs 装进去用。
方案(POC 已跑通)
阶段
实现
备注
DSL → SVG
jsfcstm 全链路(parser + diagram + ELK)打成 IIFE
走公开导出:getParser → buildAstFromTree → buildStateMachineModel → buildFcstmDiagramFromStateMachine → buildFcstmElkGraph → elk.layout → renderFcstmDiagramSvg
SVG → PNG
@resvg/resvg-wasm + 内嵌 OFL 等宽字体
全程 in-process,跑在 mini-racer 的 V8 + WASM 里,不引入任何 native 依赖
JS 宿主
mini-racer(py≥3.8)/ py-mini-racer(py<3.8)
用一个 JsEngine 适配层做版本分流
JS bundle、WASM 二进制、字体资产直接进 git (与现有 pyfcstm/template/*.zip 的"预生成资产"性质相同)。终端用户不需要装 npm 也不需要装 Node 。
POC 阶段踩出来的硬事实(写下来防止以后再踩)
mini-racer 0.14 的 V8 自带 WebAssembly 支持。
mini-racer 不带 TextEncoder / TextDecoder,需要自己写一个 ≤ 50 行的 UTF-8 polyfill。
elkjs 的 worker 模块按 typeof self 分流。必须 注入 globalThis.window = globalThis.global = globalThis,但不能 注入 self,否则 elk-worker.min.js 会安装一个 web-worker 监听器而不是导出 Worker 类。
elkjs 的 PromisedWorker 会调 setTimeout(fn, 0),加载 bundle 前要先注入一个同步桩。
esbuild 必须加 --keep-names,否则 ANTLR4 生成的 Constructor.name 字符串匹配(Def_assignmentContext 之类)在 minify 后全部失效,AST 会静默地构造出一个 rootState 缺失的空文档,buildStateMachineModel 直接返回 null。
esbuild --platform=neutral 默认不读 package.json 的 main 字段,要显式 --main-fields=main,module。
jsfcstm 的 workspace/ 和 editor/ 子模块为了跨文件 import 引用了 fs/path/node:url。单文件渲染路径用不到,bundle 时把这些 alias 成空模块即可。
renderFcstmDiagramSvg 返回的是 string 不是 {svg,width,height}(webview 自己有一份重新实现的 renderer 才返回那个对象,那个不是 jsfcstm 的公开 API)。
resvg-wasm 的 initWasm 接受 WebAssembly.Module 直接初始化,不需要 fetch。
resvg 在沙箱里看不到任何系统字体,必须自带一份 fallback 等宽字体——选 JetBrainsMono-Regular(OFL,~210 KB)正合适,因为 SVG 默认 font-family 第一项就是 JetBrains Mono。
这个 PR 为什么不 做 PDF
VSCode 扩展现有的"PDF 导出"实际上是 4× 栅格 PNG 用 jsPDF 包了一页 ,不是矢量 PDF ——见 Stage.vue 第 410 行附近的注释:作者主动放弃了 SVG → 矢量 PDF 路径,理由是任何 in-process 转换器都没法忠实处理浏览器级别的 SVG(CSS 级联 / webfont / 滤镜 / foreignObject)。要做真矢量 PDF 得引入 svg2pdf.js + @xmldom/xmldom,是一套独立的工程量、有自己的坑。为了让首个 PR 可控可发布,本 PR 只做 SVG + PNG,PDF 留单独 issue 。
对外接口
CLI:
pyfcstm visualize -i input.fcstm -o output.svg
pyfcstm visualize -i input.fcstm -o output.png --scale 2
pyfcstm visualize -i input.fcstm -o output.png --direction DOWN --palette nord --mode dark
Python:
from pyfcstm .visualize import render_svg , render_png , VisualizeOptions
svg : str = render_svg (dsl_text , VisualizeOptions (direction = "DOWN" ))
png : bytes = render_png (dsl_text , scale = 2.0 )
目录结构
pyfcstm/
├── jsruntime/
│ ├── __init__.py # 公开 API
│ ├── engine.py # JsEngine + MiniRacerEngine + PyMiniRacerEngine
│ ├── shim.js # window/global/setTimeout/atob/btoa/TextEncoder/TextDecoder
│ ├── bundle.js # jsfcstm + elkjs + resvg glue 的单文件 IIFE(预编译,进 git)
│ ├── resvg.wasm # @resvg/resvg-wasm 的 WASM 二进制(预编译,进 git)
│ └── fonts/
│ └── JetBrainsMono-Regular.ttf # OFL,~210 KB
├── visualize/
│ ├── __init__.py
│ ├── render.py # render_svg / render_png
│ └── options.py # VisualizeOptions dataclass
└── entry/
└── visualize.py # Click 子命令(在 dispatch 里注册)
构建工具:tools/package_jsbundle.py(esbuild 调度器,重新生成 pyfcstm/jsruntime/bundle.js);Makefile 加 make jsbundle / make jsbundle_clean。
依赖
extras_require = {
"viz" : [
"mini-racer>=0.12; python_version >= '3.8'" ,
"py-mini-racer>=0.6.0,<0.7; python_version < '3.8'" ,
],
}
不引入任何新的 Python 端栅格化依赖 (不用 cairosvg、不用 resvg-py、不用 Pillow、不用 img2pdf)。所有跟图像相关的事情都在 V8 + WASM 里完成。
兼容矩阵
Python 3.7 – 3.14(适配层负责 3.7 的分支)
Linux / macOS / Windows(mini-racer wheel 覆盖主流矩阵;WASM 跨平台字节级一致)
Win 7 + py 3.7 通过 py-mini-racer 0.6.x 尽力兼容,不进 CI,但适配层不阻挡
本 PR 的验收标准
pyfcstm visualize -i ... -o out.svg 输出一份合法 SVG,结构与 VSCode 预览一致
pyfcstm visualize -i ... -o out.png --scale 2 输出一份 2× PNG,文字字形正确(即字体喂进去了)
新增代码 100% 行覆盖率
现有测试套件全绿
基础安装路径不引入新的 Python 运行时依赖;viz extras 显式 opt-in
不在本 PR 范围(后续 issue)
矢量 PDF 导出(需要 svg2pdf.js + @xmldom/xmldom)
交互式预览窗口(需要 pywebview + webview 抽离成独立 build)
用户自定义字体 buffer
DSL 编辑时的 watch / 热渲染
2026-08-01 合并后权威状态
PR-D #415 已于
2026-08-01T03:43:41Z合入dev/python-diagram-umbrella,实现 head3e19c1d2,merge commit1174eec3b9e1。合并时 47 项检查 success、0 failure、1 skipping。至此伞 PR #383 下四个 sub PR 全部完成并合入 umbrella,计划中没有 PR-E: PR-A #384、PR-B #386(merge
aa745161)、PR-C #389(merge46660c4c)、PR-D #415(merge1174eec3)。本 issue 要的「嵌入 jsfcstm 可视化(SVG/PNG)」能力现已可用:
真实运行:
SVG 的
<text>=0是合同而非巧合:不展开的 SVG 里文字仍是<text>+font-family,收件人机器缺字体时排版就崩;展开后字形是路径,文件自足。PDF 的位图对象=0同理,是真矢量而非把图片塞进 PDF 壳。超出尺寸时是拒绝而不是崩溃或悄悄裁切,且限额在 WASM 之前检查——让渲染器 OOM 不是"拒绝",用户拿到的会是段错误而不是一句能读懂的话:
新增的
DiagramRenderLimitError刻意不继承DiagramRenderError:它的含义是"把 scale 调小就行",而后者意味着"重试没用"。本 issue 继续保持 open。 伞 PR 尚未合入
main,剩余门禁:同步最新main、full unit/jsfcstm/VSCode/geometry/visual/package/docs/installed-wheel/PyInstaller gates 全绿、记录同步、issue #418 关闭、VSCode webview 展开宿主能力决策(实测 resvg 2,478,606 字节 + 字体 17,700,092 单语言 / 59,403,012 全部 13 种)、最终独立 reviewC=0 / I=0、维护者人工批准。本段取代下方把 PR-D 描述为待实施、以及把同步三格式导出/Notebook/完整 CLI 描述为未交付的旧状态;旧 POC、hash 与调研材料继续保留作历史考据。
2026-07-30 合并后权威状态
PR-C #389 已于
2026-07-30T15:20:37Z合入dev/python-diagram-umbrella,实现 head8a4f465611a832bd1422213a041523df4fb1e79d,merge commit46660c4c9226c211c661349bf2e81f18fdeea50a。合并前 48 项检查通过、0 跳过。本次交付的是公共 Python 可视化接口与浏览器侧对比视图:
StateMachine.diagram()返回不可变的Diagram快照,to_dict/to_json/to_html/save/show/with_options/with_view_state构成公共面;SVG/PNG/单页矢量 PDF 下载(实测 PDF
images: 0、pages: 1);pyfcstm diagram -i ... -o ....json|....html与--open;lstat复核;本 issue 继续保持 open。 本 issue 标题所指的同步 SVG/PNG 栅格化能力仍未交付:
Diagram.to_svg()/to_png()/to_pdf()/save()这条同步导出 facade、CLI 的-o ....svg|....png|....pdf、Notebook static SVG、installed-wheel 与 PyInstaller onefile闭环,全部属于 PR-D。伞 PR #383 也仍不可合入
main:除 PR-D 之外还差同步最新main、installed-wheel/PyInstaller 门禁、记录同步、最终独立 review 与维护者批准。
本段取代下方把 PR-C 描述为待实施或待合入的旧状态;旧 POC、hash 与调研材料继续保留作历史考据。
需要特别注意:下方历史 POC 区里的
pyfcstm visualizeCLI 与pyfcstm.visualize.render_svg/render_png接口已被伞 PR 合同取代,现行接口是pyfcstm diagram与StateMachine.diagram()。2026-07-20 合并后权威状态
PR-B #386 已于
2026-07-20T12:45:18Z合入dev/python-diagram-umbrella,实现 headb78ae178b7649aea48b8a7f313642e1caf863f08,merge commitaa745161980799a4ba9e56e3654040339f7faa98。merge 前 60 项检查通过,1 项设计内跳过,template-suite-gate通过,最终 review 为 APPROVE / READY / CLEAR。这只关闭 official resvg asset/runtime foundation:公共
StateMachine.diagram()/show()、Diagram facade、HTML viewer/cache、Notebook、完整 CLI、PDF 与最终 headless delivery 仍由 PR-C/PR-D 交付。因此本 issue 继续保持 open;当前不能因 PR-B 合并而关闭。证据:https://gist.github.com/HansBug/b43d202ccc9436cada8bc57fa3763450。本段取代下方把 PR-B 描述为 pending 或 ready-to-merge 的旧状态;旧 POC、hash 与调研材料继续保留作历史考据。
PR-B pre-merge state (historical) (
b78ae178, 2026-07-20)This section supersedes conflicting current-state text below that refers to old PR-B
heads, request-only/raw-SVG rejection, fixed output limits, production strict validators,
or
DiagramRenderLimitError. Older sections remain historical research material.b78ae178b7649aea48b8a7f313642e1caf863f08Docs generated resourcesskip;template-suite-gatepassed. Codecov is complete at 94.54% (25 sessions).The current runtime uses official
@resvg/resvg-wasm@2.6.2, defaulttimeout=None,finite-positive scale without production output caps, minimal production SVG/PNG checks,
and strict maintenance/CI visual validators. DiagramData remains the normal input;
canonical SVG compatibility input is allowed but is not a general-purpose sanitizer.
PR-B now also handles real DTD/entity declarations without rejecting legal comment/CDATA,
rejects non-mapping/non-finite JSON requests at the Python boundary, redacts evaluated
source from MiniRacer errors, and contains the Python 3.7 legacy timeout+malformed-JS
native abort as a loud asset error.
The issue remains open. PR-B only completes the asset/runtime foundation;
StateMachine.diagram/show, public export APIs, HTML viewer/cache, Notebook, CLI, PDF,and final delivery remain PR-C/PR-D work.
PR-B final current state (09ce4b9, 2026-07-20)
本段是 issue 当前权威状态,旧 POC 和历史 hash 保留为考据。
当前状态 (historical, 2026-07-20)
PR-A 已通过并合入伞 PR 分支,见 PR #384。PR-B #386 已将 official
@resvg/resvg-wasm@2.6.2、closed SVG、PNG 结构与非空 ink、scale/尺寸限制、CJK、reference parity、token-bound memory、provenance 和 package 门禁实现并推送到 umbrella,当前 head 为98fd2158,正在等待最终 CI/review。当前 issue 继续保持 open:公共StateMachine.diagram/show、导出 facade、HTML viewer/cache 与整体 P0 仍由 PR-C/D 交付。当前已确定的实现边界
pyfcstm/diagram/assets/,由make build_assets唯一生成并随 wheel/sdist 发布;旧pyfcstm/assets、pyfcstm/diagram_runtime路径不再有效。mini-racer与 Python 3.7 legacypy-mini-racer,CJK 资源覆盖 SC/TC/HK/JP/KR 五地区 Regular/Bold OTF。make build_assets,已安装包提示向 pyfcstm issues 报告。orient=auto、refX=10和 18px 终段已由共享 renderer 继承。PR-A 的历史远端 required checks 为 57 SUCCESS、1 SKIPPED、0 failed;PR-A 的
490565a5保留为历史资产基线;PR-B 当前 base 为bcadb92a、head 为98fd2158,本地test_assets.py为 60 passed,完整慢测子集为 46830 passed。当前实现还增加了完整 PNG 结构校验(包括非法 scanline filter 拒绝)、固定 13-face 字体集合门禁、受控资源 ASCII/English 门禁(包括.gitignore),以及物理缺失 packaged resource 的明确错误分类。完整证据见 CJK/runtime verification Gist、asset fail-loud report 和 independent visual audit。背景
pyfcstm现在能产出 PlantUML / Mermaid 源码,但最终成图全靠下游渲染器,质量受限。VSCode 扩展那边已经证明可以做得好得多——ELK 层次化布局、palette 主题、可点击回跳源码——但这套实现完全在editors/jsfcstm(TypeScript)和editors/vscode(Vue webview)里,Python 包用不上。目标:让
pyfcstm用户不离开 Python 就能拿到同等质量的图。pip install pyfcstm[viz]之后用一个 CLI 子命令或一行 Python API 把state_machine.fcstm渲染成 SVG / PNG。为什么必须套一个 JS 沙箱
jsfcstm真正不可替代的部分是elkjs对复合状态图的层次化布局——Python 生态没有任何近似实现。把 ELK 在 Python 里重写一遍要面对几万行 Java 算法(GWT 自动转译成的 JS),不现实。务实做法:直接嵌一个 JS 沙箱,把jsfcstm+elkjs装进去用。方案(POC 已跑通)
jsfcstm全链路(parser + diagram + ELK)打成 IIFEgetParser → buildAstFromTree → buildStateMachineModel → buildFcstmDiagramFromStateMachine → buildFcstmElkGraph → elk.layout → renderFcstmDiagramSvg@resvg/resvg-wasm+ 内嵌 OFL 等宽字体mini-racer(py≥3.8)/py-mini-racer(py<3.8)JsEngine适配层做版本分流JS bundle、WASM 二进制、字体资产直接进 git(与现有
pyfcstm/template/*.zip的"预生成资产"性质相同)。终端用户不需要装 npm 也不需要装 Node。POC 阶段踩出来的硬事实(写下来防止以后再踩)
mini-racer0.14 的 V8 自带 WebAssembly 支持。mini-racer不带TextEncoder/TextDecoder,需要自己写一个 ≤ 50 行的 UTF-8 polyfill。elkjs的 worker 模块按typeof self分流。必须注入globalThis.window = globalThis.global = globalThis,但不能注入self,否则elk-worker.min.js会安装一个 web-worker 监听器而不是导出Worker类。elkjs的 PromisedWorker 会调setTimeout(fn, 0),加载 bundle 前要先注入一个同步桩。esbuild必须加--keep-names,否则 ANTLR4 生成的Constructor.name字符串匹配(Def_assignmentContext之类)在 minify 后全部失效,AST 会静默地构造出一个 rootState 缺失的空文档,buildStateMachineModel直接返回null。esbuild --platform=neutral默认不读package.json的main字段,要显式--main-fields=main,module。jsfcstm的workspace/和editor/子模块为了跨文件 import 引用了fs/path/node:url。单文件渲染路径用不到,bundle 时把这些 alias 成空模块即可。renderFcstmDiagramSvg返回的是 string 不是{svg,width,height}(webview 自己有一份重新实现的 renderer 才返回那个对象,那个不是 jsfcstm 的公开 API)。resvg-wasm的initWasm接受WebAssembly.Module直接初始化,不需要 fetch。resvg在沙箱里看不到任何系统字体,必须自带一份 fallback 等宽字体——选JetBrainsMono-Regular(OFL,~210 KB)正合适,因为 SVG 默认font-family第一项就是JetBrains Mono。这个 PR 为什么不做 PDF
VSCode 扩展现有的"PDF 导出"实际上是 4× 栅格 PNG 用 jsPDF 包了一页,不是矢量 PDF——见
Stage.vue第 410 行附近的注释:作者主动放弃了 SVG → 矢量 PDF 路径,理由是任何 in-process 转换器都没法忠实处理浏览器级别的 SVG(CSS 级联 / webfont / 滤镜 / foreignObject)。要做真矢量 PDF 得引入svg2pdf.js+@xmldom/xmldom,是一套独立的工程量、有自己的坑。为了让首个 PR 可控可发布,本 PR 只做 SVG + PNG,PDF 留单独 issue。对外接口
CLI:
Python:
目录结构
构建工具:
tools/package_jsbundle.py(esbuild 调度器,重新生成pyfcstm/jsruntime/bundle.js);Makefile加make jsbundle/make jsbundle_clean。依赖
不引入任何新的 Python 端栅格化依赖(不用
cairosvg、不用resvg-py、不用Pillow、不用img2pdf)。所有跟图像相关的事情都在 V8 + WASM 里完成。兼容矩阵
py-mini-racer0.6.x 尽力兼容,不进 CI,但适配层不阻挡本 PR 的验收标准
pyfcstm visualize -i ... -o out.svg输出一份合法 SVG,结构与 VSCode 预览一致pyfcstm visualize -i ... -o out.png --scale 2输出一份 2× PNG,文字字形正确(即字体喂进去了)不在本 PR 范围(后续 issue)
svg2pdf.js+@xmldom/xmldom)pywebview+ webview 抽离成独立 build)