Эмодзи?! Нет, не слышал

в 9:42, , рубрики: badoo, emoji, es2015, html, javascript, Unicode, web-разработка, баду, Блог компании Badoo, Разработка веб-сайтов, чат

image В нашу жизнь уже давно вошли эмодзи. И в социальных сетях, и во всевозможных мессенджерах мы используем их не задумываясь, выражая свои эмоции всего одним символом. Но для кроссплатформенного приложения отправка и отображение эмодзи — непростая задача. Проблема заключается в том, что отправленные эмодзи с мобильных приложений не всегда отображаются корректно на веб-сайтах.

Последние версии iOS и Android имеют поддержку более 1200 символов эмодзи, но «десктопный» рынок не может похвастаться такими успехами. Мы же в Badoo хотим и делаем все, чтобы пользователям было комфортно общаться на всех платформах, не имея никаких ограничений в переписке.
Далее я расскажу, каким способом мы добились 100% поддержки эмодзи для веба.

Вот так бы пользователь Windows увидел сообщение в браузере без эмодзи:

image

Основная идея состоит в том, что мы берем любой символ эмодзи, определяем его Юникод-код и заменяем на html-элемент, который будет корректно отображаться в браузере.

Теория

Рассмотрим image(улыбающееся лицо). Он имеет код U+1F600. Как получить его код с помощью JavaScript:

'image'.length //2
'image'.charCodeAt(0).toString(16) // D83D
'image'.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} вместо uD83DuDC66

RegExp.prototype.unicode: флаг u в регулярных выражениях дает лучшую поддержку при работе Юникодом:

/u{1F466}/u

На данный момент стандарт Юникод 8.0 содержит 1281 символов эмодзи, и это не считая модификаторов цвета кожи и групп (эмодзи семьи). Существуют различные реализации от известных компаний:

image

Эмодзи можно разделить на несколько групп:

  • простые: в диапозоне до 0xD7FF — image;
  • суррогатные пары: от 0xD800 до 0xDFFF — image;
  • числа: от 0x0023 до 0x0039 + 0x20E3 — image;
  • государственные флаги: 2 символа от 0xDDE6 до 0xDDFF, в результате — image;
  • модификаторы цвета кожи: image + от 0xDFFB до 0xDFFF — image;
  • семья: последовательность из image image image соединенных 0x200D или 0x200C — image

Решение:

  • получаем исходный текст с символом, ищем в нем с помощью регулярного выражения все наборы эмодзи;
  • определяем код символа с помощью функции codePointAt;
  • создаем элемент img (важно, чтобы это был именно тег img) с url, который состоит из кода этого символа;
  • заменяем символ на img в исходном тексте.

function emojiToHtml(str) {
	return str.replace(emojiRegex, buildImgFromEmoji);
}

var tpl = '<img class="emoji emoji--{code} js-smile-insert" src="{src}" srcset="{src} 1x, {src_x2} 2x" unselectable="on">';
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', // числа
	'(?:[uD83DuD83CuD83E][uDC00-uDFFF]|[u270A-u270Du261Du26F9])uD83C[uDFFB-uDFFF]', // цвет кожи
	'uD83D[uDC68uDC69][u200Du200C].+?uD83D[uDC66-uDC69](?![u200Du200C])', // семья
	'[uD83DuD83CuD83E][uDC00-uDFFF]', // суррогатная пара
	'[u3297u3299u303Du2B50u2B55u2B1Bu27BFu27A1u24C2u25B6u25C0u2600u2705u21AAu21A9]', // обычные
	'[u203Cu2049u2122u2328u2601u260Eu261du2620u2626u262Au2638u2639u263au267Bu267Fu2702u2708]',
	'[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:

<div id="t" contenteditable="true" data-placeholder="Введите сообщение"></div>

При вводе сообщения или вставки из буфера обмена мы будем чистить его содержимое от возможных html-тегов:

var tagRegex = /<[^>]+>/gim;
var styleTagRegex = /<styleb[^>]*>([sS]*?)</style>/gim;
var validTagsRegex = /<br[s/]*>|<imgs+class="emojisemoji[-ws]+"s+((src|srcset|unselectable)="[^"]*"s*)+>/i;

function cleanUp(text) {
	return text
		.replace(styleTagRegex, '')
		.replace(tagRegex, function (tag) {
			return tag.match(validTagsRegex) ? tag : '';
		})
		.replace(/n/g, '');
}

Для обработки строки, вставленной из буфера обмена, используем событие paste:

function onPaste(e) {
	e.preventDefault();
	var clp = e.clipboardData;

	if (clp !== undefined || window.clipboardData !== undefined) {
		var text;

		if (clp !== undefined) {
			text = clp.getData('text/html') || clp.getData('text/plain') || '';
		} else {
			text = window.clipboardData.getData('text') || '';
		}

		if (text) {
			text = cleanUp(text);
			text = emojiToHtml(text);
			var el = document.createElement('span');
			el.innerHTML = text;
			el.innerHTML = el.innerHTML.replace(/n/g, '');
			t.appendChild(el);
			restore();
		}
	}
}

Затем заменяем все найденные эмодзи на html-тег img, как было показано выше. Именно на img, так как contenteditable лучше всего работает с ним. С другими элементами могут возникать баги при редактировании.

После вставки img в поле для ввода требуется восстановить позицию каретки, чтобы пользователь мог продолжить набор сообщения. Для этого используем JavaScript объекты Selection и Range:

function restore() {
	var range = document.createRange();
	range.selectNodeContents(t);
	range.collapse(false);
	var sel = window.getSelection();
	sel.removeAllRanges();
	sel.addRange(range);
}

После того как набор сообщения завершен, требуется проделать обратную процедуру. А именно превратить img в символ для отправки на сервер с помощью функции fromCodePoint:

var htmlToEmojiRegex = /<img.*?class="emojisemoji--(.+?)sjs-smile-insert".*?>/gi;
function htmlToEmoji(html) {
	return html.replace(htmlToEmojiRegex, function (imgTag, codesStr) {
		var codesInt = codesStr.split('-').map(function (codePoint) {
			return parseInt(codePoint, 16);
		});

		var emoji = String.fromCodePoint.apply(null, codesInt);

		return emoji.match(emojiRegex) ? emoji : '';
	});
}

Посмотреть пример чата можно тут: https://jsfiddle.net/q9484hcc/

Так мы разработали поддержку эмодзи, чтобы наши пользователи могли выражать эмоции в полной мере и общаться друг с другом без ограничений. Если у вас есть идеи по улучшению наших методов или их изменению ― пишите в комментариях, с удовольствием их обсудим!

Полезные ссылки:
http://emojipedia.org/
http://getemoji.com/
Полифил String.fromCodePoint
Полифил String.prototype.codePointAt

Артем Кунец
Frontend-разработчик Badoo

Автор: Badoo

Источник

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js