pangu.simple.js的背后故事

原本的打算是把之前的han.simple.js给移植到新版的NexT里面,但是它删除了对之前Han那个库的支持,换用了pangu.js,所以我打算先试试看pangu.js,然后再考虑是否要做处理。

因为刚好发现本pangu.js在下面这个句子上存在错误。

原文是:

1
我打开了一张.png格式的图片

原本的预期是:

1
我打开了一张 .png 格式的图片

但是现实却是:

1
我打开了一张.png 格式的图片

由于这样的文本出现频率还是比较高的,我不想每一次都手动遵循pangu.js的处理规则去手动加这样一个空格,所以去GitHub上找到了这个仓库提了一个Issue。

不过我看到这个仓库好像很久没有维护了,等作者来修应该是不太现实,还是自己动手来得快。

发现

细看pangu.js的源码,其核心其实和han.simple.js是一样的,都是用正则做字符串处理,通过$1$2...中间加入空格来做空格的插入。

除此之外我还发现有Issue提到这个库存在过度转换的问题,也就是把作者个人认为的优雅排版也加入到了里面,不只是在中英文之间插入空格。通过查阅代码,这个库也确实存在这样的问题。

索性干脆fork一份自己改,把里面的正则全部按照han.simple.js的替换一份,然后借pangu.js现有的框架对原来我的那个排版系统做扩展。

编写

pangu.js提供了一个CJK用来代表中文等字符,我在其上添加了几个不同的SYMBOL来定义符号。

1
2
3
4
5
const SYMBOL_WIDE = '`~!@#$%*^&()/\\-+=<>?:"{}|,.;\'[\\]·~¥%——|\\\\';
const SYMBOL = '`~!@#$%^&()/\\-+=<>?:"{}|,.;\'[\\]·~¥%——|\\\\';
const SYMBOL_LEFT = '`~!@#$%^&(/\\-+=<>?:"{|,.;\'[·~¥%——|\\\\';
const SYMBOL_RIGHT = '`~!@#$%^&)/\\-+=<>?:"}|,.;\'\\]·~¥%——|\\\\';
const SYMBOL_SAFE = '`~!#$%^&/+=<>?:"|,;\'·~¥%——|\\\\';

(SYMBOL的定义需要注意二次转义的问题)

为什么要这样处理标点符号、特殊符号呢?主要是因为有几个特别的情况需要特别对待,所以需要编写不同的规则去适配。

根据pangu.js的测试用例,这一次特别考虑了下面几个特殊的情况,也是日常文字中常见的用法。

1
林先生&Lee

按照常理这个应该会被处理成,但实际上这个例子并不会被原来那些正则覆盖,这个地方需要扩增一个规则,即CJK_AND_ALPHA(汉字&字母)

1
林先生 & Lee

同样的这个规则也会有一个反向的规则。

对于括号,这一次是针对下面这个例子做了特别的处理,这也是常见的东西:

1
我的手机用的系统是iOS(苹果系统)的最新版。

这个例子的特殊性在于跟在英文后面的括号是英文括号不是中文括号,而括号里的内容是中文。原本英文标点都应该和中文隔断的,但是在这个例子里不应该被隔断,正确的输出会是这样:

1
我的手机用的系统是 iOS(苹果系统) 的最新版。

而不是:

1
我的手机用的系统是 iOS( 苹果系统 ) 的最新版。

明显上面那一种要明显优于下面那一种。虽然说括号可以统一为中文,但是考虑到文本的多遍,不如只针对这一种特殊情况进行适配。

还有一个很常见的例子是网友ID,我们经常会用@xxx来表示一位网友,这也是经常出现的,而这个例子被很多可用的排版脚本忽略。

由于pangu.js已经搭好了测试框架,所以直接加入一个案例:

1
今天我看了@神乐mea的直播

正确输出是:

1
今天我看了 @神乐mea 的直播

这个例子的特殊性在于@+ALPHABET/NUMBER+CJK+ALPHABET/NUMBER+,如果只有@+ALPHABET/NUMBER+CJK脚本是区分不能的,必须要自己做隔断,而@+ALPHABET+已经被其他的规则覆盖了。

由于脚本不能区分@+ALPHABET/NUMBER*+CJK,所以原则上类似于下面这个例子脚本不应该主动做隔断:

1
今天我看了@凑-阿库娅的直播

包括中间的短线原则上也不应该做隔断,为什么呢?因为会出现奇怪的问题,比如我输入的是:

1
今天我看了 @湊-阿库娅 的直播

然后出来的可能会是:

1
今天我看了 @湊 - 阿库娅 的直播

这一点很显然也不行,所以在一些规则里像@和短线这样的符号是要主动规避的,这也是为什么我要定义这么多个不同的SYMBOL。

规则不可能是完美的,既然存在针对某些情况的规避,就自然会存在有些情况没有办法覆盖,比如说下面这个例子:

1
这里是左边-这里是右边

更加好看的写法可能会是:

1
这里是左边 - 这里是右边

这个例子会因为规避而没有办法处理,因为它和上面需要规避的部分共用了一个规则,也就是CJK+-+CJK。

测试

因为原本的测试用例存在很多我并不需要的,我更希望针对性地测试我想要测试的特殊例子,所以我重写了核心这一部分的测试代码,但其他的测试用例没有处理,所以只有核心这一部分的测试实际上是可以正常运作的。

其他部分的测试因为剔除了一些Vinta的正则所以在重写测试用例之前没有办法工作。

后续会观察它在站点上的应用情况,适当修改规则、增加更多的例子,来让这个尽可能完善。不存在100%完美的规则,只存在更好用的规则。

兼容性

由于接口、代码、类名全部没有调整,只替换了核心逻辑和测试代码,所以应用了pangu.js的地方可以无缝切换到pangu.simple.js,不会对功能产生任何的影响。

对我来说这也是最省事的做法,而且pangu.js本身处理嵌套的HTML标签内的文本也很强,不需要我多做修改。

仓库

由于Vinta的pangu.js是在MIT许可证下,所以我可以修改和发布。

目前没有提交到npm,但是有编译好的、适用于浏览器的文件可用。这里不提供链接,因为它可能不是最新的,而且我不希望我的私链被滥用。这个代码你可以自行编译一份。

https://github.com/backrunner/pangu.simple.js

关于在NexT中的使用

把你编译好的pangu.min.js放到某个能够访问的、托管静态文件的地方,然后把链接填入_config.yml最下方的地方,主题会优先从你填入的地址加载pangu.js,这样就能够实现不修改主题本身进行替换。

未来

未来有时间打算是继承han.simple.js的设计理念,对于browser这一块增加一个可选项,让替换进去的内容从单纯的空格变成一个自定义的HTML5标签,并且向页面插入一个margin: 0 0.05em的样式。

这个做法的好处是插入的空格不会干扰文本的复制。