周末考古——ASCIIFlow中文支持探索
写在前面
ASCIIFlow(文本流程图)是一种非常古早的流程图展示方式,它使用ascii字节码来绘制,这种Geek的视觉风格深受上世纪黑客们的喜爱。并且它字符载体的特性十分易于传播,甚至可以直接出现在代码注释中,深得俺心。
生成文本流程图的工具目前有很多,比如Perl
开发的ASCIIO与Graph::Easy,它们都有着丰富的字节表现形式,个人也十分推荐Graph::Easy
,只是安装和中文支持比较麻烦,同时还需要学习一些生成流程图相关的指令,
Vim党也可以通过插件 Drawit
和 Sketch
来绘制流程图。
不过这些都不是今天要考古的对象,而是一款开源在线asciiflow编辑器asciiflow.com,它是所见即所得的,并且操作非常简单,尽管只有简单的线框样式,但也基本够用,非常适合用做小块代码逻辑的流程表达。 (中文版链接在文末! (中文版链接在文末! (中文版链接在文末! [图片来自NodeJS文档]
中文支持的难点
通过观察发现,市面大部分的ASCIIFlow工具都是不支持中文的,包括asciiflow的官方中文版asciiflow.cn也存在导入导出的瑕疵,mac上操作几乎是不可用的乱码状态。
细想一下其实不难理解,在等宽字体族的前提下(ascii art的基本依赖),中文文字的视觉宽度约等于2字符宽度,但这并不是绝对的,和最终的渲染算法有关,这意味着相同的文本在不同环境下会产生不同的视觉偏差,也就无法对流程图的线条进行对齐。
比如在vim中,中文字体的字符比是2:1,而在Mac系统的Web或其他文本编辑器中,中文字体的字符比为5:3。 如果换成windows系统,文本编辑器默认等宽字体的中文字符比则变成了9:5。
给asciiflow.com添加中文补丁
知道了问题所在,那我们就可以来动手撸它了。asciiflow.com是开源的传送门,并且常年占据ascii art热搜榜前列,时至今日已经发展出了两个版本。相对来说,我更喜欢新版的UI,但简单阅读源码之后发现作者直接监听的窗口按键来渲染文字,丝毫没有给中文输入的机会。
window.addEventListener("keypress", (e) => controller.handleKeyPress(e));
window.addEventListener("keydown", (e) => controller.handleKeyDown(e));
window.addEventListener("keyup", (e) => controller.handleKeyUp(e));
在此基础上大刀阔斧地加上中文支持,显然不是俺所剩无几的发量该考虑的事情。所以我们忍痛割爱,从古早版下手。当我打开古早版legacy
目录之后,您猜怎么着,这里面并没有找到源码,只有构建后的一堆堆甲骨文...
this.a && this.b && (new I(this.a, this.b)).contains(a)
? (this.f = a, xa(this), ya(this, a))
: (this.a = a, this.b = null, this.h = !1, this.i(a));
...
支持中文输入
我们先来看下目前输入中文时我们看到的问题,编辑器视图由网状的小方格组成,可以理解为每个方格代表1个字符视图宽度。而当我们输入中文时,编辑器获得的字符长度为1,而中文的视觉宽度却是大约2个字符,所以编辑端会发生文字重叠的现象如下图所示:
编译后的代码我今儿是读不明白了,不过不要紧,我们先从输入框idtext-tool-input
顺藤摸瓜,找到读取输入框内容的代码
g.j = function() {
var a = $("#text-tool-input").val();
S(this.b);
for (var b = this.b, c = this.c, e = 0, d = 0, a = n(a), f = a.next();!f.done;f = a.next()) {
f = f.value, "\n" == f ? (d++, e = 0) : (P(b, c.add(new p(e, d)), f), e++);
}
};
简单debug一下算是看懂了些逻辑;这里b
是渲染队列,e
是当前渲染x坐标,f
是当前循环中的渲染字符,new p(e, d)
创建一个渲染位置的x,y坐标,p(b, c.add(new p(e, d)), f)
表示将当前渲染字符与坐标推入渲染队列。那么只需要判断当前字符为中文时,将e
往右推一格,将中文字符的长度变为2
...
f = f.value;
"\n" == f
? (d++, e = 0)
: (P(b, c.add(new p(e, d)), f), /[\u4E00-\u9FA5]/g.test(f) ? e+= 2 : e++);
支持中文文本片段导出
中文输入的问题解决了,但是我们需要依靠导出功能才能获得编辑视图中的文本片段。而导出来的文本目前依然是错乱的,我们通过将输入的中文字符的占位长度扩展成了2来对齐展示宽度,而这个多余的空字符也会一同导出如下图所示:
我们接着来考古,找到导出按钮export-button
绑定的事件代码
function U(a) {
for (var b = new p(Number.MAX_VALUE, Number.MAX_VALUE), c = new p(-1, -1), e = 0;e < a.a.length;e++) {
for (var d = 0;d < a.a[e].length;d++) {
null != J(a.a[e][d]) && (e < b.x && (b.x = e), d < b.y && (b.y = d), e > c.x && (c.x = e), d > c.y && (c.y = d));
}
}
if (0 > c.x) {
return "";
}
for (var f = "", d = b.y;d <= c.y;d++) {
for (var k = "", e = b.x;e <= c.x;e++) {
var l = na(a, new p(e, d)), k = k + (null == l || "\u2009" == l ? " " : l);
}
f += k.replace(/\s+$/, "") + "\n";
}
return f;
}
大致意思是遍历画布单元格,然后拼凑出最后的字符串f
。我们先剔除掉渲染将中文字符扩展为2之后多余的字符,在遍历的过程中跳过中文字符k
的下一字符:
for (var k = "", e = b.x;e <= c.x;e++) {
... if(/[\u4E00-\u9FA5]/.test(l)) e++
}
这个时候我们可以看到导出的中文后面已经没有空格了,但是边框位置还是错乱的
+-----------------+
| 中文支持 |
| |
+-----------------+
因为我们在渲染视图中看到的中文字经过扩展之后的字符宽度为2,而上面提到过,等款字体下中文字符的渲染宽度在不同场景下是不同的,例如mac中系统渲染字符比为5:3,一个中文字符实际上只渲染了1.6667的字符宽度,那么我们渲染4个字符之后,与编辑视图中的长度少了约1.3字符宽度,也就是说每三个中文字符之后需要填补一个字符来抹平差异(windows场景为5个)。
为了视觉上的美观,我们把所有空字符添加到连续中文字符之后,并且为了最大程度上校准流程图边线,对需要插入的矫正字符数进行四舍五入。
入乡随俗,我们使用三个变量q,m,x
来分别记录偏移字符数(浮点数),待插入字符数,已插入字符数,将行循环解析的代码改成如下所示
var q = 0;
var m = 0;
var x = 0;
for (var k = "", e = b.x;e <= c.x;e++) {
var l = na(a, new p(e, d));
if (/[\u4E00-\u9FA5]/.test(l)) {
q += v; // v是每个中文字符的视觉字符差值,mac为1/3,windows为1/5
e++;
} else {
x = Math.round(q)
for (m = 0; m < x; m++) {
k += ' ';
}
q -= m;
}
k = k + (null == l || "\u2009" == l ? " " : l);
}
f += k.replace(/\s+$/, "") + "\n";
支持中文文本片段导入
asciiflow没有存档,也没有对应的数据描述,要修改此类所见即所得的文本片段只能通过导入的方式二次编辑,因此中文文本片段的导入支持也是必不可少的,而导入行为也就是“导出”行为的逆向工程:
- 导出:每个中文字符后删除一个空字符,每3(mac环境)个中文字符再添加一个空字符。
- 导入:每个中文字符后添加一个空字符,每3(mac环境)个中文字符去掉一个空字符。
让我们接着考古,找到导入按钮#import-submit-button
绑定的方法,为导入的字符串添加一个入乡随俗的反转函数方法如下
function replaceImporterText(val) {
var ls = val.replace(/\r\n/g, '\n').split('\n'); // 过滤windows的换行符
var f = '';
for (var ln of ls) {
var q = 0;
for (var i = 0, l = ln.length, c = ln.charAt(i); i < l; i++, c = ln.charAt(i)) {
// 如果当前字符为空格并且满足回退条件
if (/\s/.test(c) && Math.round(q)) {
q--;
} else if (/[\u4E00-\u9FA5]/.test(c)) {
f += c + ' ';
q += v; // v是每个中文字符的视觉字符差值,mac为1/3,windows为1/5
} else {
f += c
}
}
f += '\n';
}
return f;
}
至此,我们可以愉快的编辑ascciflow了
尾声
文章没有特别的技术难点和原理的深入的研究,只是觉得这次思考的过程很有意思,所以写篇文章记录下来,希望能够对各位读者大大有所帮助。中文补丁后的在线地址
转载自:https://juejin.cn/post/7163696728641077284