mermaid-ascii 的 CJK 对齐秘密:全角字符双格占位实现剖析
【免费下载链接】mermaid-asciiRender Mermaid graphs inside your terminal项目地址: https://gitcode.com/GitHub_Trending/me/mermaid-ascii
mermaid-ascii 是一款在终端里渲染 Mermaid 图(流程图、时序图、ER 图)的开源命令行工具。当你把节点名写成"顾客"、"数据库"这类中文字符时,很多渲染器会把图框撑得歪歪扭扭——而它靠全角字符双格占位这一关键设计让 CJK 字符在终端里严丝合缝地对齐。这篇文章带你剖析这套机制背后的原理与实现细节。
为什么终端里的 CJK 对齐这么难?
在终端的等宽网格中,每个"列"的宽度是固定的:
- 半角字符(英文、数字、ASCII 符号):占1 格
- 全角字符(中日韩汉字、全角标点):占2 格
如果渲染器按"字符个数"而不是"显示宽度"来计算位置,一个 4 个汉字的节点名就会被当成 4 格而非 8 格处理,方框、箭头、生命线随之全部错位。这是所有终端绘图工具在支持中文时的第一道坎。
核心原理:runewidth 测量显示宽度
mermaid-ascii 的解法是引入mattn/go-runewidth库——它是 Go 生态中计算 Unicode 显示宽度的事实标准。项目几乎在所有绘制路径上都用它来度量文本:
- 流程图绘制:pkg/graph/draw.go
- 节点标签排版:pkg/graph/label.go
- 时序图单元格拆分:pkg/sequence/renderer.go
- ER 图表格列宽:pkg/er/renderer.go
以流程图绘制文字为例,drawText的逻辑只有两步:
- 先用
runewidth.StringWidth(text)算出整段文字的视觉宽度,据此扩展画布; - 再逐个 rune 遍历,宽字符(如汉字)写入单元格后,把右边相邻的格子标记为续格,再前进
runeWidth列。
这样"数据库"三个字在内部画布中恰好占据 6 列,方框边界和箭头端点自然落在正确的位置上。
时序图则更进一步:pkg/sequence/renderer.go 中的runeCellWidth把每个字符拆成"单元格"数组,宽度为 2 的字符后面追加一个continuationCell(续格),保证任何写入操作都不会"插队"到汉字中间,破坏显示。
边界情况:零宽组合标记怎么办?
CJK 文本常伴随零宽组合字符(如音调符号),它们显示宽度为 0。cmd/testdata/sequence/leading_combining_marks.txt 这类测试用例就专门验证了这个场景。
实现上的处理很巧妙(见 pkg/sequence/renderer.go 的putTextBefore):
- 遇到宽度为 0 的字符时,附着到前一个有宽字符的单元格上,而不是占格;
- 如果前面还没有基础字符,就先"挂起",等下一个有效字符出现时再合并写入。
这避免了组合标记错误地附着到填充空格上导致的字符错乱。
隐藏陷阱:East Asian Width 模式
还有一个容易被忽略的坑:制表符本身(│、─)的宽度会随终端 locale 变化。在东亚宽字符模式下,这些字符可能被测量为 2 格宽,导致同一份输出在不同终端上表现不一致。
mermaid-ascii 从两个层面化解了这个问题:
- 字符集层面:在 pkg/sequence/charset.go 中,
Circle特意选用字母o而非 Unicode 圆圈○,源码注释明确写道——后者是 East-Asian-ambiguous 宽度,会在支持 CJK 的终端上破坏列对齐。这个细节体现了作者对宽度模型的深刻理解; - 测试层面:pkg/sequence/renderer_test.go 专门构造了
EastAsianWidth = true的运行环境,对 east_asian_participants.txt 等用例做回归验证,确保宽字符模式下参与者方框之间依然留白正确。
实际效果:一张全 CJK 的时序图
测试数据 cmd/testdata/sequence/cjk_feature_matrix.txt 是一份"压力测试":日语 box 标题、中日混合的参与者(顧客 / 服务 / 監査 / 数据库)、自消息、Note、alt/loop 嵌套块全部用 CJK 命名。得益于双格占位机制,所有方框宽度、箭头长度和嵌套边框的拐角都能精确对齐,即使中英日韩混排也不散架。
小结
mermaid-ascii 让终端 Mermaid 渲染稳稳支持中文,靠的不是什么魔法,而是三条工程原则:
- 一切排版以显示宽度为准——
runewidth贯穿流程图、时序图、ER 图所有绘制路径; - 全角字符双格占位 + 续格保护——后续写入永远不会撕裂一个宽字符;
- 对宽度歧义字符保持警觉——宁可换字符,也不把对齐交给终端的 locale。
如果你也在做终端绘图或 Markdown 渲染相关的项目,这套"宽度优先"的思路非常值得参考。更多用法与示例见 README.md。
【免费下载链接】mermaid-asciiRender Mermaid graphs inside your terminal项目地址: https://gitcode.com/GitHub_Trending/me/mermaid-ascii
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考