likes
comments
collection
share

周末考古——ASCIIFlow中文支持探索

作者站长头像
站长
· 阅读数 27

写在前面

ASCIIFlow(文本流程图)是一种非常古早的流程图展示方式,它使用ascii字节码来绘制,这种Geek的视觉风格深受上世纪黑客们的喜爱。并且它字符载体的特性十分易于传播,甚至可以直接出现在代码注释中,深得俺心。

生成文本流程图的工具目前有很多,比如Perl开发的ASCIIOGraph::Easy,它们都有着丰富的字节表现形式,个人也十分推荐Graph::Easy,只是安装和中文支持比较麻烦,同时还需要学习一些生成流程图相关的指令, Vim党也可以通过插件 Drawit 和 Sketch来绘制流程图。

不过这些都不是今天要考古的对象,而是一款开源在线asciiflow编辑器asciiflow.com,它是所见即所得的,并且操作非常简单,尽管只有简单的线框样式,但也基本够用,非常适合用做小块代码逻辑的流程表达。 (中文版链接在文末! (中文版链接在文末! (中文版链接在文末! 周末考古——ASCIIFlow中文支持探索 [图片来自NodeJS文档]

中文支持的难点

通过观察发现,市面大部分的ASCIIFlow工具都是不支持中文的,包括asciiflow的官方中文版asciiflow.cn也存在导入导出的瑕疵,mac上操作几乎是不可用的乱码状态。

细想一下其实不难理解,在等宽字体族的前提下(ascii art的基本依赖),中文文字的视觉宽度约等于2字符宽度,但这并不是绝对的,和最终的渲染算法有关,这意味着相同的文本在不同环境下会产生不同的视觉偏差,也就无法对流程图的线条进行对齐。

比如在vim中,中文字体的字符比是2:1,而在Mac系统的Web或其他文本编辑器中,中文字体的字符比为5:3。 如果换成windows系统,文本编辑器默认等宽字体的中文字符比则变成了9:5。

周末考古——ASCIIFlow中文支持探索

给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));
...

周末考古——ASCIIFlow中文支持探索

支持中文输入

我们先来看下目前输入中文时我们看到的问题,编辑器视图由网状的小方格组成,可以理解为每个方格代表1个字符视图宽度。而当我们输入中文时,编辑器获得的字符长度为1,而中文的视觉宽度却是大约2个字符,所以编辑端会发生文字重叠的现象如下图所示: 周末考古——ASCIIFlow中文支持探索 编译后的代码我今儿是读不明白了,不过不要紧,我们先从输入框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来对齐展示宽度,而这个多余的空字符也会一同导出如下图所示: 周末考古——ASCIIFlow中文支持探索 我们接着来考古,找到导出按钮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个)。 周末考古——ASCIIFlow中文支持探索 为了视觉上的美观,我们把所有空字符添加到连续中文字符之后,并且为了最大程度上校准流程图边线,对需要插入的矫正字符数进行四舍五入。 入乡随俗,我们使用三个变量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了

周末考古——ASCIIFlow中文支持探索

尾声

文章没有特别的技术难点和原理的深入的研究,只是觉得这次思考的过程很有意思,所以写篇文章记录下来,希望能够对各位读者大大有所帮助。中文补丁后的在线地址