长天之云

JavaScript 实现中英文空格

2018-02-05 15:56

为什么需要 JavaScript 处理?

写普通的文案时,我们有约定如何使用空格,但动态内容的处理有两种麻烦情况:

  1. 字符串插值,例如 {nameA}赞同了{nameB}的回答 有时在表达式一侧,有时在表达式两侧
  2. 各个节点之间(元素或文本),例如 <Link>{name}</Link>赞同了回答 更复杂的情况是循环中生成列表项、元素有多层有嵌套

添加空格的规则

  • 有条件地在中英文之间插入空格,例如「Kiki 赞同了回答」
  • 标点前后不加空格,例如「《Black Mirror》」,「Black Mirror。」
  • 不格式化文本,因为格式化会删除多余空格、格式化会影响原始的内容。

具体来说,将所有文本分成四类: CJK 字符、标点、空格、其他(西文字符),在 CJK 字符与其他(西文字符)之间添加空格,其他的情况不添加。

如何实现

对于字符归类,使用三个标准化 ES2018 正则(unicode property escape,可通过 transpiler 预览 ES5 转换结果):

/* 标点 */
const punctuationRegex = /\p{Punctuation}/u
/* 空格 */
const spaceRegex = /\p{Separator}/u
/* CJK 字符,中日韩 */
const cjkRegex = /\p{Script=Han}|\p{Script=Katakana}|\p{Script=Hiragana}|\p{Script=Hangul}/u

对于字符串插值,可以利用模板标签(tagged template)来简化,它本质上是一个接受一组字符串和替换表达式的函数,把字符串和表达式挨个连接起来并去空,再添加空格:

const cjkspace = (string, ...subs) =>
  join(
    zip(string, subs.concat('')).filter(c => c !== ''),
    (a, b) => (shouldSpace(a, b) ? ' ' : '')
  )

对于节点之间,要求是有样式的节点(如链接)首尾不能加入空格,例如结构 (a > span > textA) + (span > textB),可靠的做法是将生成的文本节点放在两个文本之间的公共元素之下:

const findCommonAncestor = (a, b) => {
  const pathA = unfold(a, n => n.parent)
  const pathB = unfold(b, n => n.parent)
  const index = pathA.findIndex(n => pathB.includes(n))
  // 将在 CA 下插入,在靠近 a 的一侧祖先元素之后
  return [pathA[index], pathA[index - 1]]
}

以 React 为例的处理过程是,遍历 ReactNode 节点树,转化成中间节点树(收集必要信息)并往其中添加空格节点,还原中间节点树为 ReactNode 节点树。

如何使用

在字符串之间添加空格,使用模板标签:

import cjkspace from 'cjkspace'

const userA = 'Kiki'
const userB = '喵喵'
cjkspace`${userA}${userB}赞同了你`
// => 'Kiki 和喵喵赞同了你'

在 React Node 之间添加空格,使用 React 组件:

import {CJKSpace} from 'cjkspace/react'

<CJKSpace>
  <Link>{name}</Link>
  赞同了{'Coco'}的回答
</CJKSpace>
// => <a href>Kiki<a> 赞同了 Coco 的回答

参考