Pull to refresh

Emoji.prototype.length — рассказ об эмоциональных символах в Юникоде

Reading time 7 min
Views 15K
Хабр довольно враждебно настроен к Эмодзи (здесь они просто не отображаются), считая их чем-то вроде «падонкаффского» языка. Не для серьёзных людей. Ведь и то и другое появилось примерно в одно время. И если «олбанский» йезыг быстро ушёл в небытие, то Эмодзи эволюционировали от простых точек с запятой и скобочек до полноправных символов в кодировке Юникод. Автор данной статьи предлагает посмотреть, что у этих маленьких сущностей «под капотом» (Здесь и далее курсивом — примечания переводчика).

EmojiFamilyHeader
Оригинальная статья написана для сайта Contentful Стефаном Джудисом (Twitter, GitHub).

Эмодзи – это основа текстового общения наших дней. Без этих маленьких символов, много бесед в чатах сегодня занчивались бы неловким молчанием или недопониманием. Я всё ещё помню старые добрые времена, когда СМС-ки были крутой штукой.

Предложение пообщаться в чате без смайликов скорее всего приведёт к сообщению «Ты что шутишь?». Все быстро поняли, что юмор и сарказм (кстати, нам бы не помешало быть менее саркастичными) не просто передать, используя лишь письменные знаки. В какой-то момент возник первый Эмодзи, и они быстро стали одним из фундаментальных компонентов любой беседы в текстовом формате.

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

Всё поменялось, когда я набрёл на твит Веса Боса (Wes Bos), в котором он показал несколько JavaScript-операций над строкой, содержащей семью Эмодзи.


Допустим, что использование spread-оператора в такой строке не сильно меня удивило, но тот факт, что один видимый символ был разделён на три символа и две пустые строки, несколько меня озадачил. И то, что свойство строки length вернуло значение 8, удивило меня ещё больше, так как в массиве, который вернул spread-оператор, значений было 5, но никак не 8.

Недолго думая, я открыл консоль, и убедился в том, что всё происходит именно так, как Вес описал. Так что же здесь происходит? Я решил копнуть глубже в Юникод, JavaScript и семью Эмодзи, чтобы во всём разобраться.

Юникод спешит на помощь


Чтобы понять, почему JavaScript обрабатывает Эмодзи таким образом, мы должны заглянуть глубже в сам Юникод.

Юникод – это международный стандарт кодировки символов в IT-индустрии. Он устанавливает соответствие между каждой буквой, знаком или символом и числовым значением. Благодаря Юникоду мы можем делиться документами, которые содержат например специальные немецкие символы (умлауты) ß, ä, ö, с людьми, системы которых их не используют. Благодаря Юникоду, кодировки работают на разных платформах и окружениях.

В Юникоде определяется 1 114 112 различных символов, и они обычно представляются с помощью U+ с последующим числом в шестнадцатеричной системе счисления. Диапазон символов Юникод начинается U+0000 и заканчивается U+10FFFF.

Всё кодовое пространство (более одного миллиона символов) разбито на 17 т. н. «плоскостей», и каждая плоскость включает в себя свыше 65 000 символов. Наиболее важная – нулевая, «Базовая Мультиязычная Плоскость» (“Basic Multilingual Plane”, BMP). Её диапазон от U+0000 до U+FFFF.

Базовая плоскость содержит символы почти всех современных языков, плюс большое количество других символов. Остальные 16 плоскостей называются дополнительными и используются для различных целей, таких как – вы и сами могли догадаться – определение большинства символов Эмодзи.

Как определяются Эмодзи?


Как мы знаем, Эмодзи определяются как минимум одним символом из набора Юникода. Если посмотреть на все Эмодзи, представленные в Полном списке Эмодзи, то можно заметить, что их там много. И под словом «много» я имею в виду действительно много. Вы можете спросить себя, как много различных Эмодзи определено в Юникоде на сегодняшний день? Ответом на этот вопрос, как это часто случается в IT, будет «Это зависит от…», и мы должны разобраться с этим, прежде чем получим ответ.

Как я писал выше, Эмодзи определяются как минимум одним символом. Это означает, что есть некоторые Эмодзи, которые определяются комбинацией нескольких других Эмодзи и символов. Эти комбинации называются последовательностями. Благодаря последовательностям можно изменить нейтральный Эмодзи (обычно отображаемый с жёлтым цветом кожи), и сделать его более персональным.

Модификатор последовательностей для разных цветов кожи


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

В Юникоде есть пять модификаторов для изменения нейтрального Эмодзи и представления всего разнообразия цветов кожи человечества. Модификаторы лежат в пределах от U+1F3FB до U+1F3FF и основываются на шкале Фицпатрика.

С помощью этих модификаторов мы можем превратить нейтральный Эмодзи в такой же, но с другим цветом кожи. Давайте посмотрим на пример:


Когда мы взяли девочку Эмодзи, символ которой U+1F467 и применили к ней модификатор цвета кожи (U+1F3FD), мы автоматически получили девочку с этим цветом кожи для тех систем, которые поддерживают эту последовательность.

ZWJ-последовательности для еще большего разнообразия


Цвет кожи — не единственное, что отличает людей друг от друга. Когда мы вспоминаем пример с семьей, становится понятно, что не каждая семья состоит из мужчины, женщины и мальчика.

В Юникоде есть символ для описания обычной семьи (U+1F46AEmoji family), но так выглядит не каждая семья. Мы можем создать любую семью, используя так называемую Zero-Width-Joiner (ZWJ) последовательность.

А вот как это работает: существует специальный символ, который называется объединителем нулевой ширины (U+200D).Этот символ работает как клей, показывая, что два символа должны быть отображены одним, когда это возможно.

Если подумать логически, что бы мы могли склеить, чтобы показать семью? Ответ прост — двух взрослых и ребенка. Используя ZWJ-последовательности, мы легко можем отобразить различные семьи.


Если посмотреть на список всех возможных последовательностей, можно увидеть, что вариантов там ещё больше, например, один папа с двумя девочками. К сожалению, на момент написания статьи, поддержка этих последовательностей не очень хорошая, но ZWJ-последовательности деградируют постепенно (Graceful degradation), возвращая последовательность отдельных Эмодзи. Это позволяет поддерживать семантичность.


Другая крутая штука — это то, что принципы объединения распространяются не только на семью Эмодзи. Например, давайте возьмём известный Эмодзи Дэвида Боуи (настоящее название — «певец»). Это тоже ZWJ-последовательность, состоящая из мужчины(U+1F468), ZWJ-объединителя и микрофона (U+1F3A4).
image
И, как вы уже могли догадаться, если мы заменим мужчину (U+1F468) женщиной (U+1F469), то получим певицу (или женскую версию Дэвида Боуи). Также можно добавить модификатор цвета кожи, тогда мы получим чернокожую певицу. Класс!


К сожалению, на момент написания статьи, поддержка этих новых символов также оставляет желать лучшего.

Разное количество Эмодзи


Итак, ответ на вопрос как много Эмодзи существует на сегодняшний день, зависит от того, что вы будете считать Эмодзи. Это количество символов, которые были использованы, чтобы отобразить Эмодзи? Или мы будем учитывать все варианты Эмодзи, которые могут быть отображены?

Если мы подсчитываем все варианты Эмодзи, которые могут быть отображены (включая последовательности и вариации), то получим 2 198. Если вам интересен процесс подсчёта, то вот целый раздел об этом на unicode.org.

Также к вопросу «Как подсчитывать» можно добавить тот факт, что новые Эмодзи и символы Юникода добавляются к спецификации постоянно, что делает отслеживание их точного количества ещё более сложной задачей.

Возвращаясь к строкам в JavaScript и 16-битной кодировке


В UTF-16, строковом формате, используемом в JavaScript, для представления большинства символов используется одно 16-битное кодовое значение (2 байта). Это означает, что чуть более 65000 различных кодовых значений может поместиться в один символ JavaScript. Это в точности совпадает с Базовой Мультиязычной плоскостью (BMP). Так давайте попытаемся сопоставить символы Юникода с несколькими символами, определенными в BMP.


Когда мы применяем к этим строкам свойство length, мы получаем единицу, и это полностью соответствует нашим ожиданиям. Но что произойдет, если я захочу использовать символ в JavaScript, который находится вне в диапазоне BMP?

Суррогатные пары спешат на помощь


Два символа, определенных в Базовой Плоскости, можно объединить, чтобы отобразить другой символ, который лежит за её пределами. Эта комбинация называется суррогатной парой.

Символы, лежащие в промежутке от U+D800 до U+DBFF, зарезервированы для так называемых старших или «ведущих» суррогатов, а символы в промежутке от U+DC00 до U+DFFF для младших или «замыкающих».

Эти два символа всегда должны использоваться в парах, начиная старшим и заканчивая младшим суррогатом. Затем применяется специальная формула для декодирования символов, лежащих вне диапазона.

Давайте рассмотрим пример:


Обычный мужчина в Эмодзи представлен символом U+1F468. Этот символ не может быть представлен одним 16-битным символом JavaScript. Поэтому для отображения одного символа вне BMP (U+1F468) должна быть использована суррогатная пара, состоящая из двух символов, входящих в BMP (U+D83D и U+DC68).

Для анализа символов в JavaScript существует два метода. Мы можем использовать charCodeAt, который вернет коды «суррогатных» псевдо-символов, если они используются для составления общего символа. Второй метод — codePointAt, который вернет код объединенной пары суррогатных символов, если мы «попали» в «ведущий» суррогатный символ или вернет код «замыкающего» суррогатного символа, если мы «попали» в него.

Вы думаете что это ужасно сбивает с толку? Я тоже так считаю и очень рекомендую вам внимательно прочитать статьи на MDN про эти два метода (charCodeAt, codePointAt) (также об этом можно почитать на learn.javascript.ru).

Давайте внимательнее посмотрим на символ мужчины в Эмодзи и посчитаем. Используя charCodeAt, мы можем получить коды «суррогатных» псевдо-символов, использующихся в суррогатной паре.

Первый символ имеет значение 55357, что соответствует D83D в шестнадцатеричной системе счисления. Это «ведущий» псевдосимвол. Второе значение 56424 соответствует DC68 и является «замыкающим» псевдосимволом. Это классическая суррогатная пара, которая в результате вычисления по формуле даст результат 128104, что соответствует символу мужчины в Эмодзи.


Количество символов и свойство length


Разобравшись с кодами Юникод и символами, мы можем приступить к странному поведению свойства length. Оказывается, оно возвращает количество кодовых значений Юникода, а не символов, которые мы видим, как мы думали в начале. Это может привести к трудностям в отлавливании ошибок при работе с Unicode в строках JavaScript – так что будьте внимательны, когда вы имеете дело с символами, лежащими вне BMP.

Заключение


Давайте вернёмся к примеру Веса, с которого всё началось.


Семья Эмодзи, которую мы здесь видим, состоит из мужчины, женщины и мальчика. Spread-оператор будет возвращать отдельные символы Эмодзи. Пустые строки на самом деле не пустые — это ZWJ-объединители. Свойство length, в этом случае вернёт 2 для каждого символа Эмодзи и 1 для ZWJ-объединителей. В результате мы получим 8.

Мне очень понравилось моё погружение в Юникод. Если вам также интересна эта тема, я бы порекомендовал @fakeunicode Twitter-аккаунт. Там много интересного о том, на что способен Юникод. Кстати, вы знали, что есть даже подкасты и конференции об Эмодзи? Я буду и дальше следить за всем этим, потому что мне очень интересно узнавать больше об этих маленьких символах, которые мы используем повсеместно. Возможно эта тема заинтересовала и вас.
Tags:
Hubs:
+47
Comments 15
Comments Comments 15

Articles