Javascript 中的表情符号 to/from 个代码点

Emojis to/from codepoints in Javascript

在我创建的混合 Android/Cordova 游戏中,我让用户以表情符号 + 字母数字的形式提供标识符 - 即 0..9,A..Z,a..z -姓名。例如

‍️Whosebug

服务器端用户标识符与表情符号和名称部分分开存储,只有名称部分需要唯一。游戏会不时显示 "league table",这样用户就可以看到与其他玩家相比他们的表现如何。为此,服务器发回由表情符号、名称和分数组成的十个 "high score" 值序列。

然后在 table 中将其呈现给用户,其中包含三列 - 表情符号、名称和分数各一列。这是我遇到一个小问题的地方。最初我天真地认为我可以通过简单地查看 handle.codePointAt(0) 来找出表情符号。当我意识到表情符号实际上可以是一个或多个 16 位 Unicode 值的序列时,我按如下方式更改了我的代码

第 1 部分:剖析用户提供的 "handle"

var i,username,
    codepoints = [], 
    handle = "‍️Whosebug",
    len = handle,length; 

 while ((i < len) && (255 < handle.codePointAt(i))) 
 {codepoints.push(handle.codePointAt(i));i += 2;}

 username = handle.substring(codepoints.length + 1);

此时我有 "disssected" 句柄

 codepoints =  [128587, 8205, 65039];
 username = 'Whosebug;

对上面 i += 2handle.length 的使用的解释注释。 This article 建议

第二部分 - 重新生成 "league table"

的表情符号

假设联盟 table 数据由我的服务器传回应用程序,其中包含表情符号字符 ‍️ 的条目 {emoji: [128583, 8205, 65039],username:"Stackexchange",points:100}。现在这是麻烦的事情。如果我这样做

var origCP = [],
    i = 0, 
    origEmoji = '‍️',
    origLen = origEmoji.length;

    while ((i < origLen) && (255 < origEmoji.codePointAt(i)) 
    {origCP.push(origEmoji.codePointAt(i);i += 2;}

我明白了

 origLen = 5, origCP = [128583, 8205, 65039]

但是,如果我根据提供的数据重新生成表情符号

 var reEmoji = String.fromCodePoint.apply(String,[128583, 8205, 65039]),
     reEmojiLen = reEmoji.length;

我明白了

reEmoji = '‍️' 
reEmojiLen = 4;

因此,虽然 reEmoji 具有正确的表情符号,但其报告的长度神秘地缩减为 4 个代码单元,而不是原来的 5 个。

如果我再从重新生成的表情符号中提取代码点

var reCP = [],
    i = 0;

while ((i < reEmojiLen) && (255 < reEmoji.codePointAt(i)) 
{reCP.push(reEmoji.codePointAt(i);i += 2;} 

这给了我

 reCP =  [128583, 8205];

Even curioser,origEmoji.codePointAt(3) 给出了 9794 的尾随代理对值,而 reEmoji.codePointAt(3) 给出了下一个完整代理对的值 65039.

此时我可以说

Do I really care?

毕竟,我只想在单独的专栏中显示联盟 table 表情符号,因此只要我获得正确的表情符号,引擎盖下发生的事情的细节并不重要。然而,这很可能会为未来埋下问题。

这里的任何人都可以阐明正在发生的事情吗?

表情符号比单个字符更复杂,它们出现在 "sequences",例如一个 zwj 序列(将多个表情符号组合成一个图像)或一个演示序列(提供相同符号的不同变体)等等,请参阅 tr51 了解所有令人讨厌的细节。

如果你"dump"你的字符串像这样

str = "‍️Whosebug"

console.log(...[...str].map(x => x.codePointAt(0).toString(16)))

您会发现它实际上是一个(格式不正确的)zwj 序列,包裹在演示序列中。

因此,要准确地分割表情符号,您需要将字符串迭代为代码点数组(不是单位!)并提取平面 1 CP (>0xffff) + ZWJ + 变体选择器。示例:

function sliceEmoji(str) {
    let res = ['', ''];

    for (let c of str) {
        let n = c.codePointAt(0);
        let isEmoji = n > 0xfff || n === 0x200d || (0xfe00 <= n && n <= 0xfeff);
        res[1 - isEmoji] += c;
    }
    return res;
}

function hex(str) {
    return [...str].map(x => x.codePointAt(0).toString(16))
}

myStr = "‍️Whosebug"

console.log(sliceEmoji(myStr))
console.log(sliceEmoji(myStr).map(hex))