Эмодзи?! Нет, не слышал
В нашу жизнь уже давно вошли эмодзи. И в социальных сетях, и во всевозможных мессенджерах мы используем их не задумываясь, выражая свои эмоции всего одним символом. Но для кроссплатформенного приложения отправка и отображение эмодзи — непростая задача. Проблема заключается в том, что отправленные эмодзи с мобильных приложений не всегда отображаются корректно на веб-сайтах.
Последние версии iOS и Android имеют поддержку более 1200 символов эмодзи, но «десктопный» рынок не может похвастаться такими успехами. Мы же в Badoo хотим и делаем все, чтобы пользователям было комфортно общаться на всех платформах, не имея никаких ограничений в переписке.
Далее я расскажу, каким способом мы добились 100% поддержки эмодзи для веба.
Вот так бы пользователь Windows увидел сообщение в браузере без эмодзи:
![image](https://habrastorage.org/files/b71/2ee/b1e/b712eeb1e72a42bfadc0eab8c47f871e.png)
Основная идея состоит в том, что мы берем любой символ эмодзи, определяем его Юникод-код и заменяем на html-элемент, который будет корректно отображаться в браузере.
Теория
Рассмотрим (улыбающееся лицо). Он имеет код U+1F600. Как получить его код с помощью JavaScript:
''.length //2
''.charCodeAt (0).toString (16) // D83D
''.charCodeAt (1).toString (16) // DE00
В итоге мы получили суррогатную пару: U+D83D U+DE00.
UTF-16 кодирует символы в виде последовательности 16-битных слов, это позволяет записывать символы Юникода в диапазонах от U+0000 до U+D7FF и от U+E000 до U+10FFFF (общим количеством 1 112 064). Если требуется представить в UTF-16 символ с кодом больше U+FFFF, то используются два слова: первая часть суррогатной пары (в диапазоне от 0xD800 до 0xDBFF) и вторая (от 0xDC00 до 0xDFFF).
Чтобы получить код эмодзи, который находится в диапазоне больше U+FFFF, воспользуемся формулой:
(0xD83D - 0xD800) * 0x400 + 0xDE00 - 0xDC00 + 0x10000 = 1f600
А теперь переведем обратно:
D83D = ((0x1f600 - 0x10000) >> 10) + 0xD800;
DE00 = ((0x1f600 - 0x10000) % 0x400) + 0xDC00;
Это довольно сложно и неудобно, рассмотрим, что нам может предложить ES 2015.
С новым стандартом JavaScript можно забыть про суррогатные пары и облегчить себе жизнь:
String.prototype.codePointAt // возвращает код из символа,
String.fromCodePoint // возвращает символ из кода.
Оба метода корректно работают с суррогатными парами.
Возможность вставки восьмизначных кодов в строку:
\u{1F466} вместо \uD83D\uDC66
RegExp.prototype.unicode: флаг u в регулярных выражениях дает лучшую поддержку при работе Юникодом:
/\u{1F466}/u
На данный момент стандарт Юникод 8.0 содержит 1281 символов эмодзи, и это не считая модификаторов цвета кожи и групп (эмодзи семьи). Существую различные реализации от известных компаний:
![image](https://habrastorage.org/files/d0e/000/8d4/d0e0008d435a47c3b32d74a1c6caf2d8.png)
Эмодзи можно разделить на несколько групп:
Решение:
- получаем исходный текст с символом, ищем в нем с помощью регулярного выражения все наборы эмодзи;
- определяем код символа с помощью функции codePointAt;
- создаем элемент img (важно, чтобы это был именно тег img) с url, который состоит из кода этого символа;
- заменяем символ на img в исходном тексте.
function emojiToHtml(str) {
return str.replace(emojiRegex, buildImgFromEmoji);
}
var tpl = '
';
var url = 'https://badoocdn.com/big/chat/emoji/{code}.png';
var url2 = 'https://badoocdn.com/big/chat/emoji@x2/{code}.png';
function buildImgFromEmoji(emoji) {
var codePoint = extractEmojiToCodePoint(emoji);
return $tpl(tpl, {
code: codePoint,
src: $tpl(url, {
code: codePoint
}),
src_x2: $tpl(url2, {
code: codePoint
})
});
}
function extractEmojiToCodePoint(emoji) {
return emoji
.split('')
.map(function (symbol, index) {
return emoji.codePointAt(index).toString(16);
})
.filter(function (codePoint) {
return !isSurrogatePair(codePoint);
}, this)
.join('-');
}
function isSurrogatePair(codePoint) {
codePoint = parseInt(codePoint, 16);
return codePoint >= 0xD800 && codePoint <= 0xDFFF;
}
Основная идея в регулярном выражении, которое находит символы эмодзи:
var emojiRanges = [
'(?:\uD83C[\uDDE6-\uDDFF]){2}', // флаги
'[\u0023-\u0039]\uFE0F?\u20E3', // числа
'(?:[\uD83D\uD83C\uD83E][\uDC00-\uDFFF]|[\u270A-\u270D\u261D\u26F9])\uD83C[\uDFFB-\uDFFF]', // цвет кожи
'\uD83D[\uDC68\uDC69][\u200D\u200C].+?\uD83D[\uDC66-\uDC69](?![\u200D\u200C])', // семья
'[\uD83D\uD83C\uD83E][\uDC00-\uDFFF]', // суррогатная пара
'[\u3297\u3299\u303D\u2B50\u2B55\u2B1B\u27BF\u27A1\u24C2\u25B6\u25C0\u2600\u2705\u21AA\u21A9]', // обычные
'[\u203C\u2049\u2122\u2328\u2601\u260E\u261d\u2620\u2626\u262A\u2638\u2639\u263a\u267B\u267F\u2702\u2708]',
'[\u2194-\u2199]',
'[\u2B05-\u2B07]',
'[\u2934-\u2935]',
'[\u2795-\u2797]',
'[\u2709-\u2764]',
'[\u2622-\u2623]',
'[\u262E-\u262F]',
'[\u231A-\u231B]',
'[\u23E9-\u23EF]',
'[\u23F0-\u23F4]',
'[\u23F8-\u23FA]',
'[\u25AA-\u25AB]',
'[\u25FB-\u25FE]',
'[\u2602-\u2618]',
'[\u2648-\u2653]',
'[\u2660-\u2668]',
'[\u26A0-\u26FA]',
'[\u2692-\u269C]'
];
var emojiRegex = new RegExp(emojiRanges.join('|'), 'g');
Чат
Далее рассмотрим, каким образом можно построить прототип чата с поддержкой эмодзи.
В качестве поля для ввода сообщения используется div:
При вводе сообщения или вставки из буфера обмена мы будем чистить его содержимое от возможных html-тегов:
var tagRegex = /<[^>]+>/gim;
var styleTagRegex = /