JavaScript emoji utils

栏目: JavaScript · 发布时间: 6年前

内容简介:JavaScript的字符串处理貌似不难,直到遇上了emoji:??发生了什么?得从Unicode编码说起……

写在前面

JavaScript的字符串处理貌似不难,直到遇上了emoji:

JavaScript emoji utils

javascript-emoji-issues

??发生了什么? 到底怎么回事?

得从Unicode编码说起……

一.Unicode编码

The Unicode codepoint range goes from U+0000 to U+10FFFF which is over 1 million symbols, and these are divided into groups called planes. Each plane is about 65000 characters (16^4). The first plane is the Basic Multilingual Plane (U+0000 through U+FFFF) and contains all the common symbols we use everyday and then some. The rest of the planes require more than 4 hexadecimal digits and are called supplementary planes or astral planes.

也就是说,Unicode支持的编码范围是 U+0000U+10FFFF ,能对应100多万个符号( 0x10FFFF === 1114111 )。这些符号被分组归入16个 平面(panel) ,所以每个平面放65536( 16^4 === 65536 )个

其中,常用符号都放在第一个平面( U+0000U+FFFF )里,所以称之为 基本多语言平面(Basic Multilingual Plane,也简称BMP) ,其余的平面中的 码位值(codepoint,即符号对应的Unicode编码值) 都大于4位(16进制),称为 辅助平面(supplementary plane)

P.S.辅助平面还有个看起来 很厉害 的名字,叫astral plane(星界?星界位面?)

I have no idea if there’s a good reason for the name “astral plane.” Sometimes, I think people come up with these names just to add excitement to their lives.

此外,基本多语言平面里 65536 个位置的入住率并不是 100%专门空出来一些位置 以备不时之需,比如新增特殊含义符号,或者扩展

比如UTF-16中 代理对儿(surrogate pairs) 的概念,即用两个4位(16进制)的小码位值表示一个大码位值(大于4位),算是一种从基本多语言平面到辅助平面的映射,之所以能这样做,就是因为:

基本多语言平面内,从U+D800到U+DFFF之间的码位区段是永久保留不映射到Unicode字符。UTF-16就利用保留下来的0xD800-0xDFFF区段的码位来对辅助平面的字符的码位进行编码。

二.JavaScript中的Unicode

JS中的Unicode字符有3种表示方法:

'A' === '\u0041' === '\x41' === '\u{41}'

其中 \x 仅用于 U+0000U+00FF\u 适用于任意Unicode字符( U+0000U+10FFFF ),但大于4位(大于 U+FFFF )的话,就要用花括号( {} )把十六进制序列包起来:

The \x can be used for most (but not all) of the Basic Multilingual Plane, specifically U+0000 to U+00FF. The \u can be used for any Unicode characters. The curly braces are required if there are more than 4 hexadecimal digits and optional otherwise.

注意, \u{} 转义语法是在ES 2015中定义的,称之为 UnicodeEscapeSequence 。之前用两个小Unicode来表示一个大Unicode,例如:

'' === '\u{1F4A9}'
'' === '\uD83D\uDCA9'

\uD83D\uDCA9 就是代理对儿,形如 <H,L> ,二者的转换关系如下:

let C, L, H;
C = 0x1F4A9;

// 公式:大Unicode转代理对儿
H = Math.floor((C - 0x10000) / 0x400) + 0xD800;
L = (C - 0x10000) % 0x400 + 0xDC00;

[H, L].map(v => '\\u' + v.toString(16).toUpperCase()).join('')
"\uD83D\uDCA9"

另外,JS中认为一个16位无符号整数值是一个字符,所以一个emoji可能会被认为是多个字符:

The phrase code unit and the word character will be used to refer to a 16-bit unsigned value used to represent a single 16-bit unit of text.
Unicode character only refers to entities represented by single Unicode scalar values: the components of a combining character sequence are still individual “Unicode characters”, even though a user might think of the whole sequence as a single character.

P.S.关于JavaScript的Unicode支持以及ES规范的相关内容,见 JavaScript’s internal character encoding: UCS-2 or UTF-16?

正则表达式中的Unicode

既然大Unicode(大于 U+FFFF 的)在JS中用两个小Unicode(代理对儿)来表示,那么自然会写出这样的正则表达式:

> /[\uD83D\uDCA9-\uD83D\uDE0A]/.test('')
Uncaught SyntaxError: Invalid regular expression: /[\uD83D\uDCA9-\uD83D\uDE0A]/: Range out of order in character class

报错无法识别这样的range,那 怎样用正则表达式描述大Unicode字符范围 呢?

JS提供了 u flag 来解决这个问题:

u Unicode; treat pattern as a sequence of Unicode code points
/[\uD83D\uDCA9-\uD83D\uDE0A]/u.test('')
/[-]/u.test('')

类似的, . (点号匹配任意字符)想要匹配代理对儿形式的大Unicode的话,也需要开启u flag:

> /foo.bar/.test('foobar')
false
> /foo.bar/u.test('foobar')
true

P.S. /./u 仅能匹配代理对儿形式的emoji ,其它形式的不行,例如:

> /foo.bar/u.test('foo2⃣️bar')
false

P.S.更多相关示例,见 Astral ranges in character classes

fromCodePoint与fromCharCode

String.fromCodePointString.fromCharCode 的区别在于,前者支持更大范围的16进制Unicode编码,例如:

> String.fromCodePoint(0x1F4A9)
""
> String.fromCharCode(0x1F4A9)
""

fromCodePoint 由ES 2015规范定义,兼容性不如 fromCharCode 好,对于 0x0000-0xFFFF 范围的65536个Unicode字符,建议使用 fromCharCode

三.emoji编码

类似于Unicode,emoji也是一种编码规则,也有对应的规范,还存在很多个版本:

Emoji 12.0
Emoji 11.0
Emoji 5.0
Emoji 4.0
Emoji 3.0
Emoji 2.0
Emoji 1.0

其中 12.0 计划2019年才发布,最新的 11.0 发布于 2018-02-07

像HTML、CSS规范一样,新版规范中新增的emoji不一定都被实现了,并且面临的 兼容性问题比HTML、CSS更恶劣

  • 规范版本:emoji规范发版频繁,多版本共存

  • 平台差异:除了Web浏览器环境外,emoji还依赖平台原生支持(各种屏幕显示设备)

  • 依赖Unicode:emoji是在Unicode基础上建立的,依赖Unicode规范

比如从短信复制粘贴到网页输入框,emoji可能就显示不出来或者乱码了,因为native与Web浏览器支持的emoji规范版本或实现程度存在差异。另外,Unicode新规范可能会与已定义的emoji规范有冲突,这时候自然得由emoji规范让步:

Unicode 12.0 is the new version of the Unicode Standard planned for release in March 2019. See Emoji 12.0 for a more complete list of potential emojis for 2019.
Note: All emojis listed throughout 2018 are candidates only, and subject to change before a final release.

emoji面临的环境有多恶劣呢?如图:

JavaScript emoji utils

emoji-unicode-platform

回到emoji规范本身,长这样子:

1F600 ; emoji ; L1 ;    secondary ; x   # V6.1 () GRINNING FACE
1F48F ; emoji ; L1 ;    none ;  j   # V6.0 () KISS

最左边是Unicode码位值,被成功录入Unicode规范的话, U+1F48F 就会对应 KISS 表情:

> '\u{1F48F}'
""

除了这种与Unicode一一对应的emoji,加入Unicode大家庭外,还 有几种特殊的emoji

  • variation selector-16 :一个不可见字符( U+FE0F ),表示在它前面的字符应该用emoji显示

  • zero width joiner :零宽连接符,是一个零宽空格( U+200D ),用来把多个emoji合成为一个emoji

  • tone modifier:肤色修饰,一种语法,能改变前一个emoji的肤色,语法格式是 <emoji>\ud83c[\udffb-\udfff] ,即 U+D83C 后面跟不同的几个值表示不同的肤色控制

  • keycap:键帽符号,键帽样式的 0-9#* ,以 U+20E3 结尾

  • unofficial emoji flag:存在一些非常规国旗emoji,以黑色旗子( U+1F3F4 )开头, 取消符号U+E007F )结尾

例如:

// \ufe0f让黑心字符显示成emoji,连续两个也没关系
':heart:️' === '\u2764\ufe0f\ufe0f'
'\u2764\ufe0f' === ':heart:'
'\u2764' === '❤'
// 零宽连接符\u200d合成复杂表情, + :heart: +  +  = ‍:heart:‍‍
'‍:heart:‍‍' == '\ud83d\udc69\u200d\u2764\ufe0f\u200d\ud83d\udc8b\u200d\ud83d\udc69'
// 肤色修饰,黑baby、白baby
'' === '\ud83d\udc76\ud83c\udfff'
'' === '\ud83d\udc76\ud83c\udffb'
// 键帽样式
':hash:' === '\u0023\ufe0f\u20e3'
// 非官方国旗
'' ==='\ud83c\udff4\udb40\udc67\udb40\udc62\udb40\udc73\udb40\udc63\udb40\udc74\udb40\udc7f'

四.JavaScript里的emoji

那么 在JS里,一个emoji到底含有几个Unicode字符?

> ':golf:'.length
1
> ''.length
2
> ':one:'.length
3
> ''.length
4
> '‍‍‍'.length
11
> ''.length
14

一个emoji字面量的长度从 114 (还可能存在更长的)各不相同……所以,会出现这种情况:

> '‍‍‍我们是一家人'.slice(0, 1)
"�"
> '‍‍‍我们是一家人'.substr(0, 2)
""

期望通过 slice(0, 1) 截取第一个emoji,却 得到了一个无法显示的字符 ,甚至 substr(0, 2) 从一家4口中拆出了Man()…… 这可咋整?

对于某些emoji,有一种非常简单的处理方式, Array.from

> Array.from('').length
1

字符串转数组时会保持代理对儿在一起,所以 length 正确了,但 这种方法不是万能的

> Array.from('‍‍‍').length
7

P.S.类似的,支持Unicode编码转换的 bestiejs/punycode.js 也存在类似的问题:

> punycode.ucs2.decode(' ').length
1
> punycode.ucs2.decode('‍‍‍').length
7

也就是说, 单靠JS对Unicode的原生支持,无法正确处理含emoji的字符串 。那么,在一些场景会遇到问题:

  • 表单检验字数限制

  • 截取文章摘要

  • 反转字符串

  • 逐字符处理

  • 正则匹配

  • ……其它含emoji的文本处理场景

例如:

> '‍‍‍一个打十个'.length >= 10 === true
true
> '你好hi233..。'.substr(0, 10)
"你好hi233�"
> Array.from(':one:23').reverse().join('')
"32⃣️1"
> '开心'[0] === ''
false
> /a.b/.test('ab')
false

P.S.关于JavaScript中Unicode的更多问题,见 JavaScript has a Unicode problem

五.解决方案:emoji-utils.js

要解决上面列出的一排问题,只能想办法识别emoji了,目前( 2018/09/15 )貌似还没有这样的 工具

手搓一个,类似于词法分析,逐字符匹配,挑出符合emoji编码规则的Unicode组合,具体见下面源码

Github地址: https://github.com/ayqy/emoji-utils

在线Demo(测试case): https://ayqy.github.io/emoji/index.html

API

提供了6个简单API:

// 是不是一个emoji
isEmoji(str)
// 是否包含emoji
containsEmoji(str)
// 字符串转Unicode数组
str2unicodeArray(str)
// 计算长度
length(str)
// 子串截取
substr(str = '', start = 0, len = Infinity)
// 字符串转数组,相当于split('')
toArray(str)

内部未暴露的方法有:

// 尝试匹配开头的emoji,失败返回''
matchOneEmoji(str, matched = '')

缺陷

但是,这些工具函数 并不100%靠谱 ,因为:

Not all browsers, UIs, etc even render ‍:heart:‍‍ as a single symbol. The code assumes the joiners are used between characters appropriately which could be very problematic.

所以, emoji-utils.js实现基于3点假设

  • 所有代理对儿都是emoji(事实上,有些代理对儿不是emoji)

  • 肤色控制对所有emoji都是有效的,并且只对emoji生效(对普通文本符号无效)

  • joiner连接起来的emoji都算一个,无论显示上能否被合成一个emoji

对于第一点假设,代理对儿形式的不一定是emoji,也可能是纯文本,例如:

'\ud835\udc00' === ''

后两点假设也会导致一些badcase,例如(Chrome Console环境):

// 尝试制造黑色笑脸,未遂
'\ud83d\ude0a\ud83c\udfff' === ''
// 尝试人工合成新物种,失败
'\u0023\ufe0f\u20e3\u200d\ud83d\ude0a' === ':hash:‍'
'\ud83d\ude0a\u200d\ud83d\ude0a' === '‍'

这些case都会被识别成1个emoji,而Chrome Console环境显示是2个,因为它们:

  • 符合emoji编码的语法规则

  • 但不一定是合法的emoji

  • 即便合法,当前平台也不一定支持

emoji-utils.js 假设满足第一点的就是一个独立显示的合法emoji,未考虑emoji规范版本以及平台支持性,所以存在这样的badcase。badcase可能带来的影响是:

  • isEmoji/containsEmoji() 误判类似于”的文本字符

  • length() 小于实际显示的字符长度

  • substr()/toArray() 与实际预期不符

所以能这个工具库所能识别出的字符集是emoji的超集,多出来一部分代理对儿形式的文本,以及符合emoji编码规则但在emoji规范中未定义的字符序列。尽管如此,它是 目前最健全的JS emoji处理工具 ,实际应用中足够应对大多数场景了

参考资料


以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持 码农网

查看所有标签

猜你喜欢:

本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们

Python Machine Learning

Python Machine Learning

Sebastian Raschka / Packt Publishing - ebooks Account / 2015-9 / USD 44.99

About This Book Leverage Python' s most powerful open-source libraries for deep learning, data wrangling, and data visualization Learn effective strategies and best practices to improve and opti......一起来看看 《Python Machine Learning》 这本书的介绍吧!

图片转BASE64编码
图片转BASE64编码

在线图片转Base64编码工具

随机密码生成器
随机密码生成器

多种字符组合密码

XML 在线格式化
XML 在线格式化

在线 XML 格式化压缩工具