gstack diagram-render:用单文件 HTML 页面实现完全离线的 Mermaid/Excalidraw 图表渲染
2026/9/7 5:46:31 网站建设 项目流程

gstack diagram-render:用单文件 HTML 页面实现完全离线的 Mermaid/Excalidraw 图表渲染

【免费下载链接】gstackUse Garry Tan's exact Claude Code setup: 23 opinionated tools that serve as CEO, Designer, Eng Manager, Release Manager, Doc Engineer, and QA项目地址: https://gitcode.com/GitHub_Trending/gs/gstack

本篇技术指南解析 gstack 中lib/diagram-render模块的设计与实现:它是make-pdf/diagram两个技能共用的离线图表渲染引擎,把 mermaid、Excalidraw 导出工具与官方 mermaid→Excalidraw 转换器全部打包进一个自包含 HTML 页面,由 browse daemon 以load-html加载、通过browse js驱动、以js --out取回二进制结果。读完本文,你将掌握该模块的 Page API(8 个window.__*函数)的调用契约、渲染安全约束(securityLevel strict、htmlLabels false、字体栈锁定)、确定性构建流程与防篡改的 drift 测试机制。

一、模块定位:为什么需要一个"离线渲染页"

gstack 的 文档目录说明 将其定位为 "Use Garry Tan's exact Claude Code setup",其中make-pdf负责把 Markdown 生成 PDF,/diagram负责生成图表文件。二者都依赖 mermaid 渲染,而 mermaid 依赖浏览器环境运行。diagram-render 模块给出的方案是:

  • 构建产物只有一个自包含 HTML 页面(dist/diagram-render.html,约 9MB),内置 mermaid、excalidraw 导出工具与官方 mermaid→excalidraw 转换器;
  • browse daemon 通过load-html把该页面加载进一个标签页,调用方用browse js驱动页面内的window.__*函数,再用js --out把 data URL 解码为磁盘上的字节;
  • 构建产物是提交进仓库的(eng-review D2 决策):安装时和渲染时均零网络依赖,./setup中没有 npm 供应链暴露面;
  • 漂移测试 drift 测试 会在dist/被手工编辑、或与BUILD_INFO.json不同步时让 CI 失败。

从源码结构看,该页面的消费者主要是 make-pdf 的图表预处理器,以及 diagram 技能的 e2e 测试 和 make-pdf 的多个 e2e 门(如make-pdf/test/e2e/diagram-gate.test.ts)。

二、Page API:页面暴露的 window 函数

README 完整列出了页面的 API 契约,入口源码 与之一一对应。调用方必须遵守以下约定:

函数入 → 出
__renderMermaid(id, text)mermaid 文本 → SVG 字符串。id每个 fence 必须唯一(mermaid-fence-<n>)——它命名空间化了内部所有 SVG id。
__mermaidToExcalidraw(text)mermaid 文本 →.excalidraw场景 JSON(flowchart 完整支持,其他类型在上游退化)。
__excalidrawToSvg(sceneJson)场景 JSON → SVG 字符串(内嵌 Excalifont,离线)。
__rasterize(svg, targetWidthPx)SVG → PNG data URL。调用方自行负责 DPI 换算:targetWidthPx = 版面物理宽度 (in) × 300。在 tainted canvas 上抛错。
__downscaleRaster(dataUri, targetWidthPx, mime)栅格 data URI → 缩放到targetWidthPx的 data URI(mime 不变)。make-pdf 用它把超尺寸照片归一化到印刷分辨率。
__mountForScreenshot(svg, px)防 taint 兜底:把 SVG 挂到#raster-stage上供browse screenshot --selector截图。
__probeImage(src)data URI/URL →{width, height}JSON。
__bundleInfo{ name, deps }—— 构建时烧录的固定依赖版本。

就绪探测:轮询#status文本直到变为ready(或用browse wait '#done')。页面错误累积在window.__errors

源码中几个值得注意的实现细节(entry.ts):

  1. 渲染 id 白名单校验__renderMermaidid做正则校验/^[A-Za-z][\w-]*$/,防止非法字符注入 mermaid 内部 id 命名空间。mermaid 会把 id 烘焙进每个内部 SVG id,因此同一文档内内联两张图不会在 gradient/marker 上发生 id 碰撞。
  2. DPI 上限__rasterize__downscaleRaster共享MAX_TARGET_PX = 10_000的硬上限,越界直接抛错——bundle 永不猜测视口,DPI 换算完全由调用方负责。
  3. tainted canvas 兜底链__rasterizecanvas.toDataURL,一旦 canvas 被污染即抛错;此时调用方切换到__mountForScreenshot把 SVG 挂进 DOM(返回mounted:<px>标记串,真正的产物是截图本身而非返回值),再用browse screenshot --selector "#raster-stage"完成栅格化。
  4. 字体栈锁定PRINT_SANS字体栈与 make-pdf 的 print-css 完全一致(Helvetica / Liberation Sans / Arial + 日文 Hiragino + Noto Sans CJK JP + Microsoft YaHei + 三套 Emoji 字体),保证 mermaid 在渲染页内的文本测量与最终打印文档的排版逐像素一致。
  5. Excalidraw 资源占位window.EXCALIDRAW_ASSET_PATH被设为一个绝对但不存在的https://gstack-render.localhost/excalidraw-assets/——页面天生离线,exportToSvg会直接内嵌 bundle 中自带的 Excalifont 字形,不会发起网络请求。

三、渲染安全契约(eng-review D3)

entry.ts 头部注释 与初始化代码明确了渲染契约:

mermaid.initialize({ startOnLoad: false, securityLevel: "strict", // 无点击回调、无 HTML 标签注入 theme: "neutral", fontFamily: PRINT_SANS, htmlLabels: false, // foreignObject 标签会污染 canvas 并破坏内联 flowchart: { htmlLabels: false }, });
  • securityLevel: "strict"是第一道防线:该标签页内不存在点击回调,也不存在 HTML 标签注入;make-pdf 的 sanitizer 是下游第二道防线。
  • htmlLabels: false是双重要求:foreignObject 标签既会污染 canvas(阻断toDataURL栅格化),又会在 SVG 被内联进另一份文档时失效。
  • 页面生命周期(同样记录在 entry.ts 注释中):load-html加载 dist 副本 → 轮询#status == "ready"→ N 次__renderMermaid/__excalidrawToSvg/__rasterize→ orchestrator 在 finally 中关闭标签页;若渲染出错,调用方在下一个 fence 前重新加载页面(重置契约:不残留被污染的 mermaid 全局状态,eng-review D6.2)。

四、确定性构建:bun run build 与 BUILD_INFO.json

构建脚本 用Bun.build把 src/entry.ts 打成浏览器目标、minify 后的单文件内联模块,并写入两个文件:

  • dist/diagram-render.html:完整页面;
  • dist/BUILD_INFO.json:记录{ name, sha256, srcSha256, bytes, bunVersion, deps }。当前提交的产物为 9,645,479 字节(约 9.2MB),bunVersion 1.3.13,依赖 pin 见 dist/BUILD_INFO.json。

构建脚本注释中记录了三个"不可简化的页面装配要点"(来自 spike 阶段的经验):

  1. 内联脚本必须是type="module"——mermaid bundle 内含import.meta,classic script 会直接抛错;
  2. minified JS 中的</scri序列必须转义为<\/scri,否则内联<script>会被提前终止("Unexpected end of input");
  3. 需要一个绝对 URL 的<base href>:页面运行在about:blank(page.setContent),相对 URL 构造会抛错。构建脚本写入的是<base href="https://gstack-render.localhost/">

此外脚本还埋了一个__BUNDLE_INFO_DEPS__define,构建时替换为 package.json 中的精确 pin 映射——这正是__bundleInfo的数据来源:

"dependencies": { "@excalidraw/excalidraw": "0.18.0", "@excalidraw/mermaid-to-excalidraw": "1.1.2", "mermaid": "11.12.2", "react": "18.3.1", "react-dom": "18.3.1" }

版本全部精确 pin(不带^),构建脚本头注释也明确要求:升级依赖时编辑 package.json 中的精确 pin,bun install后重建,并将 src、dist、BUILD_INFO.json 一起提交。第三方许可证清单见 THIRD-PARTY-LICENSES.md:五个依赖包均为 MIT,Excalidraw 自带的 Excalifont 等字体为 SIL Open Font License 1.1;升级 pin 时必须同步复核许可证字段并更新该表。

五、更新流程(README 原样继承)

# 1. edit the exact pin in package.json cd lib/diagram-render && bun install # 2. rebuild (deterministic; build twice → same sha) bun run build # 3. commit package.json + bun.lock + dist/ together

注意确定性是有前提的:minifier 输出只在同一 bun 版本内保证可复现,这也是 drift 测试中深检层级跳过条件的由来(见下节)。

六、漂移测试:三层防篡改守卫

drift 测试 是"dist 已提交"策略的守门人,分为三层:

  • Tier 1(始终运行,<50ms)dist/diagram-render.html的 sha256 必须精确等于BUILD_INFO.json记录的sha256,且字节数等于bytesBUILD_INFO.deps必须与package.jsondependencies完全相等。捕获手工编辑 dist 的文件、以及"升了 pin 忘了重建"的提交。
  • Tier 1.5(无需 node_modules)srcSha256src/entry.tsscripts/build.ts两个源文件内容串联后的 sha256。构建脚本在写 BUILD_INFO 时同步计算它(见 build.ts 的注释:让"改了 src 忘了 rebuild"这种漂移在不做完整重建、不装依赖的情况下也能被抓到)。
  • 字体栈守卫:print-css 组合进 body 字体栈的每个家族名(Helvetica、Liberation Sans、Arial、Hiragino Kaku Gothic ProN、Noto Sans CJK JP、Microsoft YaHei、Apple Color Emoji、Segoe UI Emoji、Noto Color Emoji)都必须出现在 entry.ts 的PRINT_SANS字面量中——mermaid 用这些字体测量文本,打印文档用 print-css 排版,漂移直接导致标签文字溢出(eng-review D3)。
  • 页面不变量:dist 必须含<script type="module"><base href="https://gstack-render.localhost/">window.__errors = [],且整个文档中</script>闭合标签恰好出现 2 次(head 错误捕获器一次 + 模块脚本一次)——任何多余的</script>都意味着转义失败。
  • Tier 2(深度重建,仅 CI/安装后):重新执行bun run scripts/build.ts并比较 sha。跳过条件有两个:该目录下node_modules不存在(fresh clone 未安装),或本地 bun 版本与 BUILD_INFO 记录的不同。

测试中还断言了错误捕获基础设施的存在(window.__errors = []),与构建脚本写入 head 的onerror/unhandledrejection钩子对应——页面运行时的任何未处理异常都会被累积,调用方可随时读取排查。

七、小结:这套方案的工程取舍

从仓库实际内容看,diagram-render 的价值不在功能本身(mermaid 渲染是标准能力),而在三条工程约束的组合:

  1. 零网络:产物提交进仓库,安装与渲染全程离线,./setup无 npm 供应链暴露面;
  2. 确定性:精确 pin + sha256 记录 + 同 bun 版本内可复现构建,任何漂移(手改 dist、忘重建、字体栈漂移、pin 与 BUILD_INFO 不一致)都会被 CI 拦截;
  3. 安全分层:页面内 mermaid strict 模式 + htmlLabels false,页面外 make-pdf sanitizer 兜底;canvas taint 场景有 DOM 截图回退路径,栅格化有 10,000px 硬上限。

若需深入阅读,建议依次查看:README(API 契约与更新流程)、src/entry.ts(渲染契约与全部 window 函数实现)、scripts/build.ts(页面装配要点与 BUILD_INFO 生成)、test/diagram-render-drift.test.ts(三层漂移守卫),以及消费侧的 make-pdf/src/diagram-prepass.ts。

【免费下载链接】gstackUse Garry Tan's exact Claude Code setup: 23 opinionated tools that serve as CEO, Designer, Eng Manager, Release Manager, Doc Engineer, and QA项目地址: https://gitcode.com/GitHub_Trending/gs/gstack

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询